Adding mapbox-gl branch
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,348 @@
|
||||
// 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 Activity Monitor.
|
||||
*
|
||||
* Fires throttled events when a user interacts with the specified document.
|
||||
* This class also exposes the amount of time since the last user event.
|
||||
*
|
||||
* If you would prefer to get BECOME_ACTIVE and BECOME_IDLE events when the
|
||||
* user changes states, then you should use the IdleTimer class instead.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ActivityMonitor');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.EventType');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Once initialized with a document, the activity monitor can be queried for
|
||||
* the current idle time.
|
||||
*
|
||||
* @param {goog.dom.DomHelper|Array<goog.dom.DomHelper>=} opt_domHelper
|
||||
* DomHelper which contains the document(s) to listen to. If null, the
|
||||
* default document is usedinstead.
|
||||
* @param {boolean=} opt_useBubble Whether to use the bubble phase to listen for
|
||||
* events. By default listens on the capture phase so that it won't miss
|
||||
* events that get stopPropagation/cancelBubble'd. However, this can cause
|
||||
* problems in IE8 if the page loads multiple scripts that include the
|
||||
* closure event handling code.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
*/
|
||||
goog.ui.ActivityMonitor = function(opt_domHelper, opt_useBubble) {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
/**
|
||||
* Array of documents that are being listened to.
|
||||
* @type {Array<Document>}
|
||||
* @private
|
||||
*/
|
||||
this.documents_ = [];
|
||||
|
||||
/**
|
||||
* Whether to use the bubble phase to listen for events.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.useBubble_ = !!opt_useBubble;
|
||||
|
||||
/**
|
||||
* The event handler.
|
||||
* @type {goog.events.EventHandler<!goog.ui.ActivityMonitor>}
|
||||
* @private
|
||||
*/
|
||||
this.eventHandler_ = new goog.events.EventHandler(this);
|
||||
|
||||
/**
|
||||
* Whether the current window is an iframe.
|
||||
* TODO(user): Move to goog.dom.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isIframe_ = window.parent != window;
|
||||
|
||||
if (!opt_domHelper) {
|
||||
this.addDocument(goog.dom.getDomHelper().getDocument());
|
||||
} else if (goog.isArray(opt_domHelper)) {
|
||||
for (var i = 0; i < opt_domHelper.length; i++) {
|
||||
this.addDocument(opt_domHelper[i].getDocument());
|
||||
}
|
||||
} else {
|
||||
this.addDocument(opt_domHelper.getDocument());
|
||||
}
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) of the last user event.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.lastEventTime_ = goog.now();
|
||||
|
||||
};
|
||||
goog.inherits(goog.ui.ActivityMonitor, goog.events.EventTarget);
|
||||
goog.tagUnsealableClass(goog.ui.ActivityMonitor);
|
||||
|
||||
|
||||
/**
|
||||
* The last event type that was detected.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.lastEventType_ = '';
|
||||
|
||||
|
||||
/**
|
||||
* The mouse x-position after the last user event.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.lastMouseX_;
|
||||
|
||||
|
||||
/**
|
||||
* The mouse y-position after the last user event.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.lastMouseY_;
|
||||
|
||||
|
||||
/**
|
||||
* The earliest time that another throttled ACTIVITY event will be dispatched
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.minEventTime_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Minimum amount of time in ms between throttled ACTIVITY events
|
||||
* @type {number}
|
||||
*/
|
||||
goog.ui.ActivityMonitor.MIN_EVENT_SPACING = 3 * 1000;
|
||||
|
||||
|
||||
/**
|
||||
* If a user executes one of these events, s/he is considered not idle.
|
||||
* @type {Array<goog.events.EventType>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.userEventTypesBody_ = [
|
||||
goog.events.EventType.CLICK,
|
||||
goog.events.EventType.DBLCLICK,
|
||||
goog.events.EventType.MOUSEDOWN,
|
||||
goog.events.EventType.MOUSEMOVE,
|
||||
goog.events.EventType.MOUSEUP
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* If a user executes one of these events, s/he is considered not idle.
|
||||
* Note: monitoring touch events within iframe cause problems in iOS.
|
||||
* @type {Array<goog.events.EventType>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.userTouchEventTypesBody_ = [
|
||||
goog.events.EventType.TOUCHEND,
|
||||
goog.events.EventType.TOUCHMOVE,
|
||||
goog.events.EventType.TOUCHSTART
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* If a user executes one of these events, s/he is considered not idle.
|
||||
* @type {Array<goog.events.EventType>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.userEventTypesDocuments_ =
|
||||
[goog.events.EventType.KEYDOWN, goog.events.EventType.KEYUP];
|
||||
|
||||
|
||||
/**
|
||||
* Event constants for the activity monitor.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.ui.ActivityMonitor.Event = {
|
||||
/** Event fired when the user does something interactive */
|
||||
ACTIVITY: 'activity'
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ActivityMonitor.prototype.disposeInternal = function() {
|
||||
goog.ui.ActivityMonitor.superClass_.disposeInternal.call(this);
|
||||
this.eventHandler_.dispose();
|
||||
this.eventHandler_ = null;
|
||||
delete this.documents_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a document to those being monitored by this class.
|
||||
*
|
||||
* @param {Document} doc Document to monitor.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.addDocument = function(doc) {
|
||||
if (goog.array.contains(this.documents_, doc)) {
|
||||
return;
|
||||
}
|
||||
this.documents_.push(doc);
|
||||
var useCapture = !this.useBubble_;
|
||||
|
||||
var eventsToListenTo = goog.array.concat(
|
||||
goog.ui.ActivityMonitor.userEventTypesDocuments_,
|
||||
goog.ui.ActivityMonitor.userEventTypesBody_);
|
||||
|
||||
if (!this.isIframe_) {
|
||||
// Monitoring touch events in iframe causes problems interacting with text
|
||||
// fields in iOS (input text, textarea, contenteditable, select/copy/paste),
|
||||
// so just ignore these events. This shouldn't matter much given that a
|
||||
// touchstart event followed by touchend event produces a click event,
|
||||
// which is being monitored correctly.
|
||||
goog.array.extend(eventsToListenTo,
|
||||
goog.ui.ActivityMonitor.userTouchEventTypesBody_);
|
||||
}
|
||||
|
||||
this.eventHandler_.listen(doc, eventsToListenTo, this.handleEvent_,
|
||||
useCapture);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a document from those being monitored by this class.
|
||||
*
|
||||
* @param {Document} doc Document to monitor.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.removeDocument = function(doc) {
|
||||
if (this.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
goog.array.remove(this.documents_, doc);
|
||||
var useCapture = !this.useBubble_;
|
||||
|
||||
var eventsToUnlistenTo = goog.array.concat(
|
||||
goog.ui.ActivityMonitor.userEventTypesDocuments_,
|
||||
goog.ui.ActivityMonitor.userEventTypesBody_);
|
||||
|
||||
if (!this.isIframe_) {
|
||||
// See note above about monitoring touch events in iframe.
|
||||
goog.array.extend(eventsToUnlistenTo,
|
||||
goog.ui.ActivityMonitor.userTouchEventTypesBody_);
|
||||
}
|
||||
|
||||
this.eventHandler_.unlisten(doc, eventsToUnlistenTo, this.handleEvent_,
|
||||
useCapture);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the last event time when a user action occurs.
|
||||
* @param {goog.events.BrowserEvent} e Event object.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.handleEvent_ = function(e) {
|
||||
var update = false;
|
||||
switch (e.type) {
|
||||
case goog.events.EventType.MOUSEMOVE:
|
||||
// In FF 1.5, we get spurious mouseover and mouseout events when the UI
|
||||
// redraws. We only want to update the idle time if the mouse has moved.
|
||||
if (typeof this.lastMouseX_ == 'number' &&
|
||||
this.lastMouseX_ != e.clientX ||
|
||||
typeof this.lastMouseY_ == 'number' &&
|
||||
this.lastMouseY_ != e.clientY) {
|
||||
update = true;
|
||||
}
|
||||
this.lastMouseX_ = e.clientX;
|
||||
this.lastMouseY_ = e.clientY;
|
||||
break;
|
||||
default:
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
var type = goog.asserts.assertString(e.type);
|
||||
this.updateIdleTime(goog.now(), type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the last event time to be the present time, useful for non-DOM
|
||||
* events that should update idle time.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.resetTimer = function() {
|
||||
this.updateIdleTime(goog.now(), 'manual');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the idle time and fires an event if time has elapsed since
|
||||
* the last update.
|
||||
* @param {number} eventTime Time (in MS) of the event that cleared the idle
|
||||
* timer.
|
||||
* @param {string} eventType Type of the event, used only for debugging.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.updateIdleTime = function(
|
||||
eventTime, eventType) {
|
||||
// update internal state noting whether the user was idle
|
||||
this.lastEventTime_ = eventTime;
|
||||
this.lastEventType_ = eventType;
|
||||
|
||||
// dispatch event
|
||||
if (eventTime > this.minEventTime_) {
|
||||
this.dispatchEvent(goog.ui.ActivityMonitor.Event.ACTIVITY);
|
||||
this.minEventTime_ = eventTime + goog.ui.ActivityMonitor.MIN_EVENT_SPACING;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the amount of time the user has been idle.
|
||||
* @param {number=} opt_now The current time can optionally be passed in for the
|
||||
* computation to avoid an extra Date allocation.
|
||||
* @return {number} The amount of time in ms that the user has been idle.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.getIdleTime = function(opt_now) {
|
||||
var now = opt_now || goog.now();
|
||||
return now - this.lastEventTime_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the type of the last user event.
|
||||
* @return {string} event type.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.getLastEventType = function() {
|
||||
return this.lastEventType_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the time of the last event
|
||||
* @return {number} last event time.
|
||||
*/
|
||||
goog.ui.ActivityMonitor.prototype.getLastEventTime = function() {
|
||||
return this.lastEventTime_;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<!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.ActivityMonitor
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ActivityMonitorTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mydiv">
|
||||
foo
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,147 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.ui.ActivityMonitorTest');
|
||||
goog.setTestOnly('goog.ui.ActivityMonitorTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.PropertyReplacer');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.recordFunction');
|
||||
goog.require('goog.ui.ActivityMonitor');
|
||||
|
||||
var mockClock;
|
||||
var stubs = new goog.testing.PropertyReplacer();
|
||||
var mydiv;
|
||||
|
||||
function setUp() {
|
||||
mydiv = document.getElementById('mydiv');
|
||||
mockClock = new goog.testing.MockClock(true);
|
||||
// ActivityMonitor initializes a private to 0 which it compares to now(),
|
||||
// so tests fail unless we start the mock clock with something besides 0.
|
||||
mockClock.tick(1);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
mockClock.dispose();
|
||||
stubs.reset();
|
||||
}
|
||||
|
||||
function testIdle() {
|
||||
var activityMonitor = new goog.ui.ActivityMonitor();
|
||||
assertEquals('Upon creation, last event time should be creation time',
|
||||
mockClock.getCurrentTime(), activityMonitor.getLastEventTime());
|
||||
|
||||
mockClock.tick(1000);
|
||||
activityMonitor.resetTimer();
|
||||
var resetTime = mockClock.getCurrentTime();
|
||||
assertEquals('Upon reset, last event time should be reset time',
|
||||
resetTime, activityMonitor.getLastEventTime());
|
||||
assertEquals('Upon reset, idle time should be zero',
|
||||
0, activityMonitor.getIdleTime());
|
||||
|
||||
mockClock.tick(1000);
|
||||
assertEquals('1s after reset, last event time should be reset time',
|
||||
resetTime, activityMonitor.getLastEventTime());
|
||||
assertEquals('1s after reset, idle time should be 1s',
|
||||
1000, activityMonitor.getIdleTime());
|
||||
}
|
||||
|
||||
function testEventFired() {
|
||||
var activityMonitor = new goog.ui.ActivityMonitor();
|
||||
var listener = goog.testing.recordFunction();
|
||||
goog.events.listen(activityMonitor, goog.ui.ActivityMonitor.Event.ACTIVITY,
|
||||
listener);
|
||||
|
||||
mockClock.tick(1000);
|
||||
goog.testing.events.fireClickEvent(mydiv);
|
||||
assertEquals('Activity event should fire when click happens after creation',
|
||||
1, listener.getCallCount());
|
||||
|
||||
mockClock.tick(3000);
|
||||
goog.testing.events.fireClickEvent(mydiv);
|
||||
assertEquals('Activity event should not fire when click happens 3s or ' +
|
||||
'less since the last activity', 1, listener.getCallCount());
|
||||
|
||||
mockClock.tick(1);
|
||||
goog.testing.events.fireClickEvent(mydiv);
|
||||
assertEquals('Activity event should fire when click happens more than ' +
|
||||
'3s since the last activity', 2, listener.getCallCount());
|
||||
}
|
||||
|
||||
function testEventFiredWhenPropagationStopped() {
|
||||
var activityMonitor = new goog.ui.ActivityMonitor();
|
||||
var listener = goog.testing.recordFunction();
|
||||
goog.events.listen(activityMonitor, goog.ui.ActivityMonitor.Event.ACTIVITY,
|
||||
listener);
|
||||
|
||||
goog.events.listenOnce(mydiv, goog.events.EventType.CLICK,
|
||||
goog.events.Event.stopPropagation);
|
||||
goog.testing.events.fireClickEvent(mydiv);
|
||||
assertEquals('Activity event should fire despite click propagation ' +
|
||||
'stopped because listening on capture', 1, listener.getCallCount());
|
||||
}
|
||||
|
||||
function testEventNotFiredWhenPropagationStopped() {
|
||||
var activityMonitor = new goog.ui.ActivityMonitor(undefined, true);
|
||||
var listener = goog.testing.recordFunction();
|
||||
goog.events.listen(activityMonitor, goog.ui.ActivityMonitor.Event.ACTIVITY,
|
||||
listener);
|
||||
|
||||
goog.events.listenOnce(mydiv, goog.events.EventType.CLICK,
|
||||
goog.events.Event.stopPropagation);
|
||||
goog.testing.events.fireClickEvent(mydiv);
|
||||
assertEquals('Activity event should not fire since click propagation ' +
|
||||
'stopped and listening on bubble', 0, listener.getCallCount());
|
||||
}
|
||||
|
||||
function testTouchSequenceFired() {
|
||||
var activityMonitor = new goog.ui.ActivityMonitor();
|
||||
var listener = goog.testing.recordFunction();
|
||||
goog.events.listen(activityMonitor, goog.ui.ActivityMonitor.Event.ACTIVITY,
|
||||
listener);
|
||||
|
||||
mockClock.tick(1000);
|
||||
goog.testing.events.fireTouchSequence(mydiv);
|
||||
assertEquals('Activity event should fire when touch happens after creation',
|
||||
1, listener.getCallCount());
|
||||
|
||||
mockClock.tick(3000);
|
||||
goog.testing.events.fireTouchSequence(mydiv);
|
||||
assertEquals('Activity event should not fire when touch happens 3s or ' +
|
||||
'less since the last activity', 1, listener.getCallCount());
|
||||
|
||||
mockClock.tick(1);
|
||||
goog.testing.events.fireTouchSequence(mydiv);
|
||||
assertEquals('Activity event should fire when touch happens more than ' +
|
||||
'3s since the last activity', 2, listener.getCallCount());
|
||||
}
|
||||
|
||||
function testAddDocument_duplicate() {
|
||||
var defaultDoc = goog.dom.getDomHelper().getDocument();
|
||||
var activityMonitor = new goog.ui.ActivityMonitor();
|
||||
assertEquals(1, activityMonitor.documents_.length);
|
||||
assertEquals(defaultDoc, activityMonitor.documents_[0]);
|
||||
var listenerCount = activityMonitor.eventHandler_.getListenerCount();
|
||||
|
||||
activityMonitor.addDocument(defaultDoc);
|
||||
assertEquals(1, activityMonitor.documents_.length);
|
||||
assertEquals(defaultDoc, activityMonitor.documents_[0]);
|
||||
assertEquals(listenerCount, activityMonitor.eventHandler_.getListenerCount());
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// 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 Advanced tooltip widget implementation.
|
||||
*
|
||||
* @author eae@google.com (Emil A Eklund)
|
||||
* @see ../demos/advancedtooltip.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.AdvancedTooltip');
|
||||
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.math.Box');
|
||||
goog.require('goog.math.Coordinate');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Tooltip');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Advanced tooltip widget with cursor tracking abilities. Works like a regular
|
||||
* tooltip but can track the cursor position and direction to determine if the
|
||||
* tooltip should be dismissed or remain open.
|
||||
*
|
||||
* @param {Element|string=} opt_el Element to display tooltip for, either
|
||||
* element reference or string id.
|
||||
* @param {?string=} opt_str Text message to display in tooltip.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Tooltip}
|
||||
*/
|
||||
goog.ui.AdvancedTooltip = function(opt_el, opt_str, opt_domHelper) {
|
||||
goog.ui.Tooltip.call(this, opt_el, opt_str, opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.AdvancedTooltip, goog.ui.Tooltip);
|
||||
goog.tagUnsealableClass(goog.ui.AdvancedTooltip);
|
||||
|
||||
|
||||
/**
|
||||
* Whether to track the cursor and thereby close the tooltip if it moves away
|
||||
* from the tooltip and keep it open if it moves towards it.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.cursorTracking_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Delay in milliseconds before tooltips are hidden if cursor tracking is
|
||||
* enabled and the cursor is moving away from the tooltip.
|
||||
*
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.cursorTrackingHideDelayMs_ = 100;
|
||||
|
||||
|
||||
/**
|
||||
* Box object representing a margin around the tooltip where the cursor is
|
||||
* allowed without dismissing the tooltip.
|
||||
*
|
||||
* @type {goog.math.Box}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.hotSpotPadding_;
|
||||
|
||||
|
||||
/**
|
||||
* Bounding box.
|
||||
*
|
||||
* @type {goog.math.Box}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.boundingBox_;
|
||||
|
||||
|
||||
/**
|
||||
* Anchor bounding box.
|
||||
*
|
||||
* @type {goog.math.Box}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.anchorBox_;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the cursor tracking is active.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.tracking_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Sets margin around the tooltip where the cursor is allowed without dismissing
|
||||
* the tooltip.
|
||||
*
|
||||
* @param {goog.math.Box=} opt_box The margin around the tooltip.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.setHotSpotPadding = function(opt_box) {
|
||||
this.hotSpotPadding_ = opt_box || null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.math.Box} box The margin around the tooltip where the cursor is
|
||||
* allowed without dismissing the tooltip.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.getHotSpotPadding = function() {
|
||||
return this.hotSpotPadding_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether to track the cursor and thereby close the tooltip if it moves
|
||||
* away from the tooltip and keep it open if it moves towards it.
|
||||
*
|
||||
* @param {boolean} b Whether to track the cursor.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.setCursorTracking = function(b) {
|
||||
this.cursorTracking_ = b;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether to track the cursor and thereby close the tooltip
|
||||
* if it moves away from the tooltip and keep it open if it moves towards
|
||||
* it.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.getCursorTracking = function() {
|
||||
return this.cursorTracking_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets delay in milliseconds before tooltips are hidden if cursor tracking is
|
||||
* enabled and the cursor is moving away from the tooltip.
|
||||
*
|
||||
* @param {number} delay The delay in milliseconds.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.setCursorTrackingHideDelayMs =
|
||||
function(delay) {
|
||||
this.cursorTrackingHideDelayMs_ = delay;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The delay in milliseconds before tooltips are hidden if
|
||||
* cursor tracking is enabled and the cursor is moving away from the
|
||||
* tooltip.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.getCursorTrackingHideDelayMs = function() {
|
||||
return this.cursorTrackingHideDelayMs_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called after the popup is shown.
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.onShow_ = function() {
|
||||
goog.ui.AdvancedTooltip.superClass_.onShow_.call(this);
|
||||
|
||||
this.boundingBox_ = goog.style.getBounds(this.getElement()).toBox();
|
||||
if (this.anchor) {
|
||||
this.anchorBox_ = goog.style.getBounds(this.anchor).toBox();
|
||||
}
|
||||
|
||||
this.tracking_ = this.cursorTracking_;
|
||||
goog.events.listen(this.getDomHelper().getDocument(),
|
||||
goog.events.EventType.MOUSEMOVE,
|
||||
this.handleMouseMove, false, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called after the popup is hidden.
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.onHide_ = function() {
|
||||
goog.events.unlisten(this.getDomHelper().getDocument(),
|
||||
goog.events.EventType.MOUSEMOVE,
|
||||
this.handleMouseMove, false, this);
|
||||
|
||||
this.boundingBox_ = null;
|
||||
this.anchorBox_ = null;
|
||||
this.tracking_ = false;
|
||||
|
||||
goog.ui.AdvancedTooltip.superClass_.onHide_.call(this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the mouse is in the tooltip.
|
||||
* @return {boolean} True if the mouse is in the tooltip.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.isMouseInTooltip = function() {
|
||||
return this.isCoordinateInTooltip(this.cursorPosition);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the supplied coordinate is inside the tooltip, including
|
||||
* padding if any.
|
||||
* @param {goog.math.Coordinate} coord Coordinate being tested.
|
||||
* @return {boolean} Whether the coord is in the tooltip.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.isCoordinateInTooltip = function(coord) {
|
||||
// Check if coord is inside the bounding box of the tooltip
|
||||
if (this.hotSpotPadding_) {
|
||||
var offset = goog.style.getPageOffset(this.getElement());
|
||||
var size = goog.style.getSize(this.getElement());
|
||||
return offset.x - this.hotSpotPadding_.left <= coord.x &&
|
||||
coord.x <= offset.x + size.width + this.hotSpotPadding_.right &&
|
||||
offset.y - this.hotSpotPadding_.top <= coord.y &&
|
||||
coord.y <= offset.y + size.height + this.hotSpotPadding_.bottom;
|
||||
}
|
||||
|
||||
return goog.ui.AdvancedTooltip.superClass_.isCoordinateInTooltip.call(this,
|
||||
coord);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if supplied coordinate is in the tooltip, its triggering anchor, or
|
||||
* a tooltip that has been triggered by a child of this tooltip.
|
||||
* Called from handleMouseMove to determine if hide timer should be started,
|
||||
* and from maybeHide to determine if tooltip should be hidden.
|
||||
* @param {goog.math.Coordinate} coord Coordinate being tested.
|
||||
* @return {boolean} Whether coordinate is in the anchor, the tooltip, or any
|
||||
* tooltip whose anchor is a child of this tooltip.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.isCoordinateActive_ = function(coord) {
|
||||
if ((this.anchorBox_ && this.anchorBox_.contains(coord)) ||
|
||||
this.isCoordinateInTooltip(coord)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if mouse might be in active child element.
|
||||
var childTooltip = this.getChildTooltip();
|
||||
return !!childTooltip && childTooltip.isCoordinateInTooltip(coord);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called by timer from mouse out handler. Hides tooltip if cursor is still
|
||||
* outside element and tooltip.
|
||||
* @param {Element} el Anchor when hide timer was started.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.maybeHide = function(el) {
|
||||
this.hideTimer = undefined;
|
||||
if (el == this.anchor) {
|
||||
// Check if cursor is inside the bounding box of the tooltip or the element
|
||||
// that triggered it, or if tooltip is active (possibly due to receiving
|
||||
// the focus), or if there is a nested tooltip being shown.
|
||||
if (!this.isCoordinateActive_(this.cursorPosition) &&
|
||||
!this.getActiveElement() &&
|
||||
!this.hasActiveChild()) {
|
||||
// Under certain circumstances gecko fires ghost mouse events with the
|
||||
// coordinates 0, 0 regardless of the cursors position.
|
||||
if (goog.userAgent.GECKO && this.cursorPosition.x == 0 &&
|
||||
this.cursorPosition.y == 0) {
|
||||
return;
|
||||
}
|
||||
this.setVisible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for mouse move events.
|
||||
*
|
||||
* @param {goog.events.BrowserEvent} event Event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.handleMouseMove = function(event) {
|
||||
var startTimer = this.isVisible();
|
||||
if (this.boundingBox_) {
|
||||
var scroll = this.getDomHelper().getDocumentScroll();
|
||||
var c = new goog.math.Coordinate(event.clientX + scroll.x,
|
||||
event.clientY + scroll.y);
|
||||
if (this.isCoordinateActive_(c)) {
|
||||
startTimer = false;
|
||||
} else if (this.tracking_) {
|
||||
var prevDist = goog.math.Box.distance(this.boundingBox_,
|
||||
this.cursorPosition);
|
||||
var currDist = goog.math.Box.distance(this.boundingBox_, c);
|
||||
startTimer = currDist >= prevDist;
|
||||
}
|
||||
}
|
||||
|
||||
if (startTimer) {
|
||||
this.startHideTimer();
|
||||
|
||||
// Even though the mouse coordinate is not on the tooltip (or nested child),
|
||||
// they may have an active element because of a focus event. Don't let
|
||||
// that prevent us from taking down the tooltip(s) on this mouse move.
|
||||
this.setActiveElement(null);
|
||||
var childTooltip = this.getChildTooltip();
|
||||
if (childTooltip) {
|
||||
childTooltip.setActiveElement(null);
|
||||
}
|
||||
} else if (this.getState() == goog.ui.Tooltip.State.WAITING_TO_HIDE) {
|
||||
this.clearHideTimer();
|
||||
}
|
||||
|
||||
goog.ui.AdvancedTooltip.superClass_.handleMouseMove.call(this, event);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for mouse over events for the tooltip element.
|
||||
*
|
||||
* @param {goog.events.BrowserEvent} event Event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.handleTooltipMouseOver = function(event) {
|
||||
if (this.getActiveElement() != this.getElement()) {
|
||||
this.tracking_ = false;
|
||||
this.setActiveElement(this.getElement());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Override hide delay with cursor tracking hide delay while tracking.
|
||||
* @return {number} Hide delay to use.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.getHideDelayMs = function() {
|
||||
return this.tracking_ ? this.cursorTrackingHideDelayMs_ :
|
||||
goog.ui.AdvancedTooltip.base(this, 'getHideDelayMs');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Forces the recalculation of the hotspot on the next mouse over event.
|
||||
* @deprecated Not ever necessary to call this function. Hot spot is calculated
|
||||
* as neccessary.
|
||||
*/
|
||||
goog.ui.AdvancedTooltip.prototype.resetHotSpot = goog.nullFunction;
|
||||
@@ -0,0 +1,33 @@
|
||||
<!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.AdvancedTooltip
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.AdvancedTooltipTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span id="hovertarget">
|
||||
Hover Here For Popup
|
||||
<span id="childtarget">
|
||||
Child of target
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p id="notpopup">
|
||||
Content
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,287 @@
|
||||
// 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.AdvancedTooltipTest');
|
||||
goog.setTestOnly('goog.ui.AdvancedTooltipTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.math.Box');
|
||||
goog.require('goog.math.Coordinate');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.AdvancedTooltip');
|
||||
goog.require('goog.ui.Tooltip');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var att;
|
||||
var clock;
|
||||
var anchor;
|
||||
var elsewhere;
|
||||
var popup;
|
||||
|
||||
var SHOWDELAY = 50;
|
||||
var HIDEDELAY = 250;
|
||||
var TRACKINGDELAY = 100;
|
||||
|
||||
function isWindowTooSmall() {
|
||||
// Firefox 3 fails if the window is too small.
|
||||
return goog.userAgent.GECKO &&
|
||||
(window.innerWidth < 350 || window.innerHeight < 100);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
popup = goog.dom.createDom('span',
|
||||
{id: 'popup', style: 'position:absolute;top:300;left:300'}, 'Hello');
|
||||
att = new goog.ui.AdvancedTooltip('hovertarget');
|
||||
att.setElement(popup);
|
||||
att.setCursorTracking(true);
|
||||
att.setHotSpotPadding(new goog.math.Box(10, 10, 10, 10));
|
||||
att.setShowDelayMs(SHOWDELAY);
|
||||
att.setHideDelayMs(HIDEDELAY);
|
||||
att.setCursorTrackingHideDelayMs(TRACKINGDELAY);
|
||||
att.setMargin(new goog.math.Box(300, 0, 0, 300));
|
||||
|
||||
clock = new goog.testing.MockClock(true);
|
||||
|
||||
anchor = goog.dom.getElement('hovertarget');
|
||||
elsewhere = goog.dom.getElement('notpopup');
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
// tooltip needs to be hidden as well as disposed of so that it doesn't
|
||||
// leave global state hanging around to trip up other tests.
|
||||
if (att.isVisible()) {
|
||||
att.onHide_();
|
||||
}
|
||||
att.dispose();
|
||||
clock.uninstall();
|
||||
}
|
||||
|
||||
function assertVisible(msg, element) {
|
||||
if (element) {
|
||||
assertEquals(msg, 'visible', element.style.visibility);
|
||||
} else {
|
||||
assertEquals('visible', msg.style.visibility);
|
||||
}
|
||||
}
|
||||
|
||||
function assertHidden(msg, element) {
|
||||
if (element) {
|
||||
assertEquals(msg, 'hidden', element.style.visibility);
|
||||
} else {
|
||||
assertEquals('hidden', msg.style.visibility);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to fire events related to moving a mouse from one element
|
||||
* to another. Fires mouseout, mouseover, and mousemove event.
|
||||
* @param {Element} from Element the mouse is moving from.
|
||||
* @param {Element} to Element the mouse is moving to.
|
||||
*/
|
||||
function fireMouseEvents(from, to) {
|
||||
goog.testing.events.fireMouseOutEvent(from, to);
|
||||
goog.testing.events.fireMouseOverEvent(to, from);
|
||||
var bounds = goog.style.getBounds(to);
|
||||
goog.testing.events.fireMouseMoveEvent(
|
||||
document, new goog.math.Coordinate(bounds.left + 1, bounds.top + 1));
|
||||
}
|
||||
|
||||
function testCursorTracking() {
|
||||
if (isWindowTooSmall()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oneThirdOfTheWay, twoThirdsOfTheWay;
|
||||
|
||||
oneThirdOfTheWay = new goog.math.Coordinate(100, 100);
|
||||
twoThirdsOfTheWay = new goog.math.Coordinate(200, 200);
|
||||
|
||||
goog.testing.events.fireMouseOverEvent(anchor, elsewhere);
|
||||
clock.tick(SHOWDELAY);
|
||||
assertVisible('Mouse over anchor should show popup', popup);
|
||||
|
||||
goog.testing.events.fireMouseOutEvent(anchor, elsewhere);
|
||||
goog.testing.events.fireMouseMoveEvent(document, oneThirdOfTheWay);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertVisible("Moving mouse towards popup shouldn't hide it", popup);
|
||||
|
||||
goog.testing.events.fireMouseMoveEvent(document, twoThirdsOfTheWay);
|
||||
goog.testing.events.fireMouseMoveEvent(document, oneThirdOfTheWay);
|
||||
clock.tick(TRACKINGDELAY);
|
||||
assertHidden('Moving mouse away from popup should hide it', popup);
|
||||
|
||||
goog.testing.events.fireMouseMoveEvent(document, twoThirdsOfTheWay);
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, anchor));
|
||||
clock.tick(SHOWDELAY);
|
||||
assertVisible('Set focus shows popup', popup);
|
||||
goog.testing.events.fireMouseMoveEvent(document, oneThirdOfTheWay);
|
||||
clock.tick(TRACKINGDELAY);
|
||||
assertHidden('Mouse move after focus should hide popup', popup);
|
||||
}
|
||||
|
||||
function testPadding() {
|
||||
if (isWindowTooSmall()) {
|
||||
return;
|
||||
}
|
||||
|
||||
goog.testing.events.fireMouseOverEvent(anchor, elsewhere);
|
||||
clock.tick(SHOWDELAY);
|
||||
|
||||
var attBounds = goog.style.getBounds(popup);
|
||||
var inPadding = new goog.math.Coordinate(attBounds.left - 5,
|
||||
attBounds.top - 5);
|
||||
var outOfPadding = new goog.math.Coordinate(attBounds.left - 15,
|
||||
attBounds.top - 15);
|
||||
|
||||
fireMouseEvents(anchor, popup);
|
||||
goog.testing.events.fireMouseOutEvent(popup, elsewhere);
|
||||
goog.testing.events.fireMouseMoveEvent(document, inPadding);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertVisible("Mouse out of popup but within padding shouldn't hide it",
|
||||
popup);
|
||||
|
||||
goog.testing.events.fireMouseMoveEvent(document, outOfPadding);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Mouse move beyond popup padding should hide it', popup);
|
||||
}
|
||||
|
||||
|
||||
function testAnchorWithChild() {
|
||||
var child = goog.dom.getElement('childtarget');
|
||||
|
||||
fireMouseEvents(elsewhere, anchor);
|
||||
fireMouseEvents(anchor, child);
|
||||
clock.tick(SHOWDELAY);
|
||||
assertVisible('Mouse into child of anchor should still show popup', popup);
|
||||
|
||||
fireMouseEvents(child, anchor);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertVisible('Mouse from child to anchor should still show popup', popup);
|
||||
}
|
||||
|
||||
function testNestedTooltip() {
|
||||
if (!isWindowTooSmall()) {
|
||||
checkNestedTooltips(false);
|
||||
}
|
||||
}
|
||||
|
||||
function testNestedAdvancedTooltip() {
|
||||
if (!isWindowTooSmall()) {
|
||||
checkNestedTooltips(true);
|
||||
}
|
||||
}
|
||||
|
||||
function testResizingTooltipWhileShown() {
|
||||
fireMouseEvents(elsewhere, anchor);
|
||||
clock.tick(SHOWDELAY);
|
||||
popup.style.height = '100px';
|
||||
var attBounds = goog.style.getBounds(popup);
|
||||
var inPadding = new goog.math.Coordinate(
|
||||
attBounds.left + 5, attBounds.top + attBounds.height + 5);
|
||||
var outOfPadding = new goog.math.Coordinate(
|
||||
attBounds.left + 5, attBounds.top + attBounds.height + 15);
|
||||
|
||||
fireMouseEvents(anchor, popup);
|
||||
goog.testing.events.fireMouseOutEvent(popup, elsewhere);
|
||||
goog.testing.events.fireMouseMoveEvent(document, inPadding);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertVisible("Mouse out of popup but within padding shouldn't hide it",
|
||||
popup);
|
||||
|
||||
goog.testing.events.fireMouseMoveEvent(document, outOfPadding);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Mouse move beyond popup padding should hide it', popup);
|
||||
}
|
||||
|
||||
function checkNestedTooltips(useAdvancedTooltip) {
|
||||
popup.appendChild(goog.dom.createDom(
|
||||
'span', {id: 'nestedAnchor'}, 'Nested Anchor'));
|
||||
var nestedAnchor = goog.dom.getElement('nestedAnchor');
|
||||
var nestedTooltip;
|
||||
if (useAdvancedTooltip) {
|
||||
nestedTooltip = new goog.ui.AdvancedTooltip(nestedAnchor, 'popup');
|
||||
} else {
|
||||
nestedTooltip = new goog.ui.Tooltip(nestedAnchor, 'popup');
|
||||
}
|
||||
var nestedPopup = nestedTooltip.getElement();
|
||||
nestedTooltip.setShowDelayMs(SHOWDELAY);
|
||||
nestedTooltip.setHideDelayMs(HIDEDELAY);
|
||||
|
||||
fireMouseEvents(elsewhere, anchor);
|
||||
clock.tick(SHOWDELAY);
|
||||
fireMouseEvents(anchor, popup);
|
||||
fireMouseEvents(popup, nestedAnchor);
|
||||
clock.tick(SHOWDELAY + HIDEDELAY);
|
||||
assertVisible('Mouse into nested anchor should show popup', nestedPopup);
|
||||
assertVisible('Mouse into nested anchor should not hide parent', popup);
|
||||
fireMouseEvents(nestedAnchor, elsewhere);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Mouse out of nested popup should hide it', nestedPopup);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Mouse out of nested popup should eventually hide parent',
|
||||
popup);
|
||||
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, anchor));
|
||||
clock.tick(SHOWDELAY);
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.BLUR, anchor));
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, nestedAnchor));
|
||||
clock.tick(SHOWDELAY + HIDEDELAY);
|
||||
assertVisible("Moving focus to child anchor doesn't hide parent", popup);
|
||||
assertVisible('Set focus shows nested popup', nestedPopup);
|
||||
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.BLUR, nestedAnchor));
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, anchor));
|
||||
clock.tick(HIDEDELAY + HIDEDELAY);
|
||||
assertHidden('Lose focus hides nested popup', nestedPopup);
|
||||
assertVisible(
|
||||
"Moving focus from nested anchor to parent doesn't hide parent", popup);
|
||||
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.BLUR, anchor));
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, nestedAnchor));
|
||||
clock.tick(SHOWDELAY);
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.BLUR, nestedAnchor));
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Lose focus hides nested popup', nestedPopup);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Nested anchor losing focus hides parent', popup);
|
||||
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, anchor));
|
||||
clock.tick(SHOWDELAY);
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.BLUR, anchor));
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, nestedAnchor));
|
||||
clock.tick(SHOWDELAY);
|
||||
var coordElsewhere = new goog.math.Coordinate(1, 1);
|
||||
goog.testing.events.fireMouseMoveEvent(document, coordElsewhere);
|
||||
clock.tick(HIDEDELAY);
|
||||
assertHidden('Mouse move should hide parent with active child', popup);
|
||||
assertHidden('Mouse move should hide nested popup', nestedPopup);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// 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 Animated zippy widget implementation.
|
||||
*
|
||||
* @author eae@google.com (Emil A Eklund)
|
||||
* @see ../demos/zippy.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.AnimatedZippy');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.fx.Animation');
|
||||
goog.require('goog.fx.Transition');
|
||||
goog.require('goog.fx.easing');
|
||||
goog.require('goog.ui.Zippy');
|
||||
goog.require('goog.ui.ZippyEvent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Zippy widget. Expandable/collapsible container, clicking the header toggles
|
||||
* the visibility of the content.
|
||||
*
|
||||
* @param {Element|string|null} header Header element, either element
|
||||
* reference, string id or null if no header exists.
|
||||
* @param {Element|string} content Content element, either element reference or
|
||||
* string id.
|
||||
* @param {boolean=} opt_expanded Initial expanded/visibility state. Defaults to
|
||||
* false.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper An optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Zippy}
|
||||
*/
|
||||
goog.ui.AnimatedZippy = function(header, content, opt_expanded, opt_domHelper) {
|
||||
var domHelper = opt_domHelper || goog.dom.getDomHelper();
|
||||
|
||||
// Create wrapper element and move content into it.
|
||||
var elWrapper = domHelper.createDom('div', {'style': 'overflow:hidden'});
|
||||
var elContent = domHelper.getElement(content);
|
||||
elContent.parentNode.replaceChild(elWrapper, elContent);
|
||||
elWrapper.appendChild(elContent);
|
||||
|
||||
/**
|
||||
* Content wrapper, used for animation.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this.elWrapper_ = elWrapper;
|
||||
|
||||
/**
|
||||
* Reference to animation or null if animation is not active.
|
||||
* @type {goog.fx.Animation}
|
||||
* @private
|
||||
*/
|
||||
this.anim_ = null;
|
||||
|
||||
// Call constructor of super class.
|
||||
goog.ui.Zippy.call(this, header, elContent, opt_expanded,
|
||||
undefined, domHelper);
|
||||
|
||||
// Set initial state.
|
||||
// NOTE: Set the class names as well otherwise animated zippys
|
||||
// start with empty class names.
|
||||
var expanded = this.isExpanded();
|
||||
this.elWrapper_.style.display = expanded ? '' : 'none';
|
||||
this.updateHeaderClassName(expanded);
|
||||
};
|
||||
goog.inherits(goog.ui.AnimatedZippy, goog.ui.Zippy);
|
||||
goog.tagUnsealableClass(goog.ui.AnimatedZippy);
|
||||
|
||||
|
||||
/**
|
||||
* Duration of expand/collapse animation, in milliseconds.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.animationDuration = 500;
|
||||
|
||||
|
||||
/**
|
||||
* Acceleration function for expand/collapse animation.
|
||||
* @type {!Function}
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.animationAcceleration = goog.fx.easing.easeOut;
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the zippy is in the process of being expanded or
|
||||
* collapsed.
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.isBusy = function() {
|
||||
return this.anim_ != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets expanded state.
|
||||
*
|
||||
* @param {boolean} expanded Expanded/visibility state.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.setExpanded = function(expanded) {
|
||||
if (this.isExpanded() == expanded && !this.anim_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset display property of wrapper to allow content element to be
|
||||
// measured.
|
||||
if (this.elWrapper_.style.display == 'none') {
|
||||
this.elWrapper_.style.display = '';
|
||||
}
|
||||
|
||||
// Measure content element.
|
||||
var h = this.getContentElement().offsetHeight;
|
||||
|
||||
// Stop active animation (if any) and determine starting height.
|
||||
var startH = 0;
|
||||
if (this.anim_) {
|
||||
expanded = this.isExpanded();
|
||||
goog.events.removeAll(this.anim_);
|
||||
this.anim_.stop(false);
|
||||
|
||||
var marginTop = parseInt(this.getContentElement().style.marginTop, 10);
|
||||
startH = h - Math.abs(marginTop);
|
||||
} else {
|
||||
startH = expanded ? 0 : h;
|
||||
}
|
||||
|
||||
// Updates header class name after the animation has been stopped.
|
||||
this.updateHeaderClassName(expanded);
|
||||
|
||||
// Set up expand/collapse animation.
|
||||
this.anim_ = new goog.fx.Animation([0, startH],
|
||||
[0, expanded ? h : 0],
|
||||
this.animationDuration,
|
||||
this.animationAcceleration);
|
||||
|
||||
var events = [goog.fx.Transition.EventType.BEGIN,
|
||||
goog.fx.Animation.EventType.ANIMATE,
|
||||
goog.fx.Transition.EventType.END];
|
||||
goog.events.listen(this.anim_, events, this.onAnimate_, false, this);
|
||||
goog.events.listen(this.anim_,
|
||||
goog.fx.Transition.EventType.END,
|
||||
goog.bind(this.onAnimationCompleted_, this, expanded));
|
||||
|
||||
// Start animation.
|
||||
this.anim_.play(false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called during animation
|
||||
*
|
||||
* @param {goog.events.Event} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.onAnimate_ = function(e) {
|
||||
var contentElement = this.getContentElement();
|
||||
var h = contentElement.offsetHeight;
|
||||
contentElement.style.marginTop = (e.y - h) + 'px';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called once the expand/collapse animation has completed.
|
||||
*
|
||||
* @param {boolean} expanded Expanded/visibility state.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AnimatedZippy.prototype.onAnimationCompleted_ = function(expanded) {
|
||||
// Fix wrong end position if the content has changed during the animation.
|
||||
if (expanded) {
|
||||
this.getContentElement().style.marginTop = '0';
|
||||
}
|
||||
|
||||
goog.events.removeAll(/** @type {!goog.fx.Animation} */ (this.anim_));
|
||||
this.setExpandedInternal(expanded);
|
||||
this.anim_ = null;
|
||||
|
||||
if (!expanded) {
|
||||
this.elWrapper_.style.display = 'none';
|
||||
}
|
||||
|
||||
// Fire toggle event.
|
||||
this.dispatchEvent(new goog.ui.ZippyEvent(goog.ui.Zippy.Events.TOGGLE,
|
||||
this, expanded));
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2011 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.AnimatedZippy
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.AnimatedZippyTest');
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.demo {
|
||||
border: solid 1px red;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.demo h2 {
|
||||
background-color: yellow;
|
||||
border: solid 1px #ccc;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
fint-size: 100%;
|
||||
}
|
||||
|
||||
.demo div {
|
||||
border: solid 1px #ccc;
|
||||
padding: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="demo" id="d1">
|
||||
<h2 id="t1">
|
||||
handler
|
||||
</h2>
|
||||
<div id="c1">
|
||||
sem. Suspendisse porta felis ac ipsum. Sed tincidunt dui vitae nulla. Ut
|
||||
blandit. Nunc non neque. Mauris placerat. Vestibulum mollis tellus id dolor.
|
||||
Phasellus ac dolor molestie nunc euismod aliquam. Mauris tellus ipsum,
|
||||
fringilla id, tincidunt eu, vestibulum sit amet, metus. Quisque congue
|
||||
varius
|
||||
ligula. Quisque ornare mollis enim. Aliquam erat volutpat. Nulla mattis
|
||||
venenatis magna.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2011 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.AnimatedZippyTest');
|
||||
goog.setTestOnly('goog.ui.AnimatedZippyTest');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.fx.Animation');
|
||||
goog.require('goog.fx.Transition');
|
||||
goog.require('goog.testing.PropertyReplacer');
|
||||
goog.require('goog.testing.asserts');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.AnimatedZippy');
|
||||
goog.require('goog.ui.Zippy');
|
||||
|
||||
var animatedZippy;
|
||||
var animatedZippyHeaderEl;
|
||||
var propertyReplacer;
|
||||
|
||||
function setUp() {
|
||||
animatedZippyHeaderEl = goog.dom.getElement('t1');
|
||||
goog.asserts.assert(animatedZippyHeaderEl);
|
||||
animatedZippy = new goog.ui.AnimatedZippy(animatedZippyHeaderEl,
|
||||
goog.dom.getElement('c1'));
|
||||
|
||||
propertyReplacer = new goog.testing.PropertyReplacer();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
propertyReplacer.reset();
|
||||
animatedZippy.dispose();
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertNotNull('must not be null', animatedZippy);
|
||||
}
|
||||
|
||||
function testExpandCollapse() {
|
||||
var animationsPlayed = 0;
|
||||
var toggleEventsFired = 0;
|
||||
|
||||
propertyReplacer.replace(goog.fx.Animation.prototype, 'play', function() {
|
||||
animationsPlayed++;
|
||||
this.dispatchAnimationEvent(goog.fx.Transition.EventType.END);
|
||||
});
|
||||
propertyReplacer.replace(goog.ui.AnimatedZippy.prototype, 'onAnimate_',
|
||||
goog.functions.NULL);
|
||||
|
||||
goog.events.listenOnce(animatedZippy, goog.ui.Zippy.Events.TOGGLE,
|
||||
function(e) {
|
||||
toggleEventsFired++;
|
||||
assertTrue('TOGGLE event must be for expansion', e.expanded);
|
||||
assertEquals('expanded must be true', true,
|
||||
animatedZippy.isExpanded());
|
||||
assertEquals('aria-expanded must be true', 'true',
|
||||
goog.a11y.aria.getState(animatedZippyHeaderEl,
|
||||
goog.a11y.aria.State.EXPANDED));
|
||||
});
|
||||
|
||||
animatedZippy.expand();
|
||||
|
||||
goog.events.listenOnce(animatedZippy, goog.ui.Zippy.Events.TOGGLE,
|
||||
function(e) {
|
||||
toggleEventsFired++;
|
||||
assertFalse('TOGGLE event must be for collapse', e.expanded);
|
||||
assertEquals('expanded must be false', false,
|
||||
animatedZippy.isExpanded());
|
||||
assertEquals('aria-expanded must be false', 'false',
|
||||
goog.a11y.aria.getState(animatedZippyHeaderEl,
|
||||
goog.a11y.aria.State.EXPANDED));
|
||||
});
|
||||
|
||||
animatedZippy.collapse();
|
||||
|
||||
assertEquals('animations must play', 2, animationsPlayed);
|
||||
assertEquals('TOGGLE events must fire', 2, toggleEventsFired);
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
// 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 Definition of the AttachableMenu class.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.AttachableMenu');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.ItemEvent');
|
||||
goog.require('goog.ui.MenuBase');
|
||||
goog.require('goog.ui.PopupBase');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of a menu that can attach itself to DOM element that
|
||||
* are annotated appropriately.
|
||||
*
|
||||
* The following attributes are used by the AttachableMenu
|
||||
*
|
||||
* menu-item - Should be set on DOM elements that function as items in the
|
||||
* menu that can be selected.
|
||||
* classNameSelected - A class that will be added to the element's class names
|
||||
* when the item is selected via keyboard or mouse.
|
||||
*
|
||||
* @param {Element=} opt_element A DOM element for the popup.
|
||||
* @constructor
|
||||
* @extends {goog.ui.MenuBase}
|
||||
* @deprecated Use goog.ui.PopupMenu.
|
||||
* @final
|
||||
*/
|
||||
goog.ui.AttachableMenu = function(opt_element) {
|
||||
goog.ui.MenuBase.call(this, opt_element);
|
||||
};
|
||||
goog.inherits(goog.ui.AttachableMenu, goog.ui.MenuBase);
|
||||
goog.tagUnsealableClass(goog.ui.AttachableMenu);
|
||||
|
||||
|
||||
/**
|
||||
* The currently selected element (mouse was moved over it or keyboard arrows)
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.selectedElement_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Class name to append to a menu item's class when it's selected
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.itemClassName_ = 'menu-item';
|
||||
|
||||
|
||||
/**
|
||||
* Class name to append to a menu item's class when it's selected
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.selectedItemClassName_ = 'menu-item-selected';
|
||||
|
||||
|
||||
/**
|
||||
* Keep track of when the last key was pressed so that a keydown-scroll doesn't
|
||||
* trigger a mouseover event
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.lastKeyDown_ = goog.now();
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.AttachableMenu.prototype.disposeInternal = function() {
|
||||
goog.ui.AttachableMenu.superClass_.disposeInternal.call(this);
|
||||
this.selectedElement_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the class name to use for menu items
|
||||
*
|
||||
* @return {string} The class name to use for items.
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.getItemClassName = function() {
|
||||
return this.itemClassName_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the class name to use for menu items
|
||||
*
|
||||
* @param {string} name The class name to use for items.
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.setItemClassName = function(name) {
|
||||
this.itemClassName_ = name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the class name to use for selected menu items
|
||||
* todo(user) - reevaluate if we can simulate pseudo classes in IE
|
||||
*
|
||||
* @return {string} The class name to use for selected items.
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.getSelectedItemClassName = function() {
|
||||
return this.selectedItemClassName_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the class name to use for selected menu items
|
||||
* todo(user) - reevaluate if we can simulate pseudo classes in IE
|
||||
*
|
||||
* @param {string} name The class name to use for selected items.
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.setSelectedItemClassName = function(name) {
|
||||
this.selectedItemClassName_ = name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the selected item
|
||||
*
|
||||
* @return {Element} The item selected or null if no item is selected.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.getSelectedItem = function() {
|
||||
return this.selectedElement_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.AttachableMenu.prototype.setSelectedItem = function(obj) {
|
||||
var elt = /** @type {Element} */ (obj);
|
||||
if (this.selectedElement_) {
|
||||
goog.dom.classlist.remove(this.selectedElement_,
|
||||
this.selectedItemClassName_);
|
||||
}
|
||||
|
||||
this.selectedElement_ = elt;
|
||||
|
||||
var el = this.getElement();
|
||||
goog.asserts.assert(el, 'The attachable menu DOM element cannot be null.');
|
||||
if (this.selectedElement_) {
|
||||
goog.dom.classlist.add(this.selectedElement_, this.selectedItemClassName_);
|
||||
|
||||
if (elt.id) {
|
||||
// Update activedescendant to reflect the new selection. ARIA roles for
|
||||
// menu and menuitem can be set statically (thru Soy templates, for
|
||||
// example) whereas this needs to be updated as the selection changes.
|
||||
goog.a11y.aria.setState(el, goog.a11y.aria.State.ACTIVEDESCENDANT,
|
||||
elt.id);
|
||||
}
|
||||
|
||||
var top = this.selectedElement_.offsetTop;
|
||||
var height = this.selectedElement_.offsetHeight;
|
||||
var scrollTop = el.scrollTop;
|
||||
var scrollHeight = el.offsetHeight;
|
||||
|
||||
// If the menu is scrollable this scrolls the selected item into view
|
||||
// (this has no effect when the menu doesn't scroll)
|
||||
if (top < scrollTop) {
|
||||
el.scrollTop = top;
|
||||
} else if (top + height > scrollTop + scrollHeight) {
|
||||
el.scrollTop = top + height - scrollHeight;
|
||||
}
|
||||
} else {
|
||||
// Clear off activedescendant to reflect no selection.
|
||||
goog.a11y.aria.setState(el, goog.a11y.aria.State.ACTIVEDESCENDANT, '');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.AttachableMenu.prototype.showPopupElement = function() {
|
||||
// The scroll position cannot be set for hidden (display: none) elements in
|
||||
// gecko browsers.
|
||||
var el = /** @type {Element} */ (this.getElement());
|
||||
goog.style.setElementShown(el, true);
|
||||
el.scrollTop = 0;
|
||||
el.style.visibility = 'visible';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called after the menu is shown.
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onShow_ = function() {
|
||||
goog.ui.AttachableMenu.superClass_.onShow_.call(this);
|
||||
|
||||
// In IE, focusing the menu causes weird scrolling to happen. Focusing the
|
||||
// first child makes the scroll behavior better, and the key handling still
|
||||
// works. In FF, focusing the first child causes us to lose key events, so we
|
||||
// still focus the menu.
|
||||
var el = this.getElement();
|
||||
goog.userAgent.IE ? el.firstChild.focus() :
|
||||
el.focus();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next or previous item. Used for up/down arrows.
|
||||
*
|
||||
* @param {boolean} prev True to go to the previous element instead of next.
|
||||
* @return {Element} The next or previous element.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.getNextPrevItem = function(prev) {
|
||||
// first find the index of the next element
|
||||
var elements = this.getElement().getElementsByTagName('*');
|
||||
var elementCount = elements.length;
|
||||
var index;
|
||||
// if there is a selected element, find its index and then inc/dec by one
|
||||
if (this.selectedElement_) {
|
||||
for (var i = 0; i < elementCount; i++) {
|
||||
if (elements[i] == this.selectedElement_) {
|
||||
index = prev ? i - 1 : i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no selected element, start from beginning or end
|
||||
if (!goog.isDef(index)) {
|
||||
index = prev ? elementCount - 1 : 0;
|
||||
}
|
||||
|
||||
// iterate forward or backwards through the elements finding the next
|
||||
// menu item
|
||||
for (var i = 0; i < elementCount; i++) {
|
||||
var multiplier = prev ? -1 : 1;
|
||||
var nextIndex = index + (multiplier * i) % elementCount;
|
||||
|
||||
// if overflowed/underflowed, wrap around
|
||||
if (nextIndex < 0) {
|
||||
nextIndex += elementCount;
|
||||
} else if (nextIndex >= elementCount) {
|
||||
nextIndex -= elementCount;
|
||||
}
|
||||
|
||||
if (this.isMenuItem_(elements[nextIndex])) {
|
||||
return elements[nextIndex];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Mouse over handler for the menu.
|
||||
* @param {goog.events.Event} e The event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onMouseOver = function(e) {
|
||||
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
|
||||
if (eltItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the keydown triggering a mouseover in FF.
|
||||
if (goog.now() - this.lastKeyDown_ > goog.ui.PopupBase.DEBOUNCE_DELAY_MS) {
|
||||
this.setSelectedItem(eltItem);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Mouse out handler for the menu.
|
||||
* @param {goog.events.Event} e The event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onMouseOut = function(e) {
|
||||
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
|
||||
if (eltItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the keydown triggering a mouseout in FF.
|
||||
if (goog.now() - this.lastKeyDown_ > goog.ui.PopupBase.DEBOUNCE_DELAY_MS) {
|
||||
this.setSelectedItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Mouse down handler for the menu. Prevents default to avoid text selection.
|
||||
* @param {!goog.events.Event} e The event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onMouseDown = goog.events.Event.preventDefault;
|
||||
|
||||
|
||||
/**
|
||||
* Mouse up handler for the menu.
|
||||
* @param {goog.events.Event} e The event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onMouseUp = function(e) {
|
||||
var eltItem = this.getAncestorMenuItem_(/** @type {Element} */ (e.target));
|
||||
if (eltItem == null) {
|
||||
return;
|
||||
}
|
||||
this.setVisible(false);
|
||||
this.onItemSelected_(eltItem);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Key down handler for the menu.
|
||||
* @param {goog.events.KeyEvent} e The event object.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onKeyDown = function(e) {
|
||||
switch (e.keyCode) {
|
||||
case goog.events.KeyCodes.DOWN:
|
||||
this.setSelectedItem(this.getNextPrevItem(false));
|
||||
this.lastKeyDown_ = goog.now();
|
||||
break;
|
||||
case goog.events.KeyCodes.UP:
|
||||
this.setSelectedItem(this.getNextPrevItem(true));
|
||||
this.lastKeyDown_ = goog.now();
|
||||
break;
|
||||
case goog.events.KeyCodes.ENTER:
|
||||
if (this.selectedElement_) {
|
||||
this.onItemSelected_();
|
||||
this.setVisible(false);
|
||||
}
|
||||
break;
|
||||
case goog.events.KeyCodes.ESC:
|
||||
this.setVisible(false);
|
||||
break;
|
||||
default:
|
||||
if (e.charCode) {
|
||||
var charStr = String.fromCharCode(e.charCode);
|
||||
this.selectByName_(charStr, 1, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Prevent the browser's default keydown behaviour when the menu is open,
|
||||
// e.g. keyboard scrolling.
|
||||
e.preventDefault();
|
||||
|
||||
// Stop propagation to prevent application level keyboard shortcuts from
|
||||
// firing.
|
||||
e.stopPropagation();
|
||||
|
||||
this.dispatchEvent(e);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Find an item that has the given prefix and select it.
|
||||
*
|
||||
* @param {string} prefix The entered prefix, so far.
|
||||
* @param {number=} opt_direction 1 to search forward from the selection
|
||||
* (default), -1 to search backward (e.g. to go to the previous match).
|
||||
* @param {boolean=} opt_skip True if should skip the current selection,
|
||||
* unless no other item has the given prefix.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.selectByName_ =
|
||||
function(prefix, opt_direction, opt_skip) {
|
||||
var elements = this.getElement().getElementsByTagName('*');
|
||||
var elementCount = elements.length;
|
||||
var index;
|
||||
|
||||
if (elementCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedElement_ ||
|
||||
(index = goog.array.indexOf(elements, this.selectedElement_)) == -1) {
|
||||
// no selection or selection isn't known => start at the beginning
|
||||
index = 0;
|
||||
}
|
||||
|
||||
var start = index;
|
||||
var re = new RegExp('^' + goog.string.regExpEscape(prefix), 'i');
|
||||
var skip = opt_skip && this.selectedElement_;
|
||||
var dir = opt_direction || 1;
|
||||
|
||||
do {
|
||||
if (elements[index] != skip && this.isMenuItem_(elements[index])) {
|
||||
var name = goog.dom.getTextContent(elements[index]);
|
||||
if (name.match(re)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index += dir;
|
||||
if (index == elementCount) {
|
||||
index = 0;
|
||||
} else if (index < 0) {
|
||||
index = elementCount - 1;
|
||||
}
|
||||
} while (index != start);
|
||||
|
||||
if (this.selectedElement_ != elements[index]) {
|
||||
this.setSelectedItem(elements[index]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dispatch an ITEM_ACTION event when an item is selected
|
||||
* @param {Object=} opt_item Item selected.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.onItemSelected_ = function(opt_item) {
|
||||
this.dispatchEvent(new goog.ui.ItemEvent(goog.ui.MenuBase.Events.ITEM_ACTION,
|
||||
this, opt_item || this.selectedElement_));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the specified element is a menu item.
|
||||
* @param {Element} elt The element to find a menu item ancestor of.
|
||||
* @return {boolean} Whether the specified element is a menu item.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.isMenuItem_ = function(elt) {
|
||||
return !!elt && goog.dom.classlist.contains(elt, this.itemClassName_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the menu-item scoping the specified element, or null if there is
|
||||
* none.
|
||||
* @param {Element|undefined} elt The element to find a menu item ancestor of.
|
||||
* @return {Element} The menu-item scoping the specified element, or null if
|
||||
* there is none.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.AttachableMenu.prototype.getAncestorMenuItem_ = function(elt) {
|
||||
if (elt) {
|
||||
var ownerDocumentBody = goog.dom.getOwnerDocument(elt).body;
|
||||
while (elt != null && elt != ownerDocumentBody) {
|
||||
if (this.isMenuItem_(elt)) {
|
||||
return elt;
|
||||
}
|
||||
elt = /** @type {Element} */ (elt.parentNode);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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 Component for an input field with bidi direction automatic
|
||||
* detection. The input element directionality is automatically set according
|
||||
* to the contents (value) of the element.
|
||||
*
|
||||
* @see ../demos/bidiinput.html
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.ui.BidiInput');
|
||||
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.InputHandler');
|
||||
goog.require('goog.i18n.bidi');
|
||||
goog.require('goog.i18n.bidi.Dir');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of BidiInput.
|
||||
*
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Component}
|
||||
*/
|
||||
goog.ui.BidiInput = function(opt_domHelper) {
|
||||
goog.ui.Component.call(this, opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.BidiInput, goog.ui.Component);
|
||||
goog.tagUnsealableClass(goog.ui.BidiInput);
|
||||
|
||||
|
||||
/**
|
||||
* The input handler that provides the input event.
|
||||
* @type {goog.events.InputHandler?}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.inputHandler_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Decorates the given HTML element as a BidiInput. The HTML element can be an
|
||||
* input element with type='text', a textarea element, or any contenteditable.
|
||||
* Overrides {@link goog.ui.Component#decorateInternal}. Considered protected.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.decorateInternal = function(element) {
|
||||
goog.ui.BidiInput.superClass_.decorateInternal.call(this, element);
|
||||
this.init_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates the element for the text input.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.createDom = function() {
|
||||
this.setElementInternal(
|
||||
this.getDomHelper().createDom('input', {'type': 'text'}));
|
||||
this.init_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the events and initial text direction.
|
||||
* Called from either decorate or createDom, after the input field has
|
||||
* been created.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.init_ = function() {
|
||||
// Set initial direction by current text
|
||||
this.setDirection_();
|
||||
|
||||
// Listen to value change events
|
||||
this.inputHandler_ = new goog.events.InputHandler(this.getElement());
|
||||
goog.events.listen(this.inputHandler_,
|
||||
goog.events.InputHandler.EventType.INPUT,
|
||||
this.setDirection_, false, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the direction of the input element based on the current value. If the
|
||||
* value does not have any strongly directional characters, remove the dir
|
||||
* attribute so that the direction is inherited instead.
|
||||
* This method is called when the user changes the input element value, or
|
||||
* when a program changes the value using
|
||||
* {@link goog.ui.BidiInput#setValue}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.setDirection_ = function() {
|
||||
var element = this.getElement();
|
||||
var text = this.getValue();
|
||||
switch (goog.i18n.bidi.estimateDirection(text)) {
|
||||
case (goog.i18n.bidi.Dir.LTR):
|
||||
element.dir = 'ltr';
|
||||
break;
|
||||
case (goog.i18n.bidi.Dir.RTL):
|
||||
element.dir = 'rtl';
|
||||
break;
|
||||
default:
|
||||
// Default for no direction, inherit from document.
|
||||
element.removeAttribute('dir');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the direction of the input element.
|
||||
* @return {?string} Return 'rtl' for right-to-left text,
|
||||
* 'ltr' for left-to-right text, or null if the value itself is not
|
||||
* enough to determine directionality (e.g. an empty value), and the
|
||||
* direction is inherited from a parent element (typically the body
|
||||
* element).
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.getDirection = function() {
|
||||
var dir = this.getElement().dir;
|
||||
if (dir == '') {
|
||||
dir = null;
|
||||
}
|
||||
return dir;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value of the underlying input field, and sets the direction
|
||||
* according to the given value.
|
||||
* @param {string} value The Value to set in the underlying input field.
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.setValue = function(value) {
|
||||
var element = this.getElement();
|
||||
if (goog.isDefAndNotNull(element.value)) {
|
||||
element.value = value;
|
||||
} else {
|
||||
goog.dom.setTextContent(element, value);
|
||||
}
|
||||
this.setDirection_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value of the underlying input field.
|
||||
* @return {string} Value of the underlying input field.
|
||||
*/
|
||||
goog.ui.BidiInput.prototype.getValue = function() {
|
||||
var element = this.getElement();
|
||||
return goog.isDefAndNotNull(element.value) ? element.value :
|
||||
goog.dom.getRawTextContent(element);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.BidiInput.prototype.disposeInternal = function() {
|
||||
if (this.inputHandler_) {
|
||||
goog.events.removeAll(this.inputHandler_);
|
||||
this.inputHandler_.dispose();
|
||||
this.inputHandler_ = null;
|
||||
goog.ui.BidiInput.superClass_.disposeInternal.call(this);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2012 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.BidiInput
|
||||
</title>
|
||||
<script type="text/javascript" src="../base.js">
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
goog.require('goog.ui.BidiInputTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="textDiv">
|
||||
<input type="text" id="emptyText" value="" />
|
||||
<input type="text" id="bidiText" value="hello, world!" />
|
||||
<div id="bidiTextDiv" contenteditable="true">
|
||||
hello,world!
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.ui.BidiInputTest');
|
||||
goog.setTestOnly('goog.ui.BidiInputTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.BidiInput');
|
||||
|
||||
function setUp() {
|
||||
document.body.focus();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
document.getElementById('emptyText').value = '';
|
||||
document.getElementById('bidiText').value = 'hello, world!';
|
||||
}
|
||||
|
||||
function testEmptyInput() {
|
||||
var bidiInput = new goog.ui.BidiInput();
|
||||
var emptyText = goog.dom.getElement('emptyText');
|
||||
bidiInput.decorate(emptyText);
|
||||
assertEquals('', bidiInput.getValue());
|
||||
bidiInput.setValue('hello!');
|
||||
assertEquals('hello!', bidiInput.getValue());
|
||||
}
|
||||
|
||||
function testSetDirection() {
|
||||
var shalomInHebrew = '\u05e9\u05dc\u05d5\u05dd';
|
||||
var isAGoodLanguageInHebrew =
|
||||
'\u05d4\u05d9\u05d0 \u05e9\u05e4\u05d4 \u05d8\u05d5\u05d1\u05d4';
|
||||
var learnInHebrew = '\u05dc\u05de\u05d3';
|
||||
|
||||
var bidiInput = new goog.ui.BidiInput();
|
||||
var bidiText = goog.dom.getElement('bidiText');
|
||||
bidiInput.decorate(bidiText);
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(shalomInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('hello!');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(':) , ? ! ' + shalomInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(':) , ? ! hello!');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(' ;) ');
|
||||
assertEquals(null, bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(shalomInHebrew + ', how are you today?');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('Hello is ' + shalomInHebrew + ' in Hebrew');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('JavaScript ' + isAGoodLanguageInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(learnInHebrew + ' JavaScript');
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('');
|
||||
assertEquals(null, bidiInput.getDirection());
|
||||
}
|
||||
|
||||
function testSetDirection_inContenteditableDiv() {
|
||||
var shalomInHebrew = '\u05e9\u05dc\u05d5\u05dd';
|
||||
var isAGoodLanguageInHebrew =
|
||||
'\u05d4\u05d9\u05d0 \u05e9\u05e4\u05d4 \u05d8\u05d5\u05d1\u05d4';
|
||||
var learnInHebrew = '\u05dc\u05de\u05d3';
|
||||
|
||||
var bidiInput = new goog.ui.BidiInput();
|
||||
var bidiTextDiv = goog.dom.getElement('bidiTextDiv');
|
||||
bidiInput.decorate(bidiTextDiv);
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(shalomInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('hello!');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(':) , ? ! ' + shalomInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(':) , ? ! hello!');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(' ;) ');
|
||||
assertEquals(null, bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(shalomInHebrew + ', how are you today?');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('Hello is ' + shalomInHebrew + ' in Hebrew');
|
||||
assertEquals('ltr', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('JavaScript ' + isAGoodLanguageInHebrew);
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue(learnInHebrew + ' JavaScript');
|
||||
assertEquals('rtl', bidiInput.getDirection());
|
||||
|
||||
bidiInput.setValue('');
|
||||
assertEquals(null, bidiInput.getDirection());
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
// 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 Definition of the Bubble class.
|
||||
*
|
||||
*
|
||||
* @see ../demos/bubble.html
|
||||
*
|
||||
* TODO: support decoration and addChild
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.Bubble');
|
||||
|
||||
goog.require('goog.Timer');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.math.Box');
|
||||
goog.require('goog.positioning');
|
||||
goog.require('goog.positioning.AbsolutePosition');
|
||||
goog.require('goog.positioning.AnchoredPosition');
|
||||
goog.require('goog.positioning.Corner');
|
||||
goog.require('goog.positioning.CornerBit');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Popup');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The Bubble provides a general purpose bubble implementation that can be
|
||||
* anchored to a particular element and displayed for a period of time.
|
||||
*
|
||||
* @param {string|Element} message HTML string or an element to display inside
|
||||
* the bubble.
|
||||
* @param {Object=} opt_config The configuration
|
||||
* for the bubble. If not specified, the default configuration will be
|
||||
* used. {@see goog.ui.Bubble.defaultConfig}.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Component}
|
||||
*/
|
||||
goog.ui.Bubble = function(message, opt_config, opt_domHelper) {
|
||||
goog.ui.Component.call(this, opt_domHelper);
|
||||
|
||||
/**
|
||||
* The HTML string or element to display inside the bubble.
|
||||
*
|
||||
* @type {string|Element}
|
||||
* @private
|
||||
*/
|
||||
this.message_ = message;
|
||||
|
||||
/**
|
||||
* The Popup element used to position and display the bubble.
|
||||
*
|
||||
* @type {goog.ui.Popup}
|
||||
* @private
|
||||
*/
|
||||
this.popup_ = new goog.ui.Popup();
|
||||
|
||||
/**
|
||||
* Configuration map that contains bubble's UI elements.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.config_ = opt_config || goog.ui.Bubble.defaultConfig;
|
||||
|
||||
/**
|
||||
* Id of the close button for this bubble.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.closeButtonId_ = this.makeId('cb');
|
||||
|
||||
/**
|
||||
* Id of the div for the embedded element.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.messageId_ = this.makeId('mi');
|
||||
|
||||
};
|
||||
goog.inherits(goog.ui.Bubble, goog.ui.Component);
|
||||
goog.tagUnsealableClass(goog.ui.Bubble);
|
||||
|
||||
|
||||
/**
|
||||
* In milliseconds, timeout after which the button auto-hides. Null means
|
||||
* infinite.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.timeout_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Key returned by the bubble timer.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.timerId_ = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Key returned by the listen function for the close button.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.listener_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Key returned by the listen function for the close button.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.anchor_ = null;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Bubble.prototype.createDom = function() {
|
||||
goog.ui.Bubble.superClass_.createDom.call(this);
|
||||
|
||||
var element = this.getElement();
|
||||
element.style.position = 'absolute';
|
||||
element.style.visibility = 'hidden';
|
||||
|
||||
this.popup_.setElement(element);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attaches the bubble to an anchor element. Computes the positioning and
|
||||
* orientation of the bubble.
|
||||
*
|
||||
* @param {Element} anchorElement The element to which we are attaching.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.attach = function(anchorElement) {
|
||||
this.setAnchoredPosition_(
|
||||
anchorElement, this.computePinnedCorner_(anchorElement));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the corner of the bubble to used in the positioning algorithm.
|
||||
*
|
||||
* @param {goog.positioning.Corner} corner The bubble corner used for
|
||||
* positioning constants.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setPinnedCorner = function(corner) {
|
||||
this.popup_.setPinnedCorner(corner);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the position of the bubble. Pass null for corner in AnchoredPosition
|
||||
* for corner to be computed automatically.
|
||||
*
|
||||
* @param {goog.positioning.AbstractPosition} position The position of the
|
||||
* bubble.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setPosition = function(position) {
|
||||
if (position instanceof goog.positioning.AbsolutePosition) {
|
||||
this.popup_.setPosition(position);
|
||||
} else if (position instanceof goog.positioning.AnchoredPosition) {
|
||||
this.setAnchoredPosition_(position.element, position.corner);
|
||||
} else {
|
||||
throw Error('Bubble only supports absolute and anchored positions!');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the timeout after which bubble hides itself.
|
||||
*
|
||||
* @param {number} timeout Timeout of the bubble.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setTimeout = function(timeout) {
|
||||
this.timeout_ = timeout;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether the bubble should be automatically hidden whenever user clicks
|
||||
* outside the bubble element.
|
||||
*
|
||||
* @param {boolean} autoHide Whether to hide if user clicks outside the bubble.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setAutoHide = function(autoHide) {
|
||||
this.popup_.setAutoHide(autoHide);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether the bubble should be visible.
|
||||
*
|
||||
* @param {boolean} visible Desired visibility state.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setVisible = function(visible) {
|
||||
if (visible && !this.popup_.isVisible()) {
|
||||
this.configureElement_();
|
||||
}
|
||||
this.popup_.setVisible(visible);
|
||||
if (!this.popup_.isVisible()) {
|
||||
this.unconfigureElement_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the bubble is visible.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.isVisible = function() {
|
||||
return this.popup_.isVisible();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Bubble.prototype.disposeInternal = function() {
|
||||
this.unconfigureElement_();
|
||||
this.popup_.dispose();
|
||||
this.popup_ = null;
|
||||
goog.ui.Bubble.superClass_.disposeInternal.call(this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates element's contents and configures all timers. This is called on
|
||||
* setVisible(true).
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.configureElement_ = function() {
|
||||
if (!this.isInDocument()) {
|
||||
throw Error('You must render the bubble before showing it!');
|
||||
}
|
||||
|
||||
var element = this.getElement();
|
||||
var corner = this.popup_.getPinnedCorner();
|
||||
element.innerHTML = this.computeHtmlForCorner_(corner);
|
||||
|
||||
if (typeof this.message_ == 'object') {
|
||||
var messageDiv = this.getDomHelper().getElement(this.messageId_);
|
||||
this.getDomHelper().appendChild(messageDiv, this.message_);
|
||||
}
|
||||
var closeButton = this.getDomHelper().getElement(this.closeButtonId_);
|
||||
this.listener_ = goog.events.listen(closeButton,
|
||||
goog.events.EventType.CLICK, this.hideBubble_, false, this);
|
||||
|
||||
if (this.timeout_) {
|
||||
this.timerId_ = goog.Timer.callOnce(this.hideBubble_, this.timeout_, this);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets rid of the element's contents and all assoicated timers and listeners.
|
||||
* This is called on dispose as well as on setVisible(false).
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.unconfigureElement_ = function() {
|
||||
if (this.listener_) {
|
||||
goog.events.unlistenByKey(this.listener_);
|
||||
this.listener_ = null;
|
||||
}
|
||||
if (this.timerId_) {
|
||||
goog.Timer.clear(this.timerId_);
|
||||
this.timerId_ = null;
|
||||
}
|
||||
|
||||
var element = this.getElement();
|
||||
if (element) {
|
||||
this.getDomHelper().removeChildren(element);
|
||||
element.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Computes bubble position based on anchored element.
|
||||
*
|
||||
* @param {Element} anchorElement The element to which we are attaching.
|
||||
* @param {goog.positioning.Corner} corner The bubble corner used for
|
||||
* positioning.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.setAnchoredPosition_ = function(anchorElement,
|
||||
corner) {
|
||||
this.popup_.setPinnedCorner(corner);
|
||||
var margin = this.createMarginForCorner_(corner);
|
||||
this.popup_.setMargin(margin);
|
||||
var anchorCorner = goog.positioning.flipCorner(corner);
|
||||
this.popup_.setPosition(new goog.positioning.AnchoredPosition(
|
||||
anchorElement, anchorCorner));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Hides the bubble. This is called asynchronously by timer of event processor
|
||||
* for the mouse click on the close button.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.hideBubble_ = function() {
|
||||
this.setVisible(false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an AnchoredPosition that will position the bubble optimally
|
||||
* given the position of the anchor element and the size of the viewport.
|
||||
*
|
||||
* @param {Element} anchorElement The element to which the bubble is attached.
|
||||
* @return {!goog.positioning.AnchoredPosition} The AnchoredPosition
|
||||
* to give to {@link #setPosition}.
|
||||
*/
|
||||
goog.ui.Bubble.prototype.getComputedAnchoredPosition = function(anchorElement) {
|
||||
return new goog.positioning.AnchoredPosition(
|
||||
anchorElement, this.computePinnedCorner_(anchorElement));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Computes the pinned corner for the bubble.
|
||||
*
|
||||
* @param {Element} anchorElement The element to which the button is attached.
|
||||
* @return {goog.positioning.Corner} The pinned corner.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.computePinnedCorner_ = function(anchorElement) {
|
||||
var doc = this.getDomHelper().getOwnerDocument(anchorElement);
|
||||
var viewportElement = goog.style.getClientViewportElement(doc);
|
||||
var viewportWidth = viewportElement.offsetWidth;
|
||||
var viewportHeight = viewportElement.offsetHeight;
|
||||
var anchorElementOffset = goog.style.getPageOffset(anchorElement);
|
||||
var anchorElementSize = goog.style.getSize(anchorElement);
|
||||
var anchorType = 0;
|
||||
// right margin or left?
|
||||
if (viewportWidth - anchorElementOffset.x - anchorElementSize.width >
|
||||
anchorElementOffset.x) {
|
||||
anchorType += 1;
|
||||
}
|
||||
// attaches to the top or to the bottom?
|
||||
if (viewportHeight - anchorElementOffset.y - anchorElementSize.height >
|
||||
anchorElementOffset.y) {
|
||||
anchorType += 2;
|
||||
}
|
||||
return goog.ui.Bubble.corners_[anchorType];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Computes the right offset for a given bubble corner
|
||||
* and creates a margin element for it. This is done to have the
|
||||
* button anchor element on its frame rather than on the corner.
|
||||
*
|
||||
* @param {goog.positioning.Corner} corner The corner.
|
||||
* @return {!goog.math.Box} the computed margin. Only left or right fields are
|
||||
* non-zero, but they may be negative.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.createMarginForCorner_ = function(corner) {
|
||||
var margin = new goog.math.Box(0, 0, 0, 0);
|
||||
if (corner & goog.positioning.CornerBit.RIGHT) {
|
||||
margin.right -= this.config_.marginShift;
|
||||
} else {
|
||||
margin.left -= this.config_.marginShift;
|
||||
}
|
||||
return margin;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Computes the HTML string for a given bubble orientation.
|
||||
*
|
||||
* @param {goog.positioning.Corner} corner The corner.
|
||||
* @return {string} The HTML string to place inside the bubble's popup.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.prototype.computeHtmlForCorner_ = function(corner) {
|
||||
var bubbleTopClass;
|
||||
var bubbleBottomClass;
|
||||
switch (corner) {
|
||||
case goog.positioning.Corner.TOP_LEFT:
|
||||
bubbleTopClass = this.config_.cssBubbleTopLeftAnchor;
|
||||
bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
|
||||
break;
|
||||
case goog.positioning.Corner.TOP_RIGHT:
|
||||
bubbleTopClass = this.config_.cssBubbleTopRightAnchor;
|
||||
bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
|
||||
break;
|
||||
case goog.positioning.Corner.BOTTOM_LEFT:
|
||||
bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
|
||||
bubbleBottomClass = this.config_.cssBubbleBottomLeftAnchor;
|
||||
break;
|
||||
case goog.positioning.Corner.BOTTOM_RIGHT:
|
||||
bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
|
||||
bubbleBottomClass = this.config_.cssBubbleBottomRightAnchor;
|
||||
break;
|
||||
default:
|
||||
throw Error('This corner type is not supported by bubble!');
|
||||
}
|
||||
var message = null;
|
||||
if (typeof this.message_ == 'object') {
|
||||
message = '<div id="' + this.messageId_ + '">';
|
||||
} else {
|
||||
message = this.message_;
|
||||
}
|
||||
var html =
|
||||
'<table border=0 cellspacing=0 cellpadding=0 style="z-index:1"' +
|
||||
' width=' + this.config_.bubbleWidth + '>' +
|
||||
'<tr><td colspan=4 class="' + bubbleTopClass + '">' +
|
||||
'<tr>' +
|
||||
'<td class="' + this.config_.cssBubbleLeft + '">' +
|
||||
'<td class="' + this.config_.cssBubbleFont + '"' +
|
||||
' style="padding:0 4px;background:white">' + message +
|
||||
'<td id="' + this.closeButtonId_ + '"' +
|
||||
' class="' + this.config_.cssCloseButton + '"/>' +
|
||||
'<td class="' + this.config_.cssBubbleRight + '">' +
|
||||
'<tr>' +
|
||||
'<td colspan=4 class="' + bubbleBottomClass + '">' +
|
||||
'</table>';
|
||||
return html;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A default configuration for the bubble.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
goog.ui.Bubble.defaultConfig = {
|
||||
bubbleWidth: 147,
|
||||
marginShift: 60,
|
||||
cssBubbleFont: goog.getCssName('goog-bubble-font'),
|
||||
cssCloseButton: goog.getCssName('goog-bubble-close-button'),
|
||||
cssBubbleTopRightAnchor: goog.getCssName('goog-bubble-top-right-anchor'),
|
||||
cssBubbleTopLeftAnchor: goog.getCssName('goog-bubble-top-left-anchor'),
|
||||
cssBubbleTopNoAnchor: goog.getCssName('goog-bubble-top-no-anchor'),
|
||||
cssBubbleBottomRightAnchor:
|
||||
goog.getCssName('goog-bubble-bottom-right-anchor'),
|
||||
cssBubbleBottomLeftAnchor: goog.getCssName('goog-bubble-bottom-left-anchor'),
|
||||
cssBubbleBottomNoAnchor: goog.getCssName('goog-bubble-bottom-no-anchor'),
|
||||
cssBubbleLeft: goog.getCssName('goog-bubble-left'),
|
||||
cssBubbleRight: goog.getCssName('goog-bubble-right')
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An auxiliary array optimizing the corner computation.
|
||||
*
|
||||
* @type {Array<goog.positioning.Corner>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Bubble.corners_ = [
|
||||
goog.positioning.Corner.BOTTOM_RIGHT,
|
||||
goog.positioning.Corner.BOTTOM_LEFT,
|
||||
goog.positioning.Corner.TOP_RIGHT,
|
||||
goog.positioning.Corner.TOP_LEFT
|
||||
];
|
||||
@@ -0,0 +1,214 @@
|
||||
// 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 A button control. This implementation extends {@link
|
||||
* goog.ui.Control}.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
* @see ../demos/button.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.Button');
|
||||
goog.provide('goog.ui.Button.Side');
|
||||
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.events.KeyHandler');
|
||||
goog.require('goog.ui.ButtonRenderer');
|
||||
goog.require('goog.ui.ButtonSide');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Control');
|
||||
goog.require('goog.ui.NativeButtonRenderer');
|
||||
goog.require('goog.ui.registry');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A button control, rendered as a native browser button by default.
|
||||
*
|
||||
* @param {goog.ui.ControlContent=} opt_content Text caption or existing DOM
|
||||
* structure to display as the button's caption (if any).
|
||||
* @param {goog.ui.ButtonRenderer=} opt_renderer Renderer used to render or
|
||||
* decorate the button; defaults to {@link goog.ui.NativeButtonRenderer}.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Control}
|
||||
*/
|
||||
goog.ui.Button = function(opt_content, opt_renderer, opt_domHelper) {
|
||||
goog.ui.Control.call(this, opt_content, opt_renderer ||
|
||||
goog.ui.NativeButtonRenderer.getInstance(), opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.Button, goog.ui.Control);
|
||||
goog.tagUnsealableClass(goog.ui.Button);
|
||||
|
||||
|
||||
/**
|
||||
* Constants for button sides, see {@link goog.ui.Button.prototype.setCollapsed}
|
||||
* for details. Aliased from goog.ui.ButtonSide to support legacy users without
|
||||
* creating a circular dependency in {@link goog.ui.ButtonRenderer}.
|
||||
* @enum {number}
|
||||
* @deprecated use {@link goog.ui.ButtonSide} instead.
|
||||
*/
|
||||
goog.ui.Button.Side = goog.ui.ButtonSide;
|
||||
|
||||
|
||||
/**
|
||||
* Value associated with the button.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Button.prototype.value_;
|
||||
|
||||
|
||||
/**
|
||||
* Tooltip text for the button, displayed on hover.
|
||||
* @type {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Button.prototype.tooltip_;
|
||||
|
||||
|
||||
// goog.ui.Button API implementation.
|
||||
|
||||
|
||||
/**
|
||||
* Returns the value associated with the button.
|
||||
* @return {*} Button value (undefined if none).
|
||||
*/
|
||||
goog.ui.Button.prototype.getValue = function() {
|
||||
return this.value_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value associated with the button, and updates its DOM.
|
||||
* @param {*} value New button value.
|
||||
*/
|
||||
goog.ui.Button.prototype.setValue = function(value) {
|
||||
this.value_ = value;
|
||||
var renderer = /** @type {!goog.ui.ButtonRenderer} */ (this.getRenderer());
|
||||
renderer.setValue(this.getElement(), /** @type {string} */ (value));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value associated with the button. Unlike {@link #setValue},
|
||||
* doesn't update the button's DOM. Considered protected; to be called only
|
||||
* by renderer code during element decoration.
|
||||
* @param {*} value New button value.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.Button.prototype.setValueInternal = function(value) {
|
||||
this.value_ = value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the tooltip for the button.
|
||||
* @return {string|undefined} Tooltip text (undefined if none).
|
||||
*/
|
||||
goog.ui.Button.prototype.getTooltip = function() {
|
||||
return this.tooltip_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the tooltip for the button, and updates its DOM.
|
||||
* @param {string} tooltip New tooltip text.
|
||||
*/
|
||||
goog.ui.Button.prototype.setTooltip = function(tooltip) {
|
||||
this.tooltip_ = tooltip;
|
||||
this.getRenderer().setTooltip(this.getElement(), tooltip);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the tooltip for the button. Unlike {@link #setTooltip}, doesn't update
|
||||
* the button's DOM. Considered protected; to be called only by renderer code
|
||||
* during element decoration.
|
||||
* @param {string} tooltip New tooltip text.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.Button.prototype.setTooltipInternal = function(tooltip) {
|
||||
this.tooltip_ = tooltip;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Collapses the border on one or both sides of the button, allowing it to be
|
||||
* combined with the adjancent button(s), forming a single UI componenet with
|
||||
* multiple targets.
|
||||
* @param {number} sides Bitmap of one or more {@link goog.ui.ButtonSide}s for
|
||||
* which borders should be collapsed.
|
||||
*/
|
||||
goog.ui.Button.prototype.setCollapsed = function(sides) {
|
||||
this.getRenderer().setCollapsed(this, sides);
|
||||
};
|
||||
|
||||
|
||||
// goog.ui.Control & goog.ui.Component API implementation.
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Button.prototype.disposeInternal = function() {
|
||||
goog.ui.Button.superClass_.disposeInternal.call(this);
|
||||
delete this.value_;
|
||||
delete this.tooltip_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Button.prototype.enterDocument = function() {
|
||||
goog.ui.Button.superClass_.enterDocument.call(this);
|
||||
if (this.isSupportedState(goog.ui.Component.State.FOCUSED)) {
|
||||
var keyTarget = this.getKeyEventTarget();
|
||||
if (keyTarget) {
|
||||
this.getHandler().listen(keyTarget, goog.events.EventType.KEYUP,
|
||||
this.handleKeyEventInternal);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to handle a keyboard event; returns true if the event was handled,
|
||||
* false otherwise. If the button is enabled and the Enter/Space key was
|
||||
* pressed, handles the event by dispatching an {@code ACTION} event,
|
||||
* and returns true. Overrides {@link goog.ui.Control#handleKeyEventInternal}.
|
||||
* @param {goog.events.KeyEvent} e Key event to handle.
|
||||
* @return {boolean} Whether the key event was handled.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.Button.prototype.handleKeyEventInternal = function(e) {
|
||||
if (e.keyCode == goog.events.KeyCodes.ENTER &&
|
||||
e.type == goog.events.KeyHandler.EventType.KEY ||
|
||||
e.keyCode == goog.events.KeyCodes.SPACE &&
|
||||
e.type == goog.events.EventType.KEYUP) {
|
||||
return this.performActionInternal(e);
|
||||
}
|
||||
// Return true for space keypress (even though the event is handled on keyup)
|
||||
// as preventDefault needs to be called up keypress to take effect in IE and
|
||||
// WebKit.
|
||||
return e.keyCode == goog.events.KeyCodes.SPACE;
|
||||
};
|
||||
|
||||
|
||||
// Register a decorator factory function for goog.ui.Buttons.
|
||||
goog.ui.registry.setDecoratorByClassName(goog.ui.ButtonRenderer.CSS_CLASS,
|
||||
function() {
|
||||
return new goog.ui.Button(null);
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Performance Tests - goog.ui.Button</title>
|
||||
<link rel="stylesheet" type="text/css" href="../testing/performancetable.css" />
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.testing.PerformanceTable');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Button');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>goog.ui.Button Performance Tests</h1>
|
||||
<p>
|
||||
<b>User-agent:</b> <script>document.write(navigator.userAgent);</script>
|
||||
</p>
|
||||
<div id="perfTable"></div>
|
||||
<hr>
|
||||
<div id="renderSandbox"></div>
|
||||
<div id="decorateSandbox"></div>
|
||||
<script>
|
||||
// The sandboxen.
|
||||
var renderSandbox = goog.dom.getElement('renderSandbox');
|
||||
var decorateSandbox = goog.dom.getElement('decorateSandbox');
|
||||
|
||||
// Arrays of rendered/decorated buttons (so we can dispose of them).
|
||||
var renderedButtons;
|
||||
var decoratedButtons;
|
||||
|
||||
// 0-based index of the button currently being rendered/decorated.
|
||||
var renderIndex;
|
||||
var decorateIndex;
|
||||
|
||||
// Element currently being decorated.
|
||||
var elementToDecorate;
|
||||
|
||||
// Number of buttons to create/decorate per test run.
|
||||
var SAMPLES_PER_RUN = 200;
|
||||
|
||||
// The performance table.
|
||||
var table;
|
||||
|
||||
function setUpPage() {
|
||||
table = new goog.testing.PerformanceTable(
|
||||
goog.dom.getElement('perfTable'));
|
||||
}
|
||||
|
||||
// Sets up a render test.
|
||||
function setUpRenderTest() {
|
||||
renderedButtons = [];
|
||||
renderIndex = 0;
|
||||
}
|
||||
|
||||
// Cleans up after a render test.
|
||||
function cleanUpAfterRenderTest() {
|
||||
for (var i = 0, count = renderedButtons.length; i < count; i++) {
|
||||
renderedButtons[i].dispose();
|
||||
}
|
||||
renderedButtons = null;
|
||||
goog.dom.removeChildren(renderSandbox);
|
||||
}
|
||||
|
||||
// Sets up a decorate test.
|
||||
function setUpDecorateTest(opt_count) {
|
||||
var count = opt_count || 1000;
|
||||
for (var i = 0; i < count; i++) {
|
||||
decorateSandbox.appendChild(goog.dom.createDom('button', 'goog-button',
|
||||
'W00t!'));
|
||||
}
|
||||
elementToDecorate = decorateSandbox.firstChild;
|
||||
decoratedButtons = [];
|
||||
decorateIndex = 0;
|
||||
}
|
||||
|
||||
// Cleans up after a decorate test.
|
||||
function cleanUpAfterDecorateTest() {
|
||||
for (var i = 0, count = decoratedButtons.length; i < count; i++) {
|
||||
decoratedButtons[i].dispose();
|
||||
}
|
||||
decoratedButtons = null;
|
||||
goog.dom.removeChildren(decorateSandbox);
|
||||
}
|
||||
|
||||
// Renders the given number of buttons. Since children are appended to
|
||||
// the same parent element in each performance test run, we keep track of
|
||||
// the current index via the global renderIndex variable.
|
||||
function renderButtons(count, autoDetectBiDi) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
var button = new goog.ui.Button('W00t!');
|
||||
if (!autoDetectBiDi) {
|
||||
button.setRightToLeft(false);
|
||||
}
|
||||
button.render(renderSandbox);
|
||||
renderedButtons[renderIndex++] = button;
|
||||
}
|
||||
}
|
||||
|
||||
// Decorates "count" buttons. The decorate sandbox contains enough child
|
||||
// elements for the whole test, but we only decorate up to "count" elements
|
||||
// per test run, so we need to keep track of where we are via the global
|
||||
// decorateIndex and elementToDecorate variables.
|
||||
function decorateButtons(count, autoDetectBiDi) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
var next = elementToDecorate.nextSibling;
|
||||
var button = new goog.ui.Button();
|
||||
if (!autoDetectBiDi) {
|
||||
button.setRightToLeft(false);
|
||||
}
|
||||
button.decorate(elementToDecorate);
|
||||
decoratedButtons[decorateIndex++] = button;
|
||||
elementToDecorate = next;
|
||||
}
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
setUpRenderTest();
|
||||
table.run(
|
||||
goog.partial(renderButtons, SAMPLES_PER_RUN, true),
|
||||
'Render ' + SAMPLES_PER_RUN + ' buttons (default)');
|
||||
cleanUpAfterRenderTest();
|
||||
assertEquals('The expected number of buttons must have been rendered',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(), renderIndex);
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
setUpDecorateTest(SAMPLES_PER_RUN * table.getTimer().getNumSamples());
|
||||
table.run(
|
||||
goog.partial(decorateButtons, SAMPLES_PER_RUN, true),
|
||||
'Decorate ' + SAMPLES_PER_RUN + ' buttons (default)');
|
||||
cleanUpAfterDecorateTest();
|
||||
assertEquals('The expected number of buttons must have been decorated',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(),
|
||||
decorateIndex);
|
||||
assertNull('All buttons must have been decorated', elementToDecorate);
|
||||
}
|
||||
|
||||
function testRenderNoBiDiAutoDetect() {
|
||||
setUpRenderTest();
|
||||
table.run(
|
||||
goog.partial(renderButtons, SAMPLES_PER_RUN, false),
|
||||
'Render ' + SAMPLES_PER_RUN + ' buttons (no BiDi auto-detect)');
|
||||
cleanUpAfterRenderTest();
|
||||
assertEquals('The expected number of buttons must have been rendered',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(),
|
||||
renderIndex);
|
||||
}
|
||||
|
||||
function testDecorateNoBiDiAutoDetect() {
|
||||
setUpDecorateTest(SAMPLES_PER_RUN * table.getTimer().getNumSamples());
|
||||
table.run(
|
||||
goog.partial(decorateButtons, SAMPLES_PER_RUN, false),
|
||||
'Decorate ' + SAMPLES_PER_RUN + ' buttons (no BiDi auto-detect)');
|
||||
cleanUpAfterDecorateTest();
|
||||
assertEquals('The expected number of buttons must have been decorated',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(),
|
||||
decorateIndex);
|
||||
assertNull('All buttons must have been decorated', elementToDecorate);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: nicksantos@google.com (Nick Santos)
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.Button
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ButtonTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Here's a button defined in markup:
|
||||
</p>
|
||||
<button id="demoButton" role="button">
|
||||
Foo
|
||||
</button>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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.ButtonTest');
|
||||
goog.setTestOnly('goog.ui.ButtonTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.events.KeyHandler');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Button');
|
||||
goog.require('goog.ui.ButtonRenderer');
|
||||
goog.require('goog.ui.ButtonSide');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.NativeButtonRenderer');
|
||||
|
||||
var sandbox;
|
||||
var button;
|
||||
var clonedButtonDom;
|
||||
var demoButtonElement;
|
||||
|
||||
function setUp() {
|
||||
sandbox = goog.dom.getElement('sandbox');
|
||||
button = new goog.ui.Button();
|
||||
demoButtonElement = goog.dom.getElement('demoButton');
|
||||
clonedButtonDom = demoButtonElement.cloneNode(true);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
button.dispose();
|
||||
demoButtonElement.parentNode.replaceChild(clonedButtonDom,
|
||||
demoButtonElement);
|
||||
goog.dom.removeChildren(sandbox);
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertNotNull('Button must not be null', button);
|
||||
assertEquals('Renderer must default to expected value',
|
||||
goog.ui.NativeButtonRenderer.getInstance(), button.getRenderer());
|
||||
|
||||
var fakeDomHelper = {};
|
||||
var testButton = new goog.ui.Button('Hello',
|
||||
goog.ui.ButtonRenderer.getInstance(), fakeDomHelper);
|
||||
assertEquals('Content must have expected value', 'Hello',
|
||||
testButton.getContent());
|
||||
assertEquals('Renderer must have expected value',
|
||||
goog.ui.ButtonRenderer.getInstance(), testButton.getRenderer());
|
||||
assertEquals('DOM helper must have expected value', fakeDomHelper,
|
||||
testButton.getDomHelper());
|
||||
testButton.dispose();
|
||||
}
|
||||
|
||||
function testGetSetValue() {
|
||||
assertUndefined('Button\'s value must default to undefined',
|
||||
button.getValue());
|
||||
button.setValue(17);
|
||||
assertEquals('Button must have expected value', 17, button.getValue());
|
||||
button.render(sandbox);
|
||||
assertEquals('Button element must have expected value', '17',
|
||||
button.getElement().value);
|
||||
button.setValue('foo');
|
||||
assertEquals('Button element must have updated value', 'foo',
|
||||
button.getElement().value);
|
||||
button.setValueInternal('bar');
|
||||
assertEquals('Button must have new internal value', 'bar',
|
||||
button.getValue());
|
||||
assertEquals('Button element must be unchanged', 'foo',
|
||||
button.getElement().value);
|
||||
}
|
||||
|
||||
function testGetSetTooltip() {
|
||||
assertUndefined('Button\'s tooltip must default to undefined',
|
||||
button.getTooltip());
|
||||
button.setTooltip('Hello');
|
||||
assertEquals('Button must have expected tooltip', 'Hello',
|
||||
button.getTooltip());
|
||||
button.render(sandbox);
|
||||
assertEquals('Button element must have expected title', 'Hello',
|
||||
button.getElement().title);
|
||||
button.setTooltip('Goodbye');
|
||||
assertEquals('Button element must have updated title', 'Goodbye',
|
||||
button.getElement().title);
|
||||
button.setTooltipInternal('World');
|
||||
assertEquals('Button must have new internal tooltip', 'World',
|
||||
button.getTooltip());
|
||||
assertEquals('Button element must be unchanged', 'Goodbye',
|
||||
button.getElement().title);
|
||||
}
|
||||
|
||||
function testSetCollapsed() {
|
||||
assertNull('Button must not have any collapsed styling by default',
|
||||
button.getExtraClassNames());
|
||||
button.setCollapsed(goog.ui.ButtonSide.START);
|
||||
assertSameElements('Button must have the start side collapsed',
|
||||
['goog-button-collapse-left'], button.getExtraClassNames());
|
||||
button.render(sandbox);
|
||||
assertSameElements('Button element must have the start side collapsed',
|
||||
['goog-button', 'goog-button-collapse-left'],
|
||||
goog.dom.classlist.get(button.getElement()));
|
||||
button.setCollapsed(goog.ui.ButtonSide.BOTH);
|
||||
assertSameElements('Button must have both sides collapsed',
|
||||
['goog-button-collapse-left', 'goog-button-collapse-right'],
|
||||
button.getExtraClassNames());
|
||||
assertSameElements('Button element must have both sides collapsed', [
|
||||
'goog-button',
|
||||
'goog-button-collapse-left',
|
||||
'goog-button-collapse-right'
|
||||
], goog.dom.classlist.get(button.getElement()));
|
||||
}
|
||||
|
||||
function testDispose() {
|
||||
assertFalse('Button must not have been disposed of', button.isDisposed());
|
||||
button.render(sandbox);
|
||||
button.setValue('foo');
|
||||
button.setTooltip('bar');
|
||||
button.dispose();
|
||||
assertTrue('Button must have been disposed of', button.isDisposed());
|
||||
assertUndefined('Button\'s value must have been deleted',
|
||||
button.getValue());
|
||||
assertUndefined('Button\'s tooltip must have been deleted',
|
||||
button.getTooltip());
|
||||
}
|
||||
|
||||
function testBasicButtonBehavior() {
|
||||
var dispatchedActionCount = 0;
|
||||
var handleAction = function() {
|
||||
dispatchedActionCount++;
|
||||
};
|
||||
goog.events.listen(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
|
||||
button.decorate(demoButtonElement);
|
||||
goog.testing.events.fireClickSequence(demoButtonElement);
|
||||
assertEquals('Button must have dispatched ACTION on click', 1,
|
||||
dispatchedActionCount);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
var e = new goog.events.Event(goog.events.KeyHandler.EventType.KEY,
|
||||
button);
|
||||
e.keyCode = goog.events.KeyCodes.ENTER;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Enabled button must have dispatched ACTION on Enter key', 1,
|
||||
dispatchedActionCount);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
e = new goog.events.Event(goog.events.EventType.KEYUP, button);
|
||||
e.keyCode = goog.events.KeyCodes.SPACE;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Enabled button must have dispatched ACTION on Space key', 1,
|
||||
dispatchedActionCount);
|
||||
|
||||
goog.events.unlisten(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
}
|
||||
|
||||
function testDisabledButtonBehavior() {
|
||||
var dispatchedActionCount = 0;
|
||||
var handleAction = function() {
|
||||
dispatchedActionCount++;
|
||||
};
|
||||
goog.events.listen(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
|
||||
button.setEnabled(false);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
button.handleKeyEvent({keyCode: goog.events.KeyCodes.ENTER});
|
||||
assertEquals('Disabled button must not dispatch ACTION on Enter key',
|
||||
0, dispatchedActionCount);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
button.handleKeyEvent({
|
||||
keyCode: goog.events.KeyCodes.SPACE,
|
||||
type: goog.events.EventType.KEYUP
|
||||
});
|
||||
assertEquals('Disabled button must not have dispatched ACTION on Space',
|
||||
0, dispatchedActionCount);
|
||||
|
||||
goog.events.unlisten(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
}
|
||||
|
||||
function testSpaceFireActionOnKeyUp() {
|
||||
var dispatchedActionCount = 0;
|
||||
var handleAction = function() {
|
||||
dispatchedActionCount++;
|
||||
};
|
||||
goog.events.listen(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
e = new goog.events.Event(goog.events.KeyHandler.EventType.KEY, button);
|
||||
e.keyCode = goog.events.KeyCodes.SPACE;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Button must not have dispatched ACTION on Space keypress',
|
||||
0, dispatchedActionCount);
|
||||
assertEquals('The default action (scrolling) must have been prevented ' +
|
||||
'for Space keypress',
|
||||
false,
|
||||
e.returnValue_);
|
||||
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
e = new goog.events.Event(goog.events.EventType.KEYUP, button);
|
||||
e.keyCode = goog.events.KeyCodes.SPACE;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Button must have dispatched ACTION on Space keyup',
|
||||
1, dispatchedActionCount);
|
||||
|
||||
goog.events.unlisten(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
}
|
||||
|
||||
function testEnterFireActionOnKeyPress() {
|
||||
var dispatchedActionCount = 0;
|
||||
var handleAction = function() {
|
||||
dispatchedActionCount++;
|
||||
};
|
||||
goog.events.listen(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
e = new goog.events.Event(goog.events.KeyHandler.EventType.KEY, button);
|
||||
e.keyCode = goog.events.KeyCodes.ENTER;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Button must have dispatched ACTION on Enter keypress',
|
||||
1, dispatchedActionCount);
|
||||
|
||||
dispatchedActionCount = 0;
|
||||
e = new goog.events.Event(goog.events.EventType.KEYUP, button);
|
||||
e.keyCode = goog.events.KeyCodes.ENTER;
|
||||
button.handleKeyEvent(e);
|
||||
assertEquals('Button must not have dispatched ACTION on Enter keyup',
|
||||
0, dispatchedActionCount);
|
||||
|
||||
goog.events.unlisten(button, goog.ui.Component.EventType.ACTION,
|
||||
handleAction);
|
||||
}
|
||||
|
||||
function testSetAriaLabel() {
|
||||
assertNull('Button must not have aria label by default',
|
||||
button.getAriaLabel());
|
||||
button.setAriaLabel('Button 1');
|
||||
button.render();
|
||||
assertEquals('Button element must have expected aria-label', 'Button 1',
|
||||
button.getElement().getAttribute('aria-label'));
|
||||
button.setAriaLabel('Button 2');
|
||||
assertEquals('Button element must have updated aria-label', 'Button 2',
|
||||
button.getElement().getAttribute('aria-label'));
|
||||
}
|
||||
|
||||
function testSetAriaLabel_decorate() {
|
||||
assertNull('Button must not have aria label by default',
|
||||
button.getAriaLabel());
|
||||
button.setAriaLabel('Button 1');
|
||||
button.decorate(demoButtonElement);
|
||||
var el = button.getElementStrict();
|
||||
assertEquals('Button element must have expected aria-label', 'Button 1',
|
||||
el.getAttribute('aria-label'));
|
||||
assertEquals('Button element must have expected aria-role', 'button',
|
||||
el.getAttribute('role'));
|
||||
button.setAriaLabel('Button 2');
|
||||
assertEquals('Button element must have updated aria-label', 'Button 2',
|
||||
el.getAttribute('aria-label'));
|
||||
assertEquals('Button element must have expected aria-role', 'button',
|
||||
el.getAttribute('role'));
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Default renderer for {@link goog.ui.Button}s.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ButtonRenderer');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.ui.ButtonSide');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.ControlRenderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default renderer for {@link goog.ui.Button}s. Extends the superclass with
|
||||
* the following button-specific API methods:
|
||||
* <ul>
|
||||
* <li>{@code getValue} - returns the button element's value
|
||||
* <li>{@code setValue} - updates the button element to reflect its new value
|
||||
* <li>{@code getTooltip} - returns the button element's tooltip text
|
||||
* <li>{@code setTooltip} - updates the button element's tooltip text
|
||||
* <li>{@code setCollapsed} - removes one or both of the button element's
|
||||
* borders
|
||||
* </ul>
|
||||
* For alternate renderers, see {@link goog.ui.NativeButtonRenderer},
|
||||
* {@link goog.ui.CustomButtonRenderer}, and {@link goog.ui.FlatButtonRenderer}.
|
||||
* @constructor
|
||||
* @extends {goog.ui.ControlRenderer}
|
||||
*/
|
||||
goog.ui.ButtonRenderer = function() {
|
||||
goog.ui.ControlRenderer.call(this);
|
||||
};
|
||||
goog.inherits(goog.ui.ButtonRenderer, goog.ui.ControlRenderer);
|
||||
goog.addSingletonGetter(goog.ui.ButtonRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of components rendered
|
||||
* by this renderer.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ButtonRenderer.CSS_CLASS = goog.getCssName('goog-button');
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ARIA role to be applied to buttons.
|
||||
* @return {goog.a11y.aria.Role|undefined} ARIA role.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.getAriaRole = function() {
|
||||
return goog.a11y.aria.Role.BUTTON;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the button's ARIA (accessibility) state if the button is being
|
||||
* treated as a checkbox. Also makes sure that attributes which aren't
|
||||
* supported by buttons aren't being added.
|
||||
* @param {Element} element Element whose ARIA state is to be updated.
|
||||
* @param {goog.ui.Component.State} state Component state being enabled or
|
||||
* disabled.
|
||||
* @param {boolean} enable Whether the state is being enabled or disabled.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.updateAriaState = function(element, state,
|
||||
enable) {
|
||||
switch (state) {
|
||||
// If button has CHECKED or SELECTED state, assign aria-pressed
|
||||
case goog.ui.Component.State.SELECTED:
|
||||
case goog.ui.Component.State.CHECKED:
|
||||
goog.asserts.assert(element,
|
||||
'The button DOM element cannot be null.');
|
||||
goog.a11y.aria.setState(element, goog.a11y.aria.State.PRESSED, enable);
|
||||
break;
|
||||
default:
|
||||
case goog.ui.Component.State.OPENED:
|
||||
case goog.ui.Component.State.DISABLED:
|
||||
goog.ui.ButtonRenderer.base(
|
||||
this, 'updateAriaState', element, state, enable);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ButtonRenderer.prototype.createDom = function(button) {
|
||||
var element = goog.ui.ButtonRenderer.base(this, 'createDom', button);
|
||||
this.setTooltip(element, button.getTooltip());
|
||||
|
||||
var value = button.getValue();
|
||||
if (value) {
|
||||
this.setValue(element, value);
|
||||
}
|
||||
|
||||
// If this is a toggle button, set ARIA state
|
||||
if (button.isSupportedState(goog.ui.Component.State.CHECKED)) {
|
||||
this.updateAriaState(element, goog.ui.Component.State.CHECKED,
|
||||
button.isChecked());
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ButtonRenderer.prototype.decorate = function(button, element) {
|
||||
// The superclass implementation takes care of common attributes; we only
|
||||
// need to set the value and the tooltip.
|
||||
element = goog.ui.ButtonRenderer.superClass_.decorate.call(this, button,
|
||||
element);
|
||||
|
||||
button.setValueInternal(this.getValue(element));
|
||||
button.setTooltipInternal(this.getTooltip(element));
|
||||
|
||||
// If this is a toggle button, set ARIA state
|
||||
if (button.isSupportedState(goog.ui.Component.State.CHECKED)) {
|
||||
this.updateAriaState(element, goog.ui.Component.State.CHECKED,
|
||||
button.isChecked());
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a button's root element, and returns the value associated with it.
|
||||
* No-op in the base class.
|
||||
* @param {Element} element The button's root element.
|
||||
* @return {string|undefined} The button's value (undefined if none).
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.getValue = goog.nullFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Takes a button's root element and a value, and updates the element to reflect
|
||||
* the new value. No-op in the base class.
|
||||
* @param {Element} element The button's root element.
|
||||
* @param {string} value New value.
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.setValue = goog.nullFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Takes a button's root element, and returns its tooltip text.
|
||||
* @param {Element} element The button's root element.
|
||||
* @return {string|undefined} The tooltip text.
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.getTooltip = function(element) {
|
||||
return element.title;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a button's root element and a tooltip string, and updates the element
|
||||
* with the new tooltip.
|
||||
* @param {Element} element The button's root element.
|
||||
* @param {string} tooltip New tooltip text.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.setTooltip = function(element, tooltip) {
|
||||
if (element) {
|
||||
// Don't set a title attribute if there isn't a tooltip. Blank title
|
||||
// attributes can be interpreted incorrectly by screen readers.
|
||||
if (tooltip) {
|
||||
element.title = tooltip;
|
||||
} else {
|
||||
element.removeAttribute('title');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Collapses the border on one or both sides of the button, allowing it to be
|
||||
* combined with the adjacent button(s), forming a single UI componenet with
|
||||
* multiple targets.
|
||||
* @param {goog.ui.Button} button Button to update.
|
||||
* @param {number} sides Bitmap of one or more {@link goog.ui.ButtonSide}s for
|
||||
* which borders should be collapsed.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ButtonRenderer.prototype.setCollapsed = function(button, sides) {
|
||||
var isRtl = button.isRightToLeft();
|
||||
var collapseLeftClassName =
|
||||
goog.getCssName(this.getStructuralCssClass(), 'collapse-left');
|
||||
var collapseRightClassName =
|
||||
goog.getCssName(this.getStructuralCssClass(), 'collapse-right');
|
||||
|
||||
button.enableClassName(isRtl ? collapseRightClassName : collapseLeftClassName,
|
||||
!!(sides & goog.ui.ButtonSide.START));
|
||||
button.enableClassName(isRtl ? collapseLeftClassName : collapseRightClassName,
|
||||
!!(sides & goog.ui.ButtonSide.END));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ButtonRenderer.prototype.getCssClass = function() {
|
||||
return goog.ui.ButtonRenderer.CSS_CLASS;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ButtonRenderer
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ButtonRendererTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,242 @@
|
||||
// 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.ButtonRendererTest');
|
||||
goog.setTestOnly('goog.ui.ButtonRendererTest');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.testing.ExpectedFailures');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.ui.rendererasserts');
|
||||
goog.require('goog.ui.Button');
|
||||
goog.require('goog.ui.ButtonRenderer');
|
||||
goog.require('goog.ui.ButtonSide');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
var button, buttonRenderer, testRenderer;
|
||||
var sandbox;
|
||||
var expectedFailures;
|
||||
|
||||
function setUpPage() {
|
||||
sandbox = goog.dom.getElement('sandbox');
|
||||
expectedFailures = new goog.testing.ExpectedFailures();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A subclass of ButtonRenderer that overrides
|
||||
* {@code getStructuralCssClass} for testing purposes.
|
||||
* @constructor
|
||||
* @extends {goog.ui.ControlRenderer}
|
||||
*/
|
||||
function TestRenderer() {
|
||||
goog.ui.ButtonRenderer.call(this);
|
||||
}
|
||||
goog.inherits(TestRenderer, goog.ui.ButtonRenderer);
|
||||
goog.addSingletonGetter(TestRenderer);
|
||||
|
||||
|
||||
/** @override */
|
||||
TestRenderer.prototype.getStructuralCssClass = function() {
|
||||
return 'goog-base';
|
||||
};
|
||||
|
||||
function setUp() {
|
||||
buttonRenderer = goog.ui.ButtonRenderer.getInstance();
|
||||
button = new goog.ui.Button('Hello', buttonRenderer);
|
||||
testRenderer = TestRenderer.getInstance();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
button.dispose();
|
||||
goog.dom.removeChildren(sandbox);
|
||||
expectedFailures.handleTearDown();
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertNotNull('ButtonRenderer singleton instance must not be null',
|
||||
buttonRenderer);
|
||||
}
|
||||
|
||||
function testGetAriaRole() {
|
||||
assertEquals('ButtonRenderer\'s ARIA role must have expected value',
|
||||
goog.a11y.aria.Role.BUTTON, buttonRenderer.getAriaRole());
|
||||
}
|
||||
|
||||
function testCreateDom() {
|
||||
var element = buttonRenderer.createDom(button);
|
||||
assertNotNull('Element must not be null', element);
|
||||
assertEquals('Element must be a DIV', 'DIV', element.tagName);
|
||||
assertHTMLEquals('Element must have expected structure',
|
||||
'<div class="goog-button">Hello</div>',
|
||||
goog.dom.getOuterHtml(element));
|
||||
|
||||
button.setTooltip('Hello, world!');
|
||||
button.setValue('foo');
|
||||
element = buttonRenderer.createDom(button);
|
||||
assertNotNull('Element must not be null', element);
|
||||
assertEquals('Element must be a DIV', 'DIV', element.tagName);
|
||||
assertSameElements('Element must have expected class name',
|
||||
['goog-button'], goog.dom.classlist.get(element));
|
||||
assertEquals('Element must have expected title', 'Hello, world!',
|
||||
element.title);
|
||||
assertUndefined('Element must have no value', element.value);
|
||||
assertEquals('Element must have expected contents', 'Hello',
|
||||
element.innerHTML);
|
||||
|
||||
button.setSupportedState(goog.ui.Component.State.CHECKED, true);
|
||||
var element = buttonRenderer.createDom(button);
|
||||
assertEquals('button\'s aria-pressed attribute must be false', 'false',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
}
|
||||
|
||||
function testSetTooltip() {
|
||||
button.createDom();
|
||||
button.setTooltip('tooltip');
|
||||
assertEquals('tooltip', button.getElement().title);
|
||||
button.setTooltip('');
|
||||
assertEquals('', button.getElement().title);
|
||||
// IE7 doesn't support hasAttribute.
|
||||
if (button.getElement().hasAttribute) {
|
||||
assertFalse(button.getElement().hasAttribute('title'));
|
||||
}
|
||||
}
|
||||
|
||||
function testCreateDomAriaState() {
|
||||
button.setSupportedState(goog.ui.Component.State.CHECKED, true);
|
||||
button.setChecked(true);
|
||||
var element = buttonRenderer.createDom(button);
|
||||
|
||||
assertEquals('button\'s aria-pressed attribute must be true', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
}
|
||||
|
||||
function testUseAriaPressedForSelected() {
|
||||
button.setSupportedState(goog.ui.Component.State.SELECTED, true);
|
||||
button.setSelected(true);
|
||||
button.setRenderer(buttonRenderer);
|
||||
button.render();
|
||||
var element = button.getElement();
|
||||
|
||||
assertEquals('button\'s aria-pressed attribute must be true', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
assertEquals('button\'s aria-selected attribute must be empty', '',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.SELECTED));
|
||||
}
|
||||
|
||||
function testAriaDisabled() {
|
||||
button.setEnabled(false);
|
||||
button.setRenderer(buttonRenderer);
|
||||
button.render();
|
||||
var element = button.getElement();
|
||||
|
||||
assertEquals('button\'s aria-disabled attribute must be true', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.DISABLED));
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
sandbox.innerHTML =
|
||||
'<div id="foo">Foo</div>\n' +
|
||||
'<div id="bar" title="Hello, world!">Bar</div>\n' +
|
||||
'<div id="toggle">Toggle</div>';
|
||||
|
||||
var foo = new goog.ui.Button(null, buttonRenderer);
|
||||
foo.decorate(goog.dom.getElement('foo'));
|
||||
assertEquals('foo\'s tooltip must be the empty string', '',
|
||||
foo.getTooltip());
|
||||
foo.dispose();
|
||||
|
||||
var bar = new goog.ui.Button(null, buttonRenderer);
|
||||
bar.decorate(goog.dom.getElement('bar'));
|
||||
assertEquals('bar\'s tooltip must be initialized', 'Hello, world!',
|
||||
bar.getTooltip());
|
||||
bar.dispose();
|
||||
|
||||
var toggle = new goog.ui.Button(null, buttonRenderer);
|
||||
toggle.setSupportedState(goog.ui.Component.State.CHECKED, true);
|
||||
var element = goog.dom.getElement('toggle');
|
||||
assertNotNull(element);
|
||||
toggle.decorate(element);
|
||||
assertEquals('toggle\'s aria-pressed attribute must be false', 'false',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
toggle.dispose();
|
||||
}
|
||||
|
||||
function testCollapse() {
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.START);
|
||||
assertSameElements('Button should have class to collapse start',
|
||||
['goog-button-collapse-left'], button.getExtraClassNames());
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.END);
|
||||
assertSameElements('Button should have class to collapse end',
|
||||
['goog-button-collapse-right'], button.getExtraClassNames());
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.BOTH);
|
||||
assertSameElements('Button should have classes to collapse both',
|
||||
['goog-button-collapse-left', 'goog-button-collapse-right'],
|
||||
button.getExtraClassNames());
|
||||
}
|
||||
|
||||
function testCollapseRtl() {
|
||||
button.setRightToLeft(true);
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.START);
|
||||
assertSameElements('Button should have class to collapse start',
|
||||
['goog-button-collapse-right'], button.getExtraClassNames());
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.END);
|
||||
assertSameElements('Button should have class to collapse end',
|
||||
['goog-button-collapse-left'], button.getExtraClassNames());
|
||||
buttonRenderer.setCollapsed(button, goog.ui.ButtonSide.BOTH);
|
||||
assertSameElements('Button should have classes to collapse both',
|
||||
['goog-button-collapse-left', 'goog-button-collapse-right'],
|
||||
button.getExtraClassNames());
|
||||
}
|
||||
|
||||
function testCollapseWithStructuralClass() {
|
||||
testRenderer.setCollapsed(button, goog.ui.ButtonSide.BOTH);
|
||||
assertSameElements('Should use structural class for collapse classes',
|
||||
['goog-base-collapse-left', 'goog-base-collapse-right'],
|
||||
button.getExtraClassNames());
|
||||
}
|
||||
|
||||
function testUpdateAriaState() {
|
||||
var element = buttonRenderer.createDom(button);
|
||||
buttonRenderer.updateAriaState(element, goog.ui.Component.State.CHECKED,
|
||||
true);
|
||||
assertEquals('Button must have pressed ARIA state', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
|
||||
// Test for updating a state other than CHECKED
|
||||
buttonRenderer.updateAriaState(element, goog.ui.Component.State.DISABLED,
|
||||
true);
|
||||
assertEquals('Button must have disabled ARIA state', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.DISABLED));
|
||||
|
||||
buttonRenderer.updateAriaState(element, goog.ui.Component.State.CHECKED,
|
||||
false);
|
||||
assertEquals('Control must no longer have pressed ARIA state',
|
||||
'false',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
buttonRenderer.updateAriaState(element, goog.ui.Component.State.SELECTED,
|
||||
true);
|
||||
assertEquals('Button must have pressed ARIA state', 'true',
|
||||
goog.a11y.aria.getState(element, goog.a11y.aria.State.PRESSED));
|
||||
}
|
||||
|
||||
function testDoesntCallGetCssClassInConstructor() {
|
||||
goog.testing.ui.rendererasserts.
|
||||
assertNoGetCssClassCallsInConstructor(goog.ui.ButtonRenderer);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2010 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 Enum for button side constants. In its own file so as to not
|
||||
* cause a circular dependency with {@link goog.ui.ButtonRenderer}.
|
||||
*
|
||||
* @author doughtie@google.com (Gavin Doughtie)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ButtonSide');
|
||||
|
||||
|
||||
/**
|
||||
* Constants for button sides, see {@link goog.ui.Button.prototype.setCollapsed}
|
||||
* for details.
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.ui.ButtonSide = {
|
||||
/** Neither side. */
|
||||
NONE: 0,
|
||||
/** Left for LTR, right for RTL. */
|
||||
START: 1,
|
||||
/** Right for LTR, left for RTL. */
|
||||
END: 2,
|
||||
/** Both sides. */
|
||||
BOTH: 3
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// 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 Character counter widget implementation.
|
||||
*
|
||||
* @author eae@google.com (Emil A Eklund)
|
||||
* @see ../demos/charcounter.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.CharCounter');
|
||||
goog.provide('goog.ui.CharCounter.Display');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.InputHandler');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* CharCounter widget. Counts the number of characters in a input field or a
|
||||
* text box and displays the number of additional characters that may be
|
||||
* entered before the maximum length is reached.
|
||||
*
|
||||
* @extends {goog.events.EventTarget}
|
||||
* @param {HTMLInputElement|HTMLTextAreaElement} elInput Input or text area
|
||||
* element to count the number of characters in.
|
||||
* @param {Element} elCount HTML element to display the remaining number of
|
||||
* characters in. You can pass in null for this if you don't want to expose
|
||||
* the number of chars remaining.
|
||||
* @param {number} maxLength The maximum length.
|
||||
* @param {goog.ui.CharCounter.Display=} opt_displayMode Display mode for this
|
||||
* char counter. Defaults to {@link goog.ui.CharCounter.Display.REMAINING}.
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.ui.CharCounter = function(elInput, elCount, maxLength, opt_displayMode) {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
/**
|
||||
* Input or text area element to count the number of characters in.
|
||||
* @type {HTMLInputElement|HTMLTextAreaElement}
|
||||
* @private
|
||||
*/
|
||||
this.elInput_ = elInput;
|
||||
|
||||
/**
|
||||
* HTML element to display the remaining number of characters in.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this.elCount_ = elCount;
|
||||
|
||||
/**
|
||||
* The maximum length.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.maxLength_ = maxLength;
|
||||
|
||||
/**
|
||||
* The display mode for this char counter.
|
||||
* @type {!goog.ui.CharCounter.Display}
|
||||
* @private
|
||||
*/
|
||||
this.display_ = opt_displayMode || goog.ui.CharCounter.Display.REMAINING;
|
||||
|
||||
elInput.removeAttribute('maxlength');
|
||||
|
||||
/**
|
||||
* The input handler that provides the input event.
|
||||
* @type {goog.events.InputHandler}
|
||||
* @private
|
||||
*/
|
||||
this.inputHandler_ = new goog.events.InputHandler(elInput);
|
||||
|
||||
goog.events.listen(this.inputHandler_,
|
||||
goog.events.InputHandler.EventType.INPUT, this.onChange_, false, this);
|
||||
|
||||
this.checkLength();
|
||||
};
|
||||
goog.inherits(goog.ui.CharCounter, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* Display mode for the char counter.
|
||||
* @enum {number}
|
||||
*/
|
||||
goog.ui.CharCounter.Display = {
|
||||
/** Widget displays the number of characters remaining (the default). */
|
||||
REMAINING: 0,
|
||||
/** Widget displays the number of characters entered. */
|
||||
INCREMENTAL: 1
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the maximum length.
|
||||
*
|
||||
* @param {number} maxLength The maximum length.
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.setMaxLength = function(maxLength) {
|
||||
this.maxLength_ = maxLength;
|
||||
this.checkLength();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the maximum length.
|
||||
*
|
||||
* @return {number} The maximum length.
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.getMaxLength = function() {
|
||||
return this.maxLength_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the display mode.
|
||||
*
|
||||
* @param {!goog.ui.CharCounter.Display} displayMode The display mode.
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.setDisplayMode = function(displayMode) {
|
||||
this.display_ = displayMode;
|
||||
this.checkLength();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the display mode.
|
||||
*
|
||||
* @return {!goog.ui.CharCounter.Display} The display mode.
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.getDisplayMode = function() {
|
||||
return this.display_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Change event handler for input field.
|
||||
*
|
||||
* @param {goog.events.BrowserEvent} event Change event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.onChange_ = function(event) {
|
||||
this.checkLength();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks length of text in input field and updates the counter. Truncates text
|
||||
* if the maximum lengths is exceeded.
|
||||
*/
|
||||
goog.ui.CharCounter.prototype.checkLength = function() {
|
||||
var count = this.elInput_.value.length;
|
||||
|
||||
// There's no maxlength property for textareas so instead we truncate the
|
||||
// text if it gets too long. It's also used to truncate the text in a input
|
||||
// field if the maximum length is changed.
|
||||
if (count > this.maxLength_) {
|
||||
|
||||
var scrollTop = this.elInput_.scrollTop;
|
||||
var scrollLeft = this.elInput_.scrollLeft;
|
||||
|
||||
this.elInput_.value = this.elInput_.value.substring(0, this.maxLength_);
|
||||
count = this.maxLength_;
|
||||
|
||||
this.elInput_.scrollTop = scrollTop;
|
||||
this.elInput_.scrollLeft = scrollLeft;
|
||||
}
|
||||
|
||||
if (this.elCount_) {
|
||||
var incremental = this.display_ == goog.ui.CharCounter.Display.INCREMENTAL;
|
||||
goog.dom.setTextContent(
|
||||
this.elCount_,
|
||||
String(incremental ? count : this.maxLength_ - count));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CharCounter.prototype.disposeInternal = function() {
|
||||
goog.ui.CharCounter.superClass_.disposeInternal.call(this);
|
||||
delete this.elInput_;
|
||||
this.inputHandler_.dispose();
|
||||
this.inputHandler_ = null;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2014 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.CharCounter
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.CharCounterTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="test-textarea-id">
|
||||
</textarea>
|
||||
<div>
|
||||
Characters remaining:
|
||||
<span class="char-count">
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright 2014 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.CharCounterTest');
|
||||
goog.setTestOnly('goog.ui.CharCounterTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.asserts');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.CharCounter');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var countElement, charCounter, inputElement;
|
||||
var incremental = goog.ui.CharCounter.Display.INCREMENTAL;
|
||||
var remaining = goog.ui.CharCounter.Display.REMAINING;
|
||||
var maxLength = 25;
|
||||
|
||||
function setUp() {
|
||||
inputElement = goog.dom.getElement('test-textarea-id');
|
||||
inputElement.value = '';
|
||||
countElement = goog.dom.getElementByClass('char-count');
|
||||
goog.dom.setTextContent(countElement, '');
|
||||
charCounter =
|
||||
new goog.ui.CharCounter(inputElement, countElement, maxLength);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
charCounter.dispose();
|
||||
}
|
||||
|
||||
function setupCheckLength(content, mode) {
|
||||
inputElement.value = content;
|
||||
charCounter.setDisplayMode(mode);
|
||||
charCounter.checkLength();
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertNotNull('Character counter can not be null', charCounter);
|
||||
assertEquals(maxLength.toString(), goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
function testSetMaxLength() {
|
||||
charCounter.setMaxLength(10);
|
||||
assertEquals('10', goog.dom.getTextContent(countElement));
|
||||
|
||||
var tooLongContent = 'This is too long text content';
|
||||
inputElement.value = tooLongContent;
|
||||
charCounter.setMaxLength(10);
|
||||
assertEquals('0', goog.dom.getTextContent(countElement));
|
||||
assertEquals('This is to', inputElement.value);
|
||||
}
|
||||
|
||||
function testGetMaxLength() {
|
||||
assertEquals(maxLength, charCounter.getMaxLength());
|
||||
}
|
||||
|
||||
function testSetDisplayMode() {
|
||||
// Test counter to be in incremental mode
|
||||
charCounter.setDisplayMode(incremental);
|
||||
assertEquals('0', goog.dom.getTextContent(countElement));
|
||||
|
||||
// Test counter to be in remaining mode
|
||||
charCounter.setDisplayMode(remaining);
|
||||
assertEquals(maxLength.toString(), goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
function testGetDisplayMode() {
|
||||
assertEquals(remaining, charCounter.getDisplayMode());
|
||||
|
||||
var incrementalCharCounter = new goog.ui.CharCounter(
|
||||
inputElement, countElement, maxLength, incremental);
|
||||
assertEquals(incremental, incrementalCharCounter.getDisplayMode());
|
||||
}
|
||||
|
||||
function testCheckLength() {
|
||||
// Test the characters remaining in DOM
|
||||
setupCheckLength('', remaining);
|
||||
assertEquals(maxLength.toString(), goog.dom.getTextContent(countElement));
|
||||
|
||||
// Test the characters incremental in DOM
|
||||
setupCheckLength('', incremental);
|
||||
assertEquals('0', goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
function testCheckLength_limitedContent() {
|
||||
var limitedContent = 'Limited text content';
|
||||
var limitedContentLength = limitedContent.length;
|
||||
var remainingLimitedContentLength = maxLength - limitedContentLength;
|
||||
|
||||
// Set some content and test the characters remaining in DOM
|
||||
setupCheckLength(limitedContent, remaining);
|
||||
|
||||
assertEquals(limitedContent, inputElement.value);
|
||||
assertEquals(remainingLimitedContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
|
||||
// Test the characters incremented in DOM with limited content
|
||||
charCounter.setDisplayMode(incremental);
|
||||
charCounter.checkLength();
|
||||
|
||||
assertEquals(limitedContent, inputElement.value);
|
||||
assertEquals(limitedContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
function testCheckLength_overflowContent() {
|
||||
var tooLongContent = 'This is too long text content';
|
||||
var truncatedContent = 'This is too long text con';
|
||||
|
||||
// Set content longer than the maxLength and test the characters remaining
|
||||
// in DOM with overflowing content
|
||||
setupCheckLength(tooLongContent, remaining);
|
||||
|
||||
assertEquals(truncatedContent, inputElement.value);
|
||||
assertEquals('0', goog.dom.getTextContent(countElement));
|
||||
|
||||
// Set content longer than the maxLength and test the characters
|
||||
// incremented in DOM with overflowing content
|
||||
setupCheckLength(tooLongContent, incremental);
|
||||
|
||||
assertEquals(truncatedContent, inputElement.value);
|
||||
assertEquals(maxLength.toString(), goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
function testCheckLength_newLineContent() {
|
||||
var newLineContent = 'New\nline';
|
||||
var newLineContentLength = newLineContent.length;
|
||||
var remainingNewLineContentLength = maxLength - newLineContentLength;
|
||||
|
||||
var carriageReturnContent = 'New\r\nline';
|
||||
var carriageReturnContentLength = carriageReturnContent.length;
|
||||
var remainingCarriageReturnContentLength =
|
||||
maxLength - carriageReturnContentLength;
|
||||
|
||||
// Set some content with new line characters and test the characters
|
||||
// remaining in DOM
|
||||
setupCheckLength(newLineContent, remaining);
|
||||
|
||||
// Test for IE 7,8 which appends \r to \n
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9.0')) {
|
||||
assertEquals(carriageReturnContent, inputElement.value);
|
||||
assertEquals(remainingCarriageReturnContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
} else {
|
||||
assertEquals(newLineContent, inputElement.value);
|
||||
assertEquals(remainingNewLineContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
// Set some content with new line characters and test the characters
|
||||
// incremental in DOM
|
||||
setupCheckLength(newLineContent, incremental);
|
||||
|
||||
// Test for IE 7,8 which appends \r to \n
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9.0')) {
|
||||
assertEquals(carriageReturnContent, inputElement.value);
|
||||
assertEquals(carriageReturnContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
} else {
|
||||
assertEquals(newLineContent, inputElement.value);
|
||||
assertEquals(newLineContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
}
|
||||
}
|
||||
|
||||
function testCheckLength_carriageReturnContent() {
|
||||
var newLineContent = 'New\nline';
|
||||
var newLineContentLength = newLineContent.length;
|
||||
var remainingNewLineContentLength = maxLength - newLineContentLength;
|
||||
|
||||
var carriageReturnContent = 'New\r\nline';
|
||||
var carriageReturnContentLength = carriageReturnContent.length;
|
||||
var remainingCarriageReturnContentLength =
|
||||
maxLength - carriageReturnContentLength;
|
||||
|
||||
// Set some content with carriage return characters and test the
|
||||
// characters remaining in DOM
|
||||
setupCheckLength(carriageReturnContent, remaining);
|
||||
|
||||
// Test for IE 7,8
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9.0')) {
|
||||
assertEquals(carriageReturnContent, inputElement.value);
|
||||
assertEquals(remainingCarriageReturnContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
} else {
|
||||
// Others replace \r\n with \n
|
||||
assertEquals(newLineContent, inputElement.value);
|
||||
assertEquals(remainingNewLineContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
}
|
||||
|
||||
// Set some content with carriage return characters and test the
|
||||
// characters incremental in DOM
|
||||
setupCheckLength(carriageReturnContent, incremental);
|
||||
|
||||
// Test for IE 7,8
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9.0')) {
|
||||
assertEquals(carriageReturnContent, inputElement.value);
|
||||
assertEquals(carriageReturnContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
} else {
|
||||
// Others replace \r\n with \n
|
||||
assertEquals(newLineContent, inputElement.value);
|
||||
assertEquals(newLineContentLength.toString(),
|
||||
goog.dom.getTextContent(countElement));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,922 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Character Picker widget for picking any Unicode character.
|
||||
*
|
||||
* @see ../demos/charpicker.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.CharPicker');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.InputHandler');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.events.KeyHandler');
|
||||
goog.require('goog.i18n.CharListDecompressor');
|
||||
goog.require('goog.i18n.uChar');
|
||||
goog.require('goog.structs.Set');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Button');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.ContainerScroller');
|
||||
goog.require('goog.ui.FlatButtonRenderer');
|
||||
goog.require('goog.ui.HoverCard');
|
||||
goog.require('goog.ui.LabelInput');
|
||||
goog.require('goog.ui.Menu');
|
||||
goog.require('goog.ui.MenuButton');
|
||||
goog.require('goog.ui.MenuItem');
|
||||
goog.require('goog.ui.Tooltip');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Character Picker Class. This widget can be used to pick any Unicode
|
||||
* character by traversing a category-subcategory structure or by inputing its
|
||||
* hex value.
|
||||
*
|
||||
* See charpicker.html demo for example usage.
|
||||
* @param {goog.i18n.CharPickerData} charPickerData Category names and charlist.
|
||||
* @param {!goog.i18n.uChar.NameFetcher} charNameFetcher Object which fetches
|
||||
* the names of the characters that are shown in the widget. These names
|
||||
* may be stored locally or come from an external source.
|
||||
* @param {Array<string>=} opt_recents List of characters to be displayed in
|
||||
* resently selected characters area.
|
||||
* @param {number=} opt_initCategory Sequence number of initial category.
|
||||
* @param {number=} opt_initSubcategory Sequence number of initial subcategory.
|
||||
* @param {number=} opt_rowCount Number of rows in the grid.
|
||||
* @param {number=} opt_columnCount Number of columns in the grid.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Component}
|
||||
* @final
|
||||
*/
|
||||
goog.ui.CharPicker = function(charPickerData, charNameFetcher, opt_recents,
|
||||
opt_initCategory, opt_initSubcategory,
|
||||
opt_rowCount, opt_columnCount, opt_domHelper) {
|
||||
goog.ui.Component.call(this, opt_domHelper);
|
||||
|
||||
/**
|
||||
* Object used to retrieve character names.
|
||||
* @type {!goog.i18n.uChar.NameFetcher}
|
||||
* @private
|
||||
*/
|
||||
this.charNameFetcher_ = charNameFetcher;
|
||||
|
||||
/**
|
||||
* Object containing character lists and category names.
|
||||
* @type {goog.i18n.CharPickerData}
|
||||
* @private
|
||||
*/
|
||||
this.data_ = charPickerData;
|
||||
|
||||
/**
|
||||
* The category number to be used on widget init.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.initCategory_ = opt_initCategory || 0;
|
||||
|
||||
/**
|
||||
* The subcategory number to be used on widget init.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.initSubcategory_ = opt_initSubcategory || 0;
|
||||
|
||||
/**
|
||||
* Number of columns in the grid.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.columnCount_ = opt_columnCount || 10;
|
||||
|
||||
/**
|
||||
* Number of entries to be added to the grid.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.gridsize_ = (opt_rowCount || 10) * this.columnCount_;
|
||||
|
||||
/**
|
||||
* Number of the recently selected characters displayed.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.recentwidth_ = this.columnCount_ + 1;
|
||||
|
||||
/**
|
||||
* List of recently used characters.
|
||||
* @type {Array<string>}
|
||||
* @private
|
||||
*/
|
||||
this.recents_ = opt_recents || [];
|
||||
|
||||
/**
|
||||
* Handler for events.
|
||||
* @type {goog.events.EventHandler<!goog.ui.CharPicker>}
|
||||
* @private
|
||||
*/
|
||||
this.eventHandler_ = new goog.events.EventHandler(this);
|
||||
|
||||
/**
|
||||
* Decompressor used to get the list of characters from a base88 encoded
|
||||
* character list.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.decompressor_ = new goog.i18n.CharListDecompressor();
|
||||
};
|
||||
goog.inherits(goog.ui.CharPicker, goog.ui.Component);
|
||||
|
||||
|
||||
/**
|
||||
* The last selected character.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.selectedChar_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Set of formatting characters whose display need to be swapped with nbsp
|
||||
* to prevent layout issues.
|
||||
* @type {goog.structs.Set}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.layoutAlteringChars_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The top category menu.
|
||||
* @type {goog.ui.Menu}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.menu_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The top category menu button.
|
||||
* @type {goog.ui.MenuButton}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.menubutton_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The subcategory menu.
|
||||
* @type {goog.ui.Menu}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.submenu_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The subcategory menu button.
|
||||
* @type {goog.ui.MenuButton}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.submenubutton_ = null;
|
||||
|
||||
|
||||
/** @type {number} */
|
||||
goog.ui.CharPicker.prototype.itempos;
|
||||
|
||||
|
||||
/** @type {!Array<string>} */
|
||||
goog.ui.CharPicker.prototype.items;
|
||||
|
||||
|
||||
/** @private {!goog.events.KeyHandler} */
|
||||
goog.ui.CharPicker.prototype.keyHandler_;
|
||||
|
||||
|
||||
/**
|
||||
* Category index used to index the data tables.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.category;
|
||||
|
||||
|
||||
/** @private {Element} */
|
||||
goog.ui.CharPicker.prototype.stick_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The element representing the number of rows visible in the grid.
|
||||
* This along with goog.ui.CharPicker.stick_ would help to create a scrollbar
|
||||
* of right size.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.stickwrap_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The component containing all the buttons for each character in display.
|
||||
* @type {goog.ui.Component}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.grid_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The component used for extra information about the character set displayed.
|
||||
* @type {goog.ui.Component}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.notice_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Grid displaying recently selected characters.
|
||||
* @type {goog.ui.Component}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.recentgrid_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Input field for entering the hex value of the character.
|
||||
* @type {goog.ui.Component}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.input_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* OK button for entering hex value of the character.
|
||||
* @private {goog.ui.Button}
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.okbutton_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Element displaying character name in preview.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.charNameEl_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Element displaying character in preview.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.zoomEl_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Element displaying character number (codepoint) in preview.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.unicodeEl_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Hover card for displaying the preview of a character.
|
||||
* Preview would contain character in large size and its U+ notation. It would
|
||||
* also display the name, if available.
|
||||
* @type {goog.ui.HoverCard}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.hc_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the last selected character.
|
||||
* @return {?string} The last selected character.
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.getSelectedChar = function() {
|
||||
return this.selectedChar_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the list of characters user selected recently.
|
||||
* @return {Array<string>} The recent character list.
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.getRecentChars = function() {
|
||||
return this.recents_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CharPicker.prototype.createDom = function() {
|
||||
goog.ui.CharPicker.superClass_.createDom.call(this);
|
||||
|
||||
this.decorateInternal(this.getDomHelper().createElement('div'));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CharPicker.prototype.disposeInternal = function() {
|
||||
goog.dispose(this.hc_);
|
||||
this.hc_ = null;
|
||||
goog.dispose(this.eventHandler_);
|
||||
this.eventHandler_ = null;
|
||||
goog.ui.CharPicker.superClass_.disposeInternal.call(this);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CharPicker.prototype.decorateInternal = function(element) {
|
||||
goog.ui.CharPicker.superClass_.decorateInternal.call(this, element);
|
||||
|
||||
// The chars below cause layout disruption or too narrow to hover:
|
||||
// \u0020, \u00AD, \u2000 - \u200f, \u2028 - \u202f, \u3000, \ufeff
|
||||
var chrs = this.decompressor_.toCharList(':2%C^O80V1H2s2G40Q%s0');
|
||||
this.layoutAlteringChars_ = new goog.structs.Set(chrs);
|
||||
|
||||
this.menu_ = new goog.ui.Menu(this.getDomHelper());
|
||||
|
||||
var categories = this.data_.categories;
|
||||
for (var i = 0; i < this.data_.categories.length; i++) {
|
||||
this.menu_.addChild(this.createMenuItem_(i, categories[i]), true);
|
||||
}
|
||||
|
||||
this.menubutton_ = new goog.ui.MenuButton('Category Menu', this.menu_,
|
||||
/* opt_renderer */ undefined, this.getDomHelper());
|
||||
this.addChild(this.menubutton_, true);
|
||||
|
||||
this.submenu_ = new goog.ui.Menu(this.getDomHelper());
|
||||
|
||||
this.submenubutton_ = new goog.ui.MenuButton('Subcategory Menu',
|
||||
this.submenu_, /* opt_renderer */ undefined, this.getDomHelper());
|
||||
this.addChild(this.submenubutton_, true);
|
||||
|
||||
// The containing component for grid component and the scroller.
|
||||
var gridcontainer = new goog.ui.Component(this.getDomHelper());
|
||||
this.addChild(gridcontainer, true);
|
||||
|
||||
var stickwrap = new goog.ui.Component(this.getDomHelper());
|
||||
gridcontainer.addChild(stickwrap, true);
|
||||
this.stickwrap_ = stickwrap.getElement();
|
||||
|
||||
var stick = new goog.ui.Component(this.getDomHelper());
|
||||
stickwrap.addChild(stick, true);
|
||||
this.stick_ = stick.getElement();
|
||||
|
||||
this.grid_ = new goog.ui.Component(this.getDomHelper());
|
||||
gridcontainer.addChild(this.grid_, true);
|
||||
|
||||
this.notice_ = new goog.ui.Component(this.getDomHelper());
|
||||
this.notice_.setElementInternal(this.getDomHelper().createDom('div'));
|
||||
this.addChild(this.notice_, true);
|
||||
|
||||
// The component used for displaying 'Recent Selections' label.
|
||||
/**
|
||||
* @desc The text label above the list of recently selected characters.
|
||||
*/
|
||||
var MSG_CHAR_PICKER_RECENT_SELECTIONS = goog.getMsg('Recent Selections:');
|
||||
var recenttext = new goog.ui.Component(this.getDomHelper());
|
||||
recenttext.setElementInternal(this.getDomHelper().createDom('span', null,
|
||||
MSG_CHAR_PICKER_RECENT_SELECTIONS));
|
||||
this.addChild(recenttext, true);
|
||||
|
||||
this.recentgrid_ = new goog.ui.Component(this.getDomHelper());
|
||||
this.addChild(this.recentgrid_, true);
|
||||
|
||||
// The component used for displaying 'U+'.
|
||||
var uplus = new goog.ui.Component(this.getDomHelper());
|
||||
uplus.setElementInternal(this.getDomHelper().createDom('span', null, 'U+'));
|
||||
this.addChild(uplus, true);
|
||||
|
||||
/**
|
||||
* @desc The text inside the input box to specify the hex code of a character.
|
||||
*/
|
||||
var MSG_CHAR_PICKER_HEX_INPUT = goog.getMsg('Hex Input');
|
||||
this.input_ = new goog.ui.LabelInput(
|
||||
MSG_CHAR_PICKER_HEX_INPUT, this.getDomHelper());
|
||||
this.addChild(this.input_, true);
|
||||
|
||||
this.okbutton_ = new goog.ui.Button(
|
||||
'OK', /* opt_renderer */ undefined, this.getDomHelper());
|
||||
this.addChild(this.okbutton_, true);
|
||||
this.okbutton_.setEnabled(false);
|
||||
|
||||
this.zoomEl_ = this.getDomHelper().createDom('div',
|
||||
{id: 'zoom', className: goog.getCssName('goog-char-picker-char-zoom')});
|
||||
|
||||
this.charNameEl_ = this.getDomHelper().createDom('div',
|
||||
{id: 'charName', className: goog.getCssName('goog-char-picker-name')});
|
||||
|
||||
this.unicodeEl_ = this.getDomHelper().createDom('div',
|
||||
{id: 'unicode', className: goog.getCssName('goog-char-picker-unicode')});
|
||||
|
||||
var card = this.getDomHelper().createDom('div',
|
||||
{'id': 'preview'},
|
||||
this.zoomEl_, this.charNameEl_, this.unicodeEl_);
|
||||
goog.style.setElementShown(card, false);
|
||||
this.hc_ = new goog.ui.HoverCard({'DIV': 'char'},
|
||||
/* opt_checkDescendants */ undefined, this.getDomHelper());
|
||||
this.hc_.setElement(card);
|
||||
var self = this;
|
||||
|
||||
/**
|
||||
* Function called by hover card just before it is visible to collect data.
|
||||
*/
|
||||
function onBeforeShow() {
|
||||
var trigger = self.hc_.getAnchorElement();
|
||||
var ch = self.getChar_(trigger);
|
||||
if (ch) {
|
||||
goog.dom.setTextContent(self.zoomEl_, self.displayChar_(ch));
|
||||
goog.dom.setTextContent(self.unicodeEl_, goog.i18n.uChar.toHexString(ch));
|
||||
// Clear the character name since we don't want to show old data because
|
||||
// it is retrieved asynchronously and the DOM object is re-used
|
||||
goog.dom.setTextContent(self.charNameEl_, '');
|
||||
self.charNameFetcher_.getName(ch, function(charName) {
|
||||
if (charName) {
|
||||
goog.dom.setTextContent(self.charNameEl_, charName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goog.events.listen(this.hc_, goog.ui.HoverCard.EventType.BEFORE_SHOW,
|
||||
onBeforeShow);
|
||||
goog.asserts.assert(element);
|
||||
goog.dom.classlist.add(element, goog.getCssName('goog-char-picker'));
|
||||
goog.dom.classlist.add(goog.asserts.assert(this.stick_),
|
||||
goog.getCssName('goog-stick'));
|
||||
goog.dom.classlist.add(goog.asserts.assert(this.stickwrap_),
|
||||
goog.getCssName('goog-stickwrap'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(gridcontainer.getElement()),
|
||||
goog.getCssName('goog-char-picker-grid-container'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.grid_.getElement()),
|
||||
goog.getCssName('goog-char-picker-grid'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.recentgrid_.getElement()),
|
||||
goog.getCssName('goog-char-picker-grid'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.recentgrid_.getElement()),
|
||||
goog.getCssName('goog-char-picker-recents'));
|
||||
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.notice_.getElement()),
|
||||
goog.getCssName('goog-char-picker-notice'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(uplus.getElement()),
|
||||
goog.getCssName('goog-char-picker-uplus'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.input_.getElement()),
|
||||
goog.getCssName('goog-char-picker-input-box'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.okbutton_.getElement()),
|
||||
goog.getCssName('goog-char-picker-okbutton'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(card),
|
||||
goog.getCssName('goog-char-picker-hovercard'));
|
||||
|
||||
this.hc_.className = goog.getCssName('goog-char-picker-hovercard');
|
||||
|
||||
this.grid_.buttoncount = this.gridsize_;
|
||||
this.recentgrid_.buttoncount = this.recentwidth_;
|
||||
this.populateGridWithButtons_(this.grid_);
|
||||
this.populateGridWithButtons_(this.recentgrid_);
|
||||
|
||||
this.updateGrid_(this.recentgrid_, this.recents_);
|
||||
this.setSelectedCategory_(this.initCategory_, this.initSubcategory_);
|
||||
new goog.ui.ContainerScroller(this.menu_);
|
||||
new goog.ui.ContainerScroller(this.submenu_);
|
||||
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.menu_.getElement()),
|
||||
goog.getCssName('goog-char-picker-menu'));
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.submenu_.getElement()),
|
||||
goog.getCssName('goog-char-picker-menu'));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CharPicker.prototype.enterDocument = function() {
|
||||
goog.ui.CharPicker.superClass_.enterDocument.call(this);
|
||||
var inputkh = new goog.events.InputHandler(this.input_.getElement());
|
||||
this.keyHandler_ = new goog.events.KeyHandler(this.input_.getElement());
|
||||
|
||||
// Stop the propagation of ACTION events at menu and submenu buttons.
|
||||
// If stopped at capture phase, the button will not be set to normal state.
|
||||
// If not stopped, the user widget will receive the event, which is
|
||||
// undesired. User widget should receive an event only on the character
|
||||
// click.
|
||||
this.eventHandler_.
|
||||
listen(
|
||||
this.menubutton_,
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.events.Event.stopPropagation).
|
||||
listen(
|
||||
this.submenubutton_,
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.events.Event.stopPropagation).
|
||||
listen(
|
||||
this,
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
this.handleSelectedItem_,
|
||||
true).
|
||||
listen(
|
||||
inputkh,
|
||||
goog.events.InputHandler.EventType.INPUT,
|
||||
this.handleInput_).
|
||||
listen(
|
||||
this.keyHandler_,
|
||||
goog.events.KeyHandler.EventType.KEY,
|
||||
this.handleEnter_).
|
||||
listen(
|
||||
this.recentgrid_,
|
||||
goog.ui.Component.EventType.FOCUS,
|
||||
this.handleFocus_).
|
||||
listen(
|
||||
this.grid_,
|
||||
goog.ui.Component.EventType.FOCUS,
|
||||
this.handleFocus_);
|
||||
|
||||
goog.events.listen(this.okbutton_.getElement(),
|
||||
goog.events.EventType.MOUSEDOWN, this.handleOkClick_, true, this);
|
||||
|
||||
goog.events.listen(this.stickwrap_, goog.events.EventType.SCROLL,
|
||||
this.handleScroll_, true, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the button focus by updating the aria label with the character name
|
||||
* so it becomes possible to get spoken feedback while tabbing through the
|
||||
* visible symbols.
|
||||
* @param {goog.events.Event} e The focus event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleFocus_ = function(e) {
|
||||
var button = e.target;
|
||||
var element = button.getElement();
|
||||
var ch = this.getChar_(element);
|
||||
|
||||
// Clear the aria label to avoid speaking the old value in case the button
|
||||
// element has no char attribute or the character name cannot be retrieved.
|
||||
goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, '');
|
||||
|
||||
if (ch) {
|
||||
// This is working with screen readers because the call to getName is
|
||||
// synchronous once the values have been prefetched by the RemoteNameFetcher
|
||||
// and because it is always synchronous when using the LocalNameFetcher.
|
||||
// Also, the special character itself is not used as the label because some
|
||||
// screen readers, notably ChromeVox, are not able to speak them.
|
||||
// TODO(user): Consider changing the NameFetcher API to provide a
|
||||
// method that lets the caller retrieve multiple character names at once
|
||||
// so that this asynchronous gymnastic can be avoided.
|
||||
this.charNameFetcher_.getName(ch, function(charName) {
|
||||
if (charName) {
|
||||
goog.a11y.aria.setState(
|
||||
element, goog.a11y.aria.State.LABEL, charName);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On scroll, updates the grid with characters correct to the scroll position.
|
||||
* @param {goog.events.Event} e Scroll event to handle.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleScroll_ = function(e) {
|
||||
var height = e.target.scrollHeight;
|
||||
var top = e.target.scrollTop;
|
||||
var itempos = Math.ceil(top * this.items.length / (this.columnCount_ *
|
||||
height)) * this.columnCount_;
|
||||
if (this.itempos != itempos) {
|
||||
this.itempos = itempos;
|
||||
this.modifyGridWithItems_(this.grid_, this.items, itempos);
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On a menu click, sets correct character set in the grid; on a grid click
|
||||
* accept the character as the selected one and adds to recent selection, if not
|
||||
* already present.
|
||||
* @param {goog.events.Event} e Event for the click on menus or grid.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleSelectedItem_ = function(e) {
|
||||
var parent = /** @type {goog.ui.Component} */ (e.target).getParent();
|
||||
if (parent == this.menu_) {
|
||||
this.menu_.setVisible(false);
|
||||
this.setSelectedCategory_(e.target.getValue());
|
||||
} else if (parent == this.submenu_) {
|
||||
this.submenu_.setVisible(false);
|
||||
this.setSelectedSubcategory_(e.target.getValue());
|
||||
} else if (parent == this.grid_) {
|
||||
var button = e.target.getElement();
|
||||
this.selectedChar_ = this.getChar_(button);
|
||||
this.updateRecents_(this.selectedChar_);
|
||||
} else if (parent == this.recentgrid_) {
|
||||
this.selectedChar_ = this.getChar_(e.target.getElement());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* When user types the characters displays the preview. Enables the OK button,
|
||||
* if the character is valid.
|
||||
* @param {goog.events.Event} e Event for typing in input field.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleInput_ = function(e) {
|
||||
var ch = this.getInputChar();
|
||||
if (ch) {
|
||||
goog.dom.setTextContent(this.zoomEl_, ch);
|
||||
goog.dom.setTextContent(this.unicodeEl_, goog.i18n.uChar.toHexString(ch));
|
||||
goog.dom.setTextContent(this.charNameEl_, '');
|
||||
var coord =
|
||||
new goog.ui.Tooltip.ElementTooltipPosition(this.input_.getElement());
|
||||
this.hc_.setPosition(coord);
|
||||
this.hc_.triggerForElement(this.input_.getElement());
|
||||
this.okbutton_.setEnabled(true);
|
||||
} else {
|
||||
this.hc_.cancelTrigger();
|
||||
this.hc_.setVisible(false);
|
||||
this.okbutton_.setEnabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* On OK click accepts the character and updates the recent char list.
|
||||
* @param {goog.events.Event=} opt_event Event for click on OK button.
|
||||
* @return {boolean} Indicates whether to propagate event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleOkClick_ = function(opt_event) {
|
||||
var ch = this.getInputChar();
|
||||
if (ch && ch.charCodeAt(0)) {
|
||||
this.selectedChar_ = ch;
|
||||
this.updateRecents_(ch);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Behaves exactly like the OK button on Enter key.
|
||||
* @param {goog.events.KeyEvent} e Event for enter on the input field.
|
||||
* @return {boolean} Indicates whether to propagate event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.handleEnter_ = function(e) {
|
||||
if (e.keyCode == goog.events.KeyCodes.ENTER) {
|
||||
return this.handleOkClick_() ?
|
||||
this.dispatchEvent(goog.ui.Component.EventType.ACTION) : false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the character from the event target.
|
||||
* @param {Element} e Event target containing the 'char' attribute.
|
||||
* @return {string} The character specified in the event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.getChar_ = function(e) {
|
||||
return e.getAttribute('char');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a menu entry for either the category listing or subcategory listing.
|
||||
* @param {number} id Id to be used for the entry.
|
||||
* @param {string} caption Text displayed for the menu item.
|
||||
* @return {!goog.ui.MenuItem} Menu item to be added to the menu listing.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.createMenuItem_ = function(id, caption) {
|
||||
var item = new goog.ui.MenuItem(caption, /* model */ id, this.getDomHelper());
|
||||
item.setVisible(true);
|
||||
return item;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the category and updates the submenu items and grid accordingly.
|
||||
* @param {number} category Category index used to index the data tables.
|
||||
* @param {number=} opt_subcategory Subcategory index used with category index.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.setSelectedCategory_ = function(category,
|
||||
opt_subcategory) {
|
||||
this.category = category;
|
||||
this.menubutton_.setCaption(this.data_.categories[category]);
|
||||
while (this.submenu_.hasChildren()) {
|
||||
this.submenu_.removeChildAt(0, true).dispose();
|
||||
}
|
||||
|
||||
var subcategories = this.data_.subcategories[category];
|
||||
var charList = this.data_.charList[category];
|
||||
for (var i = 0; i < subcategories.length; i++) {
|
||||
var subtitle = charList[i].length == 0;
|
||||
var item = this.createMenuItem_(i, subcategories[i]);
|
||||
this.submenu_.addChild(item, true);
|
||||
}
|
||||
this.setSelectedSubcategory_(opt_subcategory || 0);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the subcategory and updates the grid accordingly.
|
||||
* @param {number} subcategory Sub-category index used to index the data tables.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.setSelectedSubcategory_ = function(subcategory) {
|
||||
var subcategories = this.data_.subcategories;
|
||||
var name = subcategories[this.category][subcategory];
|
||||
this.submenubutton_.setCaption(name);
|
||||
this.setSelectedGrid_(this.category, subcategory);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the grid according to a given category and subcategory.
|
||||
* @param {number} category Index to the category table.
|
||||
* @param {number} subcategory Index to the subcategory table.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.setSelectedGrid_ = function(category,
|
||||
subcategory) {
|
||||
var charLists = this.data_.charList;
|
||||
var charListStr = charLists[category][subcategory];
|
||||
var content = this.decompressor_.toCharList(charListStr);
|
||||
this.charNameFetcher_.prefetch(charListStr);
|
||||
this.updateGrid_(this.grid_, content);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the grid with new character list.
|
||||
* @param {goog.ui.Component} grid The grid which is updated with a new set of
|
||||
* characters.
|
||||
* @param {Array<string>} items Characters to be added to the grid.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.updateGrid_ = function(grid, items) {
|
||||
if (grid == this.grid_) {
|
||||
/**
|
||||
* @desc The message used when there are invisible characters like space
|
||||
* or format control characters.
|
||||
*/
|
||||
var MSG_PLEASE_HOVER =
|
||||
goog.getMsg('Please hover over each cell for the character name.');
|
||||
|
||||
goog.dom.setTextContent(this.notice_.getElement(),
|
||||
this.charNameFetcher_.isNameAvailable(
|
||||
items[0]) ? MSG_PLEASE_HOVER : '');
|
||||
this.items = items;
|
||||
if (this.stickwrap_.offsetHeight > 0) {
|
||||
this.stick_.style.height =
|
||||
this.stickwrap_.offsetHeight * items.length / this.gridsize_ + 'px';
|
||||
} else {
|
||||
// This is the last ditch effort if height is not avaialble.
|
||||
// Maximum of 3em is assumed to the the cell height. Extra space after
|
||||
// last character in the grid is OK.
|
||||
this.stick_.style.height = 3 * this.columnCount_ * items.length /
|
||||
this.gridsize_ + 'em';
|
||||
}
|
||||
this.stickwrap_.scrollTop = 0;
|
||||
}
|
||||
|
||||
this.modifyGridWithItems_(grid, items, 0);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the grid with new character list for a given starting point.
|
||||
* @param {goog.ui.Component} grid The grid which is updated with a new set of
|
||||
* characters.
|
||||
* @param {Array<string>} items Characters to be added to the grid.
|
||||
* @param {number} start The index from which the characters should be
|
||||
* displayed.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.modifyGridWithItems_ = function(grid, items,
|
||||
start) {
|
||||
for (var buttonpos = 0, itempos = start;
|
||||
buttonpos < grid.buttoncount && itempos < items.length;
|
||||
buttonpos++, itempos++) {
|
||||
this.modifyCharNode_(
|
||||
/** @type {!goog.ui.Button} */ (grid.getChildAt(buttonpos)),
|
||||
items[itempos]);
|
||||
}
|
||||
|
||||
for (; buttonpos < grid.buttoncount; buttonpos++) {
|
||||
grid.getChildAt(buttonpos).setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates the grid for characters to displayed for selection.
|
||||
* @param {goog.ui.Component} grid The grid which is updated with a new set of
|
||||
* characters.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.populateGridWithButtons_ = function(grid) {
|
||||
for (var i = 0; i < grid.buttoncount; i++) {
|
||||
var button = new goog.ui.Button(' ',
|
||||
goog.ui.FlatButtonRenderer.getInstance(),
|
||||
this.getDomHelper());
|
||||
|
||||
// Dispatch the focus event so we can update the aria description while
|
||||
// the user tabs through the cells.
|
||||
button.setDispatchTransitionEvents(goog.ui.Component.State.FOCUSED, true);
|
||||
|
||||
grid.addChild(button, true);
|
||||
button.setVisible(false);
|
||||
|
||||
var buttonEl = button.getElement();
|
||||
goog.asserts.assert(buttonEl, 'The button DOM element cannot be null.');
|
||||
|
||||
// Override the button role so the user doesn't hear "button" each time he
|
||||
// tabs through the cells.
|
||||
goog.a11y.aria.removeRole(buttonEl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the grid cell with new character.
|
||||
* @param {goog.ui.Button} button This button is popped up for new character.
|
||||
* @param {string} ch Character to be displayed by the button.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.modifyCharNode_ = function(button, ch) {
|
||||
var text = this.displayChar_(ch);
|
||||
var buttonEl = button.getElement();
|
||||
goog.dom.setTextContent(buttonEl, text);
|
||||
buttonEl.setAttribute('char', ch);
|
||||
button.setVisible(true);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a given character to the recent character list.
|
||||
* @param {string} character Character to be added to the recent list.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.updateRecents_ = function(character) {
|
||||
if (character && character.charCodeAt(0) &&
|
||||
!goog.array.contains(this.recents_, character)) {
|
||||
this.recents_.unshift(character);
|
||||
if (this.recents_.length > this.recentwidth_) {
|
||||
this.recents_.pop();
|
||||
}
|
||||
this.updateGrid_(this.recentgrid_, this.recents_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user inputed unicode character.
|
||||
* @return {string} Unicode character inputed by user.
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.getInputChar = function() {
|
||||
var text = this.input_.getValue();
|
||||
var code = parseInt(text, 16);
|
||||
return /** @type {string} */ (goog.i18n.uChar.fromCharCode(code));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the display character for the given character.
|
||||
* @param {string} ch Character whose display is fetched.
|
||||
* @return {string} The display of the given character.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CharPicker.prototype.displayChar_ = function(ch) {
|
||||
return this.layoutAlteringChars_.contains(ch) ? '\u00A0' : ch;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<!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.CharPicker
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.CharPickerTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="charpicker">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.CharPickerTest');
|
||||
goog.setTestOnly('goog.ui.CharPickerTest');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.dispose');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.i18n.CharPickerData');
|
||||
goog.require('goog.i18n.uChar.NameFetcher');
|
||||
goog.require('goog.testing.MockControl');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.mockmatchers');
|
||||
goog.require('goog.ui.CharPicker');
|
||||
goog.require('goog.ui.FlatButtonRenderer');
|
||||
|
||||
var charPicker, charPickerData, charPickerElement;
|
||||
var mockControl, charNameFetcherMock;
|
||||
|
||||
function setUp() {
|
||||
mockControl = new goog.testing.MockControl();
|
||||
charNameFetcherMock = mockControl.createLooseMock(
|
||||
goog.i18n.uChar.NameFetcher, true /* opt_ignoreUnexpectedCalls */);
|
||||
|
||||
charPickerData = new goog.i18n.CharPickerData();
|
||||
charPickerElement = goog.dom.getElement('charpicker');
|
||||
|
||||
charPicker = new goog.ui.CharPicker(
|
||||
charPickerData, charNameFetcherMock);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.dispose(charPicker);
|
||||
goog.dom.removeChildren(charPickerElement);
|
||||
mockControl.$tearDown();
|
||||
}
|
||||
|
||||
function testAriaLabelIsUpdatedOnFocus() {
|
||||
var character = '←';
|
||||
var characterName = 'right arrow';
|
||||
|
||||
charNameFetcherMock.getName(
|
||||
character,
|
||||
goog.testing.mockmatchers.isFunction).
|
||||
$does(function(c, callback) {
|
||||
callback(characterName);
|
||||
});
|
||||
|
||||
mockControl.$replayAll();
|
||||
|
||||
charPicker.decorate(charPickerElement);
|
||||
|
||||
// Get the first button elements within the grid div and override its
|
||||
// char attribute so the test doesn't depend on the actual grid content.
|
||||
var gridElement = goog.dom.getElementByClass(
|
||||
goog.getCssName('goog-char-picker-grid'), charPickerElement);
|
||||
var buttonElement = goog.dom.getElementsByClass(
|
||||
goog.ui.FlatButtonRenderer.CSS_CLASS, gridElement)[0];
|
||||
buttonElement.setAttribute('char', character);
|
||||
|
||||
// Trigger a focus event on the button element.
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.FOCUS, buttonElement));
|
||||
|
||||
mockControl.$verifyAll();
|
||||
|
||||
var ariaLabel = goog.a11y.aria.getState(
|
||||
buttonElement, goog.a11y.aria.State.LABEL);
|
||||
assertEquals('The aria label should be updated when the button' +
|
||||
'gains focus.', characterName, ariaLabel);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Tristate checkbox widget.
|
||||
*
|
||||
* @see ../demos/checkbox.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.Checkbox');
|
||||
goog.provide('goog.ui.Checkbox.State');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.ui.CheckboxRenderer');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Control');
|
||||
goog.require('goog.ui.registry');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 3-state checkbox widget. Fires CHECK or UNCHECK events before toggled and
|
||||
* CHANGE event after toggled by user.
|
||||
* The checkbox can also be enabled/disabled and get focused and highlighted.
|
||||
*
|
||||
* @param {goog.ui.Checkbox.State=} opt_checked Checked state to set.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @param {goog.ui.CheckboxRenderer=} opt_renderer Renderer used to render or
|
||||
* decorate the checkbox; defaults to {@link goog.ui.CheckboxRenderer}.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Control}
|
||||
*/
|
||||
goog.ui.Checkbox = function(opt_checked, opt_domHelper, opt_renderer) {
|
||||
var renderer = opt_renderer || goog.ui.CheckboxRenderer.getInstance();
|
||||
goog.ui.Control.call(this, null, renderer, opt_domHelper);
|
||||
// The checkbox maintains its own tri-state CHECKED state.
|
||||
// The control class maintains DISABLED, ACTIVE, and FOCUSED (which enable tab
|
||||
// navigation, and keyHandling with SPACE).
|
||||
|
||||
/**
|
||||
* Checked state of the checkbox.
|
||||
* @type {goog.ui.Checkbox.State}
|
||||
* @private
|
||||
*/
|
||||
this.checked_ = goog.isDef(opt_checked) ?
|
||||
opt_checked : goog.ui.Checkbox.State.UNCHECKED;
|
||||
};
|
||||
goog.inherits(goog.ui.Checkbox, goog.ui.Control);
|
||||
goog.tagUnsealableClass(goog.ui.Checkbox);
|
||||
|
||||
|
||||
/**
|
||||
* Possible checkbox states.
|
||||
* @enum {?boolean}
|
||||
*/
|
||||
goog.ui.Checkbox.State = {
|
||||
CHECKED: true,
|
||||
UNCHECKED: false,
|
||||
UNDETERMINED: null
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Label element bound to the checkbox.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.label_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.ui.Checkbox.State} Checked state of the checkbox.
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.getChecked = function() {
|
||||
return this.checked_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the checkbox is checked.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.isChecked = function() {
|
||||
return this.checked_ == goog.ui.Checkbox.State.CHECKED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the checkbox is not checked.
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.isUnchecked = function() {
|
||||
return this.checked_ == goog.ui.Checkbox.State.UNCHECKED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the checkbox is in partially checked state.
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.isUndetermined = function() {
|
||||
return this.checked_ == goog.ui.Checkbox.State.UNDETERMINED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the checked state of the checkbox.
|
||||
* @param {?boolean} checked The checked state to set.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.setChecked = function(checked) {
|
||||
if (checked != this.checked_) {
|
||||
this.checked_ = /** @type {goog.ui.Checkbox.State} */ (checked);
|
||||
this.getRenderer().setCheckboxState(this.getElement(), this.checked_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the checked state for the checkbox. Unlike {@link #setChecked},
|
||||
* doesn't update the checkbox's DOM. Considered protected; to be called
|
||||
* only by renderer code during element decoration.
|
||||
* @param {goog.ui.Checkbox.State} checked New checkbox state.
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.setCheckedInternal = function(checked) {
|
||||
this.checked_ = checked;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Binds an HTML element to the checkbox which if clicked toggles the checkbox.
|
||||
* Behaves the same way as the 'label' HTML tag. The label element has to be the
|
||||
* direct or non-direct ancestor of the checkbox element because it will get the
|
||||
* focus when keyboard support is implemented.
|
||||
* Note: Control#enterDocument also sets aria-label on the element but
|
||||
* Checkbox#enterDocument sets aria-labeledby on the same element which
|
||||
* overrides the aria-label in all modern screen readers.
|
||||
*
|
||||
* @param {Element} label The label control to set. If null, only the checkbox
|
||||
* reacts to clicks.
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.setLabel = function(label) {
|
||||
if (this.isInDocument()) {
|
||||
this.exitDocument();
|
||||
this.label_ = label;
|
||||
this.enterDocument();
|
||||
} else {
|
||||
this.label_ = label;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Toggles the checkbox. State transitions:
|
||||
* <ul>
|
||||
* <li>unchecked -> checked
|
||||
* <li>undetermined -> checked
|
||||
* <li>checked -> unchecked
|
||||
* </ul>
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.toggle = function() {
|
||||
this.setChecked(this.checked_ ? goog.ui.Checkbox.State.UNCHECKED :
|
||||
goog.ui.Checkbox.State.CHECKED);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Checkbox.prototype.enterDocument = function() {
|
||||
goog.ui.Checkbox.base(this, 'enterDocument');
|
||||
if (this.isHandleMouseEvents()) {
|
||||
var handler = this.getHandler();
|
||||
// Listen to the label, if it was set.
|
||||
if (this.label_) {
|
||||
// Any mouse events that happen to the associated label should have the
|
||||
// same effect on the checkbox as if they were happening to the checkbox
|
||||
// itself.
|
||||
handler.
|
||||
listen(this.label_, goog.events.EventType.CLICK,
|
||||
this.handleClickOrSpace_).
|
||||
listen(this.label_, goog.events.EventType.MOUSEOVER,
|
||||
this.handleMouseOver).
|
||||
listen(this.label_, goog.events.EventType.MOUSEOUT,
|
||||
this.handleMouseOut).
|
||||
listen(this.label_, goog.events.EventType.MOUSEDOWN,
|
||||
this.handleMouseDown).
|
||||
listen(this.label_, goog.events.EventType.MOUSEUP,
|
||||
this.handleMouseUp);
|
||||
}
|
||||
// Checkbox needs to explicitly listen for click event.
|
||||
handler.listen(this.getElement(),
|
||||
goog.events.EventType.CLICK, this.handleClickOrSpace_);
|
||||
}
|
||||
|
||||
// Set aria label.
|
||||
var checkboxElement = this.getElementStrict();
|
||||
if (this.label_ && checkboxElement != this.label_ &&
|
||||
goog.string.isEmptyOrWhitespace(goog.a11y.aria.getLabel(checkboxElement))) {
|
||||
if (!this.label_.id) {
|
||||
this.label_.id = this.makeId('lbl');
|
||||
}
|
||||
goog.a11y.aria.setState(checkboxElement,
|
||||
goog.a11y.aria.State.LABELLEDBY,
|
||||
this.label_.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Fix for tabindex not being updated so that disabled checkbox is not
|
||||
* focusable. In particular this fails in Chrome.
|
||||
* Note: in general tabIndex=-1 will prevent from keyboard focus but enables
|
||||
* mouse focus, however in this case the control class prevents mouse focus.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.setEnabled = function(enabled) {
|
||||
goog.ui.Checkbox.base(this, 'setEnabled', enabled);
|
||||
var el = this.getElement();
|
||||
if (el) {
|
||||
el.tabIndex = this.isEnabled() ? 0 : -1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the click event.
|
||||
* @param {!goog.events.BrowserEvent} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.Checkbox.prototype.handleClickOrSpace_ = function(e) {
|
||||
e.stopPropagation();
|
||||
var eventType = this.checked_ ? goog.ui.Component.EventType.UNCHECK :
|
||||
goog.ui.Component.EventType.CHECK;
|
||||
if (this.isEnabled() && !e.target.href && this.dispatchEvent(eventType)) {
|
||||
e.preventDefault(); // Prevent scrolling in Chrome if SPACE is pressed.
|
||||
this.toggle();
|
||||
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.Checkbox.prototype.handleKeyEventInternal = function(e) {
|
||||
if (e.keyCode == goog.events.KeyCodes.SPACE) {
|
||||
this.performActionInternal(e);
|
||||
this.handleClickOrSpace_(e);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Register this control so it can be created from markup.
|
||||
*/
|
||||
goog.ui.registry.setDecoratorByClassName(
|
||||
goog.ui.CheckboxRenderer.CSS_CLASS,
|
||||
function() {
|
||||
return new goog.ui.Checkbox();
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<!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.Checkbox
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.CheckboxTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<span id="decorate" class="goog-checkbox">
|
||||
</span>
|
||||
<span id="normal" class="goog-checkbox">
|
||||
</span>
|
||||
<span id="checked" class="goog-checkbox goog-checkbox-checked">
|
||||
</span>
|
||||
<span id="unchecked" class="goog-checkbox goog-checkbox-unchecked">
|
||||
</span>
|
||||
<span id="undetermined" class="goog-checkbox goog-checkbox-undetermined">
|
||||
</span>
|
||||
<span id="disabled" class="goog-checkbox goog-checkbox-disabled">
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,451 @@
|
||||
// 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.CheckboxTest');
|
||||
goog.setTestOnly('goog.ui.CheckboxTest');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Checkbox');
|
||||
goog.require('goog.ui.CheckboxRenderer');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.ControlRenderer');
|
||||
goog.require('goog.ui.decorate');
|
||||
|
||||
var checkbox;
|
||||
|
||||
function setUp() {
|
||||
checkbox = new goog.ui.Checkbox();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
checkbox.dispose();
|
||||
}
|
||||
|
||||
function testClassNames() {
|
||||
checkbox.createDom();
|
||||
|
||||
checkbox.setChecked(false);
|
||||
assertSameElements('classnames of unchecked checkbox',
|
||||
['goog-checkbox', 'goog-checkbox-unchecked'],
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
|
||||
checkbox.setChecked(true);
|
||||
assertSameElements('classnames of checked checkbox',
|
||||
['goog-checkbox', 'goog-checkbox-checked'],
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
|
||||
checkbox.setChecked(null);
|
||||
assertSameElements('classnames of partially checked checkbox',
|
||||
['goog-checkbox', 'goog-checkbox-undetermined'],
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
|
||||
checkbox.setEnabled(false);
|
||||
assertSameElements('classnames of partially checked disabled checkbox',
|
||||
['goog-checkbox',
|
||||
'goog-checkbox-undetermined',
|
||||
'goog-checkbox-disabled'],
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
}
|
||||
|
||||
function testIsEnabled() {
|
||||
assertTrue('enabled by default', checkbox.isEnabled());
|
||||
checkbox.setEnabled(false);
|
||||
assertFalse('has been disabled', checkbox.isEnabled());
|
||||
}
|
||||
|
||||
function testCheckedState() {
|
||||
assertTrue('unchecked by default', !checkbox.isChecked() &&
|
||||
checkbox.isUnchecked() && !checkbox.isUndetermined());
|
||||
|
||||
checkbox.setChecked(true);
|
||||
assertTrue('set to checked', checkbox.isChecked() &&
|
||||
!checkbox.isUnchecked() && !checkbox.isUndetermined());
|
||||
|
||||
checkbox.setChecked(null);
|
||||
assertTrue('set to partially checked', !checkbox.isChecked() &&
|
||||
!checkbox.isUnchecked() && checkbox.isUndetermined());
|
||||
}
|
||||
|
||||
function testToggle() {
|
||||
checkbox.setChecked(null);
|
||||
checkbox.toggle();
|
||||
assertTrue('undetermined -> checked', checkbox.getChecked());
|
||||
checkbox.toggle();
|
||||
assertFalse('checked -> unchecked', checkbox.getChecked());
|
||||
checkbox.toggle();
|
||||
assertTrue('unchecked -> checked', checkbox.getChecked());
|
||||
}
|
||||
|
||||
function testEvents() {
|
||||
checkbox.render();
|
||||
|
||||
var events = [];
|
||||
goog.events.listen(checkbox,
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK,
|
||||
goog.ui.Component.EventType.UNCHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
function(e) {
|
||||
events.push(e.type);
|
||||
});
|
||||
|
||||
checkbox.setEnabled(false);
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertArrayEquals('disabled => no events', [], events);
|
||||
assertFalse('checked state did not change', checkbox.getChecked());
|
||||
events = [];
|
||||
|
||||
checkbox.setEnabled(true);
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertArrayEquals('ACTION+CHECK+CHANGE fired',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
events);
|
||||
assertTrue('checkbox became checked', checkbox.getChecked());
|
||||
events = [];
|
||||
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertArrayEquals('ACTION+UNCHECK+CHANGE fired',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.UNCHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
events);
|
||||
assertFalse('checkbox became unchecked', checkbox.getChecked());
|
||||
events = [];
|
||||
|
||||
goog.events.listen(checkbox, goog.ui.Component.EventType.CHECK,
|
||||
function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertArrayEquals('ACTION+CHECK fired',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK
|
||||
],
|
||||
events);
|
||||
assertFalse('toggling has been prevented', checkbox.getChecked());
|
||||
}
|
||||
|
||||
function testCheckboxAriaLabelledby() {
|
||||
var label = goog.dom.createElement('div');
|
||||
var label2 = goog.dom.createElement('div', {id: checkbox.makeId('foo')});
|
||||
document.body.appendChild(label);
|
||||
document.body.appendChild(label2);
|
||||
try {
|
||||
checkbox.setChecked(false);
|
||||
checkbox.setLabel(label);
|
||||
checkbox.render(label);
|
||||
assertNotNull(checkbox.getElement());
|
||||
assertEquals(label.id,
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.LABELLEDBY));
|
||||
|
||||
checkbox.setLabel(label2);
|
||||
assertEquals(label2.id,
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.LABELLEDBY));
|
||||
} finally {
|
||||
document.body.removeChild(label);
|
||||
document.body.removeChild(label2);
|
||||
}
|
||||
}
|
||||
|
||||
function testLabel() {
|
||||
var label = goog.dom.createElement('div');
|
||||
document.body.appendChild(label);
|
||||
try {
|
||||
checkbox.setChecked(false);
|
||||
checkbox.setLabel(label);
|
||||
checkbox.render(label);
|
||||
|
||||
// Clicking on label toggles checkbox.
|
||||
goog.testing.events.fireClickSequence(label);
|
||||
assertTrue('checkbox toggled if the label is clicked',
|
||||
checkbox.getChecked());
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertFalse('checkbox toggled if it is clicked', checkbox.getChecked());
|
||||
|
||||
// Test that mouse events on the label have the correct effect on the
|
||||
// checkbox state when it is enabled.
|
||||
checkbox.setEnabled(true);
|
||||
goog.testing.events.fireMouseOverEvent(label);
|
||||
assertTrue(checkbox.hasState(goog.ui.Component.State.HOVER));
|
||||
assertContains('checkbox gets hover state on mouse over',
|
||||
'goog-checkbox-hover', goog.dom.classlist.get(checkbox.getElement()));
|
||||
goog.testing.events.fireMouseDownEvent(label);
|
||||
assertTrue(checkbox.hasState(goog.ui.Component.State.ACTIVE));
|
||||
assertContains('checkbox gets active state on label mousedown',
|
||||
'goog-checkbox-active',
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
goog.testing.events.fireMouseOutEvent(checkbox.getElement());
|
||||
assertFalse(checkbox.hasState(goog.ui.Component.State.HOVER));
|
||||
assertNotContains('checkbox does not have hover state after mouse out',
|
||||
'goog-checkbox-hover', goog.dom.classlist.get(checkbox.getElement()));
|
||||
assertFalse(checkbox.hasState(goog.ui.Component.State.ACTIVE));
|
||||
assertNotContains('checkbox does not have active state after mouse out',
|
||||
'goog-checkbox-active', goog.dom.classlist.get(checkbox.getElement()));
|
||||
|
||||
// Test label mouse events on disabled checkbox.
|
||||
checkbox.setEnabled(false);
|
||||
goog.testing.events.fireMouseOverEvent(label);
|
||||
assertFalse(checkbox.hasState(goog.ui.Component.State.HOVER));
|
||||
assertNotContains(
|
||||
'disabled checkbox does not get hover state on mouseover',
|
||||
'goog-checkbox-hover', goog.dom.classlist.get(checkbox.getElement()));
|
||||
goog.testing.events.fireMouseDownEvent(label);
|
||||
assertFalse(checkbox.hasState(goog.ui.Component.State.ACTIVE));
|
||||
assertNotContains('disabled checkbox does not get active state mousedown',
|
||||
'goog-checkbox-active',
|
||||
goog.dom.classlist.get(checkbox.getElement()));
|
||||
goog.testing.events.fireMouseOutEvent(checkbox.getElement());
|
||||
assertFalse(checkbox.hasState(goog.ui.Component.State.ACTIVE));
|
||||
assertNotContains('checkbox does not get stuck in hover state',
|
||||
'goog-checkbox-hover', goog.dom.classlist.get(checkbox.getElement()));
|
||||
|
||||
// Making the label null prevents it from affecting checkbox state.
|
||||
checkbox.setEnabled(true);
|
||||
checkbox.setLabel(null);
|
||||
goog.testing.events.fireClickSequence(label);
|
||||
assertFalse('label element deactivated', checkbox.getChecked());
|
||||
goog.testing.events.fireClickSequence(checkbox.getElement());
|
||||
assertTrue('checkbox still active', checkbox.getChecked());
|
||||
} finally {
|
||||
document.body.removeChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertEquals('state is unchecked', goog.ui.Checkbox.State.UNCHECKED,
|
||||
checkbox.getChecked());
|
||||
|
||||
var testCheckboxWithState = new goog.ui.Checkbox(
|
||||
goog.ui.Checkbox.State.UNDETERMINED);
|
||||
assertNotNull('checkbox created with custom state', testCheckboxWithState);
|
||||
assertEquals('checkbox state is undetermined',
|
||||
goog.ui.Checkbox.State.UNDETERMINED,
|
||||
testCheckboxWithState.getChecked());
|
||||
testCheckboxWithState.dispose();
|
||||
}
|
||||
|
||||
function testCustomRenderer() {
|
||||
var cssClass = 'my-custom-checkbox';
|
||||
var renderer = goog.ui.ControlRenderer.getCustomRenderer(
|
||||
goog.ui.CheckboxRenderer, cssClass);
|
||||
var customCheckbox = new goog.ui.Checkbox(
|
||||
undefined, undefined, renderer);
|
||||
customCheckbox.createDom();
|
||||
assertElementsEquals(
|
||||
['my-custom-checkbox', 'my-custom-checkbox-unchecked'],
|
||||
goog.dom.classlist.get(customCheckbox.getElement()));
|
||||
customCheckbox.setChecked(true);
|
||||
assertElementsEquals(
|
||||
['my-custom-checkbox', 'my-custom-checkbox-checked'],
|
||||
goog.dom.classlist.get(customCheckbox.getElement()));
|
||||
customCheckbox.setChecked(null);
|
||||
assertElementsEquals(
|
||||
['my-custom-checkbox', 'my-custom-checkbox-undetermined'],
|
||||
goog.dom.classlist.get(customCheckbox.getElement()));
|
||||
customCheckbox.dispose();
|
||||
}
|
||||
|
||||
function testGetAriaRole() {
|
||||
checkbox.createDom();
|
||||
assertNotNull(checkbox.getElement());
|
||||
assertEquals("Checkbox's ARIA role should be 'checkbox'",
|
||||
goog.a11y.aria.Role.CHECKBOX,
|
||||
goog.a11y.aria.getRole(checkbox.getElement()));
|
||||
}
|
||||
|
||||
function testCreateDomUpdateAriaState() {
|
||||
checkbox.createDom();
|
||||
assertNotNull(checkbox.getElement());
|
||||
assertEquals('Checkbox must have default false ARIA state aria-checked',
|
||||
'false', goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.CHECKED);
|
||||
assertEquals('Checkbox must have true ARIA state aria-checked', 'true',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.UNCHECKED);
|
||||
assertEquals('Checkbox must have false ARIA state aria-checked', 'false',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.UNDETERMINED);
|
||||
assertEquals('Checkbox must have mixed ARIA state aria-checked', 'mixed',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
}
|
||||
|
||||
function testDecorateUpdateAriaState() {
|
||||
var decorateSpan = goog.dom.getElement('decorate');
|
||||
checkbox.decorate(decorateSpan);
|
||||
|
||||
assertEquals('Checkbox must have default false ARIA state aria-checked',
|
||||
'false', goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.CHECKED);
|
||||
assertEquals('Checkbox must have true ARIA state aria-checked', 'true',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.UNCHECKED);
|
||||
assertEquals('Checkbox must have false ARIA state aria-checked', 'false',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
|
||||
checkbox.setChecked(goog.ui.Checkbox.State.UNDETERMINED);
|
||||
assertEquals('Checkbox must have mixed ARIA state aria-checked', 'mixed',
|
||||
goog.a11y.aria.getState(checkbox.getElement(),
|
||||
goog.a11y.aria.State.CHECKED));
|
||||
}
|
||||
|
||||
function testSpaceKey() {
|
||||
var normalSpan = goog.dom.getElement('normal');
|
||||
|
||||
checkbox.decorate(normalSpan);
|
||||
assertEquals('default state is unchecked',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.SPACE);
|
||||
assertEquals('SPACE toggles checkbox to be checked',
|
||||
goog.ui.Checkbox.State.CHECKED, checkbox.getChecked());
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.SPACE);
|
||||
assertEquals('another SPACE toggles checkbox to be unchecked',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
|
||||
// Enter for example doesn't work
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.ENTER);
|
||||
assertEquals('Enter leaves checkbox unchecked',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
}
|
||||
|
||||
function testSpaceKeyFiresEvents() {
|
||||
var normalSpan = goog.dom.getElement('normal');
|
||||
|
||||
checkbox.decorate(normalSpan);
|
||||
var events = [];
|
||||
goog.events.listen(checkbox,
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK,
|
||||
goog.ui.Component.EventType.UNCHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
function(e) {
|
||||
events.push(e.type);
|
||||
});
|
||||
|
||||
assertEquals('Unexpected default state.',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.SPACE);
|
||||
assertArrayEquals('Unexpected events fired when checking with spacebar.',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
events);
|
||||
assertEquals('Unexpected state after checking.',
|
||||
goog.ui.Checkbox.State.CHECKED, checkbox.getChecked());
|
||||
|
||||
events = [];
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.SPACE);
|
||||
assertArrayEquals('Unexpected events fired when unchecking with spacebar.',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.UNCHECK,
|
||||
goog.ui.Component.EventType.CHANGE
|
||||
],
|
||||
events);
|
||||
assertEquals('Unexpected state after unchecking.',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
|
||||
events = [];
|
||||
goog.events.listenOnce(checkbox, goog.ui.Component.EventType.CHECK,
|
||||
function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
goog.testing.events.fireKeySequence(normalSpan, goog.events.KeyCodes.SPACE);
|
||||
assertArrayEquals('Unexpected events fired when checking with spacebar and ' +
|
||||
'the check event is cancelled.',
|
||||
[
|
||||
goog.ui.Component.EventType.ACTION,
|
||||
goog.ui.Component.EventType.CHECK
|
||||
],
|
||||
events);
|
||||
assertEquals('Unexpected state after check event is cancelled.',
|
||||
goog.ui.Checkbox.State.UNCHECKED, checkbox.getChecked());
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
var normalSpan = goog.dom.getElement('normal');
|
||||
var checkedSpan = goog.dom.getElement('checked');
|
||||
var uncheckedSpan = goog.dom.getElement('unchecked');
|
||||
var undeterminedSpan = goog.dom.getElement('undetermined');
|
||||
var disabledSpan = goog.dom.getElement('disabled');
|
||||
|
||||
validateCheckBox(normalSpan, goog.ui.Checkbox.State.UNCHECKED);
|
||||
validateCheckBox(checkedSpan, goog.ui.Checkbox.State.CHECKED);
|
||||
validateCheckBox(uncheckedSpan, goog.ui.Checkbox.State.UNCHECKED);
|
||||
validateCheckBox(undeterminedSpan, goog.ui.Checkbox.State.UNDETERMINED);
|
||||
validateCheckBox(disabledSpan, goog.ui.Checkbox.State.UNCHECKED, true);
|
||||
}
|
||||
|
||||
function validateCheckBox(span, state, opt_disabled) {
|
||||
var testCheckbox = goog.ui.decorate(span);
|
||||
assertNotNull('checkbox created', testCheckbox);
|
||||
assertEquals('decorate was successful',
|
||||
goog.ui.Checkbox, testCheckbox.constructor);
|
||||
assertEquals('checkbox state should be: ' + state, state,
|
||||
testCheckbox.getChecked());
|
||||
assertEquals('checkbox is ' + (!opt_disabled ? 'enabled' : 'disabled'),
|
||||
!opt_disabled, testCheckbox.isEnabled());
|
||||
testCheckbox.dispose();
|
||||
}
|
||||
|
||||
function testSetAriaLabel() {
|
||||
assertNull('Checkbox must not have aria label by default',
|
||||
checkbox.getAriaLabel());
|
||||
checkbox.setAriaLabel('Checkbox 1');
|
||||
checkbox.render();
|
||||
var el = checkbox.getElementStrict();
|
||||
assertEquals('Checkbox element must have expected aria-label', 'Checkbox 1',
|
||||
el.getAttribute('aria-label'));
|
||||
assertEquals('Checkbox element must have expected aria-role', 'checkbox',
|
||||
el.getAttribute('role'));
|
||||
checkbox.setAriaLabel('Checkbox 2');
|
||||
assertEquals('Checkbox element must have updated aria-label', 'Checkbox 2',
|
||||
el.getAttribute('aria-label'));
|
||||
assertEquals('Checkbox element must have expected aria-role', 'checkbox',
|
||||
el.getAttribute('role'));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 A menu item class that supports checkbox semantics.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.CheckBoxMenuItem');
|
||||
|
||||
goog.require('goog.ui.MenuItem');
|
||||
goog.require('goog.ui.registry');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Class representing a checkbox menu item. This is just a convenience class
|
||||
* that extends {@link goog.ui.MenuItem} by making it checkable.
|
||||
*
|
||||
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
|
||||
* display as the content of the item (use to add icons or styling to
|
||||
* menus).
|
||||
* @param {*=} opt_model Data/model associated with the menu item.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper used for
|
||||
* document interactions.
|
||||
* @constructor
|
||||
* @extends {goog.ui.MenuItem}
|
||||
*/
|
||||
goog.ui.CheckBoxMenuItem = function(content, opt_model, opt_domHelper) {
|
||||
goog.ui.MenuItem.call(this, content, opt_model, opt_domHelper);
|
||||
this.setCheckable(true);
|
||||
};
|
||||
goog.inherits(goog.ui.CheckBoxMenuItem, goog.ui.MenuItem);
|
||||
|
||||
|
||||
// Register a decorator factory function for goog.ui.CheckBoxMenuItems.
|
||||
goog.ui.registry.setDecoratorByClassName(
|
||||
goog.getCssName('goog-checkbox-menuitem'), function() {
|
||||
// CheckBoxMenuItem defaults to using MenuItemRenderer.
|
||||
return new goog.ui.CheckBoxMenuItem(null);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2011 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 Default renderer for {@link goog.ui.Checkbox}s.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.CheckboxRenderer');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.ui.ControlRenderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default renderer for {@link goog.ui.Checkbox}s. Extends the superclass
|
||||
* to support checkbox states:
|
||||
* @constructor
|
||||
* @extends {goog.ui.ControlRenderer}
|
||||
*/
|
||||
goog.ui.CheckboxRenderer = function() {
|
||||
goog.ui.CheckboxRenderer.base(this, 'constructor');
|
||||
};
|
||||
goog.inherits(goog.ui.CheckboxRenderer, goog.ui.ControlRenderer);
|
||||
goog.addSingletonGetter(goog.ui.CheckboxRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of components rendered
|
||||
* by this renderer.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.CheckboxRenderer.CSS_CLASS = goog.getCssName('goog-checkbox');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CheckboxRenderer.prototype.createDom = function(checkbox) {
|
||||
var element = checkbox.getDomHelper().createDom(
|
||||
'span', this.getClassNames(checkbox).join(' '));
|
||||
|
||||
var state = checkbox.getChecked();
|
||||
this.setCheckboxState(element, state);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CheckboxRenderer.prototype.decorate = function(checkbox, element) {
|
||||
// The superclass implementation takes care of common attributes; we only
|
||||
// need to set the checkbox state.
|
||||
element = goog.ui.CheckboxRenderer.base(this, 'decorate', checkbox, element);
|
||||
goog.asserts.assert(element);
|
||||
var classes = goog.dom.classlist.get(element);
|
||||
// Update the checked state of the element based on its css classNames
|
||||
// with the following order: undetermined -> checked -> unchecked.
|
||||
var checked = /** @suppress {missingRequire} */ (
|
||||
goog.ui.Checkbox.State.UNCHECKED);
|
||||
if (goog.array.contains(classes,
|
||||
this.getClassForCheckboxState(
|
||||
/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNDETERMINED))) {
|
||||
checked = (/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNDETERMINED);
|
||||
} else if (goog.array.contains(classes,
|
||||
this.getClassForCheckboxState(
|
||||
/** @suppress {missingRequire} */ goog.ui.Checkbox.State.CHECKED))) {
|
||||
checked = /** @suppress {missingRequire} */ goog.ui.Checkbox.State.CHECKED;
|
||||
} else if (goog.array.contains(classes,
|
||||
this.getClassForCheckboxState(/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNCHECKED))) {
|
||||
checked = (/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNCHECKED);
|
||||
}
|
||||
checkbox.setCheckedInternal(checked);
|
||||
goog.asserts.assert(element, 'The element cannot be null.');
|
||||
goog.a11y.aria.setState(element, goog.a11y.aria.State.CHECKED,
|
||||
this.ariaStateFromCheckState_(checked));
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ARIA role to be applied to checkboxes.
|
||||
* @return {goog.a11y.aria.Role} ARIA role.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.CheckboxRenderer.prototype.getAriaRole = function() {
|
||||
return goog.a11y.aria.Role.CHECKBOX;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the appearance of the control in response to a checkbox state
|
||||
* change.
|
||||
* @param {Element} element Checkbox element.
|
||||
* @param {goog.ui.Checkbox.State} state Updated checkbox state.
|
||||
*/
|
||||
goog.ui.CheckboxRenderer.prototype.setCheckboxState = function(
|
||||
element, state) {
|
||||
if (element) {
|
||||
goog.asserts.assert(element);
|
||||
var classToAdd = this.getClassForCheckboxState(state);
|
||||
goog.asserts.assert(classToAdd);
|
||||
goog.asserts.assert(element);
|
||||
if (goog.dom.classlist.contains(element, classToAdd)) {
|
||||
return;
|
||||
}
|
||||
goog.object.forEach(
|
||||
/** @suppress {missingRequire} */ goog.ui.Checkbox.State,
|
||||
function(state) {
|
||||
var className = this.getClassForCheckboxState(state);
|
||||
goog.asserts.assert(element);
|
||||
goog.dom.classlist.enable(element, className,
|
||||
className == classToAdd);
|
||||
}, this);
|
||||
goog.a11y.aria.setState(element, goog.a11y.aria.State.CHECKED,
|
||||
this.ariaStateFromCheckState_(state));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the checkbox's ARIA (accessibility) state from its checked state.
|
||||
* @param {goog.ui.Checkbox.State} state Checkbox state.
|
||||
* @return {string} The value of goog.a11y.aria.state.CHECKED. Either 'true',
|
||||
* 'false', or 'mixed'.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CheckboxRenderer.prototype.ariaStateFromCheckState_ = function(state) {
|
||||
if (state ==
|
||||
/** @suppress {missingRequire} */ goog.ui.Checkbox.State.UNDETERMINED) {
|
||||
return 'mixed';
|
||||
} else if (state ==
|
||||
/** @suppress {missingRequire} */ goog.ui.Checkbox.State.CHECKED) {
|
||||
return 'true';
|
||||
} else {
|
||||
return 'false';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CheckboxRenderer.prototype.getCssClass = function() {
|
||||
return goog.ui.CheckboxRenderer.CSS_CLASS;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a single {@link goog.ui.Checkbox.State}, and returns the
|
||||
* corresponding CSS class name.
|
||||
* @param {goog.ui.Checkbox.State} state Checkbox state.
|
||||
* @return {string} CSS class representing the given state.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.CheckboxRenderer.prototype.getClassForCheckboxState = function(state) {
|
||||
var baseClass = this.getStructuralCssClass();
|
||||
if (state ==
|
||||
/** @suppress {missingRequire} */ goog.ui.Checkbox.State.CHECKED) {
|
||||
return goog.getCssName(baseClass, 'checked');
|
||||
} else if (state ==
|
||||
/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNCHECKED) {
|
||||
return goog.getCssName(baseClass, 'unchecked');
|
||||
} else if (state ==
|
||||
/** @suppress {missingRequire} */
|
||||
goog.ui.Checkbox.State.UNDETERMINED) {
|
||||
return goog.getCssName(baseClass, 'undetermined');
|
||||
}
|
||||
throw Error('Invalid checkbox state: ' + state);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2010 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 A color button rendered via
|
||||
* {@link goog.ui.ColorButtonRenderer}.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorButton');
|
||||
|
||||
goog.require('goog.ui.Button');
|
||||
goog.require('goog.ui.ColorButtonRenderer');
|
||||
goog.require('goog.ui.registry');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A color button control. Identical to {@link goog.ui.Button}, except it
|
||||
* defaults its renderer to {@link goog.ui.ColorButtonRenderer}.
|
||||
* This button displays a particular color and is clickable.
|
||||
* It is primarily useful with {@link goog.ui.ColorSplitBehavior} to cache the
|
||||
* last selected color.
|
||||
*
|
||||
* @param {goog.ui.ControlContent} content Text caption or existing DOM
|
||||
* structure to display as the button's caption.
|
||||
* @param {goog.ui.ButtonRenderer=} opt_renderer Optional renderer used to
|
||||
* render or decorate the button; defaults to
|
||||
* {@link goog.ui.ColorButtonRenderer}.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Button}
|
||||
* @final
|
||||
*/
|
||||
goog.ui.ColorButton = function(content, opt_renderer, opt_domHelper) {
|
||||
goog.ui.Button.call(this, content, opt_renderer ||
|
||||
goog.ui.ColorButtonRenderer.getInstance(), opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorButton, goog.ui.Button);
|
||||
|
||||
// Register a decorator factory function for goog.ui.ColorButtons.
|
||||
goog.ui.registry.setDecoratorByClassName(goog.ui.ColorButtonRenderer.CSS_CLASS,
|
||||
function() {
|
||||
// ColorButton defaults to using ColorButtonRenderer.
|
||||
return new goog.ui.ColorButton(null);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!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.ColorButton
|
||||
</title>
|
||||
<script type="text/javascript" src="../base.js">
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
goog.require('goog.ui.ColorButtonTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="buttonDiv">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2010 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.ColorButtonTest');
|
||||
goog.setTestOnly('goog.ui.ColorButtonTest');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ColorButton');
|
||||
goog.require('goog.ui.decorate');
|
||||
|
||||
var button, button2;
|
||||
var buttonDiv;
|
||||
|
||||
function setUp() {
|
||||
buttonDiv = goog.dom.getElement('buttonDiv');
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.dom.removeChildren(buttonDiv);
|
||||
goog.dispose(button);
|
||||
goog.dispose(button2);
|
||||
button = null;
|
||||
button2 = null;
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
button = new goog.ui.ColorButton('test');
|
||||
assertNotNull('button should be valid', button);
|
||||
button.render(buttonDiv);
|
||||
assertColorButton(button.getElement());
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
var colorDiv = goog.dom.createDom('div', 'goog-color-button');
|
||||
goog.dom.appendChild(buttonDiv, colorDiv);
|
||||
button2 = goog.ui.decorate(colorDiv);
|
||||
assertNotNull('button should be valid', button2);
|
||||
assertColorButton(button2.getElement());
|
||||
}
|
||||
|
||||
function assertColorButton(div) {
|
||||
assertTrue('button className contains goog-color-button',
|
||||
goog.array.contains(goog.dom.classlist.get(div), 'goog-color-button'));
|
||||
var caption = goog.asserts.assertElement(
|
||||
div.firstChild.firstChild.firstChild);
|
||||
assertTrue('caption className contains goog-color-button',
|
||||
goog.array.contains(goog.dom.classlist.get(caption),
|
||||
'goog-color-button'));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2010 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 Renderer for {@link goog.ui.ColorButton}s.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorButtonRenderer');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.ui.ColorMenuButtonRenderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Renderer for {@link goog.ui.ColorButton}s.
|
||||
* Uses {@link goog.ui.ColorMenuButton}s but disables the dropdown.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.ui.ColorMenuButtonRenderer}
|
||||
* @final
|
||||
*/
|
||||
goog.ui.ColorButtonRenderer = function() {
|
||||
goog.ui.ColorButtonRenderer.base(this, 'constructor');
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
// TODO(user): enable disabling the dropdown in goog.ui.ColorMenuButton
|
||||
this.createDropdown = goog.functions.NULL;
|
||||
|
||||
};
|
||||
goog.inherits(goog.ui.ColorButtonRenderer, goog.ui.ColorMenuButtonRenderer);
|
||||
goog.addSingletonGetter(goog.ui.ColorButtonRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of components rendered
|
||||
* by this renderer. Additionally, applies class to the button's caption.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ColorButtonRenderer.CSS_CLASS = goog.getCssName('goog-color-button');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ColorButtonRenderer.prototype.createCaption = function(content, dom) {
|
||||
var caption = goog.ui.ColorButtonRenderer.base(
|
||||
this, 'createCaption', content, dom);
|
||||
goog.asserts.assert(caption);
|
||||
goog.dom.classlist.add(caption, goog.ui.ColorButtonRenderer.CSS_CLASS);
|
||||
return caption;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ColorButtonRenderer.prototype.initializeDom = function(button) {
|
||||
goog.ui.ColorButtonRenderer.base(this, 'initializeDom', button);
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(button.getElement()),
|
||||
goog.ui.ColorButtonRenderer.CSS_CLASS);
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview A color menu button. Extends {@link goog.ui.MenuButton} by
|
||||
* showing the currently selected color in the button caption.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorMenuButton');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.ui.ColorMenuButtonRenderer');
|
||||
goog.require('goog.ui.ColorPalette');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Menu');
|
||||
goog.require('goog.ui.MenuButton');
|
||||
goog.require('goog.ui.registry');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A color menu button control. Extends {@link goog.ui.MenuButton} by adding
|
||||
* an API for getting and setting the currently selected color from a menu of
|
||||
* color palettes.
|
||||
*
|
||||
* @param {goog.ui.ControlContent} content Text caption or existing DOM
|
||||
* structure to display as the button's caption.
|
||||
* @param {goog.ui.Menu=} opt_menu Menu to render under the button when clicked;
|
||||
* should contain at least one {@link goog.ui.ColorPalette} if present.
|
||||
* @param {goog.ui.MenuButtonRenderer=} opt_renderer Button renderer;
|
||||
* defaults to {@link goog.ui.ColorMenuButtonRenderer}.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @constructor
|
||||
* @extends {goog.ui.MenuButton}
|
||||
*/
|
||||
goog.ui.ColorMenuButton = function(content, opt_menu, opt_renderer,
|
||||
opt_domHelper) {
|
||||
goog.ui.MenuButton.call(this, content, opt_menu, opt_renderer ||
|
||||
goog.ui.ColorMenuButtonRenderer.getInstance(), opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorMenuButton, goog.ui.MenuButton);
|
||||
|
||||
|
||||
/**
|
||||
* Default color palettes.
|
||||
* @type {!Object}
|
||||
*/
|
||||
goog.ui.ColorMenuButton.PALETTES = {
|
||||
/** Default grayscale colors. */
|
||||
GRAYSCALE: [
|
||||
'#000', '#444', '#666', '#999', '#ccc', '#eee', '#f3f3f3', '#fff'
|
||||
],
|
||||
|
||||
/** Default solid colors. */
|
||||
SOLID: [
|
||||
'#f00', '#f90', '#ff0', '#0f0', '#0ff', '#00f', '#90f', '#f0f'
|
||||
],
|
||||
|
||||
/** Default pastel colors. */
|
||||
PASTEL: [
|
||||
'#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#cfe2f3', '#d9d2e9',
|
||||
'#ead1dc',
|
||||
'#ea9999', '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9', '#9fc5e8', '#b4a7d6',
|
||||
'#d5a6bd',
|
||||
'#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6fa8dc', '#8e7cc3',
|
||||
'#c27ba0',
|
||||
'#cc0000', '#e69138', '#f1c232', '#6aa84f', '#45818e', '#3d85c6', '#674ea7',
|
||||
'#a64d79',
|
||||
'#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#0b5394', '#351c75',
|
||||
'#741b47',
|
||||
'#660000', '#783f04', '#7f6000', '#274e13', '#0c343d', '#073763', '#20124d',
|
||||
'#4c1130'
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Value for the "no color" menu item object in the color menu (if present).
|
||||
* The {@link goog.ui.ColorMenuButton#handleMenuAction} method interprets
|
||||
* ACTION events dispatched by an item with this value as meaning "clear the
|
||||
* selected color."
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ColorMenuButton.NO_COLOR = 'none';
|
||||
|
||||
|
||||
/**
|
||||
* Factory method that creates and returns a new {@link goog.ui.Menu} instance
|
||||
* containing default color palettes.
|
||||
* @param {Array<goog.ui.Control>=} opt_extraItems Optional extra menu items to
|
||||
* add before the color palettes.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @return {!goog.ui.Menu} Color menu.
|
||||
*/
|
||||
goog.ui.ColorMenuButton.newColorMenu = function(opt_extraItems, opt_domHelper) {
|
||||
var menu = new goog.ui.Menu(opt_domHelper);
|
||||
|
||||
if (opt_extraItems) {
|
||||
goog.array.forEach(opt_extraItems, function(item) {
|
||||
menu.addChild(item, true);
|
||||
});
|
||||
}
|
||||
|
||||
goog.object.forEach(goog.ui.ColorMenuButton.PALETTES, function(colors) {
|
||||
var palette = new goog.ui.ColorPalette(colors, null, opt_domHelper);
|
||||
palette.setSize(8);
|
||||
menu.addChild(palette, true);
|
||||
});
|
||||
|
||||
return menu;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the currently selected color (null if none).
|
||||
* @return {string} The selected color.
|
||||
*/
|
||||
goog.ui.ColorMenuButton.prototype.getSelectedColor = function() {
|
||||
return /** @type {string} */ (this.getValue());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the selected color, or clears the selected color if the argument is
|
||||
* null or not any of the available color choices.
|
||||
* @param {?string} color New color.
|
||||
*/
|
||||
goog.ui.ColorMenuButton.prototype.setSelectedColor = function(color) {
|
||||
this.setValue(color);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the value associated with the color menu button. Overrides
|
||||
* {@link goog.ui.Button#setValue} by interpreting the value as a color
|
||||
* spec string.
|
||||
* @param {*} value New button value; should be a color spec string.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButton.prototype.setValue = function(value) {
|
||||
var color = /** @type {?string} */ (value);
|
||||
for (var i = 0, item; item = this.getItemAt(i); i++) {
|
||||
if (typeof item.setSelectedColor == 'function') {
|
||||
// This menu item looks like a color palette.
|
||||
item.setSelectedColor(color);
|
||||
}
|
||||
}
|
||||
goog.ui.ColorMenuButton.superClass_.setValue.call(this, color);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles {@link goog.ui.Component.EventType.ACTION} events dispatched by
|
||||
* the menu item clicked by the user. Updates the button, calls the superclass
|
||||
* implementation to hide the menu, stops the propagation of the event, and
|
||||
* dispatches an ACTION event on behalf of the button itself. Overrides
|
||||
* {@link goog.ui.MenuButton#handleMenuAction}.
|
||||
* @param {goog.events.Event} e Action event to handle.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButton.prototype.handleMenuAction = function(e) {
|
||||
if (typeof e.target.getSelectedColor == 'function') {
|
||||
// User clicked something that looks like a color palette.
|
||||
this.setValue(e.target.getSelectedColor());
|
||||
} else if (e.target.getValue() == goog.ui.ColorMenuButton.NO_COLOR) {
|
||||
// User clicked the special "no color" menu item.
|
||||
this.setValue(null);
|
||||
}
|
||||
goog.ui.ColorMenuButton.superClass_.handleMenuAction.call(this, e);
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Opens or closes the menu. Overrides {@link goog.ui.MenuButton#setOpen} by
|
||||
* generating a default color menu on the fly if needed.
|
||||
* @param {boolean} open Whether to open or close the menu.
|
||||
* @param {goog.events.Event=} opt_e Mousedown event that caused the menu to
|
||||
* be opened.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButton.prototype.setOpen = function(open, opt_e) {
|
||||
if (open && this.getItemCount() == 0) {
|
||||
this.setMenu(
|
||||
goog.ui.ColorMenuButton.newColorMenu(null, this.getDomHelper()));
|
||||
this.setValue(/** @type {?string} */ (this.getValue()));
|
||||
}
|
||||
goog.ui.ColorMenuButton.superClass_.setOpen.call(this, open, opt_e);
|
||||
};
|
||||
|
||||
|
||||
// Register a decorator factory function for goog.ui.ColorMenuButtons.
|
||||
goog.ui.registry.setDecoratorByClassName(
|
||||
goog.ui.ColorMenuButtonRenderer.CSS_CLASS,
|
||||
function() {
|
||||
return new goog.ui.ColorMenuButton(null);
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Renderer for {@link goog.ui.ColorMenuButton}s.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorMenuButtonRenderer');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.color');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.ui.MenuButtonRenderer');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Renderer for {@link goog.ui.ColorMenuButton}s.
|
||||
* @constructor
|
||||
* @extends {goog.ui.MenuButtonRenderer}
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer = function() {
|
||||
goog.ui.MenuButtonRenderer.call(this);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorMenuButtonRenderer, goog.ui.MenuButtonRenderer);
|
||||
goog.addSingletonGetter(goog.ui.ColorMenuButtonRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of components rendered
|
||||
* by this renderer.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.CSS_CLASS =
|
||||
goog.getCssName('goog-color-menu-button');
|
||||
|
||||
|
||||
/**
|
||||
* Overrides the superclass implementation by wrapping the caption text or DOM
|
||||
* structure in a color indicator element. Creates the following DOM structure:
|
||||
* <div class="goog-inline-block goog-menu-button-caption">
|
||||
* <div class="goog-color-menu-button-indicator">
|
||||
* Contents...
|
||||
* </div>
|
||||
* </div>
|
||||
* The 'goog-color-menu-button-indicator' style should be defined to have a
|
||||
* bottom border of nonzero width and a default color that blends into its
|
||||
* background.
|
||||
* @param {goog.ui.ControlContent} content Text caption or DOM structure.
|
||||
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
|
||||
* @return {Element} Caption element.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.prototype.createCaption = function(content,
|
||||
dom) {
|
||||
return goog.ui.ColorMenuButtonRenderer.superClass_.createCaption.call(this,
|
||||
goog.ui.ColorMenuButtonRenderer.wrapCaption(content, dom), dom);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Wrap a caption in a div with the color-menu-button-indicator CSS class.
|
||||
* @param {goog.ui.ControlContent} content Text caption or DOM structure.
|
||||
* @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
|
||||
* @return {!Element} Caption element.
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.wrapCaption = function(content, dom) {
|
||||
return dom.createDom('div',
|
||||
goog.getCssName(goog.ui.ColorMenuButtonRenderer.CSS_CLASS, 'indicator'),
|
||||
content);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a color menu button control's root element and a value object
|
||||
* (which is assumed to be a color), and updates the button's DOM to reflect
|
||||
* the new color. Overrides {@link goog.ui.ButtonRenderer#setValue}.
|
||||
* @param {Element} element The button control's root element (if rendered).
|
||||
* @param {*} value New value; assumed to be a color spec string.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.prototype.setValue = function(element, value) {
|
||||
if (element) {
|
||||
goog.ui.ColorMenuButtonRenderer.setCaptionValue(
|
||||
this.getContentElement(element), value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a control's content element and a value object (which is assumed
|
||||
* to be a color), and updates its DOM to reflect the new color.
|
||||
* @param {Element} caption A content element of a control.
|
||||
* @param {*} value New value; assumed to be a color spec string.
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.setCaptionValue = function(caption, value) {
|
||||
// Assume that the caption's first child is the indicator.
|
||||
if (caption && caption.firstChild) {
|
||||
// Normalize the value to a hex color spec or null (otherwise setting
|
||||
// borderBottomColor will cause a JS error on IE).
|
||||
var hexColor;
|
||||
|
||||
var strValue = /** @type {string} */ (value);
|
||||
hexColor = strValue && goog.color.isValidColor(strValue) ?
|
||||
goog.color.parse(strValue).hex :
|
||||
null;
|
||||
|
||||
// Stupid IE6/7 doesn't do transparent borders.
|
||||
// TODO(attila): Add user-agent version check when IE8 comes out...
|
||||
caption.firstChild.style.borderBottomColor = hexColor ||
|
||||
(goog.userAgent.IE ? '' : 'transparent');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the button's DOM when it enters the document. Overrides the
|
||||
* superclass implementation by making sure the button's color indicator is
|
||||
* initialized.
|
||||
* @param {goog.ui.Control} button goog.ui.ColorMenuButton whose DOM is to be
|
||||
* initialized as it enters the document.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorMenuButtonRenderer.prototype.initializeDom = function(button) {
|
||||
var buttonElement = button.getElement();
|
||||
goog.asserts.assert(buttonElement);
|
||||
this.setValue(buttonElement, button.getValue());
|
||||
goog.dom.classlist.add(buttonElement,
|
||||
goog.ui.ColorMenuButtonRenderer.CSS_CLASS);
|
||||
goog.ui.ColorMenuButtonRenderer.superClass_.initializeDom.call(this,
|
||||
button);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
|
||||
@author nicksantos@google.com (Nick Santos)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests for ColorMenuButtonRenderer
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ColorMenuButtonTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<!-- A parent to attach rendered buttons to -->
|
||||
<div id="parent"></div>
|
||||
<!-- A button to decorate -->
|
||||
<div id="decoratedButton"><div>Foo</div></div></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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.ColorMenuButtonTest');
|
||||
goog.setTestOnly('goog.ui.ColorMenuButtonTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.ui.RendererHarness');
|
||||
goog.require('goog.testing.ui.rendererasserts');
|
||||
goog.require('goog.ui.ColorMenuButton');
|
||||
goog.require('goog.ui.ColorMenuButtonRenderer');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var harness;
|
||||
|
||||
function setUp() {
|
||||
harness = new goog.testing.ui.RendererHarness(
|
||||
goog.ui.ColorMenuButtonRenderer.getInstance(),
|
||||
goog.dom.getElement('parent'),
|
||||
goog.dom.getElement('decoratedButton'));
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
harness.dispose();
|
||||
}
|
||||
|
||||
function testEquality() {
|
||||
harness.attachControlAndRender(
|
||||
new goog.ui.ColorMenuButton('Foo'));
|
||||
harness.attachControlAndDecorate(
|
||||
new goog.ui.ColorMenuButton());
|
||||
harness.assertDomMatches();
|
||||
}
|
||||
|
||||
function testWrapCaption() {
|
||||
var caption = goog.dom.createDom('div', null, 'Foo');
|
||||
var wrappedCaption = goog.ui.ColorMenuButtonRenderer.wrapCaption(caption,
|
||||
goog.dom.getDomHelper());
|
||||
assertNotEquals('Caption should have been wrapped', caption, wrappedCaption);
|
||||
assertEquals('Wrapped caption should have indicator css class',
|
||||
'goog-color-menu-button-indicator', wrappedCaption.className);
|
||||
}
|
||||
|
||||
function testSetCaptionValue() {
|
||||
var caption = goog.dom.createDom('div', null, 'Foo');
|
||||
var wrappedCaption = goog.ui.ColorMenuButtonRenderer.wrapCaption(caption,
|
||||
goog.dom.getDomHelper());
|
||||
goog.ui.ColorMenuButtonRenderer.setCaptionValue(wrappedCaption, 'red');
|
||||
|
||||
var expectedColor =
|
||||
goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) ?
|
||||
'#ff0000' : 'rgb(255, 0, 0)';
|
||||
assertEquals(expectedColor, caption.style.borderBottomColor);
|
||||
}
|
||||
|
||||
function testDoesntCallGetCssClassInConstructor() {
|
||||
goog.testing.ui.rendererasserts.
|
||||
assertNoGetCssClassCallsInConstructor(goog.ui.ColorMenuButtonRenderer);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// 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 A control for representing a palette of colors, that the user
|
||||
* can highlight or select via the keyboard or the mouse.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorPalette');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.color');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Palette');
|
||||
goog.require('goog.ui.PaletteRenderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A color palette is a grid of color swatches that the user can highlight or
|
||||
* select via the keyboard or the mouse. The selection state of the palette is
|
||||
* controlled by a selection model. When the user makes a selection, the
|
||||
* component fires an ACTION event. Event listeners may retrieve the selected
|
||||
* color using the {@link #getSelectedColor} method.
|
||||
*
|
||||
* @param {Array<string>=} opt_colors Array of colors in any valid CSS color
|
||||
* format.
|
||||
* @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
|
||||
* decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Palette}
|
||||
*/
|
||||
goog.ui.ColorPalette = function(opt_colors, opt_renderer, opt_domHelper) {
|
||||
/**
|
||||
* Array of colors to show in the palette.
|
||||
* @type {Array<string>}
|
||||
* @private
|
||||
*/
|
||||
this.colors_ = opt_colors || [];
|
||||
|
||||
goog.ui.Palette.call(this, null,
|
||||
opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);
|
||||
|
||||
// Set the colors separately from the super call since we need the correct
|
||||
// DomHelper to be initialized for this class.
|
||||
this.setColors(this.colors_);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorPalette, goog.ui.Palette);
|
||||
goog.tagUnsealableClass(goog.ui.ColorPalette);
|
||||
|
||||
|
||||
/**
|
||||
* Array of normalized colors. Initialized lazily as often never needed.
|
||||
* @type {?Array<string>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.normalizedColors_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Array of labels for the colors. Will be used for the tooltips and
|
||||
* accessibility.
|
||||
* @type {?Array<string>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.labels_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the array of colors represented in the color palette.
|
||||
* @return {Array<string>} Array of colors.
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.getColors = function() {
|
||||
return this.colors_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the colors that are contained in the palette.
|
||||
* @param {Array<string>} colors Array of colors in any valid CSS color format.
|
||||
* @param {Array<string>=} opt_labels The array of labels to be used as
|
||||
* tooltips. When not provided, the color value will be used.
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.setColors = function(colors, opt_labels) {
|
||||
this.colors_ = colors;
|
||||
this.labels_ = opt_labels || null;
|
||||
this.normalizedColors_ = null;
|
||||
this.setContent(this.createColorNodes());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {?string} The current selected color in hex, or null.
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.getSelectedColor = function() {
|
||||
var selectedItem = /** @type {Element} */ (this.getSelectedItem());
|
||||
if (selectedItem) {
|
||||
var color = goog.style.getStyle(selectedItem, 'background-color');
|
||||
return goog.ui.ColorPalette.parseColor_(color);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the selected color. Clears the selection if the argument is null or
|
||||
* can't be parsed as a color.
|
||||
* @param {?string} color The color to set as selected; null clears the
|
||||
* selection.
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.setSelectedColor = function(color) {
|
||||
var hexColor = goog.ui.ColorPalette.parseColor_(color);
|
||||
if (!this.normalizedColors_) {
|
||||
this.normalizedColors_ = goog.array.map(this.colors_, function(color) {
|
||||
return goog.ui.ColorPalette.parseColor_(color);
|
||||
});
|
||||
}
|
||||
this.setSelectedIndex(hexColor ?
|
||||
goog.array.indexOf(this.normalizedColors_, hexColor) : -1);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Array<!Node>} An array of DOM nodes for each color.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ColorPalette.prototype.createColorNodes = function() {
|
||||
return goog.array.map(this.colors_, function(color, index) {
|
||||
var swatch = this.getDomHelper().createDom('div', {
|
||||
'class': goog.getCssName(this.getRenderer().getCssClass(),
|
||||
'colorswatch'),
|
||||
'style': 'background-color:' + color
|
||||
});
|
||||
if (this.labels_ && this.labels_[index]) {
|
||||
swatch.title = this.labels_[index];
|
||||
} else {
|
||||
swatch.title = color.charAt(0) == '#' ?
|
||||
'RGB (' + goog.color.hexToRgb(color).join(', ') + ')' : color;
|
||||
}
|
||||
return swatch;
|
||||
}, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a string, attempts to parse it as a color spec, and returns a
|
||||
* normalized hex color spec if successful (null otherwise).
|
||||
* @param {?string} color String possibly containing a color spec; may be null.
|
||||
* @return {?string} Normalized hex color spec, or null if the argument can't
|
||||
* be parsed as a color.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPalette.parseColor_ = function(color) {
|
||||
if (color) {
|
||||
/** @preserveTry */
|
||||
try {
|
||||
return goog.color.parse(color).hex;
|
||||
} catch (ex) {
|
||||
// Fall through.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<!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.
|
||||
-->
|
||||
<!-- Author: attila@google.com (Attila Bodis) -->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ColorPalette
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ColorPaletteTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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.ColorPaletteTest');
|
||||
goog.setTestOnly('goog.ui.ColorPaletteTest');
|
||||
|
||||
goog.require('goog.color');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ColorPalette');
|
||||
|
||||
var emptyPalette, samplePalette;
|
||||
|
||||
function setUp() {
|
||||
emptyPalette = new goog.ui.ColorPalette();
|
||||
samplePalette = new goog.ui.ColorPalette([
|
||||
'red', '#00FF00', 'rgb(0, 0, 255)'
|
||||
]);
|
||||
samplePalette.setSelectedColor('blue');
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
emptyPalette.dispose();
|
||||
samplePalette.dispose();
|
||||
document.getElementById('sandbox').innerHTML = '';
|
||||
}
|
||||
|
||||
function testEmptyColorPalette() {
|
||||
var colors = emptyPalette.getColors();
|
||||
assertNotNull(colors);
|
||||
assertEquals(0, colors.length);
|
||||
|
||||
var nodes = emptyPalette.getContent();
|
||||
assertNotNull(nodes);
|
||||
assertEquals(0, nodes.length);
|
||||
}
|
||||
|
||||
function testSampleColorPalette() {
|
||||
var colors = samplePalette.getColors();
|
||||
assertNotNull(colors);
|
||||
assertEquals(3, colors.length);
|
||||
assertEquals('red', colors[0]);
|
||||
assertEquals('#00FF00', colors[1]);
|
||||
assertEquals('rgb(0, 0, 255)', colors[2]);
|
||||
|
||||
var nodes = samplePalette.getContent();
|
||||
assertNotNull(nodes);
|
||||
assertEquals(3, nodes.length);
|
||||
assertEquals('goog-palette-colorswatch', nodes[0].className);
|
||||
assertEquals('goog-palette-colorswatch', nodes[1].className);
|
||||
assertEquals('goog-palette-colorswatch', nodes[2].className);
|
||||
assertEquals('#ff0000',
|
||||
goog.color.parse(nodes[0].style.backgroundColor).hex);
|
||||
assertEquals('#00ff00',
|
||||
goog.color.parse(nodes[1].style.backgroundColor).hex);
|
||||
assertEquals('#0000ff',
|
||||
goog.color.parse(nodes[2].style.backgroundColor).hex);
|
||||
}
|
||||
|
||||
function testGetColors() {
|
||||
var emptyColors = emptyPalette.getColors();
|
||||
assertNotNull(emptyColors);
|
||||
assertEquals(0, emptyColors.length);
|
||||
|
||||
var sampleColors = samplePalette.getColors();
|
||||
assertNotNull(sampleColors);
|
||||
assertEquals(3, sampleColors.length);
|
||||
assertEquals('red', sampleColors[0]);
|
||||
assertEquals('#00FF00', sampleColors[1]);
|
||||
assertEquals('rgb(0, 0, 255)', sampleColors[2]);
|
||||
}
|
||||
|
||||
function testSetColors() {
|
||||
emptyPalette.setColors(['black', '#FFFFFF']);
|
||||
|
||||
var colors = emptyPalette.getColors();
|
||||
assertNotNull(colors);
|
||||
assertEquals(2, colors.length);
|
||||
assertEquals('black', colors[0]);
|
||||
assertEquals('#FFFFFF', colors[1]);
|
||||
|
||||
var nodes = emptyPalette.getContent();
|
||||
assertNotNull(nodes);
|
||||
assertEquals(2, nodes.length);
|
||||
assertEquals('goog-palette-colorswatch', nodes[0].className);
|
||||
assertEquals('goog-palette-colorswatch', nodes[1].className);
|
||||
assertEquals('#000000',
|
||||
goog.color.parse(nodes[0].style.backgroundColor).hex);
|
||||
assertEquals('#ffffff',
|
||||
goog.color.parse(nodes[1].style.backgroundColor).hex);
|
||||
assertEquals('black', nodes[0].title);
|
||||
assertEquals('RGB (255, 255, 255)', nodes[1].title);
|
||||
|
||||
samplePalette.setColors(['#336699', 'cyan']);
|
||||
|
||||
var newColors = samplePalette.getColors();
|
||||
assertNotNull(newColors);
|
||||
assertEquals(2, newColors.length);
|
||||
assertEquals('#336699', newColors[0]);
|
||||
assertEquals('cyan', newColors[1]);
|
||||
|
||||
var newNodes = samplePalette.getContent();
|
||||
assertNotNull(newNodes);
|
||||
assertEquals(2, newNodes.length);
|
||||
assertEquals('goog-palette-colorswatch', newNodes[0].className);
|
||||
assertEquals('goog-palette-colorswatch', newNodes[1].className);
|
||||
assertEquals('#336699',
|
||||
goog.color.parse(newNodes[0].style.backgroundColor).hex);
|
||||
assertEquals('#00ffff',
|
||||
goog.color.parse(newNodes[1].style.backgroundColor).hex);
|
||||
}
|
||||
|
||||
function testSetColorsWithLabels() {
|
||||
emptyPalette.setColors(['#00f', '#FFFFFF', 'black'], ['blue', 'white']);
|
||||
var nodes = emptyPalette.getContent();
|
||||
assertEquals('blue', nodes[0].title);
|
||||
assertEquals('white', nodes[1].title);
|
||||
assertEquals('black', nodes[2].title);
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
samplePalette.render(document.getElementById('sandbox'));
|
||||
|
||||
assertTrue(samplePalette.isInDocument());
|
||||
|
||||
var elem = samplePalette.getElement();
|
||||
assertNotNull(elem);
|
||||
assertEquals('DIV', elem.tagName);
|
||||
assertEquals('goog-palette', elem.className);
|
||||
|
||||
var table = elem.firstChild;
|
||||
assertEquals('TABLE', table.tagName);
|
||||
assertEquals('goog-palette-table', table.className);
|
||||
}
|
||||
|
||||
function testGetSelectedColor() {
|
||||
assertNull(emptyPalette.getSelectedColor());
|
||||
assertEquals('#0000ff', samplePalette.getSelectedColor());
|
||||
}
|
||||
|
||||
function testSetSelectedColor() {
|
||||
emptyPalette.setSelectedColor('red');
|
||||
assertNull(emptyPalette.getSelectedColor());
|
||||
|
||||
samplePalette.setSelectedColor('red');
|
||||
assertEquals('#ff0000', samplePalette.getSelectedColor());
|
||||
samplePalette.setSelectedColor(17); // Invalid color spec.
|
||||
assertNull(samplePalette.getSelectedColor());
|
||||
|
||||
samplePalette.setSelectedColor('rgb(0, 255, 0)');
|
||||
assertEquals('#00ff00', samplePalette.getSelectedColor());
|
||||
samplePalette.setSelectedColor(false); // Invalid color spec.
|
||||
assertNull(samplePalette.getSelectedColor());
|
||||
|
||||
samplePalette.setSelectedColor('#0000FF');
|
||||
assertEquals('#0000ff', samplePalette.getSelectedColor());
|
||||
samplePalette.setSelectedColor(null); // Invalid color spec.
|
||||
assertNull(samplePalette.getSelectedColor());
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// 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 A color picker component. A color picker can compose several
|
||||
* instances of goog.ui.ColorPalette.
|
||||
*
|
||||
* NOTE: The ColorPicker is in a state of transition towards the common
|
||||
* component/control/container interface we are developing. If the API changes
|
||||
* we will do our best to update your code. The end result will be that a
|
||||
* color picker will compose multiple color palettes. In the simple case this
|
||||
* will be one grid, but may consistute 3 distinct grids, a custom color picker
|
||||
* or even a color wheel.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorPicker');
|
||||
goog.provide('goog.ui.ColorPicker.EventType');
|
||||
|
||||
goog.require('goog.ui.ColorPalette');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new, empty color picker.
|
||||
*
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @param {goog.ui.ColorPalette=} opt_colorPalette Optional color palette to
|
||||
* use for this color picker.
|
||||
* @extends {goog.ui.Component}
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.ui.ColorPicker = function(opt_domHelper, opt_colorPalette) {
|
||||
goog.ui.Component.call(this, opt_domHelper);
|
||||
|
||||
/**
|
||||
* The color palette used inside the color picker.
|
||||
* @type {goog.ui.ColorPalette?}
|
||||
* @private
|
||||
*/
|
||||
this.colorPalette_ = opt_colorPalette || null;
|
||||
|
||||
this.getHandler().listen(
|
||||
this, goog.ui.Component.EventType.ACTION, this.onColorPaletteAction_);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorPicker, goog.ui.Component);
|
||||
|
||||
|
||||
/**
|
||||
* Default number of columns in the color palette. May be overridden by calling
|
||||
* setSize.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
goog.ui.ColorPicker.DEFAULT_NUM_COLS = 5;
|
||||
|
||||
|
||||
/**
|
||||
* Constants for event names.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.ui.ColorPicker.EventType = {
|
||||
CHANGE: 'change'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether the component is focusable.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.focusable_ = true;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the array of colors displayed by the color picker.
|
||||
* Modifying this array will lead to unexpected behavior.
|
||||
* @return {Array<string>?} The colors displayed by this widget.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.getColors = function() {
|
||||
return this.colorPalette_ ? this.colorPalette_.getColors() : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the array of colors to be displayed by the color picker.
|
||||
* @param {Array<string>} colors The array of colors to be added.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setColors = function(colors) {
|
||||
// TODO(user): Don't add colors directly, we should add palettes and the
|
||||
// picker should support multiple palettes.
|
||||
if (!this.colorPalette_) {
|
||||
this.createColorPalette_(colors);
|
||||
} else {
|
||||
this.colorPalette_.setColors(colors);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the array of colors to be displayed by the color picker.
|
||||
* @param {Array<string>} colors The array of colors to be added.
|
||||
* @deprecated Use setColors.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.addColors = function(colors) {
|
||||
this.setColors(colors);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the size of the palette. Will throw an error after the picker has been
|
||||
* rendered.
|
||||
* @param {goog.math.Size|number} size The size of the grid.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setSize = function(size) {
|
||||
// TODO(user): The color picker should contain multiple palettes which will
|
||||
// all be resized at this point.
|
||||
if (!this.colorPalette_) {
|
||||
this.createColorPalette_([]);
|
||||
}
|
||||
this.colorPalette_.setSize(size);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the number of columns displayed.
|
||||
* @return {goog.math.Size?} The size of the grid.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.getSize = function() {
|
||||
return this.colorPalette_ ? this.colorPalette_.getSize() : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the number of columns. Will throw an error after the picker has been
|
||||
* rendered.
|
||||
* @param {number} n The number of columns.
|
||||
* @deprecated Use setSize.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setColumnCount = function(n) {
|
||||
this.setSize(n);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The index of the color selected.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.getSelectedIndex = function() {
|
||||
return this.colorPalette_ ? this.colorPalette_.getSelectedIndex() : -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets which color is selected. A value that is out-of-range means that no
|
||||
* color is selected.
|
||||
* @param {number} ind The index in this.colors_ of the selected color.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setSelectedIndex = function(ind) {
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.setSelectedIndex(ind);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the color that is currently selected in this color picker.
|
||||
* @return {?string} The hex string of the color selected, or null if no
|
||||
* color is selected.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.getSelectedColor = function() {
|
||||
return this.colorPalette_ ? this.colorPalette_.getSelectedColor() : null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets which color is selected. Noop if the color palette hasn't been created
|
||||
* yet.
|
||||
* @param {string} color The selected color.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setSelectedColor = function(color) {
|
||||
// TODO(user): This will set the color in the first available palette that
|
||||
// contains it
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.setSelectedColor(color);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the component is focusable, false otherwise. The default
|
||||
* is true. Focusable components always have a tab index and allocate a key
|
||||
* handler to handle keyboard events while focused.
|
||||
* @return {boolean} True iff the component is focusable.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.isFocusable = function() {
|
||||
return this.focusable_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether the component is focusable. The default is true.
|
||||
* Focusable components always have a tab index and allocate a key handler to
|
||||
* handle keyboard events while focused.
|
||||
* @param {boolean} focusable True iff the component is focusable.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.setFocusable = function(focusable) {
|
||||
this.focusable_ = focusable;
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.setSupportedState(goog.ui.Component.State.FOCUSED,
|
||||
focusable);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* ColorPickers cannot be used to decorate pre-existing html, since the
|
||||
* structure they build is fairly complicated.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {boolean} Returns always false.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.canDecorate = function(element) {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the color picker inside the provided element. This will override the
|
||||
* current content of the element.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.enterDocument = function() {
|
||||
goog.ui.ColorPicker.superClass_.enterDocument.call(this);
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.render(this.getElement());
|
||||
}
|
||||
this.getElement().unselectable = 'on';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ColorPicker.prototype.disposeInternal = function() {
|
||||
goog.ui.ColorPicker.superClass_.disposeInternal.call(this);
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.dispose();
|
||||
this.colorPalette_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the focus to the color picker's palette.
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.focus = function() {
|
||||
if (this.colorPalette_) {
|
||||
this.colorPalette_.getElement().focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles actions from the color palette.
|
||||
*
|
||||
* @param {goog.events.Event} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.onColorPaletteAction_ = function(e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(goog.ui.ColorPicker.EventType.CHANGE);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a color palette for the color picker.
|
||||
* @param {Array<string>} colors Array of colors.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorPicker.prototype.createColorPalette_ = function(colors) {
|
||||
// TODO(user): The color picker should eventually just contain a number of
|
||||
// palettes and manage the interactions between them. This will go away then.
|
||||
var cp = new goog.ui.ColorPalette(colors, null, this.getDomHelper());
|
||||
cp.setSize(goog.ui.ColorPicker.DEFAULT_NUM_COLS);
|
||||
cp.setSupportedState(goog.ui.Component.State.FOCUSED, this.focusable_);
|
||||
// TODO(user): Use addChild(cp, true) and remove calls to render.
|
||||
this.addChild(cp);
|
||||
this.colorPalette_ = cp;
|
||||
if (this.isInDocument()) {
|
||||
this.colorPalette_.render(this.getElement());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an unrendered instance of the color picker. The colors and layout
|
||||
* are a simple color grid, the same as the old Gmail color picker.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @return {!goog.ui.ColorPicker} The unrendered instance.
|
||||
*/
|
||||
goog.ui.ColorPicker.createSimpleColorGrid = function(opt_domHelper) {
|
||||
var cp = new goog.ui.ColorPicker(opt_domHelper);
|
||||
cp.setSize(7);
|
||||
cp.setColors(goog.ui.ColorPicker.SIMPLE_GRID_COLORS);
|
||||
return cp;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Array of colors for a 7-cell wide simple-grid color picker.
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
goog.ui.ColorPicker.SIMPLE_GRID_COLORS = [
|
||||
// grays
|
||||
'#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
|
||||
// reds
|
||||
'#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
|
||||
// oranges
|
||||
'#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
|
||||
// yellows
|
||||
'#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
|
||||
// olives
|
||||
'#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
|
||||
// greens
|
||||
'#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
|
||||
// turquoises
|
||||
'#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
|
||||
// blues
|
||||
'#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
|
||||
// purples
|
||||
'#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
|
||||
// violets
|
||||
'#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
|
||||
];
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2010 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 Behavior for combining a color button and a menu.
|
||||
*
|
||||
* @see ../demos/split.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ColorSplitBehavior');
|
||||
|
||||
goog.require('goog.ui.ColorMenuButton');
|
||||
goog.require('goog.ui.SplitBehavior');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a ColorSplitBehavior for combining a color button and a menu.
|
||||
* To use this, provide a goog.ui.ColorButton which will be attached with
|
||||
* a goog.ui.ColorMenuButton (with no caption).
|
||||
* Whenever a color is selected from the ColorMenuButton, it will be placed in
|
||||
* the ColorButton and the user can apply it over and over (by clicking the
|
||||
* ColorButton).
|
||||
* Primary use case - setting the color of text/background in a text editor.
|
||||
*
|
||||
* @param {!goog.ui.Button} colorButton A button to interact with a color menu
|
||||
* button (preferably a goog.ui.ColorButton).
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
|
||||
* document interaction.
|
||||
* @extends {goog.ui.SplitBehavior}
|
||||
* @constructor
|
||||
* @final
|
||||
*/
|
||||
goog.ui.ColorSplitBehavior = function(colorButton, opt_domHelper) {
|
||||
goog.ui.ColorSplitBehavior.base(this, 'constructor', colorButton,
|
||||
new goog.ui.ColorMenuButton(goog.ui.ColorSplitBehavior.ZERO_WIDTH_SPACE_),
|
||||
goog.ui.SplitBehavior.DefaultHandlers.VALUE,
|
||||
undefined,
|
||||
opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.ColorSplitBehavior, goog.ui.SplitBehavior);
|
||||
|
||||
|
||||
/**
|
||||
* A zero width space character.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ColorSplitBehavior.ZERO_WIDTH_SPACE_ = '\uFEFF';
|
||||
|
||||
@@ -0,0 +1,987 @@
|
||||
// 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 A combo box control that allows user input with
|
||||
* auto-suggestion from a limited set of options.
|
||||
*
|
||||
* @see ../demos/combobox.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ComboBox');
|
||||
goog.provide('goog.ui.ComboBoxItem');
|
||||
|
||||
goog.require('goog.Timer');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.InputHandler');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.events.KeyHandler');
|
||||
goog.require('goog.log');
|
||||
goog.require('goog.positioning.Corner');
|
||||
goog.require('goog.positioning.MenuAnchoredPosition');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.ItemEvent');
|
||||
goog.require('goog.ui.LabelInput');
|
||||
goog.require('goog.ui.Menu');
|
||||
goog.require('goog.ui.MenuItem');
|
||||
goog.require('goog.ui.MenuSeparator');
|
||||
goog.require('goog.ui.registry');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A ComboBox control.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @param {goog.ui.Menu=} opt_menu Optional menu component.
|
||||
* This menu is disposed of by this control.
|
||||
* @param {goog.ui.LabelInput=} opt_labelInput Optional label input.
|
||||
* This label input is disposed of by this control.
|
||||
* @extends {goog.ui.Component}
|
||||
* @constructor
|
||||
*/
|
||||
goog.ui.ComboBox = function(opt_domHelper, opt_menu, opt_labelInput) {
|
||||
goog.ui.Component.call(this, opt_domHelper);
|
||||
|
||||
this.labelInput_ = opt_labelInput || new goog.ui.LabelInput();
|
||||
this.enabled_ = true;
|
||||
|
||||
// TODO(user): Allow lazy creation of menus/menu items
|
||||
this.menu_ = opt_menu || new goog.ui.Menu(this.getDomHelper());
|
||||
this.setupMenu_();
|
||||
};
|
||||
goog.inherits(goog.ui.ComboBox, goog.ui.Component);
|
||||
goog.tagUnsealableClass(goog.ui.ComboBox);
|
||||
|
||||
|
||||
/**
|
||||
* Number of milliseconds to wait before dismissing combobox after blur.
|
||||
* @type {number}
|
||||
*/
|
||||
goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS = 250;
|
||||
|
||||
|
||||
/**
|
||||
* A logger to help debugging of combo box behavior.
|
||||
* @type {goog.log.Logger}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.logger_ =
|
||||
goog.log.getLogger('goog.ui.ComboBox');
|
||||
|
||||
|
||||
/**
|
||||
* Whether the combo box is enabled.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.enabled_;
|
||||
|
||||
|
||||
/**
|
||||
* Keyboard event handler to manage key events dispatched by the input element.
|
||||
* @type {goog.events.KeyHandler}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.keyHandler_;
|
||||
|
||||
|
||||
/**
|
||||
* Input handler to take care of firing events when the user inputs text in
|
||||
* the input.
|
||||
* @type {goog.events.InputHandler?}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.inputHandler_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The last input token.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.lastToken_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* A LabelInput control that manages the focus/blur state of the input box.
|
||||
* @type {goog.ui.LabelInput?}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.labelInput_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Drop down menu for the combo box. Will be created at construction time.
|
||||
* @type {goog.ui.Menu?}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.menu_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The cached visible count.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.visibleCount_ = -1;
|
||||
|
||||
|
||||
/**
|
||||
* The input element.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.input_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The match function. The first argument for the match function will be
|
||||
* a MenuItem's caption and the second will be the token to evaluate.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.matchFunction_ = goog.string.startsWith;
|
||||
|
||||
|
||||
/**
|
||||
* Element used as the combo boxes button.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.button_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Default text content for the input box when it is unchanged and unfocussed.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.defaultText_ = '';
|
||||
|
||||
|
||||
/**
|
||||
* Name for the input box created
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.fieldName_ = '';
|
||||
|
||||
|
||||
/**
|
||||
* Timer identifier for delaying the dismissal of the combo menu.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.dismissTimer_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* True if the unicode inverted triangle should be displayed in the dropdown
|
||||
* button. Defaults to false.
|
||||
* @type {boolean} useDropdownArrow
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.useDropdownArrow_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Create the DOM objects needed for the combo box. A span and text input.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.createDom = function() {
|
||||
this.input_ = this.getDomHelper().createDom(
|
||||
'input', {name: this.fieldName_, type: 'text', autocomplete: 'off'});
|
||||
this.button_ = this.getDomHelper().createDom('span',
|
||||
goog.getCssName('goog-combobox-button'));
|
||||
this.setElementInternal(this.getDomHelper().createDom('span',
|
||||
goog.getCssName('goog-combobox'), this.input_, this.button_));
|
||||
if (this.useDropdownArrow_) {
|
||||
goog.dom.setTextContent(this.button_, '\u25BC');
|
||||
goog.style.setUnselectable(this.button_, true /* unselectable */);
|
||||
}
|
||||
this.input_.setAttribute('label', this.defaultText_);
|
||||
this.labelInput_.decorate(this.input_);
|
||||
this.menu_.setFocusable(false);
|
||||
if (!this.menu_.isInDocument()) {
|
||||
this.addChild(this.menu_, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Enables/Disables the combo box.
|
||||
* @param {boolean} enabled Whether to enable (true) or disable (false) the
|
||||
* combo box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setEnabled = function(enabled) {
|
||||
this.enabled_ = enabled;
|
||||
this.labelInput_.setEnabled(enabled);
|
||||
goog.dom.classlist.enable(
|
||||
goog.asserts.assert(this.getElement()),
|
||||
goog.getCssName('goog-combobox-disabled'), !enabled);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the menu item is enabled.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.isEnabled = function() {
|
||||
return this.enabled_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ComboBox.prototype.enterDocument = function() {
|
||||
goog.ui.ComboBox.superClass_.enterDocument.call(this);
|
||||
|
||||
var handler = this.getHandler();
|
||||
handler.listen(this.getElement(),
|
||||
goog.events.EventType.MOUSEDOWN, this.onComboMouseDown_);
|
||||
handler.listen(this.getDomHelper().getDocument(),
|
||||
goog.events.EventType.MOUSEDOWN, this.onDocClicked_);
|
||||
|
||||
handler.listen(this.input_,
|
||||
goog.events.EventType.BLUR, this.onInputBlur_);
|
||||
|
||||
this.keyHandler_ = new goog.events.KeyHandler(this.input_);
|
||||
handler.listen(this.keyHandler_,
|
||||
goog.events.KeyHandler.EventType.KEY, this.handleKeyEvent);
|
||||
|
||||
this.inputHandler_ = new goog.events.InputHandler(this.input_);
|
||||
handler.listen(this.inputHandler_,
|
||||
goog.events.InputHandler.EventType.INPUT, this.onInputEvent_);
|
||||
|
||||
handler.listen(this.menu_,
|
||||
goog.ui.Component.EventType.ACTION, this.onMenuSelected_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ComboBox.prototype.exitDocument = function() {
|
||||
this.keyHandler_.dispose();
|
||||
delete this.keyHandler_;
|
||||
this.inputHandler_.dispose();
|
||||
this.inputHandler_ = null;
|
||||
goog.ui.ComboBox.superClass_.exitDocument.call(this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Combo box currently can't decorate elements.
|
||||
* @return {boolean} The value false.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.canDecorate = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ComboBox.prototype.disposeInternal = function() {
|
||||
goog.ui.ComboBox.superClass_.disposeInternal.call(this);
|
||||
|
||||
this.clearDismissTimer_();
|
||||
|
||||
this.labelInput_.dispose();
|
||||
this.menu_.dispose();
|
||||
|
||||
this.labelInput_ = null;
|
||||
this.menu_ = null;
|
||||
this.input_ = null;
|
||||
this.button_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dismisses the menu and resets the value of the edit field.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.dismiss = function() {
|
||||
this.clearDismissTimer_();
|
||||
this.hideMenu_();
|
||||
this.menu_.setHighlightedIndex(-1);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new menu item at the end of the menu.
|
||||
* @param {goog.ui.MenuItem} item Menu item to add to the menu.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.addItem = function(item) {
|
||||
this.menu_.addChild(item, true);
|
||||
this.visibleCount_ = -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new menu item at a specific index in the menu.
|
||||
* @param {goog.ui.MenuItem} item Menu item to add to the menu.
|
||||
* @param {number} n Index at which to insert the menu item.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.addItemAt = function(item, n) {
|
||||
this.menu_.addChildAt(item, n, true);
|
||||
this.visibleCount_ = -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes an item from the menu and disposes it.
|
||||
* @param {goog.ui.MenuItem} item The menu item to remove.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.removeItem = function(item) {
|
||||
var child = this.menu_.removeChild(item, true);
|
||||
if (child) {
|
||||
child.dispose();
|
||||
this.visibleCount_ = -1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove all of the items from the ComboBox menu
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.removeAllItems = function() {
|
||||
for (var i = this.getItemCount() - 1; i >= 0; --i) {
|
||||
this.removeItem(this.getItemAt(i));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a menu item at a given index in the menu.
|
||||
* @param {number} n Index of item.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.removeItemAt = function(n) {
|
||||
var child = this.menu_.removeChildAt(n, true);
|
||||
if (child) {
|
||||
child.dispose();
|
||||
this.visibleCount_ = -1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a reference to the menu item at a given index.
|
||||
* @param {number} n Index of menu item.
|
||||
* @return {goog.ui.MenuItem?} Reference to the menu item.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getItemAt = function(n) {
|
||||
return /** @type {goog.ui.MenuItem?} */(this.menu_.getChildAt(n));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the number of items in the list, including non-visible items,
|
||||
* such as separators.
|
||||
* @return {number} Number of items in the menu for this combobox.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getItemCount = function() {
|
||||
return this.menu_.getChildCount();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.ui.Menu} The menu that pops up.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getMenu = function() {
|
||||
return this.menu_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Element} The input element.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getInputElement = function() {
|
||||
return this.input_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.ui.LabelInput} A LabelInput control that manages the
|
||||
* focus/blur state of the input box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getLabelInput = function() {
|
||||
return this.labelInput_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The number of visible items in the menu.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getNumberOfVisibleItems_ = function() {
|
||||
if (this.visibleCount_ == -1) {
|
||||
var count = 0;
|
||||
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
|
||||
var item = this.menu_.getChildAt(i);
|
||||
if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
this.visibleCount_ = count;
|
||||
}
|
||||
|
||||
goog.log.info(this.logger_,
|
||||
'getNumberOfVisibleItems() - ' + this.visibleCount_);
|
||||
return this.visibleCount_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the match function to be used when filtering the combo box menu.
|
||||
* @param {Function} matchFunction The match function to be used when filtering
|
||||
* the combo box menu.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setMatchFunction = function(matchFunction) {
|
||||
this.matchFunction_ = matchFunction;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Function} The match function for the combox box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getMatchFunction = function() {
|
||||
return this.matchFunction_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the default text for the combo box.
|
||||
* @param {string} text The default text for the combo box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setDefaultText = function(text) {
|
||||
this.defaultText_ = text;
|
||||
if (this.labelInput_) {
|
||||
this.labelInput_.setLabel(this.defaultText_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} text The default text for the combox box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getDefaultText = function() {
|
||||
return this.defaultText_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the field name for the combo box.
|
||||
* @param {string} fieldName The field name for the combo box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setFieldName = function(fieldName) {
|
||||
this.fieldName_ = fieldName;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} The field name for the combo box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getFieldName = function() {
|
||||
return this.fieldName_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set to true if a unicode inverted triangle should be displayed in the
|
||||
* dropdown button.
|
||||
* This option defaults to false for backwards compatibility.
|
||||
* @param {boolean} useDropdownArrow True to use the dropdown arrow.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setUseDropdownArrow = function(useDropdownArrow) {
|
||||
this.useDropdownArrow_ = !!useDropdownArrow;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the current value of the combo box.
|
||||
* @param {string} value The new value.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setValue = function(value) {
|
||||
goog.log.info(this.logger_, 'setValue() - ' + value);
|
||||
if (this.labelInput_.getValue() != value) {
|
||||
this.labelInput_.setValue(value);
|
||||
this.handleInputChange_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} The current value of the combo box.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getValue = function() {
|
||||
return this.labelInput_.getValue();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} HTML escaped token.
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getToken = function() {
|
||||
// TODO(user): Remove HTML escaping and fix the existing calls.
|
||||
return goog.string.htmlEscape(this.getTokenText_());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string} The token for the current cursor position in the
|
||||
* input box, when multi-input is disabled it will be the full input value.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.getTokenText_ = function() {
|
||||
// TODO(user): Implement multi-input such that getToken returns a substring
|
||||
// of the whole input delimited by commas.
|
||||
return goog.string.trim(this.labelInput_.getValue().toLowerCase());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setupMenu_ = function() {
|
||||
var sm = this.menu_;
|
||||
sm.setVisible(false);
|
||||
sm.setAllowAutoFocus(false);
|
||||
sm.setAllowHighlightDisabled(true);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Shows the menu if it isn't already showing. Also positions the menu
|
||||
* correctly, resets the menu item visibilities and highlights the relevent
|
||||
* item.
|
||||
* @param {boolean} showAll Whether to show all items, with the first matching
|
||||
* item highlighted.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.maybeShowMenu_ = function(showAll) {
|
||||
var isVisible = this.menu_.isVisible();
|
||||
var numVisibleItems = this.getNumberOfVisibleItems_();
|
||||
|
||||
if (isVisible && numVisibleItems == 0) {
|
||||
goog.log.fine(this.logger_, 'no matching items, hiding');
|
||||
this.hideMenu_();
|
||||
|
||||
} else if (!isVisible && numVisibleItems > 0) {
|
||||
if (showAll) {
|
||||
goog.log.fine(this.logger_, 'showing menu');
|
||||
this.setItemVisibilityFromToken_('');
|
||||
this.setItemHighlightFromToken_(this.getTokenText_());
|
||||
}
|
||||
// In Safari 2.0, when clicking on the combox box, the blur event is
|
||||
// received after the click event that invokes this function. Since we want
|
||||
// to cancel the dismissal after the blur event is processed, we have to
|
||||
// wait for all event processing to happen.
|
||||
goog.Timer.callOnce(this.clearDismissTimer_, 1, this);
|
||||
|
||||
this.showMenu_();
|
||||
}
|
||||
|
||||
this.positionMenu();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Positions the menu.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.positionMenu = function() {
|
||||
if (this.menu_ && this.menu_.isVisible()) {
|
||||
var position = new goog.positioning.MenuAnchoredPosition(this.getElement(),
|
||||
goog.positioning.Corner.BOTTOM_START, true);
|
||||
position.reposition(this.menu_.getElement(),
|
||||
goog.positioning.Corner.TOP_START);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Show the menu and add an active class to the combo box's element.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.showMenu_ = function() {
|
||||
this.menu_.setVisible(true);
|
||||
goog.dom.classlist.add(
|
||||
goog.asserts.assert(this.getElement()),
|
||||
goog.getCssName('goog-combobox-active'));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Hide the menu and remove the active class from the combo box's element.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.hideMenu_ = function() {
|
||||
this.menu_.setVisible(false);
|
||||
goog.dom.classlist.remove(
|
||||
goog.asserts.assert(this.getElement()),
|
||||
goog.getCssName('goog-combobox-active'));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the dismiss timer if it's active.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.clearDismissTimer_ = function() {
|
||||
if (this.dismissTimer_) {
|
||||
goog.Timer.clear(this.dismissTimer_);
|
||||
this.dismissTimer_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Event handler for when the combo box area has been clicked.
|
||||
* @param {goog.events.BrowserEvent} e The browser event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.onComboMouseDown_ = function(e) {
|
||||
// We only want this event on the element itself or the input or the button.
|
||||
if (this.enabled_ &&
|
||||
(e.target == this.getElement() || e.target == this.input_ ||
|
||||
goog.dom.contains(this.button_, /** @type {Node} */ (e.target)))) {
|
||||
if (this.menu_.isVisible()) {
|
||||
goog.log.fine(this.logger_, 'Menu is visible, dismissing');
|
||||
this.dismiss();
|
||||
} else {
|
||||
goog.log.fine(this.logger_, 'Opening dropdown');
|
||||
this.maybeShowMenu_(true);
|
||||
if (goog.userAgent.OPERA) {
|
||||
// select() doesn't focus <input> elements in Opera.
|
||||
this.input_.focus();
|
||||
}
|
||||
this.input_.select();
|
||||
this.menu_.setMouseButtonPressed(true);
|
||||
// Stop the click event from stealing focus
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
// Stop the event from propagating outside of the combo box
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Event handler for when the document is clicked.
|
||||
* @param {goog.events.BrowserEvent} e The browser event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.onDocClicked_ = function(e) {
|
||||
if (!goog.dom.contains(
|
||||
this.menu_.getElement(), /** @type {Node} */ (e.target))) {
|
||||
goog.log.info(this.logger_, 'onDocClicked_() - dismissing immediately');
|
||||
this.dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle the menu's select event.
|
||||
* @param {goog.events.Event} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.onMenuSelected_ = function(e) {
|
||||
goog.log.info(this.logger_, 'onMenuSelected_()');
|
||||
var item = /** @type {!goog.ui.MenuItem} */ (e.target);
|
||||
// Stop propagation of the original event and redispatch to allow the menu
|
||||
// select to be cancelled at this level. i.e. if a menu item should cause
|
||||
// some behavior such as a user prompt instead of assigning the caption as
|
||||
// the value.
|
||||
if (this.dispatchEvent(new goog.ui.ItemEvent(
|
||||
goog.ui.Component.EventType.ACTION, this, item))) {
|
||||
var caption = item.getCaption();
|
||||
goog.log.fine(this.logger_,
|
||||
'Menu selection: ' + caption + '. Dismissing menu');
|
||||
if (this.labelInput_.getValue() != caption) {
|
||||
this.labelInput_.setValue(caption);
|
||||
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
|
||||
}
|
||||
this.dismiss();
|
||||
}
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Event handler for when the input box looses focus -- hide the menu
|
||||
* @param {goog.events.BrowserEvent} e The browser event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.onInputBlur_ = function(e) {
|
||||
goog.log.info(this.logger_, 'onInputBlur_() - delayed dismiss');
|
||||
this.clearDismissTimer_();
|
||||
this.dismissTimer_ = goog.Timer.callOnce(
|
||||
this.dismiss, goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles keyboard events from the input box. Returns true if the combo box
|
||||
* was able to handle the event, false otherwise.
|
||||
* @param {goog.events.KeyEvent} e Key event to handle.
|
||||
* @return {boolean} Whether the event was handled by the combo box.
|
||||
* @protected
|
||||
* @suppress {visibility} performActionInternal
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.handleKeyEvent = function(e) {
|
||||
var isMenuVisible = this.menu_.isVisible();
|
||||
|
||||
// Give the menu a chance to handle the event.
|
||||
if (isMenuVisible && this.menu_.handleKeyEvent(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The menu is either hidden or didn't handle the event.
|
||||
var handled = false;
|
||||
switch (e.keyCode) {
|
||||
case goog.events.KeyCodes.ESC:
|
||||
// If the menu is visible and the user hit Esc, dismiss the menu.
|
||||
if (isMenuVisible) {
|
||||
goog.log.fine(this.logger_,
|
||||
'Dismiss on Esc: ' + this.labelInput_.getValue());
|
||||
this.dismiss();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case goog.events.KeyCodes.TAB:
|
||||
// If the menu is open and an option is highlighted, activate it.
|
||||
if (isMenuVisible) {
|
||||
var highlighted = this.menu_.getHighlighted();
|
||||
if (highlighted) {
|
||||
goog.log.fine(this.logger_,
|
||||
'Select on Tab: ' + this.labelInput_.getValue());
|
||||
highlighted.performActionInternal(e);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case goog.events.KeyCodes.UP:
|
||||
case goog.events.KeyCodes.DOWN:
|
||||
// If the menu is hidden and the user hit the up/down arrow, show it.
|
||||
if (!isMenuVisible) {
|
||||
goog.log.fine(this.logger_, 'Up/Down - maybe show menu');
|
||||
this.maybeShowMenu_(true);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the content of the input box changing.
|
||||
* @param {goog.events.Event} e The INPUT event to handle.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.onInputEvent_ = function(e) {
|
||||
// If the key event is text-modifying, update the menu.
|
||||
goog.log.fine(this.logger_,
|
||||
'Key is modifying: ' + this.labelInput_.getValue());
|
||||
this.handleInputChange_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the content of the input box changing, either because of user
|
||||
* interaction or programmatic changes.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.handleInputChange_ = function() {
|
||||
var token = this.getTokenText_();
|
||||
this.setItemVisibilityFromToken_(token);
|
||||
if (goog.dom.getActiveElement(this.getDomHelper().getDocument()) ==
|
||||
this.input_) {
|
||||
// Do not alter menu visibility unless the user focus is currently on the
|
||||
// combobox (otherwise programmatic changes may cause the menu to become
|
||||
// visible).
|
||||
this.maybeShowMenu_(false);
|
||||
}
|
||||
var highlighted = this.menu_.getHighlighted();
|
||||
if (token == '' || !highlighted || !highlighted.isVisible()) {
|
||||
this.setItemHighlightFromToken_(token);
|
||||
}
|
||||
this.lastToken_ = token;
|
||||
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Loops through all menu items setting their visibility according to a token.
|
||||
* @param {string} token The token.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setItemVisibilityFromToken_ = function(token) {
|
||||
goog.log.info(this.logger_, 'setItemVisibilityFromToken_() - ' + token);
|
||||
var isVisibleItem = false;
|
||||
var count = 0;
|
||||
var recheckHidden = !this.matchFunction_(token, this.lastToken_);
|
||||
|
||||
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
|
||||
var item = this.menu_.getChildAt(i);
|
||||
if (item instanceof goog.ui.MenuSeparator) {
|
||||
// Ensure that separators are only shown if there is at least one visible
|
||||
// item before them.
|
||||
item.setVisible(isVisibleItem);
|
||||
isVisibleItem = false;
|
||||
} else if (item instanceof goog.ui.MenuItem) {
|
||||
if (!item.isVisible() && !recheckHidden) continue;
|
||||
|
||||
var caption = item.getCaption();
|
||||
var visible = this.isItemSticky_(item) ||
|
||||
caption && this.matchFunction_(caption.toLowerCase(), token);
|
||||
if (typeof item.setFormatFromToken == 'function') {
|
||||
item.setFormatFromToken(token);
|
||||
}
|
||||
item.setVisible(!!visible);
|
||||
isVisibleItem = visible || isVisibleItem;
|
||||
|
||||
} else {
|
||||
// Assume all other items are correctly using their visibility.
|
||||
isVisibleItem = item.isVisible() || isVisibleItem;
|
||||
}
|
||||
|
||||
if (!(item instanceof goog.ui.MenuSeparator) && item.isVisible()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
this.visibleCount_ = count;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Highlights the first token that matches the given token.
|
||||
* @param {string} token The token.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.setItemHighlightFromToken_ = function(token) {
|
||||
goog.log.info(this.logger_, 'setItemHighlightFromToken_() - ' + token);
|
||||
|
||||
if (token == '') {
|
||||
this.menu_.setHighlightedIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0, n = this.menu_.getChildCount(); i < n; i++) {
|
||||
var item = this.menu_.getChildAt(i);
|
||||
var caption = item.getCaption();
|
||||
if (caption && this.matchFunction_(caption.toLowerCase(), token)) {
|
||||
this.menu_.setHighlightedIndex(i);
|
||||
if (item.setFormatFromToken) {
|
||||
item.setFormatFromToken(token);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.menu_.setHighlightedIndex(-1);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the item has an isSticky method and the method returns true.
|
||||
* @param {goog.ui.MenuItem} item The item.
|
||||
* @return {boolean} Whether the item has an isSticky method and the method
|
||||
* returns true.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBox.prototype.isItemSticky_ = function(item) {
|
||||
return typeof item.isSticky == 'function' && item.isSticky();
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Class for combo box items.
|
||||
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
|
||||
* display as the content of the item (use to add icons or styling to
|
||||
* menus).
|
||||
* @param {Object=} opt_data Identifying data for the menu item.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper used for dom
|
||||
* interactions.
|
||||
* @param {goog.ui.MenuItemRenderer=} opt_renderer Optional renderer.
|
||||
* @constructor
|
||||
* @extends {goog.ui.MenuItem}
|
||||
*/
|
||||
goog.ui.ComboBoxItem = function(content, opt_data, opt_domHelper,
|
||||
opt_renderer) {
|
||||
goog.ui.MenuItem.call(this, content, opt_data, opt_domHelper, opt_renderer);
|
||||
};
|
||||
goog.inherits(goog.ui.ComboBoxItem, goog.ui.MenuItem);
|
||||
goog.tagUnsealableClass(goog.ui.ComboBoxItem);
|
||||
|
||||
|
||||
// Register a decorator factory function for goog.ui.ComboBoxItems.
|
||||
goog.ui.registry.setDecoratorByClassName(goog.getCssName('goog-combobox-item'),
|
||||
function() {
|
||||
// ComboBoxItem defaults to using MenuItemRenderer.
|
||||
return new goog.ui.ComboBoxItem(null);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Whether the menu item is sticky, non-sticky items will be hidden as the
|
||||
* user types.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ComboBoxItem.prototype.isSticky_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the menu item to be sticky or not sticky.
|
||||
* @param {boolean} sticky Whether the menu item should be sticky.
|
||||
*/
|
||||
goog.ui.ComboBoxItem.prototype.setSticky = function(sticky) {
|
||||
this.isSticky_ = sticky;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the menu item is sticky.
|
||||
*/
|
||||
goog.ui.ComboBoxItem.prototype.isSticky = function() {
|
||||
return this.isSticky_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the format for a menu item based on a token, bolding the token.
|
||||
* @param {string} token The token.
|
||||
*/
|
||||
goog.ui.ComboBoxItem.prototype.setFormatFromToken = function(token) {
|
||||
if (this.isEnabled()) {
|
||||
var caption = this.getCaption();
|
||||
var index = caption.toLowerCase().indexOf(token);
|
||||
if (index >= 0) {
|
||||
var domHelper = this.getDomHelper();
|
||||
this.setContent([
|
||||
domHelper.createTextNode(caption.substr(0, index)),
|
||||
domHelper.createDom('b', null, caption.substr(index, token.length)),
|
||||
domHelper.createTextNode(caption.substr(index + token.length))
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
|
||||
@author gboyer@google.com (Garrett Boyer)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ComboBox
|
||||
</title>
|
||||
<style type="text/css">
|
||||
.goog-menu {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ComboBoxTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h2 style="color:red">
|
||||
This test is rudimentary.
|
||||
The fact that it passes should not (yet) make you too confident.
|
||||
</h2>
|
||||
<div id="combo">
|
||||
</div>
|
||||
<div id="menu">
|
||||
<div class="goog-combobox-item">
|
||||
Red
|
||||
</div>
|
||||
<div class="goog-combobox-item">
|
||||
Green
|
||||
</div>
|
||||
<div class="goog-combobox-item">
|
||||
Blue
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,271 @@
|
||||
// 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.ComboBoxTest');
|
||||
goog.setTestOnly('goog.ui.ComboBoxTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ComboBox');
|
||||
goog.require('goog.ui.ComboBoxItem');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.ControlRenderer');
|
||||
goog.require('goog.ui.LabelInput');
|
||||
goog.require('goog.ui.Menu');
|
||||
goog.require('goog.ui.MenuItem');
|
||||
|
||||
var comboBox;
|
||||
var input;
|
||||
|
||||
function setUp() {
|
||||
goog.dom.getElement('combo').innerHTML = '';
|
||||
|
||||
comboBox = new goog.ui.ComboBox();
|
||||
comboBox.setDefaultText('Select a color...');
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Red'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Maroon'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Gre<en'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Blue'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Royal Blue'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Yellow'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Magenta'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Mouve'));
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Grey'));
|
||||
comboBox.render(goog.dom.getElement('combo'));
|
||||
|
||||
input = comboBox.getInputElement();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
comboBox.dispose();
|
||||
}
|
||||
|
||||
function testInputElementAttributes() {
|
||||
var comboBox = new goog.ui.ComboBox();
|
||||
comboBox.setFieldName('a_form_field');
|
||||
comboBox.createDom();
|
||||
var inputElement = comboBox.getInputElement();
|
||||
assertEquals('text', inputElement.type);
|
||||
assertEquals('a_form_field', inputElement.name);
|
||||
assertEquals('off', inputElement.autocomplete);
|
||||
comboBox.dispose();
|
||||
}
|
||||
|
||||
function testSetDefaultText() {
|
||||
assertEquals('Select a color...', comboBox.getDefaultText());
|
||||
comboBox.setDefaultText('new default text...');
|
||||
assertEquals('new default text...', comboBox.getDefaultText());
|
||||
assertEquals('new default text...', comboBox.labelInput_.getLabel());
|
||||
}
|
||||
|
||||
function testGetMenu() {
|
||||
assertTrue('Menu should be instance of goog.ui.Menu',
|
||||
comboBox.getMenu() instanceof goog.ui.Menu);
|
||||
assertEquals('Menu should have correct number of children',
|
||||
9, comboBox.getMenu().getChildCount());
|
||||
}
|
||||
|
||||
function testMenuBeginsInvisible() {
|
||||
assertFalse('Menu should begin invisible', comboBox.getMenu().isVisible());
|
||||
}
|
||||
|
||||
function testClickCausesPopup() {
|
||||
goog.testing.events.fireClickSequence(input);
|
||||
assertTrue('Menu becomes visible after click',
|
||||
comboBox.getMenu().isVisible());
|
||||
}
|
||||
|
||||
function testUpKeyCausesPopup() {
|
||||
goog.testing.events.fireKeySequence(input, goog.events.KeyCodes.UP);
|
||||
assertTrue('Menu becomes visible after UP key',
|
||||
comboBox.getMenu().isVisible());
|
||||
}
|
||||
|
||||
function testActionSelectsItem() {
|
||||
comboBox.getMenu().getItemAt(2).dispatchEvent(
|
||||
goog.ui.Component.EventType.ACTION);
|
||||
assertEquals('Gre<en', input.value);
|
||||
}
|
||||
|
||||
function testActionSelectsItemWithModel() {
|
||||
var itemWithModel = new goog.ui.MenuItem('one', 1);
|
||||
comboBox.addItem(itemWithModel);
|
||||
itemWithModel.dispatchEvent(goog.ui.Component.EventType.ACTION);
|
||||
assertEquals('one', comboBox.getValue());
|
||||
}
|
||||
|
||||
function testRedisplayMenuAfterBackspace() {
|
||||
input.value = 'mx';
|
||||
comboBox.onInputEvent_();
|
||||
input.value = 'm';
|
||||
comboBox.onInputEvent_();
|
||||
assertEquals('Three items should be displayed',
|
||||
3, comboBox.getNumberOfVisibleItems_());
|
||||
}
|
||||
|
||||
function testExternallyCreatedMenu() {
|
||||
var menu = new goog.ui.Menu();
|
||||
menu.decorate(goog.dom.getElement('menu'));
|
||||
assertTrue('Menu items should be instances of goog.ui.ComboBoxItem',
|
||||
menu.getChildAt(0) instanceof goog.ui.ComboBoxItem);
|
||||
|
||||
comboBox = new goog.ui.ComboBox(null, menu);
|
||||
comboBox.render(goog.dom.getElement('combo'));
|
||||
|
||||
input = comboBox.getElement().getElementsByTagName(
|
||||
goog.dom.TagName.INPUT)[0];
|
||||
menu.getItemAt(2).dispatchEvent(
|
||||
goog.ui.Component.EventType.ACTION);
|
||||
assertEquals('Blue', input.value);
|
||||
}
|
||||
|
||||
function testRecomputeVisibleCountAfterChangingItems() {
|
||||
input.value = 'Black';
|
||||
comboBox.onInputEvent_();
|
||||
assertEquals('No items should be displayed',
|
||||
0, comboBox.getNumberOfVisibleItems_());
|
||||
comboBox.addItem(new goog.ui.ComboBoxItem('Black'));
|
||||
assertEquals('One item should be displayed',
|
||||
1, comboBox.getNumberOfVisibleItems_());
|
||||
|
||||
input.value = 'Red';
|
||||
comboBox.onInputEvent_();
|
||||
assertEquals('One item should be displayed',
|
||||
1, comboBox.getNumberOfVisibleItems_());
|
||||
comboBox.removeItemAt(0); // Red
|
||||
assertEquals('No items should be displayed',
|
||||
0, comboBox.getNumberOfVisibleItems_());
|
||||
}
|
||||
|
||||
function testSetEnabled() {
|
||||
// By default, everything should be enabled.
|
||||
assertFalse('Text input should initially not be disabled', input.disabled);
|
||||
assertFalse('Text input should initially not look disabled',
|
||||
goog.dom.classlist.contains(input,
|
||||
goog.getCssName(goog.ui.LabelInput.prototype.labelCssClassName,
|
||||
'disabled')));
|
||||
assertFalse('Combo box should initially not look disabled',
|
||||
goog.dom.classlist.contains(comboBox.getElement(),
|
||||
goog.getCssName('goog-combobox-disabled')));
|
||||
goog.testing.events.fireClickSequence(comboBox.getElement());
|
||||
assertTrue('Menu initially becomes visible after click',
|
||||
comboBox.getMenu().isVisible());
|
||||
goog.testing.events.fireClickSequence(document);
|
||||
assertFalse('Menu initially becomes invisible after document click',
|
||||
comboBox.getMenu().isVisible());
|
||||
|
||||
assertTrue(comboBox.isEnabled());
|
||||
comboBox.setEnabled(false);
|
||||
assertFalse(comboBox.isEnabled());
|
||||
assertTrue('Text input should be disabled after being disabled',
|
||||
input.disabled);
|
||||
assertTrue('Text input should appear disabled after being disabled',
|
||||
goog.dom.classlist.contains(input,
|
||||
goog.getCssName(goog.ui.LabelInput.prototype.labelCssClassName,
|
||||
'disabled')));
|
||||
assertTrue('Combo box should appear disabled after being disabled',
|
||||
goog.dom.classlist.contains(comboBox.getElement(),
|
||||
goog.getCssName('goog-combobox-disabled')));
|
||||
goog.testing.events.fireClickSequence(comboBox.getElement());
|
||||
assertFalse('Menu should not become visible after click if disabled',
|
||||
comboBox.getMenu().isVisible());
|
||||
|
||||
comboBox.setEnabled(true);
|
||||
assertTrue(comboBox.isEnabled());
|
||||
assertFalse('Text input should not be disabled after being re-enabled',
|
||||
input.disabled);
|
||||
assertFalse('Text input should not appear disabled after being re-enabled',
|
||||
goog.dom.classlist.contains(input,
|
||||
goog.getCssName(goog.ui.LabelInput.prototype.labelCssClassName,
|
||||
'disabled')));
|
||||
assertFalse('Combo box should not appear disabled after being re-enabled',
|
||||
goog.dom.classlist.contains(comboBox.getElement(),
|
||||
goog.getCssName('goog-combobox-disabled')));
|
||||
goog.testing.events.fireClickSequence(comboBox.getElement());
|
||||
assertTrue('Menu becomes visible after click when re-enabled',
|
||||
comboBox.getMenu().isVisible());
|
||||
goog.testing.events.fireClickSequence(document);
|
||||
assertFalse('Menu becomes invisible after document click when re-enabled',
|
||||
comboBox.getMenu().isVisible());
|
||||
}
|
||||
|
||||
function testSetFormatFromToken() {
|
||||
var item = new goog.ui.ComboBoxItem('ABc');
|
||||
item.setFormatFromToken('b');
|
||||
var div = goog.dom.createDom('div');
|
||||
new goog.ui.ControlRenderer().setContent(div, item.getContent());
|
||||
assertTrue(div.innerHTML == 'A<b>B</b>c' || div.innerHTML == 'A<B>B</B>c');
|
||||
}
|
||||
|
||||
function testSetValue() {
|
||||
var clock = new goog.testing.MockClock(/* autoInstall */ true);
|
||||
|
||||
// Get the input focus. Note that both calls are needed to correctly
|
||||
// simulate the focus (and setting document.activeElement) across all
|
||||
// browsers.
|
||||
input.focus();
|
||||
goog.testing.events.fireClickSequence(input);
|
||||
|
||||
// Simulate text input.
|
||||
input.value = 'Black';
|
||||
comboBox.onInputEvent_();
|
||||
clock.tick();
|
||||
assertEquals('No items should be displayed',
|
||||
0, comboBox.getNumberOfVisibleItems_());
|
||||
assertFalse('Menu should be invisible', comboBox.getMenu().isVisible());
|
||||
|
||||
// Programmatic change with the input focus causes the menu visibility to
|
||||
// change if needed.
|
||||
comboBox.setValue('Blue');
|
||||
clock.tick();
|
||||
assertTrue('Menu should be visible1', comboBox.getMenu().isVisible());
|
||||
assertEquals('One item should be displayed',
|
||||
1, comboBox.getNumberOfVisibleItems_());
|
||||
|
||||
// Simulate user input to ensure all the items are invisible again, then
|
||||
// blur away.
|
||||
input.value = 'Black';
|
||||
comboBox.onInputEvent_();
|
||||
clock.tick();
|
||||
input.blur();
|
||||
document.body.focus();
|
||||
clock.tick(goog.ui.ComboBox.BLUR_DISMISS_TIMER_MS);
|
||||
assertEquals('No items should be displayed',
|
||||
0, comboBox.getNumberOfVisibleItems_());
|
||||
assertFalse('Menu should be invisible', comboBox.getMenu().isVisible());
|
||||
|
||||
// Programmatic change without the input focus does not pop up the menu,
|
||||
// but still updates the list of visible items within it.
|
||||
comboBox.setValue('Blue');
|
||||
clock.tick();
|
||||
assertFalse('Menu should be invisible', comboBox.getMenu().isVisible());
|
||||
assertEquals('Menu should contain one item',
|
||||
1, comboBox.getNumberOfVisibleItems_());
|
||||
|
||||
// Click on the combobox. The entire menu becomes visible, the last item
|
||||
// (programmatically) set is highlighted.
|
||||
goog.testing.events.fireClickSequence(comboBox.getElement());
|
||||
assertTrue('Menu should be visible2', comboBox.getMenu().isVisible());
|
||||
assertEquals('All items should be displayed',
|
||||
comboBox.getMenu().getItemCount(), comboBox.getNumberOfVisibleItems_());
|
||||
assertEquals('The last item set should be highlighted',
|
||||
/* Blue= */ 3, comboBox.getMenu().getHighlightedIndex());
|
||||
|
||||
clock.uninstall();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.Component
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ComponentTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,892 @@
|
||||
// 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.ComponentTest');
|
||||
goog.setTestOnly('goog.ui.ComponentTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.DomHelper');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.testing.PropertyReplacer');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
var component;
|
||||
var propertyReplacer = new goog.testing.PropertyReplacer();
|
||||
var sandbox;
|
||||
|
||||
function setUp() {
|
||||
sandbox = goog.dom.getElement('sandbox');
|
||||
component = new goog.ui.Component();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
component.dispose();
|
||||
goog.dom.removeChildren(sandbox);
|
||||
propertyReplacer.reset();
|
||||
}
|
||||
|
||||
function testConstructor() {
|
||||
assertTrue('Instance must be non-null and have the expected class',
|
||||
component instanceof goog.ui.Component);
|
||||
assertTrue('DOM helper must be non-null and have the expected class',
|
||||
component.dom_ instanceof goog.dom.DomHelper);
|
||||
|
||||
var fakeDom = {};
|
||||
var otherComponent = new goog.ui.Component(fakeDom);
|
||||
assertEquals('DOM helper must refer to expected object', fakeDom,
|
||||
otherComponent.dom_);
|
||||
|
||||
otherComponent.dispose();
|
||||
}
|
||||
|
||||
function testGetId() {
|
||||
assertNull('Component ID should be initialized to null', component.id_);
|
||||
var id = component.getId();
|
||||
assertNotNull('Component ID should be generated on demand', id);
|
||||
assertEquals('Subsequent calls to getId() must return same value', id,
|
||||
component.getId());
|
||||
}
|
||||
|
||||
function testSetId() {
|
||||
component.setId('myId');
|
||||
assertEquals('getId() must return explicitly set ID', 'myId',
|
||||
component.getId());
|
||||
|
||||
var child = new goog.ui.Component();
|
||||
var childId = child.getId();
|
||||
component.addChild(child);
|
||||
assertEquals('Parent component must find child by ID', child,
|
||||
component.getChild(childId));
|
||||
|
||||
child.setId('someNewId');
|
||||
assertEquals('Parent component must find child by new ID', child,
|
||||
component.getChild('someNewId'));
|
||||
|
||||
child.dispose();
|
||||
}
|
||||
|
||||
function testGetSetElement() {
|
||||
assertNull('Element must be null by default', component.getElement());
|
||||
var element = goog.dom.createElement(goog.dom.TagName.DIV);
|
||||
component.setElementInternal(element);
|
||||
assertEquals('getElement() must return expected element', element,
|
||||
component.getElement());
|
||||
}
|
||||
|
||||
function testGetSetParent() {
|
||||
assertNull('Parent must be null by default', component.getParent());
|
||||
|
||||
var parent = new goog.ui.Component();
|
||||
component.setParent(parent);
|
||||
assertEquals('getParent() must return expected component', parent,
|
||||
component.getParent());
|
||||
|
||||
component.setParent(null);
|
||||
assertNull('Parent must be null', component.getParent());
|
||||
|
||||
assertThrows('Setting a component\'s parent to itself must throw error',
|
||||
function() {
|
||||
component.setParent(component);
|
||||
});
|
||||
|
||||
parent.addChild(component);
|
||||
assertEquals('getParent() must return expected component', parent,
|
||||
component.getParent());
|
||||
assertThrows('Changing a child component\'s parent must throw error',
|
||||
function() {
|
||||
component.setParent(new goog.ui.Component());
|
||||
});
|
||||
|
||||
parent.dispose();
|
||||
}
|
||||
|
||||
function testGetParentEventTarget() {
|
||||
assertNull('Parent event target must be null by default',
|
||||
component.getParentEventTarget());
|
||||
|
||||
var parent = new goog.ui.Component();
|
||||
component.setParent(parent);
|
||||
assertEquals('Parent event target must be the parent component', parent,
|
||||
component.getParentEventTarget());
|
||||
assertThrows('Directly setting the parent event target to other than ' +
|
||||
'the parent component when the parent component is set must throw ' +
|
||||
'error',
|
||||
function() {
|
||||
component.setParentEventTarget(new goog.ui.Component());
|
||||
});
|
||||
|
||||
parent.dispose();
|
||||
}
|
||||
|
||||
function testSetParentEventTarget() {
|
||||
var parentEventTarget = new goog.events.EventTarget();
|
||||
component.setParentEventTarget(parentEventTarget);
|
||||
assertEquals('Parent component must be null', null,
|
||||
component.getParent());
|
||||
|
||||
parentEventTarget.dispose();
|
||||
}
|
||||
|
||||
function testGetDomHelper() {
|
||||
var domHelper = new goog.dom.DomHelper();
|
||||
var component = new goog.ui.Component(domHelper);
|
||||
assertEquals('Component must return the same DomHelper passed', domHelper,
|
||||
component.getDomHelper());
|
||||
}
|
||||
|
||||
function testIsInDocument() {
|
||||
assertFalse('Component must not be in the document by default',
|
||||
component.isInDocument());
|
||||
component.enterDocument();
|
||||
assertTrue('Component must be in the document', component.isInDocument());
|
||||
}
|
||||
|
||||
function testCreateDom() {
|
||||
assertNull('Component must not have DOM by default',
|
||||
component.getElement());
|
||||
component.createDom();
|
||||
assertEquals('Component\'s DOM must be an element node',
|
||||
goog.dom.NodeType.ELEMENT, component.getElement().nodeType);
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
assertFalse('Component must not be in the document by default',
|
||||
component.isInDocument());
|
||||
assertNull('Component must not have DOM by default',
|
||||
component.getElement());
|
||||
assertFalse('wasDecorated() must be false before component is rendered',
|
||||
component.wasDecorated());
|
||||
|
||||
component.render(sandbox);
|
||||
assertTrue('Rendered component must be in the document',
|
||||
component.isInDocument());
|
||||
assertEquals('Component\'s element must be a child of the parent element',
|
||||
sandbox, component.getElement().parentNode);
|
||||
assertFalse('wasDecorated() must still be false for rendered component',
|
||||
component.wasDecorated());
|
||||
|
||||
assertThrows('Trying to re-render component must throw error',
|
||||
function() {
|
||||
component.render();
|
||||
});
|
||||
}
|
||||
|
||||
function testRender_NoParent() {
|
||||
component.render();
|
||||
assertTrue('Rendered component must be in the document',
|
||||
component.isInDocument());
|
||||
assertEquals('Component\'s element must be a child of the document body',
|
||||
document.body, component.getElement().parentNode);
|
||||
}
|
||||
|
||||
function testRender_ParentNotInDocument() {
|
||||
var parent = new goog.ui.Component();
|
||||
component.setParent(parent);
|
||||
|
||||
assertFalse('Parent component must not be in the document',
|
||||
parent.isInDocument());
|
||||
assertFalse('Child component must not be in the document',
|
||||
component.isInDocument());
|
||||
assertNull('Child component must not have DOM', component.getElement());
|
||||
|
||||
component.render();
|
||||
assertFalse('Parent component must not be in the document',
|
||||
parent.isInDocument());
|
||||
assertFalse('Child component must not be in the document',
|
||||
component.isInDocument());
|
||||
assertNotNull('Child component must have DOM', component.getElement());
|
||||
|
||||
parent.dispose();
|
||||
}
|
||||
|
||||
|
||||
function testRenderBefore() {
|
||||
var sibling = goog.dom.createElement(goog.dom.TagName.DIV);
|
||||
sandbox.appendChild(sibling);
|
||||
|
||||
component.renderBefore(sibling);
|
||||
assertTrue('Rendered component must be in the document',
|
||||
component.isInDocument());
|
||||
assertEquals('Component\'s element must be a child of the parent element',
|
||||
sandbox, component.getElement().parentNode);
|
||||
assertEquals('Component\'s element must have expected nextSibling',
|
||||
sibling, component.getElement().nextSibling);
|
||||
}
|
||||
|
||||
|
||||
function testRenderChild() {
|
||||
var parent = new goog.ui.Component();
|
||||
|
||||
parent.createDom();
|
||||
assertFalse('Parent must not be in the document', parent.isInDocument());
|
||||
assertNotNull('Parent must have a DOM', parent.getElement());
|
||||
|
||||
parent.addChild(component);
|
||||
assertFalse('Child must not be in the document',
|
||||
component.isInDocument());
|
||||
assertNull('Child must not have a DOM', component.getElement());
|
||||
|
||||
component.render(parent.getElement());
|
||||
assertFalse('Parent must not be in the document', parent.isInDocument());
|
||||
assertFalse('Child must not be in the document if the parent isn\'t',
|
||||
component.isInDocument());
|
||||
assertNotNull('Child must have a DOM', component.getElement());
|
||||
assertEquals('Child\'s element must be a child of the parent\'s element',
|
||||
parent.getElement(), component.getElement().parentNode);
|
||||
|
||||
parent.render(sandbox);
|
||||
assertTrue('Parent must be in the document', parent.isInDocument());
|
||||
assertTrue('Child must be in the document', component.isInDocument());
|
||||
|
||||
parent.dispose();
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
sandbox.innerHTML = '<div id="foo">Foo</div>';
|
||||
var foo = goog.dom.getElement('foo');
|
||||
|
||||
assertFalse('wasDecorated() must be false by default',
|
||||
component.wasDecorated());
|
||||
|
||||
component.decorate(foo);
|
||||
assertTrue('Component must be in the document', component.isInDocument());
|
||||
assertEquals('Component\'s element must be the decorated element', foo,
|
||||
component.getElement());
|
||||
assertTrue('wasDecorated() must be true for decorated component',
|
||||
component.wasDecorated());
|
||||
|
||||
assertThrows('Trying to decorate with a control already in the document' +
|
||||
' must throw error',
|
||||
function() {
|
||||
component.decorate(foo);
|
||||
});
|
||||
}
|
||||
|
||||
function testDecorate_AllowDetached_NotInDocument() {
|
||||
goog.ui.Component.ALLOW_DETACHED_DECORATION = true;
|
||||
var element = document.createElement('div');
|
||||
component.decorate(element);
|
||||
assertFalse('Component should not call enterDocument when decorated ' +
|
||||
'with an element that is not in the document.',
|
||||
component.isInDocument());
|
||||
goog.ui.Component.ALLOW_DETACHED_DECORATION = false;
|
||||
}
|
||||
|
||||
function testDecorate_AllowDetached_InDocument() {
|
||||
goog.ui.Component.ALLOW_DETACHED_DECORATION = true;
|
||||
var element = document.createElement('div');
|
||||
sandbox.appendChild(element);
|
||||
component.decorate(element);
|
||||
assertTrue('Component should call enterDocument when decorated ' +
|
||||
'with an element that is in the document.',
|
||||
component.isInDocument());
|
||||
goog.ui.Component.ALLOW_DETACHED_DECORATION = false;
|
||||
}
|
||||
|
||||
function testCannotDecorate() {
|
||||
sandbox.innerHTML = '<div id="foo">Foo</div>';
|
||||
var foo = goog.dom.getElement('foo');
|
||||
|
||||
// Have canDecorate() return false.
|
||||
propertyReplacer.set(component, 'canDecorate', function() {
|
||||
return false;
|
||||
});
|
||||
|
||||
assertThrows('Trying to decorate an element for which canDecorate()' +
|
||||
' returns false must throw error',
|
||||
function() {
|
||||
component.decorate(foo);
|
||||
});
|
||||
}
|
||||
|
||||
function testCanDecorate() {
|
||||
assertTrue('canDecorate() must return true by default',
|
||||
component.canDecorate(sandbox));
|
||||
}
|
||||
|
||||
function testWasDecorated() {
|
||||
assertFalse('wasDecorated() must return false by default',
|
||||
component.wasDecorated());
|
||||
}
|
||||
|
||||
function testDecorateInternal() {
|
||||
assertNull('Element must be null by default', component.getElement());
|
||||
var element = goog.dom.createElement(goog.dom.TagName.DIV);
|
||||
component.decorateInternal(element);
|
||||
assertEquals('Element must have expected value', element,
|
||||
component.getElement());
|
||||
}
|
||||
|
||||
function testGetElementAndGetElementsByClass() {
|
||||
sandbox.innerHTML =
|
||||
'<ul id="task-list">' +
|
||||
'<li class="task">Unclog drain' +
|
||||
'</ul>' +
|
||||
'<ul id="completed-tasks">' +
|
||||
'<li id="groceries" class="task">Buy groceries' +
|
||||
'<li class="task">Rotate tires' +
|
||||
'<li class="task">Clean kitchen' +
|
||||
'</ul>' +
|
||||
assertNull(
|
||||
'Should be nothing to return before the component has a DOM',
|
||||
component.getElementByClass('task'));
|
||||
assertEquals('Should return an empty list before the component has a DOM',
|
||||
0,
|
||||
component.getElementsByClass('task').length);
|
||||
|
||||
component.decorate(goog.dom.getElement('completed-tasks'));
|
||||
assertEquals(
|
||||
'getElementByClass() should return the first completed task',
|
||||
'groceries',
|
||||
component.getElementByClass('task').id);
|
||||
assertEquals(
|
||||
'getElementsByClass() should return only the completed tasks',
|
||||
3,
|
||||
component.getElementsByClass('task').length);
|
||||
}
|
||||
|
||||
function testGetRequiredElementByClass() {
|
||||
sandbox.innerHTML =
|
||||
'<ul id="task-list">' +
|
||||
'<li class="task">Unclog drain' +
|
||||
'</ul>' +
|
||||
'<ul id="completed-tasks">' +
|
||||
'<li id="groceries" class="task">Buy groceries' +
|
||||
'<li class="task">Rotate tires' +
|
||||
'<li class="task">Clean kitchen' +
|
||||
'</ul>';
|
||||
component.decorate(goog.dom.getElement('completed-tasks'));
|
||||
assertEquals(
|
||||
'getRequiredElementByClass() should return the first completed task',
|
||||
'groceries',
|
||||
component.getRequiredElementByClass('task').id);
|
||||
assertThrows('Attempting to retrieve a required element that does not' +
|
||||
'exist should fail', function() {
|
||||
component.getRequiredElementByClass('undefinedClass');
|
||||
});
|
||||
}
|
||||
|
||||
function testEnterExitDocument() {
|
||||
var c1 = new goog.ui.Component();
|
||||
var c2 = new goog.ui.Component();
|
||||
|
||||
component.addChild(c1);
|
||||
component.addChild(c2);
|
||||
|
||||
component.createDom();
|
||||
c1.createDom();
|
||||
c2.createDom();
|
||||
|
||||
assertFalse('Parent must not be in the document',
|
||||
component.isInDocument());
|
||||
assertFalse('Neither child must be in the document',
|
||||
c1.isInDocument() || c2.isInDocument());
|
||||
|
||||
component.enterDocument();
|
||||
assertTrue('Parent must be in the document', component.isInDocument());
|
||||
assertTrue('Both children must be in the document',
|
||||
c1.isInDocument() && c2.isInDocument());
|
||||
|
||||
component.exitDocument();
|
||||
assertFalse('Parent must not be in the document',
|
||||
component.isInDocument());
|
||||
assertFalse('Neither child must be in the document',
|
||||
c1.isInDocument() || c2.isInDocument());
|
||||
|
||||
c1.dispose();
|
||||
c2.dispose();
|
||||
}
|
||||
|
||||
function testDispose() {
|
||||
var c1, c2;
|
||||
|
||||
component.createDom();
|
||||
component.addChild((c1 = new goog.ui.Component()), true);
|
||||
component.addChild((c2 = new goog.ui.Component()), true);
|
||||
|
||||
var element = component.getElement();
|
||||
var c1Element = c1.getElement();
|
||||
var c2Element = c2.getElement();
|
||||
|
||||
component.render(sandbox);
|
||||
assertTrue('Parent must be in the document', component.isInDocument());
|
||||
assertEquals('Parent\'s element must be a child of the sandbox element',
|
||||
sandbox, element.parentNode);
|
||||
assertTrue('Both children must be in the document',
|
||||
c1.isInDocument() && c2.isInDocument());
|
||||
assertEquals('First child\'s element must be a child of the parent\'s' +
|
||||
' element', element, c1Element.parentNode);
|
||||
assertEquals('Second child\'s element must be a child of the parent\'s' +
|
||||
' element', element, c2Element.parentNode);
|
||||
|
||||
assertFalse('Parent must not have been disposed of',
|
||||
component.isDisposed());
|
||||
assertFalse('Neither child must have been disposed of',
|
||||
c1.isDisposed() || c2.isDisposed());
|
||||
|
||||
component.dispose();
|
||||
assertTrue('Parent must have been disposed of', component.isDisposed());
|
||||
assertFalse('Parent must not be in the document',
|
||||
component.isInDocument());
|
||||
assertNotEquals('Parent\'s element must no longer be a child of the' +
|
||||
' sandbox element', sandbox, element.parentNode);
|
||||
assertTrue('Both children must have been disposed of',
|
||||
c1.isDisposed() && c2.isDisposed());
|
||||
assertFalse('Neither child must be in the document',
|
||||
c1.isInDocument() || c2.isInDocument());
|
||||
assertNotEquals('First child\'s element must no longer be a child of' +
|
||||
' the parent\'s element', element, c1Element.parentNode);
|
||||
assertNotEquals('Second child\'s element must no longer be a child of' +
|
||||
' the parent\'s element', element, c2Element.parentNode);
|
||||
}
|
||||
|
||||
function testDispose_Decorated() {
|
||||
sandbox.innerHTML = '<div id="foo">Foo</div>';
|
||||
var foo = goog.dom.getElement('foo');
|
||||
|
||||
component.decorate(foo);
|
||||
assertTrue('Component must be in the document', component.isInDocument());
|
||||
assertFalse('Component must not have been disposed of',
|
||||
component.isDisposed());
|
||||
assertEquals('Component\'s element must have expected value', foo,
|
||||
component.getElement());
|
||||
assertEquals('Decorated element must be a child of the sandbox', sandbox,
|
||||
foo.parentNode);
|
||||
|
||||
component.dispose();
|
||||
assertFalse('Component must not be in the document',
|
||||
component.isInDocument());
|
||||
assertTrue('Component must have been disposed of',
|
||||
component.isDisposed());
|
||||
assertNull('Component\'s element must be null', component.getElement());
|
||||
assertEquals('Previously decorated element must still be a child of the' +
|
||||
' sandbox', sandbox, foo.parentNode);
|
||||
}
|
||||
|
||||
function testMakeIdAndGetFragmentFromId() {
|
||||
assertEquals('Unique id must have expected value',
|
||||
component.getId() + '.foo', component.makeId('foo'));
|
||||
assertEquals('Fragment must have expected value', 'foo',
|
||||
component.getFragmentFromId(component.makeId('foo')));
|
||||
}
|
||||
|
||||
function testMakeIdsWithObject() {
|
||||
var EnumDef = {
|
||||
ENUM_1: 'enum 1',
|
||||
ENUM_2: 'enum 2',
|
||||
ENUM_3: 'enum 3'
|
||||
};
|
||||
var ids = component.makeIds(EnumDef);
|
||||
assertEquals(component.makeId(EnumDef.ENUM_1), ids.ENUM_1);
|
||||
assertEquals(component.makeId(EnumDef.ENUM_2), ids.ENUM_2);
|
||||
assertEquals(component.makeId(EnumDef.ENUM_3), ids.ENUM_3);
|
||||
}
|
||||
|
||||
function testGetElementByFragment() {
|
||||
component.render(sandbox);
|
||||
|
||||
var element = component.dom_.createDom('DIV', {
|
||||
id: component.makeId('foo')
|
||||
}, 'Hello');
|
||||
sandbox.appendChild(element);
|
||||
|
||||
assertEquals('Element must have expected value', element,
|
||||
component.getElementByFragment('foo'));
|
||||
}
|
||||
|
||||
function testGetSetModel() {
|
||||
assertNull('Model must be null by default', component.getModel());
|
||||
|
||||
var model = 'someModel';
|
||||
component.setModel(model);
|
||||
assertEquals('Model must have expected value', model,
|
||||
component.getModel());
|
||||
|
||||
component.setModel(null);
|
||||
assertNull('Model must be null', component.getModel());
|
||||
}
|
||||
|
||||
function testAddChild() {
|
||||
var child = new goog.ui.Component();
|
||||
child.setId('child');
|
||||
|
||||
assertFalse('Parent must not be in the document',
|
||||
component.isInDocument());
|
||||
|
||||
component.addChild(child);
|
||||
assertTrue('Parent must have children.', component.hasChildren());
|
||||
assertEquals('Child must have expected parent', component,
|
||||
child.getParent());
|
||||
assertEquals('Parent must find child by ID', child,
|
||||
component.getChild('child'));
|
||||
}
|
||||
|
||||
function testAddChild_Render() {
|
||||
var child = new goog.ui.Component();
|
||||
|
||||
component.render(sandbox);
|
||||
assertTrue('Parent must be in the document', component.isInDocument());
|
||||
assertEquals('Parent must be in the sandbox', sandbox,
|
||||
component.getElement().parentNode);
|
||||
|
||||
component.addChild(child, true);
|
||||
assertTrue('Child must be in the document', child.isInDocument());
|
||||
assertEquals('Child element must be a child of the parent element',
|
||||
component.getElement(), child.getElement().parentNode);
|
||||
}
|
||||
|
||||
function testAddChild_DomOnly() {
|
||||
var child = new goog.ui.Component();
|
||||
|
||||
component.createDom();
|
||||
assertNotNull('Parent must have a DOM', component.getElement());
|
||||
assertFalse('Parent must not be in the document',
|
||||
component.isInDocument());
|
||||
|
||||
component.addChild(child, true);
|
||||
assertNotNull('Child must have a DOM', child.getElement());
|
||||
assertEquals('Child element must be a child of the parent element',
|
||||
component.getElement(), child.getElement().parentNode);
|
||||
assertFalse('Child must not be in the document', child.isInDocument());
|
||||
}
|
||||
|
||||
function testAddChildAt() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
var c = new goog.ui.Component();
|
||||
var d = new goog.ui.Component();
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
d.setId('d');
|
||||
|
||||
component.addChildAt(b, 0);
|
||||
assertEquals('b', component.getChildIds().join(''));
|
||||
component.addChildAt(d, 1);
|
||||
assertEquals('bd', component.getChildIds().join(''));
|
||||
component.addChildAt(a, 0);
|
||||
assertEquals('abd', component.getChildIds().join(''));
|
||||
component.addChildAt(c, 2);
|
||||
assertEquals('abcd', component.getChildIds().join(''));
|
||||
|
||||
assertEquals(a, component.getChildAt(0));
|
||||
assertEquals(b, component.getChildAt(1));
|
||||
assertEquals(c, component.getChildAt(2));
|
||||
assertEquals(d, component.getChildAt(3));
|
||||
|
||||
assertThrows('Adding child at out-of-bounds index must throw error',
|
||||
function() {
|
||||
component.addChildAt(new goog.ui.Component(), 5);
|
||||
});
|
||||
}
|
||||
|
||||
function testAddChildAtThrowsIfNull() {
|
||||
assertThrows('Adding a null child must throw an error',
|
||||
function() {
|
||||
component.addChildAt(null, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function testHasChildren() {
|
||||
assertFalse('Component must not have children', component.hasChildren());
|
||||
|
||||
component.addChildAt(new goog.ui.Component(), 0);
|
||||
assertTrue('Component must have children', component.hasChildren());
|
||||
|
||||
component.removeChildAt(0);
|
||||
assertFalse('Component must not have children', component.hasChildren());
|
||||
}
|
||||
|
||||
function testGetChildCount() {
|
||||
assertEquals('Component must have 0 children', 0,
|
||||
component.getChildCount());
|
||||
|
||||
component.addChild(new goog.ui.Component());
|
||||
assertEquals('Component must have 1 child', 1,
|
||||
component.getChildCount());
|
||||
|
||||
component.addChild(new goog.ui.Component());
|
||||
assertEquals('Component must have 2 children', 2,
|
||||
component.getChildCount());
|
||||
|
||||
component.removeChildAt(1);
|
||||
assertEquals('Component must have 1 child', 1,
|
||||
component.getChildCount());
|
||||
|
||||
component.removeChildAt(0);
|
||||
assertEquals('Component must have 0 children', 0,
|
||||
component.getChildCount());
|
||||
}
|
||||
|
||||
function testGetChildIds() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
|
||||
component.addChild(a);
|
||||
assertEquals('a', component.getChildIds().join(''));
|
||||
|
||||
component.addChild(b);
|
||||
assertEquals('ab', component.getChildIds().join(''));
|
||||
|
||||
var ids = component.getChildIds();
|
||||
ids.push('c');
|
||||
assertEquals('Changes to the array returned by getChildIds() must not' +
|
||||
' affect the component', 'ab', component.getChildIds().join(''));
|
||||
}
|
||||
|
||||
function testGetChild() {
|
||||
assertNull('Parent must have no children', component.getChild('myId'));
|
||||
|
||||
var c = new goog.ui.Component();
|
||||
c.setId('myId');
|
||||
component.addChild(c);
|
||||
assertEquals('Parent must find child by ID', c,
|
||||
component.getChild('myId'));
|
||||
|
||||
c.setId('newId');
|
||||
assertNull('Parent must not find child by old ID',
|
||||
component.getChild('myId'));
|
||||
assertEquals('Parent must find child by new ID', c,
|
||||
component.getChild('newId'));
|
||||
}
|
||||
|
||||
function testGetChildAt() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
|
||||
component.addChildAt(a, 0);
|
||||
assertEquals('Parent must find child by index', a,
|
||||
component.getChildAt(0));
|
||||
|
||||
component.addChildAt(b, 1);
|
||||
assertEquals('Parent must find child by index', b,
|
||||
component.getChildAt(1));
|
||||
|
||||
assertNull('Parent must return null for out-of-bounds index',
|
||||
component.getChildAt(3));
|
||||
}
|
||||
|
||||
function testForEachChild() {
|
||||
var invoked = false;
|
||||
component.forEachChild(function(child) {
|
||||
assertNotNull('Child must never be null', child);
|
||||
invoked = true;
|
||||
});
|
||||
assertFalse('forEachChild must not call its argument if the parent has ' +
|
||||
'no children', invoked);
|
||||
|
||||
component.addChild(new goog.ui.Component());
|
||||
component.addChild(new goog.ui.Component());
|
||||
component.addChild(new goog.ui.Component());
|
||||
var callCount = 0;
|
||||
component.forEachChild(function(child, index) {
|
||||
assertEquals(component, this);
|
||||
callCount++;
|
||||
}, component);
|
||||
assertEquals(3, callCount);
|
||||
}
|
||||
|
||||
function testIndexOfChild() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
var c = new goog.ui.Component();
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
|
||||
component.addChild(a);
|
||||
assertEquals(0, component.indexOfChild(a));
|
||||
|
||||
component.addChild(b);
|
||||
assertEquals(1, component.indexOfChild(b));
|
||||
|
||||
component.addChild(c);
|
||||
assertEquals(2, component.indexOfChild(c));
|
||||
|
||||
assertEquals('indexOfChild must return -1 for nonexistent child', -1,
|
||||
component.indexOfChild(new goog.ui.Component()));
|
||||
}
|
||||
|
||||
function testRemoveChild() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
var c = new goog.ui.Component();
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
|
||||
component.addChild(a);
|
||||
component.addChild(b);
|
||||
component.addChild(c);
|
||||
|
||||
assertEquals('Parent must remove and return child', c,
|
||||
component.removeChild(c));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('c'));
|
||||
|
||||
assertEquals('Parent must remove and return child by ID', b,
|
||||
component.removeChild('b'));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('b'));
|
||||
|
||||
assertEquals('Parent must remove and return child by index', a,
|
||||
component.removeChildAt(0));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('a'));
|
||||
}
|
||||
|
||||
function testMovingChildrenUsingAddChildAt() {
|
||||
component.render(sandbox);
|
||||
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
var c = new goog.ui.Component();
|
||||
var d = new goog.ui.Component();
|
||||
a.setElementInternal(goog.dom.createElement('a'));
|
||||
b.setElementInternal(goog.dom.createElement('b'));
|
||||
c.setElementInternal(goog.dom.createElement('c'));
|
||||
d.setElementInternal(goog.dom.createElement('d'));
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
d.setId('d');
|
||||
|
||||
component.addChild(a, true);
|
||||
component.addChild(b, true);
|
||||
component.addChild(c, true);
|
||||
component.addChild(d, true);
|
||||
|
||||
assertEquals('abcd', component.getChildIds().join(''));
|
||||
assertEquals(a, component.getChildAt(0));
|
||||
assertEquals(b, component.getChildAt(1));
|
||||
assertEquals(c, component.getChildAt(2));
|
||||
assertEquals(d, component.getChildAt(3));
|
||||
|
||||
// Move child d to the top and b to the bottom.
|
||||
component.addChildAt(d, 0);
|
||||
component.addChildAt(b, 3);
|
||||
|
||||
assertEquals('dacb', component.getChildIds().join(''));
|
||||
assertEquals(d, component.getChildAt(0));
|
||||
assertEquals(a, component.getChildAt(1));
|
||||
assertEquals(c, component.getChildAt(2));
|
||||
assertEquals(b, component.getChildAt(3));
|
||||
|
||||
// Move child a to the top, and check that DOM nodes are in correct order.
|
||||
component.addChildAt(a, 0);
|
||||
assertEquals('adcb', component.getChildIds().join(''));
|
||||
assertEquals(a, component.getChildAt(0));
|
||||
assertEquals(a.getElement(), component.getElement().childNodes[0]);
|
||||
}
|
||||
|
||||
function testAddChildAfterDomCreatedDoesNotEnterDocument() {
|
||||
var parent = new goog.ui.Component();
|
||||
var child = new goog.ui.Component();
|
||||
|
||||
var nestedDiv = goog.dom.createDom('div');
|
||||
parent.setElementInternal(
|
||||
goog.dom.createDom('div', undefined, nestedDiv));
|
||||
parent.render();
|
||||
|
||||
// Now add a child, whose DOM already exists. This happens, for example,
|
||||
// if the child itself performs an addChild(x, true).
|
||||
child.createDom();
|
||||
parent.addChild(child, false);
|
||||
// The parent shouldn't call enterDocument on the child, since the child
|
||||
// actually isn't in the document yet.
|
||||
assertFalse(child.isInDocument());
|
||||
|
||||
// Now, actually render the child; it should be in the document.
|
||||
child.render(nestedDiv);
|
||||
assertTrue(child.isInDocument());
|
||||
assertEquals('Child should be rendered in the expected div',
|
||||
nestedDiv, child.getElement().parentNode);
|
||||
}
|
||||
|
||||
function testAddChildAfterDomManuallyInserted() {
|
||||
var parent = new goog.ui.Component();
|
||||
var child = new goog.ui.Component();
|
||||
|
||||
var nestedDiv = goog.dom.createDom('div');
|
||||
parent.setElementInternal(
|
||||
goog.dom.createDom('div', undefined, nestedDiv));
|
||||
parent.render();
|
||||
|
||||
// This sequence is weird, but some people do it instead of just manually
|
||||
// doing render. The addChild will detect that the child is in the DOM
|
||||
// and call enterDocument.
|
||||
child.createDom();
|
||||
nestedDiv.appendChild(child.getElement());
|
||||
parent.addChild(child, false);
|
||||
|
||||
assertTrue(child.isInDocument());
|
||||
assertEquals('Child should be rendered in the expected div',
|
||||
nestedDiv, child.getElement().parentNode);
|
||||
}
|
||||
|
||||
function testRemoveChildren() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
var c = new goog.ui.Component();
|
||||
|
||||
component.addChild(a);
|
||||
component.addChild(b);
|
||||
component.addChild(c);
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
|
||||
assertArrayEquals('Parent must remove and return children.', [a, b, c],
|
||||
component.removeChildren());
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('a'));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('b'));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('c'));
|
||||
}
|
||||
|
||||
function testRemoveChildren_Unrender() {
|
||||
var a = new goog.ui.Component();
|
||||
var b = new goog.ui.Component();
|
||||
|
||||
component.render(sandbox);
|
||||
component.addChild(a);
|
||||
component.addChild(b);
|
||||
|
||||
assertArrayEquals('Prent must remove and return children.', [a, b],
|
||||
component.removeChildren(true));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('a'));
|
||||
assertFalse('Child must no longer be in the document.',
|
||||
a.isInDocument());
|
||||
assertNull('Parent must no longer contain this child',
|
||||
component.getChild('b'));
|
||||
assertFalse('Child must no longer be in the document.',
|
||||
b.isInDocument());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
<!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 Performance Tests - goog.ui.Container</title>
|
||||
<link rel="stylesheet" type="text/css" href="../testing/performancetable.css"/>
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.testing.PerformanceTable');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Container');
|
||||
goog.require('goog.ui.Control');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>goog.ui.Container Performance Tests</h1>
|
||||
<p>
|
||||
<strong>User-agent:</strong>
|
||||
<script>document.write(navigator.userAgent);</script>
|
||||
</p>
|
||||
<div id="perfTable"></div>
|
||||
<hr>
|
||||
|
||||
<script>
|
||||
var table = new goog.testing.PerformanceTable(
|
||||
goog.dom.getElement('perfTable'));
|
||||
|
||||
// Number of controls to add to the container per test run.
|
||||
var SAMPLES_PER_RUN = 500;
|
||||
|
||||
function testContainerAddChild() {
|
||||
var description = 'Add ' + SAMPLES_PER_RUN +
|
||||
' child controls to the container with immediate rendering';
|
||||
table.run(function() {
|
||||
var container = new goog.ui.Container();
|
||||
for (var i = 0; i < SAMPLES_PER_RUN; i++) {
|
||||
container.addChild(new goog.ui.Control(), true);
|
||||
}
|
||||
container.render();
|
||||
assertEquals('container child count', SAMPLES_PER_RUN,
|
||||
container.getChildCount());
|
||||
container.dispose();
|
||||
}, description);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.Container
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ContainerTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,612 @@
|
||||
// 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.ContainerTest');
|
||||
goog.setTestOnly('goog.ui.ContainerTest');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.events.KeyEvent');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Container');
|
||||
goog.require('goog.ui.Control');
|
||||
|
||||
var sandbox;
|
||||
var containerElement;
|
||||
var container;
|
||||
var keyContainer;
|
||||
var listContainer;
|
||||
|
||||
function setUpPage() {
|
||||
sandbox = goog.dom.getElement('sandbox');
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
container = new goog.ui.Container();
|
||||
keyContainer = null;
|
||||
listContainer = null;
|
||||
|
||||
sandbox.innerHTML =
|
||||
'<div id="containerElement" class="goog-container">\n' +
|
||||
' <div class="goog-control" id="hello">Hello</div>\n' +
|
||||
' <div class="goog-control" id="world">World</div>\n' +
|
||||
'</div>';
|
||||
containerElement = goog.dom.getElement('containerElement');
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.dom.removeChildren(sandbox);
|
||||
container.dispose();
|
||||
goog.dispose(keyContainer);
|
||||
goog.dispose(listContainer);
|
||||
}
|
||||
|
||||
function testDecorateHidden() {
|
||||
containerElement.style.display = 'none';
|
||||
|
||||
assertTrue('Container must be visible', container.isVisible());
|
||||
container.decorate(containerElement);
|
||||
assertFalse('Container must be hidden', container.isVisible());
|
||||
container.forEachChild(function(control) {
|
||||
assertTrue('Child control ' + control.getId() + ' must report being ' +
|
||||
'visible, even if in a hidden container', control.isVisible());
|
||||
});
|
||||
}
|
||||
|
||||
function testDecorateDisabled() {
|
||||
goog.dom.classlist.add(containerElement, 'goog-container-disabled');
|
||||
|
||||
assertTrue('Container must be enabled', container.isEnabled());
|
||||
container.decorate(containerElement);
|
||||
assertFalse('Container must be disabled', container.isEnabled());
|
||||
container.forEachChild(function(control) {
|
||||
assertFalse('Child control ' + control.getId() + ' must be disabled, ' +
|
||||
'because the host container is disabled', control.isEnabled());
|
||||
});
|
||||
}
|
||||
|
||||
function testDecorateFocusableContainer() {
|
||||
container.decorate(containerElement);
|
||||
assertTrue('Container must be focusable', container.isFocusable());
|
||||
container.forEachChild(function(control) {
|
||||
assertFalse('Child control ' + control.getId() + ' must not be ' +
|
||||
'focusable',
|
||||
control.isSupportedState(goog.ui.Component.State.FOCUSED));
|
||||
});
|
||||
}
|
||||
|
||||
function testDecorateFocusableChildrenContainer() {
|
||||
container.setFocusable(false);
|
||||
container.setFocusableChildrenAllowed(true);
|
||||
container.decorate(containerElement);
|
||||
assertFalse('Container must not be focusable', container.isFocusable());
|
||||
container.forEachChild(function(control) {
|
||||
assertTrue('Child control ' + control.getId() + ' must be ' +
|
||||
'focusable',
|
||||
control.isSupportedState(goog.ui.Component.State.FOCUSED));
|
||||
});
|
||||
}
|
||||
|
||||
function testHighlightOnEnter() {
|
||||
// This interaction test ensures that containers enforce that children
|
||||
// get highlighted on mouseover, and that one and only one child may
|
||||
// be highlighted at a time. Although integration tests aren't the
|
||||
// best, it's difficult to test these event-based interactions due to
|
||||
// their disposition toward the "misunderstood contract" problem.
|
||||
|
||||
container.decorate(containerElement);
|
||||
assertFalse('Child 0 should initially not be highlighted',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(0).getElement(), sandbox);
|
||||
assertTrue('Child 0 should become highlighted after a mouse over',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
assertEquals('Child 0 should be the active descendant',
|
||||
container.getChildAt(0).getElement(),
|
||||
goog.a11y.aria.getActiveDescendant(container.getElement()));
|
||||
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(1).getElement(),
|
||||
container.getChildAt(0).getElement());
|
||||
assertFalse('Child 0 should lose highlight when child 1 is moused ' +
|
||||
'over, even if no mouseout occurs.',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
assertTrue('Child 1 should now be highlighted.',
|
||||
container.getChildAt(1).isHighlighted());
|
||||
assertEquals('Child 1 should be the active descendant',
|
||||
container.getChildAt(1).getElement(),
|
||||
goog.a11y.aria.getActiveDescendant(container.getElement()));
|
||||
}
|
||||
|
||||
function testHighlightOnEnterPreventable() {
|
||||
container.decorate(containerElement);
|
||||
goog.events.listen(container, goog.ui.Component.EventType.ENTER,
|
||||
function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(0).getElement(), sandbox);
|
||||
assertFalse('Child 0 should not be highlighted if preventDefault called',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
}
|
||||
|
||||
function testHighlightDisabled() {
|
||||
// Another interaction test. Already tested in control_test.
|
||||
container.decorate(containerElement);
|
||||
container.getChildAt(0).setEnabled(false);
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(0).getElement(), sandbox);
|
||||
assertFalse('Disabled children should not be highlighted',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
}
|
||||
|
||||
function testGetOwnerControl() {
|
||||
container.decorate(containerElement);
|
||||
|
||||
assertEquals('Must return appropriate control given an element in the ' +
|
||||
'control.',
|
||||
container.getChildAt(1),
|
||||
container.getOwnerControl(container.getChildAt(1).getElement()));
|
||||
|
||||
assertNull('Must return null for element not associated with control.',
|
||||
container.getOwnerControl(document.body));
|
||||
assertNull('Must return null if given null node',
|
||||
container.getOwnerControl(null));
|
||||
}
|
||||
|
||||
function testShowEvent() {
|
||||
container.decorate(containerElement);
|
||||
container.setVisible(false);
|
||||
var eventFired = false;
|
||||
goog.events.listen(container, goog.ui.Component.EventType.SHOW,
|
||||
function() {
|
||||
assertFalse('Container must not be visible when SHOW event is ' +
|
||||
'fired',
|
||||
container.isVisible());
|
||||
eventFired = true;
|
||||
});
|
||||
container.setVisible(true);
|
||||
assertTrue('SHOW event expected', eventFired);
|
||||
}
|
||||
|
||||
function testAfterShowEvent() {
|
||||
container.decorate(containerElement);
|
||||
container.setVisible(false);
|
||||
var eventFired = false;
|
||||
goog.events.listen(container, goog.ui.Container.EventType.AFTER_SHOW,
|
||||
function() {
|
||||
assertTrue('Container must be visible when AFTER_SHOW event is ' +
|
||||
'fired',
|
||||
container.isVisible());
|
||||
eventFired = true;
|
||||
});
|
||||
container.setVisible(true);
|
||||
assertTrue('AFTER_SHOW event expected', eventFired);
|
||||
}
|
||||
|
||||
function testHideEvents() {
|
||||
var events = [];
|
||||
container.decorate(containerElement);
|
||||
container.setVisible(true);
|
||||
var eventFired = false;
|
||||
goog.events.listen(container, goog.ui.Component.EventType.HIDE,
|
||||
function(e) {
|
||||
assertTrue(
|
||||
'Container must be visible when HIDE event is fired',
|
||||
container.isVisible());
|
||||
events.push(e.type);
|
||||
});
|
||||
goog.events.listen(container, goog.ui.Container.EventType.AFTER_HIDE,
|
||||
function(e) {
|
||||
assertFalse(
|
||||
'Container must not be visible when AFTER_HIDE event is fired',
|
||||
container.isVisible());
|
||||
events.push(e.type);
|
||||
});
|
||||
container.setVisible(false);
|
||||
assertArrayEquals('HIDE event followed by AFTER_HIDE expected', [
|
||||
goog.ui.Component.EventType.HIDE,
|
||||
goog.ui.Container.EventType.AFTER_HIDE
|
||||
], events);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Test container to which the elements have to be added with
|
||||
* {@code container.addChild(element, false)}
|
||||
* @constructor
|
||||
* @extends {goog.ui.Container}
|
||||
*/
|
||||
function ListContainer() {
|
||||
goog.ui.Container.call(this);
|
||||
}
|
||||
goog.inherits(ListContainer, goog.ui.Container);
|
||||
|
||||
|
||||
/** @override */
|
||||
ListContainer.prototype.createDom = function() {
|
||||
ListContainer.superClass_.createDom.call(this);
|
||||
var ul = this.getDomHelper().createDom('ul');
|
||||
this.forEachChild(function(child) {
|
||||
child.createDom();
|
||||
var childEl = child.getElement();
|
||||
ul.appendChild(this.getDomHelper().createDom('li', {}, childEl));
|
||||
}, this);
|
||||
this.getContentElement().appendChild(ul);
|
||||
};
|
||||
|
||||
function testGetOwnerControlWithNoRenderingInAddChild() {
|
||||
listContainer = new ListContainer();
|
||||
var control = new goog.ui.Control('item');
|
||||
listContainer.addChild(control);
|
||||
listContainer.render();
|
||||
var ownerControl = listContainer.getOwnerControl(control.getElement());
|
||||
|
||||
assertEquals('Control was added with addChild(control, false)',
|
||||
control, ownerControl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Test container for tracking key events being handled.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Container}
|
||||
*/
|
||||
function KeyHandlingContainer() {
|
||||
goog.ui.Container.call(this);
|
||||
this.keyEventsHandled = 0;
|
||||
}
|
||||
goog.inherits(KeyHandlingContainer, goog.ui.Container);
|
||||
|
||||
|
||||
/** @override */
|
||||
KeyHandlingContainer.prototype.handleKeyEventInternal = function() {
|
||||
this.keyEventsHandled++;
|
||||
return false;
|
||||
};
|
||||
|
||||
function testHandleKeyEvent_onlyHandlesWhenVisible() {
|
||||
keyContainer = new KeyHandlingContainer();
|
||||
keyContainer.decorate(containerElement);
|
||||
|
||||
keyContainer.setVisible(false);
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('No key events should be handled',
|
||||
0, keyContainer.keyEventsHandled);
|
||||
|
||||
keyContainer.setVisible(true);
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('One key event should be handled',
|
||||
1, keyContainer.keyEventsHandled);
|
||||
}
|
||||
|
||||
function testHandleKeyEvent_onlyHandlesWhenEnabled() {
|
||||
keyContainer = new KeyHandlingContainer();
|
||||
keyContainer.decorate(containerElement);
|
||||
keyContainer.setVisible(true);
|
||||
|
||||
keyContainer.setEnabled(false);
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('No key events should be handled',
|
||||
0, keyContainer.keyEventsHandled);
|
||||
|
||||
keyContainer.setEnabled(true);
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('One key event should be handled',
|
||||
1, keyContainer.keyEventsHandled);
|
||||
}
|
||||
|
||||
function testHandleKeyEvent_childlessContainersIgnoreKeyEvents() {
|
||||
keyContainer = new KeyHandlingContainer();
|
||||
keyContainer.render();
|
||||
keyContainer.setVisible(true);
|
||||
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('No key events should be handled',
|
||||
0, keyContainer.keyEventsHandled);
|
||||
|
||||
keyContainer.addChild(new goog.ui.Control());
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('One key event should be handled',
|
||||
1, keyContainer.keyEventsHandled);
|
||||
}
|
||||
|
||||
function testHandleKeyEvent_alwaysHandlesWithKeyEventTarget() {
|
||||
keyContainer = new KeyHandlingContainer();
|
||||
keyContainer.render();
|
||||
keyContainer.setKeyEventTarget(goog.dom.createDom('div'));
|
||||
keyContainer.setVisible(true);
|
||||
|
||||
keyContainer.handleKeyEvent(new goog.events.Event());
|
||||
assertEquals('One key events should be handled',
|
||||
1, keyContainer.keyEventsHandled);
|
||||
}
|
||||
|
||||
function testHandleKeyEventInternal_onlyHandlesUnmodified() {
|
||||
container.setKeyEventTarget(sandbox);
|
||||
var event = new goog.events.KeyEvent(
|
||||
goog.events.KeyCodes.ESC, 0, false, null);
|
||||
|
||||
var propertyNames = [
|
||||
'shiftKey',
|
||||
'altKey',
|
||||
'ctrlKey',
|
||||
'metaKey'
|
||||
];
|
||||
|
||||
// Verify that the event is not handled whenever a modifier key is true.
|
||||
for (var i = 0, propertyName; propertyName = propertyNames[i]; i++) {
|
||||
assertTrue('Event should be handled when modifer key is not pressed.',
|
||||
container.handleKeyEventInternal(event));
|
||||
event[propertyName] = true;
|
||||
assertFalse('Event should not be handled when modifer key is pressed.',
|
||||
container.handleKeyEventInternal(event));
|
||||
event[propertyName] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function testOpenFollowsHighlight() {
|
||||
container.decorate(containerElement);
|
||||
container.setOpenFollowsHighlight(true);
|
||||
assertTrue('isOpenFollowsHighlight should return true',
|
||||
container.isOpenFollowsHighlight());
|
||||
|
||||
// Make the children openable.
|
||||
container.forEachChild(function(child) {
|
||||
child.setSupportedState(goog.ui.Component.State.OPENED, true);
|
||||
});
|
||||
// Open child 1 initially.
|
||||
container.getChildAt(1).setOpen(true);
|
||||
|
||||
assertFalse('Child 0 should initially not be highlighted',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(0).getElement(), sandbox);
|
||||
assertTrue('Child 0 should become highlighted after a mouse over',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
assertTrue('Child 0 should become open after higlighted',
|
||||
container.getChildAt(0).isOpen());
|
||||
assertFalse('Child 1 should become closed once 0 is open',
|
||||
container.getChildAt(1).isOpen());
|
||||
assertEquals('OpenItem should be child 0',
|
||||
container.getChildAt(0), container.getOpenItem());
|
||||
}
|
||||
|
||||
function testOpenNotFollowsHighlight() {
|
||||
container.decorate(containerElement);
|
||||
container.setOpenFollowsHighlight(false);
|
||||
assertFalse('isOpenFollowsHighlight should return false',
|
||||
container.isOpenFollowsHighlight());
|
||||
|
||||
// Make the children openable.
|
||||
container.forEachChild(function(child) {
|
||||
child.setSupportedState(goog.ui.Component.State.OPENED, true);
|
||||
});
|
||||
// Open child 1 initially.
|
||||
container.getChildAt(1).setOpen(true);
|
||||
|
||||
assertFalse('Child 0 should initially not be highlighted',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
container.getChildAt(0).getElement(), sandbox);
|
||||
assertTrue('Child 0 should become highlighted after a mouse over',
|
||||
container.getChildAt(0).isHighlighted());
|
||||
assertFalse('Child 0 should remain closed after higlighted',
|
||||
container.getChildAt(0).isOpen());
|
||||
assertTrue('Child 1 should remain open',
|
||||
container.getChildAt(1).isOpen());
|
||||
assertEquals('OpenItem should be child 1',
|
||||
container.getChildAt(1), container.getOpenItem());
|
||||
}
|
||||
|
||||
function testRemoveChild() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
var b = new goog.ui.Control('B');
|
||||
var c = new goog.ui.Control('C');
|
||||
|
||||
a.setId('a');
|
||||
b.setId('b');
|
||||
c.setId('c');
|
||||
|
||||
container.addChild(a, true);
|
||||
container.addChild(b, true);
|
||||
container.addChild(c, true);
|
||||
|
||||
container.setHighlightedIndex(2);
|
||||
|
||||
assertEquals('Parent must remove and return child by ID', b,
|
||||
container.removeChild('b'));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
container.getChild('b'));
|
||||
assertEquals('Highlighted index must be decreased', 1,
|
||||
container.getHighlightedIndex());
|
||||
assertTrue('The removed control must handle its own mouse events',
|
||||
b.isHandleMouseEvents());
|
||||
|
||||
assertEquals('Parent must remove and return child', c,
|
||||
container.removeChild(c));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
container.getChild('c'));
|
||||
assertFalse('This child must no longer be highlighted',
|
||||
c.isHighlighted());
|
||||
assertTrue('The removed control must handle its own mouse events',
|
||||
c.isHandleMouseEvents());
|
||||
|
||||
assertEquals('Parent must remove and return child by index', a,
|
||||
container.removeChildAt(0));
|
||||
assertNull('Parent must no longer contain this child',
|
||||
container.getChild('a'));
|
||||
assertTrue('The removed control must handle its own mouse events',
|
||||
a.isHandleMouseEvents());
|
||||
}
|
||||
|
||||
function testRemoveHighlightedDisposedChild() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
container.addChild(a, true);
|
||||
|
||||
container.setHighlightedIndex(0);
|
||||
a.dispose();
|
||||
container.removeChild(a);
|
||||
container.dispose();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks that getHighlighted() returns the expected value and checks
|
||||
* that the child at this index is highlighted and other children are not.
|
||||
* @param {string} explanation Message indicating what is expected.
|
||||
* @param {number} index Expected return value of getHighlightedIndex().
|
||||
*/
|
||||
function assertHighlightedIndex(explanation, index) {
|
||||
assertEquals(explanation, index, container.getHighlightedIndex());
|
||||
for (var i = 0; i < container.getChildCount(); i++) {
|
||||
if (i == index) {
|
||||
assertTrue('Child at highlighted index should be highlighted',
|
||||
container.getChildAt(i).isHighlighted());
|
||||
} else {
|
||||
assertFalse('Only child at highlighted index should be highlighted',
|
||||
container.getChildAt(i).isHighlighted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testUpdateHighlightedIndex_updatesWhenChildrenAreAdded() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
var b = new goog.ui.Control('B');
|
||||
var c = new goog.ui.Control('C');
|
||||
|
||||
container.addChild(a);
|
||||
container.setHighlightedIndex(0);
|
||||
assertHighlightedIndex('Highlighted index should match set value', 0);
|
||||
|
||||
// Add child before the highlighted one.
|
||||
container.addChildAt(b, 0);
|
||||
assertHighlightedIndex('Highlighted index should be increased', 1);
|
||||
|
||||
// Add child after the highlighted one.
|
||||
container.addChildAt(c, 2);
|
||||
assertHighlightedIndex('Highlighted index should not change', 1);
|
||||
|
||||
container.dispose();
|
||||
}
|
||||
|
||||
function testUpdateHighlightedIndex_updatesWhenChildrenAreMoved() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
var b = new goog.ui.Control('B');
|
||||
var c = new goog.ui.Control('C');
|
||||
|
||||
container.addChild(a);
|
||||
container.addChild(b);
|
||||
container.addChild(c);
|
||||
|
||||
// Highlight 'c' and swap 'a' and 'b'
|
||||
// [a, b, c] -> [a, b, *c] -> [b, a, *c] (* indicates the highlighted child)
|
||||
container.setHighlightedIndex(2);
|
||||
container.addChildAt(a, 1, false);
|
||||
assertHighlightedIndex('Highlighted index should not change', 2);
|
||||
|
||||
// Move the highlighted child 'c' from index 2 to index 1.
|
||||
// [b, a, *c] -> [b, *c, a]
|
||||
container.addChildAt(c, 1, false);
|
||||
assertHighlightedIndex('Highlighted index must follow the moved child', 1);
|
||||
|
||||
// Take the element in front of the highlighted index and move it behind it.
|
||||
// [b, *c, a] -> [*c, a, b]
|
||||
container.addChildAt(b, 2, false);
|
||||
assertHighlightedIndex('Highlighted index must be decreased', 0);
|
||||
|
||||
// And move the element back to the front.
|
||||
// [*c, a, b] -> [b, *c, a]
|
||||
container.addChildAt(b, 0, false);
|
||||
assertHighlightedIndex('Highlighted index must be increased', 1);
|
||||
|
||||
container.dispose();
|
||||
}
|
||||
|
||||
function testUpdateHighlightedIndex_notChangedOnNoOp() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
container.addChild(new goog.ui.Control('A'));
|
||||
container.addChild(new goog.ui.Control('B'));
|
||||
container.setHighlightedIndex(1);
|
||||
|
||||
// Re-add a child to its current position.
|
||||
container.addChildAt(container.getChildAt(0), 0, false);
|
||||
assertHighlightedIndex('Highlighted index must not change', 1);
|
||||
|
||||
container.dispose();
|
||||
}
|
||||
|
||||
function testUpdateHighlightedIndex_notChangedWhenNoChildSelected() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
var b = new goog.ui.Control('B');
|
||||
var c = new goog.ui.Control('C');
|
||||
container.addChild(a);
|
||||
container.addChild(b);
|
||||
container.addChild(c);
|
||||
|
||||
// Move children around.
|
||||
container.addChildAt(a, 2, false);
|
||||
container.addChildAt(b, 1, false);
|
||||
container.addChildAt(c, 2, false);
|
||||
|
||||
assertHighlightedIndex('Highlighted index must not change', -1);
|
||||
|
||||
container.dispose();
|
||||
}
|
||||
|
||||
function testUpdateHighlightedIndex_indexStaysInBoundsWhenMovedToMaxIndex() {
|
||||
goog.dom.removeChildren(containerElement);
|
||||
container.decorate(containerElement);
|
||||
|
||||
var a = new goog.ui.Control('A');
|
||||
var b = new goog.ui.Control('B');
|
||||
container.addChild(a);
|
||||
container.addChild(b);
|
||||
|
||||
// Move higlighted child to an index one behind last child.
|
||||
container.setHighlightedIndex(0);
|
||||
container.addChildAt(a, 2);
|
||||
|
||||
assertEquals('Child should be moved to index 1', a, container.getChildAt(1));
|
||||
assertEquals('Child count should not change', 2, container.getChildCount());
|
||||
assertHighlightedIndex('Highlighted index must point to new index', 1);
|
||||
|
||||
container.dispose();
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Base class for container renderers.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ContainerRenderer');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.registry');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default renderer for {@link goog.ui.Container}. Can be used as-is, but
|
||||
* subclasses of Container will probably want to use renderers specifically
|
||||
* tailored for them by extending this class.
|
||||
* @param {string=} opt_ariaRole Optional ARIA role used for the element.
|
||||
* @constructor
|
||||
*/
|
||||
goog.ui.ContainerRenderer = function(opt_ariaRole) {
|
||||
// By default, the ARIA role is unspecified.
|
||||
/** @private {string|undefined} */
|
||||
this.ariaRole_ = opt_ariaRole;
|
||||
};
|
||||
goog.addSingletonGetter(goog.ui.ContainerRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new renderer and sets the CSS class that the renderer will use
|
||||
* as the base CSS class to apply to all elements rendered by that renderer.
|
||||
* An example to use this function using a menu is:
|
||||
*
|
||||
* <pre>
|
||||
* var myCustomRenderer = goog.ui.ContainerRenderer.getCustomRenderer(
|
||||
* goog.ui.MenuRenderer, 'my-special-menu');
|
||||
* var newMenu = new goog.ui.Menu(opt_domHelper, myCustomRenderer);
|
||||
* </pre>
|
||||
*
|
||||
* Your styles for the menu can now be:
|
||||
* <pre>
|
||||
* .my-special-menu { }
|
||||
* </pre>
|
||||
*
|
||||
* <em>instead</em> of
|
||||
* <pre>
|
||||
* .CSS_MY_SPECIAL_MENU .goog-menu { }
|
||||
* </pre>
|
||||
*
|
||||
* You would want to use this functionality when you want an instance of a
|
||||
* component to have specific styles different than the other components of the
|
||||
* same type in your application. This avoids using descendant selectors to
|
||||
* apply the specific styles to this component.
|
||||
*
|
||||
* @param {Function} ctor The constructor of the renderer you want to create.
|
||||
* @param {string} cssClassName The name of the CSS class for this renderer.
|
||||
* @return {goog.ui.ContainerRenderer} An instance of the desired renderer with
|
||||
* its getCssClass() method overridden to return the supplied custom CSS
|
||||
* class name.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.getCustomRenderer = function(ctor, cssClassName) {
|
||||
var renderer = new ctor();
|
||||
|
||||
/**
|
||||
* Returns the CSS class to be applied to the root element of components
|
||||
* rendered using this renderer.
|
||||
* @return {string} Renderer-specific CSS class.
|
||||
*/
|
||||
renderer.getCssClass = function() {
|
||||
return cssClassName;
|
||||
};
|
||||
|
||||
return renderer;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of containers rendered
|
||||
* by this renderer.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ContainerRenderer.CSS_CLASS = goog.getCssName('goog-container');
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ARIA role to be applied to the container.
|
||||
* See http://wiki/Main/ARIA for more info.
|
||||
* @return {undefined|string} ARIA role.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getAriaRole = function() {
|
||||
return this.ariaRole_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Enables or disables the tab index of the element. Only elements with a
|
||||
* valid tab index can receive focus.
|
||||
* @param {Element} element Element whose tab index is to be changed.
|
||||
* @param {boolean} enable Whether to add or remove the element's tab index.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.enableTabIndex = function(element, enable) {
|
||||
if (element) {
|
||||
element.tabIndex = enable ? 0 : -1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates and returns the container's root element. The default
|
||||
* simply creates a DIV and applies the renderer's own CSS class name to it.
|
||||
* To be overridden in subclasses.
|
||||
* @param {goog.ui.Container} container Container to render.
|
||||
* @return {Element} Root element for the container.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.createDom = function(container) {
|
||||
return container.getDomHelper().createDom('div',
|
||||
this.getClassNames(container).join(' '));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the DOM element into which child components are to be rendered,
|
||||
* or null if the container hasn't been rendered yet.
|
||||
* @param {Element} element Root element of the container whose content element
|
||||
* is to be returned.
|
||||
* @return {Element} Element to contain child elements (null if none).
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getContentElement = function(element) {
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@code canDecorate}; returns true if the element
|
||||
* is a DIV, false otherwise.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {boolean} Whether the renderer can decorate the element.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.canDecorate = function(element) {
|
||||
return element.tagName == 'DIV';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@code decorate} for {@link goog.ui.Container}s.
|
||||
* Decorates the element with the container, and attempts to decorate its child
|
||||
* elements. Returns the decorated element.
|
||||
* @param {goog.ui.Container} container Container to decorate the element.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {!Element} Decorated element.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.decorate = function(container, element) {
|
||||
// Set the container's ID to the decorated element's DOM ID, if any.
|
||||
if (element.id) {
|
||||
container.setId(element.id);
|
||||
}
|
||||
|
||||
// Configure the container's state based on the CSS class names it has.
|
||||
var baseClass = this.getCssClass();
|
||||
var hasBaseClass = false;
|
||||
var classNames = goog.dom.classlist.get(element);
|
||||
if (classNames) {
|
||||
goog.array.forEach(classNames, function(className) {
|
||||
if (className == baseClass) {
|
||||
hasBaseClass = true;
|
||||
} else if (className) {
|
||||
this.setStateFromClassName(container, className, baseClass);
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (!hasBaseClass) {
|
||||
// Make sure the container's root element has the renderer's own CSS class.
|
||||
goog.dom.classlist.add(element, baseClass);
|
||||
}
|
||||
|
||||
// Decorate the element's children, if applicable. This should happen after
|
||||
// the container's own state has been initialized, since how children are
|
||||
// decorated may depend on the state of the container.
|
||||
this.decorateChildren(container, this.getContentElement(element));
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the container's state based on the given CSS class name, encountered
|
||||
* during decoration. CSS class names that don't represent container states
|
||||
* are ignored. Considered protected; subclasses should override this method
|
||||
* to support more states and CSS class names.
|
||||
* @param {goog.ui.Container} container Container to update.
|
||||
* @param {string} className CSS class name.
|
||||
* @param {string} baseClass Base class name used as the root of state-specific
|
||||
* class names (typically the renderer's own class name).
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.setStateFromClassName = function(container,
|
||||
className, baseClass) {
|
||||
if (className == goog.getCssName(baseClass, 'disabled')) {
|
||||
container.setEnabled(false);
|
||||
} else if (className == goog.getCssName(baseClass, 'horizontal')) {
|
||||
container.setOrientation(goog.ui.Container.Orientation.HORIZONTAL);
|
||||
} else if (className == goog.getCssName(baseClass, 'vertical')) {
|
||||
container.setOrientation(goog.ui.Container.Orientation.VERTICAL);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a container and an element that may contain child elements, decorates
|
||||
* the child elements, and adds the corresponding components to the container
|
||||
* as child components. Any non-element child nodes (e.g. empty text nodes
|
||||
* introduced by line breaks in the HTML source) are removed from the element.
|
||||
* @param {goog.ui.Container} container Container whose children are to be
|
||||
* discovered.
|
||||
* @param {Element} element Element whose children are to be decorated.
|
||||
* @param {Element=} opt_firstChild the first child to be decorated.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.decorateChildren = function(container,
|
||||
element, opt_firstChild) {
|
||||
if (element) {
|
||||
var node = opt_firstChild || element.firstChild, next;
|
||||
// Tag soup HTML may result in a DOM where siblings have different parents.
|
||||
while (node && node.parentNode == element) {
|
||||
// Get the next sibling here, since the node may be replaced or removed.
|
||||
next = node.nextSibling;
|
||||
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
|
||||
// Decorate element node.
|
||||
var child = this.getDecoratorForChild(/** @type {!Element} */(node));
|
||||
if (child) {
|
||||
// addChild() may need to look at the element.
|
||||
child.setElementInternal(/** @type {!Element} */(node));
|
||||
// If the container is disabled, mark the child disabled too. See
|
||||
// bug 1263729. Note that this must precede the call to addChild().
|
||||
if (!container.isEnabled()) {
|
||||
child.setEnabled(false);
|
||||
}
|
||||
container.addChild(child);
|
||||
child.decorate(/** @type {!Element} */(node));
|
||||
}
|
||||
} else if (!node.nodeValue || goog.string.trim(node.nodeValue) == '') {
|
||||
// Remove empty text node, otherwise madness ensues (e.g. controls that
|
||||
// use goog-inline-block will flicker and shift on hover on Gecko).
|
||||
element.removeChild(node);
|
||||
}
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inspects the element, and creates an instance of {@link goog.ui.Control} or
|
||||
* an appropriate subclass best suited to decorate it. Returns the control (or
|
||||
* null if no suitable class was found). This default implementation uses the
|
||||
* element's CSS class to find the appropriate control class to instantiate.
|
||||
* May be overridden in subclasses.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {goog.ui.Control?} A new control suitable to decorate the element
|
||||
* (null if none).
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getDecoratorForChild = function(element) {
|
||||
return /** @type {goog.ui.Control} */ (
|
||||
goog.ui.registry.getDecorator(element));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the container's DOM when the container enters the document.
|
||||
* Called from {@link goog.ui.Container#enterDocument}.
|
||||
* @param {goog.ui.Container} container Container whose DOM is to be initialized
|
||||
* as it enters the document.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.initializeDom = function(container) {
|
||||
var elem = container.getElement();
|
||||
goog.asserts.assert(elem, 'The container DOM element cannot be null.');
|
||||
// Make sure the container's element isn't selectable. On Gecko, recursively
|
||||
// marking each child element unselectable is expensive and unnecessary, so
|
||||
// only mark the root element unselectable.
|
||||
goog.style.setUnselectable(elem, true, goog.userAgent.GECKO);
|
||||
|
||||
// IE doesn't support outline:none, so we have to use the hideFocus property.
|
||||
if (goog.userAgent.IE) {
|
||||
elem.hideFocus = true;
|
||||
}
|
||||
|
||||
// Set the ARIA role.
|
||||
var ariaRole = this.getAriaRole();
|
||||
if (ariaRole) {
|
||||
goog.a11y.aria.setRole(elem, ariaRole);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the element within the container's DOM that should receive keyboard
|
||||
* focus (null if none). The default implementation returns the container's
|
||||
* root element.
|
||||
* @param {goog.ui.Container} container Container whose key event target is
|
||||
* to be returned.
|
||||
* @return {Element} Key event target (null if none).
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getKeyEventTarget = function(container) {
|
||||
return container.getElement();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the CSS class to be applied to the root element of containers
|
||||
* rendered using this renderer.
|
||||
* @return {string} Renderer-specific CSS class.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getCssClass = function() {
|
||||
return goog.ui.ContainerRenderer.CSS_CLASS;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns all CSS class names applicable to the given container, based on its
|
||||
* state. The array of class names returned includes the renderer's own CSS
|
||||
* class, followed by a CSS class indicating the container's orientation,
|
||||
* followed by any state-specific CSS classes.
|
||||
* @param {goog.ui.Container} container Container whose CSS classes are to be
|
||||
* returned.
|
||||
* @return {!Array<string>} Array of CSS class names applicable to the
|
||||
* container.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getClassNames = function(container) {
|
||||
var baseClass = this.getCssClass();
|
||||
var isHorizontal =
|
||||
container.getOrientation() == goog.ui.Container.Orientation.HORIZONTAL;
|
||||
var classNames = [
|
||||
baseClass,
|
||||
(isHorizontal ?
|
||||
goog.getCssName(baseClass, 'horizontal') :
|
||||
goog.getCssName(baseClass, 'vertical'))
|
||||
];
|
||||
if (!container.isEnabled()) {
|
||||
classNames.push(goog.getCssName(baseClass, 'disabled'));
|
||||
}
|
||||
return classNames;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the default orientation of containers rendered or decorated by this
|
||||
* renderer. The base class implementation returns {@code VERTICAL}.
|
||||
* @return {goog.ui.Container.Orientation} Default orientation for containers
|
||||
* created or decorated by this renderer.
|
||||
*/
|
||||
goog.ui.ContainerRenderer.prototype.getDefaultOrientation = function() {
|
||||
return goog.ui.Container.Orientation.VERTICAL;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ContainerRenderer
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ContainerRendererTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,225 @@
|
||||
// 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.ContainerRendererTest');
|
||||
goog.setTestOnly('goog.ui.ContainerRendererTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.testing.ExpectedFailures');
|
||||
goog.require('goog.testing.PropertyReplacer');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.ui.rendererasserts');
|
||||
goog.require('goog.ui.Container');
|
||||
goog.require('goog.ui.ContainerRenderer');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
var renderer;
|
||||
var expectedFailures;
|
||||
var stubs = new goog.testing.PropertyReplacer();
|
||||
|
||||
function setUpPage() {
|
||||
expectedFailures = new goog.testing.ExpectedFailures();
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
var sandbox = goog.dom.getElement('sandbox');
|
||||
|
||||
sandbox.appendChild(goog.dom.createDom('span', {
|
||||
id: 'noTabIndex'
|
||||
}, 'Test'));
|
||||
sandbox.appendChild(goog.dom.createDom('div', {
|
||||
id: 'container',
|
||||
'class': 'goog-container-horizontal'
|
||||
}, goog.dom.createDom('div', {
|
||||
id: 'control',
|
||||
'class': 'goog-control'
|
||||
}, 'Hello, world!')));
|
||||
|
||||
renderer = goog.ui.ContainerRenderer.getInstance();
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.dom.getElement('sandbox').innerHTML = '';
|
||||
stubs.reset();
|
||||
expectedFailures.handleTearDown();
|
||||
}
|
||||
|
||||
function testGetInstance() {
|
||||
assertTrue('getInstance() must return a ContainerRenderer',
|
||||
renderer instanceof goog.ui.ContainerRenderer);
|
||||
assertEquals('getInstance() must return the same object each time',
|
||||
renderer, goog.ui.ContainerRenderer.getInstance());
|
||||
}
|
||||
|
||||
function testGetCustomRenderer() {
|
||||
var cssClass = 'special-css-class';
|
||||
var containerRenderer = goog.ui.ContainerRenderer.getCustomRenderer(
|
||||
goog.ui.ContainerRenderer, cssClass);
|
||||
assertEquals(
|
||||
'Renderer should have returned the custom CSS class.',
|
||||
cssClass,
|
||||
containerRenderer.getCssClass());
|
||||
}
|
||||
|
||||
function testGetAriaRole() {
|
||||
assertUndefined('ARIA role must be undefined', renderer.getAriaRole());
|
||||
}
|
||||
|
||||
function testEnableTabIndex() {
|
||||
var container = goog.dom.getElement('container');
|
||||
assertFalse('Container must not have any tab index',
|
||||
goog.dom.isFocusableTabIndex(container));
|
||||
|
||||
// WebKit on Mac doesn't support tabIndex for arbitrary DOM elements
|
||||
// until version 527 or later.
|
||||
expectedFailures.expectFailureFor(goog.userAgent.WEBKIT &&
|
||||
goog.userAgent.MAC && !goog.userAgent.isVersionOrHigher('527'));
|
||||
try {
|
||||
renderer.enableTabIndex(container, true);
|
||||
assertTrue('Container must have a tab index',
|
||||
goog.dom.isFocusableTabIndex(container));
|
||||
assertEquals('Container\'s tab index must be 0', 0, container.tabIndex);
|
||||
|
||||
renderer.enableTabIndex(container, false);
|
||||
assertFalse('Container must not have a tab index',
|
||||
goog.dom.isFocusableTabIndex(container));
|
||||
assertEquals('Container\'s tab index must be -1', -1,
|
||||
container.tabIndex);
|
||||
} catch (e) {
|
||||
expectedFailures.handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
function testCreateDom() {
|
||||
var horizontal = new goog.ui.Container(
|
||||
goog.ui.Container.Orientation.HORIZONTAL);
|
||||
var element1 = renderer.createDom(horizontal);
|
||||
assertEquals('Element must be a DIV', 'DIV', element1.tagName);
|
||||
assertEquals('Element must have the expected class name',
|
||||
'goog-container goog-container-horizontal',
|
||||
element1.className);
|
||||
|
||||
var vertical = new goog.ui.Container(
|
||||
goog.ui.Container.Orientation.VERTICAL);
|
||||
var element2 = renderer.createDom(vertical);
|
||||
assertEquals('Element must be a DIV', 'DIV', element2.tagName);
|
||||
assertEquals('Element must have the expected class name',
|
||||
'goog-container goog-container-vertical',
|
||||
element2.className);
|
||||
}
|
||||
|
||||
function testGetContentElement() {
|
||||
assertNull('getContentElement() must return null if element is null',
|
||||
renderer.getContentElement(null));
|
||||
var element = goog.dom.getElement('container');
|
||||
assertEquals('getContentElement() must return its argument',
|
||||
element, renderer.getContentElement(element));
|
||||
}
|
||||
|
||||
function testCanDecorate() {
|
||||
assertFalse('canDecorate() must return false for a SPAN',
|
||||
renderer.canDecorate(goog.dom.getElement('noTabIndex')));
|
||||
assertTrue('canDecorate() must return true for a DIV',
|
||||
renderer.canDecorate(goog.dom.getElement('container')));
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
var container = new goog.ui.Container();
|
||||
var element = goog.dom.getElement('container');
|
||||
|
||||
assertFalse('Container must not be in the document',
|
||||
container.isInDocument());
|
||||
container.decorate(element);
|
||||
assertTrue('Container must be in the document',
|
||||
container.isInDocument());
|
||||
|
||||
assertEquals('Container\'s ID must match the decorated element\'s ID',
|
||||
element.id, container.getId());
|
||||
assertEquals('Element must have the expected class name',
|
||||
'goog-container-horizontal goog-container', element.className);
|
||||
assertEquals('Container must have one child', 1,
|
||||
container.getChildCount());
|
||||
assertEquals('Child component\'s ID must be as expected', 'control',
|
||||
container.getChildAt(0).getId());
|
||||
|
||||
assertThrows('Redecorating must throw error', function() {
|
||||
container.decorate(element);
|
||||
});
|
||||
}
|
||||
|
||||
function testDecorateWithCustomContainerElement() {
|
||||
var element = goog.dom.getElement('container');
|
||||
var alternateContainerElement = goog.dom.createElement('div');
|
||||
element.appendChild(alternateContainerElement);
|
||||
|
||||
var container = new goog.ui.Container();
|
||||
stubs.set(renderer, 'getContentElement', function() {
|
||||
return alternateContainerElement;
|
||||
});
|
||||
|
||||
assertFalse('Container must not be in the document',
|
||||
container.isInDocument());
|
||||
container.decorate(element);
|
||||
assertTrue('Container must be in the document',
|
||||
container.isInDocument());
|
||||
|
||||
assertEquals('Container\'s ID must match the decorated element\'s ID',
|
||||
element.id, container.getId());
|
||||
assertEquals('Element must have the expected class name',
|
||||
'goog-container-horizontal goog-container', element.className);
|
||||
assertEquals('Container must have 0 children', 0,
|
||||
container.getChildCount());
|
||||
|
||||
assertThrows('Redecorating must throw error', function() {
|
||||
container.decorate(element);
|
||||
});
|
||||
}
|
||||
|
||||
function testSetStateFromClassName() {
|
||||
var container = new goog.ui.Container();
|
||||
|
||||
assertEquals('Container must be vertical',
|
||||
goog.ui.Container.Orientation.VERTICAL, container.getOrientation());
|
||||
renderer.setStateFromClassName(container, 'goog-container-horizontal',
|
||||
'goog-container');
|
||||
assertEquals('Container must be horizontal',
|
||||
goog.ui.Container.Orientation.HORIZONTAL, container.getOrientation());
|
||||
renderer.setStateFromClassName(container, 'goog-container-vertical',
|
||||
'goog-container');
|
||||
assertEquals('Container must be vertical',
|
||||
goog.ui.Container.Orientation.VERTICAL, container.getOrientation());
|
||||
|
||||
assertTrue('Container must be enabled', container.isEnabled());
|
||||
renderer.setStateFromClassName(container, 'goog-container-disabled',
|
||||
'goog-container');
|
||||
assertFalse('Container must be disabled', container.isEnabled());
|
||||
}
|
||||
|
||||
function testInitializeDom() {
|
||||
var container = new goog.ui.Container();
|
||||
var element = goog.dom.getElement('container');
|
||||
container.decorate(element);
|
||||
|
||||
assertTrue('Container\'s root element must be unselectable',
|
||||
goog.style.isUnselectable(container.getElement()));
|
||||
|
||||
assertEquals('On IE, container\'s root element must have hideFocus=true',
|
||||
goog.userAgent.IE, !!container.getElement().hideFocus);
|
||||
}
|
||||
|
||||
function testDoesntCallGetCssClassInConstructor() {
|
||||
goog.testing.ui.rendererasserts.
|
||||
assertNoGetCssClassCallsInConstructor(goog.ui.ContainerRenderer);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Scroll behavior that can be added onto a container.
|
||||
* @author gboyer@google.com (Garry Boyer)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ContainerScroller');
|
||||
|
||||
goog.require('goog.Disposable');
|
||||
goog.require('goog.Timer');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.ui.Container');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plug-on scrolling behavior for a container.
|
||||
*
|
||||
* Use this to style containers, such as pop-up menus, to be scrolling, and
|
||||
* automatically keep the highlighted element visible.
|
||||
*
|
||||
* To use this, first style your container with the desired overflow
|
||||
* properties and height to achieve vertical scrolling. Also, the scrolling
|
||||
* div should have no vertical padding, for two reasons: it is difficult to
|
||||
* compensate for, and is generally not what you want due to the strange way
|
||||
* CSS handles padding on the scrolling dimension.
|
||||
*
|
||||
* The container must already be rendered before this may be constructed.
|
||||
*
|
||||
* @param {!goog.ui.Container} container The container to attach behavior to.
|
||||
* @constructor
|
||||
* @extends {goog.Disposable}
|
||||
* @final
|
||||
*/
|
||||
goog.ui.ContainerScroller = function(container) {
|
||||
goog.Disposable.call(this);
|
||||
|
||||
/**
|
||||
* The container that we are bestowing scroll behavior on.
|
||||
* @type {!goog.ui.Container}
|
||||
* @private
|
||||
*/
|
||||
this.container_ = container;
|
||||
|
||||
/**
|
||||
* Event handler for this object.
|
||||
* @type {!goog.events.EventHandler<!goog.ui.ContainerScroller>}
|
||||
* @private
|
||||
*/
|
||||
this.eventHandler_ = new goog.events.EventHandler(this);
|
||||
|
||||
this.eventHandler_.listen(container, goog.ui.Component.EventType.HIGHLIGHT,
|
||||
this.onHighlight_);
|
||||
this.eventHandler_.listen(container, goog.ui.Component.EventType.ENTER,
|
||||
this.onEnter_);
|
||||
this.eventHandler_.listen(container, goog.ui.Container.EventType.AFTER_SHOW,
|
||||
this.onAfterShow_);
|
||||
this.eventHandler_.listen(container, goog.ui.Component.EventType.HIDE,
|
||||
this.onHide_);
|
||||
|
||||
// TODO(gboyer): Allow a ContainerScroller to be attached with a Container
|
||||
// before the container is rendered.
|
||||
|
||||
this.doScrolling_(true);
|
||||
};
|
||||
goog.inherits(goog.ui.ContainerScroller, goog.Disposable);
|
||||
|
||||
|
||||
/**
|
||||
* The last target the user hovered over.
|
||||
*
|
||||
* @see #onEnter_
|
||||
* @type {goog.ui.Component}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.lastEnterTarget_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* The scrollTop of the container before it was hidden.
|
||||
* Used to restore the scroll position when the container is shown again.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.scrollTopBeforeHide_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Whether we are disabling the default handler for hovering.
|
||||
*
|
||||
* @see #onEnter_
|
||||
* @see #temporarilyDisableHover_
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.disableHover_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Handles hover events on the container's children.
|
||||
*
|
||||
* Helps enforce two constraints: scrolling should not cause mouse highlights,
|
||||
* and mouse highlights should not cause scrolling.
|
||||
*
|
||||
* @param {goog.events.Event} e The container's ENTER event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.onEnter_ = function(e) {
|
||||
if (this.disableHover_) {
|
||||
// The container was scrolled recently. Since the mouse may be over the
|
||||
// container, stop the default action of the ENTER event from causing
|
||||
// highlights.
|
||||
e.preventDefault();
|
||||
} else {
|
||||
// The mouse is moving and causing hover events. Stop the resulting
|
||||
// highlight (if it happens) from causing a scroll.
|
||||
this.lastEnterTarget_ = /** @type {goog.ui.Component} */ (e.target);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles highlight events on the container's children.
|
||||
* @param {goog.events.Event} e The container's highlight event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.onHighlight_ = function(e) {
|
||||
this.doScrolling_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles AFTER_SHOW events on the container. Makes the container
|
||||
* scroll to the previously scrolled position (if there was one),
|
||||
* then adjust it to make the highlighted element be in view (if there is one).
|
||||
* If there was no previous scroll position, then center the highlighted
|
||||
* element (if there is one).
|
||||
* @param {goog.events.Event} e The container's AFTER_SHOW event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.onAfterShow_ = function(e) {
|
||||
if (this.scrollTopBeforeHide_ != null) {
|
||||
this.container_.getElement().scrollTop = this.scrollTopBeforeHide_;
|
||||
// Make sure the highlighted item is still visible, in case the list
|
||||
// or its hilighted item has changed.
|
||||
this.doScrolling_(false);
|
||||
} else {
|
||||
this.doScrolling_(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles hide events on the container. Clears out the last enter target,
|
||||
* since it is no longer applicable, and remembers the scroll position of
|
||||
* the menu so that it can be restored when the menu is reopened.
|
||||
* @param {goog.events.Event} e The container's hide event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.onHide_ = function(e) {
|
||||
if (e.target == this.container_) {
|
||||
this.lastEnterTarget_ = null;
|
||||
this.scrollTopBeforeHide_ = this.container_.getElement().scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Centers the currently highlighted item, if this is scrollable.
|
||||
* @param {boolean=} opt_center Whether to center the highlighted element
|
||||
* rather than simply ensure it is in view. Useful for the first
|
||||
* render.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.doScrolling_ = function(opt_center) {
|
||||
var highlighted = this.container_.getHighlighted();
|
||||
|
||||
// Only scroll if we're visible and there is a highlighted item.
|
||||
if (this.container_.isVisible() && highlighted &&
|
||||
highlighted != this.lastEnterTarget_) {
|
||||
var element = this.container_.getElement();
|
||||
goog.style.scrollIntoContainerView(highlighted.getElement(), element,
|
||||
opt_center);
|
||||
this.temporarilyDisableHover_();
|
||||
this.lastEnterTarget_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Temporarily disables hover events from changing highlight.
|
||||
* @see #onEnter_
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ContainerScroller.prototype.temporarilyDisableHover_ = function() {
|
||||
this.disableHover_ = true;
|
||||
goog.Timer.callOnce(function() {
|
||||
this.disableHover_ = false;
|
||||
}, 0, this);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ContainerScroller.prototype.disposeInternal = function() {
|
||||
goog.ui.ContainerScroller.superClass_.disposeInternal.call(this);
|
||||
this.eventHandler_.dispose();
|
||||
this.lastEnterTarget_ = null;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: gboyer@google.com (Garrett Boyer)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ContainerScroller
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ContainerScrollerTest');
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.goog-container {
|
||||
height: 100px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
/* Give a border and margin to ensure ContainerScroller is tolerant to
|
||||
* them. It is, however, not tolerant to padding. */
|
||||
border: 11px solid #666;
|
||||
margin: 7px 13px 17px 19px;
|
||||
}
|
||||
.goog-control {
|
||||
font-size: 10px;
|
||||
height: 14px;
|
||||
padding: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox" class="goog-container">
|
||||
<div class="goog-control" id="control-0">
|
||||
0
|
||||
</div>
|
||||
<div class="goog-control" id="control-1">
|
||||
1
|
||||
</div>
|
||||
<div class="goog-control" id="control-2">
|
||||
2
|
||||
</div>
|
||||
<div class="goog-control" id="control-3">
|
||||
3
|
||||
</div>
|
||||
<div class="goog-control" id="control-4">
|
||||
4
|
||||
</div>
|
||||
<div class="goog-control" id="control-5">
|
||||
5
|
||||
</div>
|
||||
<div class="goog-control" id="control-6">
|
||||
6
|
||||
</div>
|
||||
<div class="goog-control" id="control-7">
|
||||
7
|
||||
</div>
|
||||
<div class="goog-control" id="control-8">
|
||||
8
|
||||
</div>
|
||||
<div class="goog-control" id="control-9">
|
||||
9
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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.ContainerScrollerTest');
|
||||
goog.setTestOnly('goog.ui.ContainerScrollerTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Container');
|
||||
goog.require('goog.ui.ContainerScroller');
|
||||
|
||||
var sandbox;
|
||||
var sandboxHtml;
|
||||
var container;
|
||||
var mockClock;
|
||||
var scroller;
|
||||
|
||||
function setUpPage() {
|
||||
sandbox = goog.dom.getElement('sandbox');
|
||||
sandboxHtml = sandbox.innerHTML;
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
container = new goog.ui.Container();
|
||||
container.decorate(sandbox);
|
||||
container.getElement().scrollTop = 0;
|
||||
mockClock = new goog.testing.MockClock(true);
|
||||
scroller = null;
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
container.dispose();
|
||||
if (scroller) {
|
||||
scroller.dispose();
|
||||
}
|
||||
// Tick one second to clear all the extra registered events.
|
||||
mockClock.tick(1000);
|
||||
mockClock.uninstall();
|
||||
sandbox.innerHTML = sandboxHtml;
|
||||
}
|
||||
|
||||
function testHighlightFirstStaysAtTop() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(0).setHighlighted(true);
|
||||
assertEquals(0, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testHighlightSecondStaysAtTop() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(1).setHighlighted(true);
|
||||
assertEquals(0, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testHighlightSecondLastScrollsNearTheBottom() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(8).setHighlighted(true);
|
||||
assertEquals('Since scrolling is lazy, when highlighting the second' +
|
||||
' last, the item should be the last visible one.',
|
||||
80, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testHighlightLastScrollsToBottom() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(9).setHighlighted(true);
|
||||
assertEquals(100, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testScrollRestoreIfStillVisible() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(9).setHighlighted(true);
|
||||
var scrollTop = container.getElement().scrollTop;
|
||||
container.setVisible(false);
|
||||
container.setVisible(true);
|
||||
assertEquals('Scroll position should be the same after restore, if it ' +
|
||||
'still makes highlighted item visible',
|
||||
scrollTop, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testNoScrollRestoreIfNotVisible() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getElement().scrollTop = 100;
|
||||
container.setVisible(false);
|
||||
container.getChildAt(0).setHighlighted(true);
|
||||
container.setVisible(true);
|
||||
assertNotEquals('Scroll position should not be the same after restore, if ' +
|
||||
'the scroll position when the menu was hidden no longer ' +
|
||||
'makes the highlighted item visible when the container is ' +
|
||||
'shown again',
|
||||
100, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testCenterOnHighlightedOnFirstOpen() {
|
||||
container.setVisible(false);
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(4).setHighlighted(true);
|
||||
container.setVisible(true);
|
||||
// #2 should be at the top when 4 is centered, meaning a scroll top
|
||||
// of 40 pixels.
|
||||
assertEquals(
|
||||
'On the very first display of the scroller, the item should be ' +
|
||||
'centered, rather than just assured in view.',
|
||||
40, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testHighlightsAreIgnoredInResponseToScrolling() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(9).setHighlighted(true);
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
goog.dom.getElement('control-5'),
|
||||
goog.dom.getElement('control-9'));
|
||||
assertEquals('Mouseovers due to scrolls should be ignored',
|
||||
9, container.getHighlightedIndex());
|
||||
}
|
||||
|
||||
function testHighlightsAreNotIgnoredWhenNotScrolling() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getChildAt(5).setHighlighted(true);
|
||||
mockClock.tick(1000);
|
||||
goog.testing.events.fireMouseOutEvent(
|
||||
goog.dom.getElement('control-5'),
|
||||
goog.dom.getElement('control-6'));
|
||||
goog.testing.events.fireMouseOverEvent(
|
||||
goog.dom.getElement('control-6'),
|
||||
goog.dom.getElement('control-5'));
|
||||
assertEquals('Mousovers not due to scrolls should not be ignored',
|
||||
6, container.getHighlightedIndex());
|
||||
}
|
||||
|
||||
function testFastSynchronousHighlightsNotIgnored() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
// Whereas subsequent highlights from mouseovers due to a scroll, should
|
||||
// be ignored, they should not ignored if they are made synchronusly
|
||||
// from the code and not from a mouseover. Imagine how bad it would be
|
||||
// if you could only set the highligted index a certain number of
|
||||
// times in the same execution context.
|
||||
container.getChildAt(9).setHighlighted(true);
|
||||
container.getChildAt(1).setHighlighted(true);
|
||||
assertEquals('Synchronous highlights should NOT be ignored.',
|
||||
1, container.getHighlightedIndex());
|
||||
container.getChildAt(8).setHighlighted(true);
|
||||
assertEquals('Synchronous highlights should NOT be ignored.',
|
||||
8, container.getHighlightedIndex());
|
||||
}
|
||||
|
||||
function testInitialItemIsCentered() {
|
||||
container.getChildAt(4).setHighlighted(true);
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
// #2 should be at the top when 4 is centered, meaning a scroll top
|
||||
// of 40 pixels.
|
||||
assertEquals(
|
||||
'On the very first attachment of the scroller, the item should be ' +
|
||||
'centered, rather than just assured in view.',
|
||||
40, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testInitialItemIsCenteredTopItem() {
|
||||
container.getChildAt(0).setHighlighted(true);
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
assertEquals(0, container.getElement().scrollTop);
|
||||
}
|
||||
|
||||
function testHidingMenuItemsDoesntAffectContainerScroller() {
|
||||
scroller = new goog.ui.ContainerScroller(container);
|
||||
container.getElement = function() {
|
||||
fail('getElement() must not be called when a control in the container is ' +
|
||||
'being hidden');
|
||||
};
|
||||
container.getChildAt(0).setVisible(false);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Closure Performance Tests - goog.ui.Control</title>
|
||||
<link rel="stylesheet" type="text/css" href="../testing/performancetable.css" />
|
||||
<script src="../base.js"></script>
|
||||
<script>
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.testing.PerformanceTable');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.Control');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>goog.ui.Control Performance Tests</h1>
|
||||
<p>
|
||||
<b>User-agent:</b> <script>document.write(navigator.userAgent);</script>
|
||||
</p>
|
||||
<div id="perfTable"></div>
|
||||
<hr>
|
||||
<div id="renderSandbox"></div>
|
||||
<div id="decorateSandbox"></div>
|
||||
<script>
|
||||
// The sandboxen.
|
||||
var renderSandbox = goog.dom.getElement('renderSandbox');
|
||||
var decorateSandbox = goog.dom.getElement('decorateSandbox');
|
||||
|
||||
// Arrays of rendered/decorated controls (so we can dispose of them).
|
||||
var renderedControls;
|
||||
var decoratedControls;
|
||||
|
||||
// 0-based index of the control currently being rendered/decorated.
|
||||
var renderIndex;
|
||||
var decorateIndex;
|
||||
|
||||
// Element currently being decorated.
|
||||
var elementToDecorate;
|
||||
|
||||
// Number of controls to create/decorate per test run.
|
||||
var SAMPLES_PER_RUN = 100;
|
||||
|
||||
// The performance table.
|
||||
var table;
|
||||
|
||||
// Sets up a render test.
|
||||
function setUpRenderTest() {
|
||||
renderedControls = [];
|
||||
renderIndex = 0;
|
||||
}
|
||||
|
||||
// Cleans up after a render test.
|
||||
function cleanUpAfterRenderTest() {
|
||||
for (var i = 0, count = renderedControls.length; i < count; i++) {
|
||||
renderedControls[i].dispose();
|
||||
}
|
||||
renderedControls = null;
|
||||
goog.dom.removeChildren(renderSandbox);
|
||||
}
|
||||
|
||||
// Sets up a decorate test.
|
||||
function setUpDecorateTest(opt_count) {
|
||||
var count = opt_count || 1000;
|
||||
for (var i = 0; i < count; i++) {
|
||||
decorateSandbox.appendChild(goog.dom.createDom('div', 'goog-control',
|
||||
'W00t!'));
|
||||
}
|
||||
elementToDecorate = decorateSandbox.firstChild;
|
||||
decoratedControls = [];
|
||||
decorateIndex = 0;
|
||||
}
|
||||
|
||||
// Cleans up after a decorate test.
|
||||
function cleanUpAfterDecorateTest() {
|
||||
for (var i = 0, count = decoratedControls.length; i < count; i++) {
|
||||
decoratedControls[i].dispose();
|
||||
}
|
||||
decoratedControls = null;
|
||||
goog.dom.removeChildren(decorateSandbox);
|
||||
}
|
||||
|
||||
// Renders the given number of controls. Since children are appended to
|
||||
// the same parent element in each performance test run, we keep track of
|
||||
// the current index via the global renderIndex variable.
|
||||
function renderControls(count, autoDetectBiDi) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
var control = new goog.ui.Control('W00t!');
|
||||
if (!autoDetectBiDi) {
|
||||
control.setRightToLeft(false);
|
||||
}
|
||||
control.render(renderSandbox);
|
||||
renderedControls[renderIndex++] = control;
|
||||
}
|
||||
}
|
||||
|
||||
// Decorates "count" controls. The decorate sandbox contains enough child
|
||||
// elements for the whole test, but we only decorate up to "count" elements
|
||||
// per test run, so we need to keep track of where we are via the global
|
||||
// decorateIndex and elementToDecorate variables.
|
||||
function decorateControls(count, autoDetectBiDi) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
var next = elementToDecorate.nextSibling;
|
||||
var control = new goog.ui.Control();
|
||||
if (!autoDetectBiDi) {
|
||||
control.setRightToLeft(false);
|
||||
}
|
||||
control.decorate(elementToDecorate);
|
||||
decoratedControls[decorateIndex++] = control;
|
||||
elementToDecorate = next;
|
||||
}
|
||||
}
|
||||
|
||||
function setUpPage() {
|
||||
table = new goog.testing.PerformanceTable(
|
||||
goog.dom.getElement('perfTable'));
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
setUpRenderTest();
|
||||
table.run(goog.partial(renderControls, SAMPLES_PER_RUN, true),
|
||||
'Render ' + SAMPLES_PER_RUN + ' controls (default)');
|
||||
cleanUpAfterRenderTest();
|
||||
assertEquals('The expected number of controls must have been rendered',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(), renderIndex);
|
||||
}
|
||||
|
||||
function testDecorate() {
|
||||
setUpDecorateTest(SAMPLES_PER_RUN * table.getTimer().getNumSamples());
|
||||
table.run(goog.partial(decorateControls, SAMPLES_PER_RUN, true),
|
||||
'Decorate ' + SAMPLES_PER_RUN + ' controls (default)');
|
||||
cleanUpAfterDecorateTest();
|
||||
assertEquals('The expected number of controls must have been decorated',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(), decorateIndex);
|
||||
assertNull('All controls must have been decorated', elementToDecorate);
|
||||
}
|
||||
|
||||
function testRenderNoBiDiAutoDetect() {
|
||||
setUpRenderTest();
|
||||
table.run(goog.partial(renderControls, SAMPLES_PER_RUN, false),
|
||||
'Render ' + SAMPLES_PER_RUN + ' controls (no BiDi auto-detect)');
|
||||
cleanUpAfterRenderTest();
|
||||
assertEquals('The expected number of controls must have been rendered',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(), renderIndex);
|
||||
}
|
||||
|
||||
function testDecorateNoBiDiAutoDetect() {
|
||||
setUpDecorateTest(SAMPLES_PER_RUN * table.getTimer().getNumSamples());
|
||||
table.run(goog.partial(decorateControls, SAMPLES_PER_RUN, false),
|
||||
'Decorate ' + SAMPLES_PER_RUN + ' controls (no BiDi auto-detect)');
|
||||
cleanUpAfterDecorateTest();
|
||||
assertEquals('The expected number of controls must have been decorated',
|
||||
SAMPLES_PER_RUN * table.getTimer().getNumSamples(), decorateIndex);
|
||||
assertNull('All controls must have been decorated', elementToDecorate);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.Control
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ControlTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Type declaration for control content.
|
||||
*
|
||||
* @author nicksantos@google.com (Nick Santos)
|
||||
*/
|
||||
goog.provide('goog.ui.ControlContent');
|
||||
|
||||
|
||||
/**
|
||||
* Type declaration for text caption or DOM structure to be used as the content
|
||||
* of {@link goog.ui.Control}s.
|
||||
* @typedef {string|Node|Array<Node>|NodeList}
|
||||
*/
|
||||
goog.ui.ControlContent;
|
||||
@@ -0,0 +1,947 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Base class for control renderers.
|
||||
* TODO(attila): If the renderer framework works well, pull it into Component.
|
||||
*
|
||||
* @author attila@google.com (Attila Bodis)
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ControlRenderer');
|
||||
|
||||
goog.require('goog.a11y.aria');
|
||||
goog.require('goog.a11y.aria.Role');
|
||||
goog.require('goog.a11y.aria.State');
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Component');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Default renderer for {@link goog.ui.Control}s. Can be used as-is, but
|
||||
* subclasses of Control will probably want to use renderers specifically
|
||||
* tailored for them by extending this class. Controls that use renderers
|
||||
* delegate one or more of the following API methods to the renderer:
|
||||
* <ul>
|
||||
* <li>{@code createDom} - renders the DOM for the component
|
||||
* <li>{@code canDecorate} - determines whether an element can be decorated
|
||||
* by the component
|
||||
* <li>{@code decorate} - decorates an existing element with the component
|
||||
* <li>{@code setState} - updates the appearance of the component based on
|
||||
* its state
|
||||
* <li>{@code getContent} - returns the component's content
|
||||
* <li>{@code setContent} - sets the component's content
|
||||
* </ul>
|
||||
* Controls are stateful; renderers, on the other hand, should be stateless and
|
||||
* reusable.
|
||||
* @constructor
|
||||
*/
|
||||
goog.ui.ControlRenderer = function() {
|
||||
};
|
||||
goog.addSingletonGetter(goog.ui.ControlRenderer);
|
||||
goog.tagUnsealableClass(goog.ui.ControlRenderer);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new renderer and sets the CSS class that the renderer will use
|
||||
* as the base CSS class to apply to all elements rendered by that renderer.
|
||||
* An example to use this function using a color palette:
|
||||
*
|
||||
* <pre>
|
||||
* var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(
|
||||
* goog.ui.PaletteRenderer, 'my-special-palette');
|
||||
* var newColorPalette = new goog.ui.ColorPalette(
|
||||
* colors, myCustomRenderer, opt_domHelper);
|
||||
* </pre>
|
||||
*
|
||||
* Your CSS can look like this now:
|
||||
* <pre>
|
||||
* .my-special-palette { }
|
||||
* .my-special-palette-table { }
|
||||
* .my-special-palette-cell { }
|
||||
* etc.
|
||||
* </pre>
|
||||
*
|
||||
* <em>instead</em> of
|
||||
* <pre>
|
||||
* .CSS_MY_SPECIAL_PALETTE .goog-palette { }
|
||||
* .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
|
||||
* .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
|
||||
* etc.
|
||||
* </pre>
|
||||
*
|
||||
* You would want to use this functionality when you want an instance of a
|
||||
* component to have specific styles different than the other components of the
|
||||
* same type in your application. This avoids using descendant selectors to
|
||||
* apply the specific styles to this component.
|
||||
*
|
||||
* @param {Function} ctor The constructor of the renderer you are trying to
|
||||
* create.
|
||||
* @param {string} cssClassName The name of the CSS class for this renderer.
|
||||
* @return {goog.ui.ControlRenderer} An instance of the desired renderer with
|
||||
* its getCssClass() method overridden to return the supplied custom CSS
|
||||
* class name.
|
||||
*/
|
||||
goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
|
||||
var renderer = new ctor();
|
||||
|
||||
/**
|
||||
* Returns the CSS class to be applied to the root element of components
|
||||
* rendered using this renderer.
|
||||
* @return {string} Renderer-specific CSS class.
|
||||
*/
|
||||
renderer.getCssClass = function() {
|
||||
return cssClassName;
|
||||
};
|
||||
|
||||
return renderer;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Default CSS class to be applied to the root element of components rendered
|
||||
* by this renderer.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');
|
||||
|
||||
|
||||
/**
|
||||
* Array of arrays of CSS classes that we want composite classes added and
|
||||
* removed for in IE6 and lower as a workaround for lack of multi-class CSS
|
||||
* selector support.
|
||||
*
|
||||
* Subclasses that have accompanying CSS requiring this workaround should define
|
||||
* their own static IE6_CLASS_COMBINATIONS constant and override
|
||||
* getIe6ClassCombinations to return it.
|
||||
*
|
||||
* For example, if your stylesheet uses the selector .button.collapse-left
|
||||
* (and is compiled to .button_collapse-left for the IE6 version of the
|
||||
* stylesheet,) you should include ['button', 'collapse-left'] in this array
|
||||
* and the class button_collapse-left will be applied to the root element
|
||||
* whenever both button and collapse-left are applied individually.
|
||||
*
|
||||
* Members of each class name combination will be joined with underscores in the
|
||||
* order that they're defined in the array. You should alphabetize them (for
|
||||
* compatibility with the CSS compiler) unless you are doing something special.
|
||||
* @type {Array<Array<string>>}
|
||||
*/
|
||||
goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];
|
||||
|
||||
|
||||
/**
|
||||
* Map of component states to corresponding ARIA attributes. Since the mapping
|
||||
* of component states to ARIA attributes is neither component- nor
|
||||
* renderer-specific, this is a static property of the renderer class, and is
|
||||
* initialized on first use.
|
||||
* @type {Object<goog.ui.Component.State, goog.a11y.aria.State>}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.ariaAttributeMap_;
|
||||
|
||||
|
||||
/**
|
||||
* Map of certain ARIA states to ARIA roles that support them. Used for checked
|
||||
* and selected Component states because they are used on Components with ARIA
|
||||
* roles that do not support the corresponding ARIA state.
|
||||
* @private {!Object<goog.a11y.aria.Role, goog.a11y.aria.State>}
|
||||
* @const
|
||||
*/
|
||||
goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_ = goog.object.create(
|
||||
goog.a11y.aria.Role.BUTTON, goog.a11y.aria.State.PRESSED,
|
||||
goog.a11y.aria.Role.CHECKBOX, goog.a11y.aria.State.CHECKED,
|
||||
goog.a11y.aria.Role.MENU_ITEM, goog.a11y.aria.State.SELECTED,
|
||||
goog.a11y.aria.Role.MENU_ITEM_CHECKBOX, goog.a11y.aria.State.CHECKED,
|
||||
goog.a11y.aria.Role.MENU_ITEM_RADIO, goog.a11y.aria.State.CHECKED,
|
||||
goog.a11y.aria.Role.RADIO, goog.a11y.aria.State.CHECKED,
|
||||
goog.a11y.aria.Role.TAB, goog.a11y.aria.State.SELECTED,
|
||||
goog.a11y.aria.Role.TREEITEM, goog.a11y.aria.State.SELECTED);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ARIA role to be applied to the control.
|
||||
* See http://wiki/Main/ARIA for more info.
|
||||
* @return {goog.a11y.aria.Role|undefined} ARIA role.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getAriaRole = function() {
|
||||
// By default, the ARIA role is unspecified.
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the control's contents wrapped in a DIV, with the renderer's own
|
||||
* CSS class and additional state-specific classes applied to it.
|
||||
* @param {goog.ui.Control} control Control to render.
|
||||
* @return {Element} Root element for the control.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.createDom = function(control) {
|
||||
// Create and return DIV wrapping contents.
|
||||
var element = control.getDomHelper().createDom(
|
||||
'div', this.getClassNames(control).join(' '), control.getContent());
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes the control's root element and returns the parent element of the
|
||||
* control's contents. Since by default controls are rendered as a single
|
||||
* DIV, the default implementation returns the element itself. Subclasses
|
||||
* with more complex DOM structures must override this method as needed.
|
||||
* @param {Element} element Root element of the control whose content element
|
||||
* is to be returned.
|
||||
* @return {Element} The control's content element.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the control's DOM by adding or removing the specified class name
|
||||
* to/from its root element. May add additional combined classes as needed in
|
||||
* IE6 and lower. Because of this, subclasses should use this method when
|
||||
* modifying class names on the control's root element.
|
||||
* @param {goog.ui.Control|Element} control Control instance (or root element)
|
||||
* to be updated.
|
||||
* @param {string} className CSS class name to add or remove.
|
||||
* @param {boolean} enable Whether to add or remove the class name.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.enableClassName = function(control,
|
||||
className, enable) {
|
||||
var element = /** @type {Element} */ (
|
||||
control.getElement ? control.getElement() : control);
|
||||
if (element) {
|
||||
var classNames = [className];
|
||||
|
||||
// For IE6, we need to enable any combined classes involving this class
|
||||
// as well.
|
||||
// TODO(user): Remove this as IE6 is no longer in use.
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
|
||||
classNames = this.getAppliedCombinedClassNames_(
|
||||
goog.dom.classlist.get(element), className);
|
||||
classNames.push(className);
|
||||
}
|
||||
|
||||
goog.dom.classlist.enableAll(element, classNames, enable);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the control's DOM by adding or removing the specified extra class
|
||||
* name to/from its element.
|
||||
* @param {goog.ui.Control} control Control to be updated.
|
||||
* @param {string} className CSS class name to add or remove.
|
||||
* @param {boolean} enable Whether to add or remove the class name.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.enableExtraClassName = function(control,
|
||||
className, enable) {
|
||||
// The base class implementation is trivial; subclasses should override as
|
||||
// needed.
|
||||
this.enableClassName(control, className, enable);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if this renderer can decorate the element, false otherwise.
|
||||
* The default implementation always returns true.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {boolean} Whether the renderer can decorate the element.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@code decorate} for {@link goog.ui.Control}s.
|
||||
* Initializes the control's ID, content, and state based on the ID of the
|
||||
* element, its child nodes, and its CSS classes, respectively. Returns the
|
||||
* element.
|
||||
* @param {goog.ui.Control} control Control instance to decorate the element.
|
||||
* @param {Element} element Element to decorate.
|
||||
* @return {Element} Decorated element.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
|
||||
// Set the control's ID to the decorated element's DOM ID, if any.
|
||||
if (element.id) {
|
||||
control.setId(element.id);
|
||||
}
|
||||
|
||||
// Set the control's content to the decorated element's content.
|
||||
var contentElem = this.getContentElement(element);
|
||||
if (contentElem && contentElem.firstChild) {
|
||||
control.setContentInternal(contentElem.firstChild.nextSibling ?
|
||||
goog.array.clone(contentElem.childNodes) : contentElem.firstChild);
|
||||
} else {
|
||||
control.setContentInternal(null);
|
||||
}
|
||||
|
||||
// Initialize the control's state based on the decorated element's CSS class.
|
||||
// This implementation is optimized to minimize object allocations, string
|
||||
// comparisons, and DOM access.
|
||||
var state = 0x00;
|
||||
var rendererClassName = this.getCssClass();
|
||||
var structuralClassName = this.getStructuralCssClass();
|
||||
var hasRendererClassName = false;
|
||||
var hasStructuralClassName = false;
|
||||
var hasCombinedClassName = false;
|
||||
var classNames = goog.array.toArray(goog.dom.classlist.get(element));
|
||||
goog.array.forEach(classNames, function(className) {
|
||||
if (!hasRendererClassName && className == rendererClassName) {
|
||||
hasRendererClassName = true;
|
||||
if (structuralClassName == rendererClassName) {
|
||||
hasStructuralClassName = true;
|
||||
}
|
||||
} else if (!hasStructuralClassName && className == structuralClassName) {
|
||||
hasStructuralClassName = true;
|
||||
} else {
|
||||
state |= this.getStateFromClass(className);
|
||||
}
|
||||
if (this.getStateFromClass(className) == goog.ui.Component.State.DISABLED) {
|
||||
goog.asserts.assertElement(contentElem);
|
||||
if (goog.dom.isFocusableTabIndex(contentElem)) {
|
||||
goog.dom.setFocusableTabIndex(contentElem, false);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
control.setStateInternal(state);
|
||||
|
||||
// Make sure the element has the renderer's CSS classes applied, as well as
|
||||
// any extra class names set on the control.
|
||||
if (!hasRendererClassName) {
|
||||
classNames.push(rendererClassName);
|
||||
if (structuralClassName == rendererClassName) {
|
||||
hasStructuralClassName = true;
|
||||
}
|
||||
}
|
||||
if (!hasStructuralClassName) {
|
||||
classNames.push(structuralClassName);
|
||||
}
|
||||
var extraClassNames = control.getExtraClassNames();
|
||||
if (extraClassNames) {
|
||||
classNames.push.apply(classNames, extraClassNames);
|
||||
}
|
||||
|
||||
// For IE6, rewrite all classes on the decorated element if any combined
|
||||
// classes apply.
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
|
||||
var combinedClasses = this.getAppliedCombinedClassNames_(
|
||||
classNames);
|
||||
if (combinedClasses.length > 0) {
|
||||
classNames.push.apply(classNames, combinedClasses);
|
||||
hasCombinedClassName = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only write to the DOM if new class names had to be added to the element.
|
||||
if (!hasRendererClassName || !hasStructuralClassName ||
|
||||
extraClassNames || hasCombinedClassName) {
|
||||
goog.dom.classlist.set(element, classNames.join(' '));
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the control's DOM by configuring properties that can only be set
|
||||
* after the DOM has entered the document. This implementation sets up BiDi
|
||||
* and keyboard focus. Called from {@link goog.ui.Control#enterDocument}.
|
||||
* @param {goog.ui.Control} control Control whose DOM is to be initialized
|
||||
* as it enters the document.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.initializeDom = function(control) {
|
||||
// Initialize render direction (BiDi). We optimize the left-to-right render
|
||||
// direction by assuming that elements are left-to-right by default, and only
|
||||
// updating their styling if they are explicitly set to right-to-left.
|
||||
if (control.isRightToLeft()) {
|
||||
this.setRightToLeft(control.getElement(), true);
|
||||
}
|
||||
|
||||
// Initialize keyboard focusability (tab index). We assume that components
|
||||
// aren't focusable by default (i.e have no tab index), and only touch the
|
||||
// DOM if the component is focusable, enabled, and visible, and therefore
|
||||
// needs a tab index.
|
||||
if (control.isEnabled()) {
|
||||
this.setFocusable(control, control.isVisible());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the element's ARIA role.
|
||||
* @param {Element} element Element to update.
|
||||
* @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setAriaRole = function(element,
|
||||
opt_preferredRole) {
|
||||
var ariaRole = opt_preferredRole || this.getAriaRole();
|
||||
if (ariaRole) {
|
||||
goog.asserts.assert(element,
|
||||
'The element passed as a first parameter cannot be null.');
|
||||
var currentRole = goog.a11y.aria.getRole(element);
|
||||
if (ariaRole == currentRole) {
|
||||
return;
|
||||
}
|
||||
goog.a11y.aria.setRole(element, ariaRole);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the element's ARIA attributes, including distinguishing between
|
||||
* universally supported ARIA properties and ARIA states that are only
|
||||
* supported by certain ARIA roles. Only attributes which are initialized to be
|
||||
* true will be set.
|
||||
* @param {!goog.ui.Control} control Control whose ARIA state will be updated.
|
||||
* @param {!Element} element Element whose ARIA state is to be updated.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
|
||||
goog.asserts.assert(control);
|
||||
goog.asserts.assert(element);
|
||||
|
||||
var ariaLabel = control.getAriaLabel();
|
||||
if (goog.isDefAndNotNull(ariaLabel)) {
|
||||
this.setAriaLabel(element, ariaLabel);
|
||||
}
|
||||
|
||||
if (!control.isVisible()) {
|
||||
goog.a11y.aria.setState(
|
||||
element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
|
||||
}
|
||||
if (!control.isEnabled()) {
|
||||
this.updateAriaState(
|
||||
element, goog.ui.Component.State.DISABLED, !control.isEnabled());
|
||||
}
|
||||
if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
|
||||
this.updateAriaState(
|
||||
element, goog.ui.Component.State.SELECTED, control.isSelected());
|
||||
}
|
||||
if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
|
||||
this.updateAriaState(
|
||||
element, goog.ui.Component.State.CHECKED, control.isChecked());
|
||||
}
|
||||
if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
|
||||
this.updateAriaState(
|
||||
element, goog.ui.Component.State.OPENED, control.isOpen());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the element's ARIA label. This should be overriden by subclasses that
|
||||
* don't apply the role directly on control.element_.
|
||||
* @param {!Element} element Element whose ARIA label is to be updated.
|
||||
* @param {string} ariaLabel Label to add to the element.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setAriaLabel = function(element, ariaLabel) {
|
||||
goog.a11y.aria.setLabel(element, ariaLabel);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Allows or disallows text selection within the control's DOM.
|
||||
* @param {Element} element The control's root element.
|
||||
* @param {boolean} allow Whether the element should allow text selection.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(element,
|
||||
allow) {
|
||||
// On all browsers other than IE and Opera, it isn't necessary to recursively
|
||||
// apply unselectable styling to the element's children.
|
||||
goog.style.setUnselectable(element, !allow,
|
||||
!goog.userAgent.IE && !goog.userAgent.OPERA);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Applies special styling to/from the control's element if it is rendered
|
||||
* right-to-left, and removes it if it is rendered left-to-right.
|
||||
* @param {Element} element The control's root element.
|
||||
* @param {boolean} rightToLeft Whether the component is rendered
|
||||
* right-to-left.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setRightToLeft = function(element,
|
||||
rightToLeft) {
|
||||
this.enableClassName(element,
|
||||
goog.getCssName(this.getStructuralCssClass(), 'rtl'), rightToLeft);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the control's key event target supports keyboard focus
|
||||
* (based on its {@code tabIndex} attribute), false otherwise.
|
||||
* @param {goog.ui.Control} control Control whose key event target is to be
|
||||
* checked.
|
||||
* @return {boolean} Whether the control's key event target is focusable.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
|
||||
var keyTarget;
|
||||
if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
|
||||
(keyTarget = control.getKeyEventTarget())) {
|
||||
return goog.dom.isFocusableTabIndex(keyTarget);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the control's key event target to make it focusable or non-focusable
|
||||
* via its {@code tabIndex} attribute. Does nothing if the control doesn't
|
||||
* support the {@code FOCUSED} state, or if it has no key event target.
|
||||
* @param {goog.ui.Control} control Control whose key event target is to be
|
||||
* updated.
|
||||
* @param {boolean} focusable Whether to enable keyboard focus support on the
|
||||
* control's key event target.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
|
||||
var keyTarget;
|
||||
if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
|
||||
(keyTarget = control.getKeyEventTarget())) {
|
||||
if (!focusable && control.isFocused()) {
|
||||
// Blur before hiding. Note that IE calls onblur handlers asynchronously.
|
||||
try {
|
||||
keyTarget.blur();
|
||||
} catch (e) {
|
||||
// TODO(user|user): Find out why this fails on IE.
|
||||
}
|
||||
// The blur event dispatched by the key event target element when blur()
|
||||
// was called on it should have been handled by the control's handleBlur()
|
||||
// method, so at this point the control should no longer be focused.
|
||||
// However, blur events are unreliable on IE and FF3, so if at this point
|
||||
// the control is still focused, we trigger its handleBlur() method
|
||||
// programmatically.
|
||||
if (control.isFocused()) {
|
||||
control.handleBlur(null);
|
||||
}
|
||||
}
|
||||
// Don't overwrite existing tab index values unless needed.
|
||||
if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
|
||||
goog.dom.setFocusableTabIndex(keyTarget, focusable);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Shows or hides the element.
|
||||
* @param {Element} element Element to update.
|
||||
* @param {boolean} visible Whether to show the element.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {
|
||||
// The base class implementation is trivial; subclasses should override as
|
||||
// needed. It should be possible to do animated reveals, for example.
|
||||
goog.style.setElementShown(element, visible);
|
||||
if (element) {
|
||||
goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the appearance of the control in response to a state change.
|
||||
* @param {goog.ui.Control} control Control instance to update.
|
||||
* @param {goog.ui.Component.State} state State to enable or disable.
|
||||
* @param {boolean} enable Whether the control is entering or exiting the state.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
|
||||
var element = control.getElement();
|
||||
if (element) {
|
||||
var className = this.getClassForState(state);
|
||||
if (className) {
|
||||
this.enableClassName(control, className, enable);
|
||||
}
|
||||
this.updateAriaState(element, state, enable);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the element's ARIA (accessibility) attributes , including
|
||||
* distinguishing between universally supported ARIA properties and ARIA states
|
||||
* that are only supported by certain ARIA roles.
|
||||
* @param {Element} element Element whose ARIA state is to be updated.
|
||||
* @param {goog.ui.Component.State} state Component state being enabled or
|
||||
* disabled.
|
||||
* @param {boolean} enable Whether the state is being enabled or disabled.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.updateAriaState = function(element, state,
|
||||
enable) {
|
||||
// Ensure the ARIA attribute map exists.
|
||||
if (!goog.ui.ControlRenderer.ariaAttributeMap_) {
|
||||
goog.ui.ControlRenderer.ariaAttributeMap_ = goog.object.create(
|
||||
goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,
|
||||
goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,
|
||||
goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,
|
||||
goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);
|
||||
}
|
||||
goog.asserts.assert(element,
|
||||
'The element passed as a first parameter cannot be null.');
|
||||
var ariaAttr = goog.ui.ControlRenderer.getAriaStateForAriaRole_(
|
||||
element, goog.ui.ControlRenderer.ariaAttributeMap_[state]);
|
||||
if (ariaAttr) {
|
||||
goog.a11y.aria.setState(element, ariaAttr, enable);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the appropriate ARIA attribute based on ARIA role if the ARIA
|
||||
* attribute is an ARIA state.
|
||||
* @param {!Element} element The element from which to get the ARIA role for
|
||||
* matching ARIA state.
|
||||
* @param {goog.a11y.aria.State} attr The ARIA attribute to check to see if it
|
||||
* can be applied to the given ARIA role.
|
||||
* @return {goog.a11y.aria.State} An ARIA attribute that can be applied to the
|
||||
* given ARIA role.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {
|
||||
var role = goog.a11y.aria.getRole(element);
|
||||
if (!role) {
|
||||
return attr;
|
||||
}
|
||||
role = /** @type {goog.a11y.aria.Role} */ (role);
|
||||
var matchAttr = goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_[role] || attr;
|
||||
return goog.ui.ControlRenderer.isAriaState_(attr) ? matchAttr : attr;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the given ARIA attribute is an ARIA property or ARIA state.
|
||||
* @param {goog.a11y.aria.State} attr The ARIA attribute to classify.
|
||||
* @return {boolean} If the ARIA attribute is an ARIA state.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.isAriaState_ = function(attr) {
|
||||
return attr == goog.a11y.aria.State.CHECKED ||
|
||||
attr == goog.a11y.aria.State.SELECTED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a control's root element, and sets its content to the given text
|
||||
* caption or DOM structure. The default implementation replaces the children
|
||||
* of the given element. Renderers that create more complex DOM structures
|
||||
* must override this method accordingly.
|
||||
* @param {Element} element The control's root element.
|
||||
* @param {goog.ui.ControlContent} content Text caption or DOM structure to be
|
||||
* set as the control's content. The DOM nodes will not be cloned, they
|
||||
* will only moved under the content element of the control.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
|
||||
var contentElem = this.getContentElement(element);
|
||||
if (contentElem) {
|
||||
goog.dom.removeChildren(contentElem);
|
||||
if (content) {
|
||||
if (goog.isString(content)) {
|
||||
goog.dom.setTextContent(contentElem, content);
|
||||
} else {
|
||||
var childHandler = function(child) {
|
||||
if (child) {
|
||||
var doc = goog.dom.getOwnerDocument(contentElem);
|
||||
contentElem.appendChild(goog.isString(child) ?
|
||||
doc.createTextNode(child) : child);
|
||||
}
|
||||
};
|
||||
if (goog.isArray(content)) {
|
||||
// Array of nodes.
|
||||
goog.array.forEach(content, childHandler);
|
||||
} else if (goog.isArrayLike(content) && !('nodeType' in content)) {
|
||||
// NodeList. The second condition filters out TextNode which also has
|
||||
// length attribute but is not array like. The nodes have to be cloned
|
||||
// because childHandler removes them from the list during iteration.
|
||||
goog.array.forEach(
|
||||
goog.array.clone(/** @type {!NodeList} */(content)),
|
||||
childHandler);
|
||||
} else {
|
||||
// Node or string.
|
||||
childHandler(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the element within the component's DOM that should receive keyboard
|
||||
* focus (null if none). The default implementation returns the control's root
|
||||
* element.
|
||||
* @param {goog.ui.Control} control Control whose key event target is to be
|
||||
* returned.
|
||||
* @return {Element} The key event target.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
|
||||
return control.getElement();
|
||||
};
|
||||
|
||||
|
||||
// CSS class name management.
|
||||
|
||||
|
||||
/**
|
||||
* Returns the CSS class name to be applied to the root element of all
|
||||
* components rendered or decorated using this renderer. The class name
|
||||
* is expected to uniquely identify the renderer class, i.e. no two
|
||||
* renderer classes are expected to share the same CSS class name.
|
||||
* @return {string} Renderer-specific CSS class name.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getCssClass = function() {
|
||||
return goog.ui.ControlRenderer.CSS_CLASS;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of combinations of classes to apply combined class names for
|
||||
* in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This
|
||||
* method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be
|
||||
* compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS
|
||||
* static constant instead.
|
||||
* @return {Array<Array<string>>} Array of class name combinations.
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the name of a DOM structure-specific CSS class to be applied to the
|
||||
* root element of all components rendered or decorated using this renderer.
|
||||
* Unlike the class name returned by {@link #getCssClass}, the structural class
|
||||
* name may be shared among different renderers that generate similar DOM
|
||||
* structures. The structural class name also serves as the basis of derived
|
||||
* class names used to identify and style structural elements of the control's
|
||||
* DOM, as well as the basis for state-specific class names. The default
|
||||
* implementation returns the same class name as {@link #getCssClass}, but
|
||||
* subclasses are expected to override this method as needed.
|
||||
* @return {string} DOM structure-specific CSS class name (same as the renderer-
|
||||
* specific CSS class name by default).
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
|
||||
return this.getCssClass();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns all CSS class names applicable to the given control, based on its
|
||||
* state. The return value is an array of strings containing
|
||||
* <ol>
|
||||
* <li>the renderer-specific CSS class returned by {@link #getCssClass},
|
||||
* followed by
|
||||
* <li>the structural CSS class returned by {@link getStructuralCssClass} (if
|
||||
* different from the renderer-specific CSS class), followed by
|
||||
* <li>any state-specific classes returned by {@link #getClassNamesForState},
|
||||
* followed by
|
||||
* <li>any extra classes returned by the control's {@code getExtraClassNames}
|
||||
* method and
|
||||
* <li>for IE6 and lower, additional combined classes from
|
||||
* {@link getAppliedCombinedClassNames_}.
|
||||
* </ol>
|
||||
* Since all controls have at least one renderer-specific CSS class name, this
|
||||
* method is guaranteed to return an array of at least one element.
|
||||
* @param {goog.ui.Control} control Control whose CSS classes are to be
|
||||
* returned.
|
||||
* @return {!Array<string>} Array of CSS class names applicable to the control.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
|
||||
var cssClass = this.getCssClass();
|
||||
|
||||
// Start with the renderer-specific class name.
|
||||
var classNames = [cssClass];
|
||||
|
||||
// Add structural class name, if different.
|
||||
var structuralCssClass = this.getStructuralCssClass();
|
||||
if (structuralCssClass != cssClass) {
|
||||
classNames.push(structuralCssClass);
|
||||
}
|
||||
|
||||
// Add state-specific class names, if any.
|
||||
var classNamesForState = this.getClassNamesForState(control.getState());
|
||||
classNames.push.apply(classNames, classNamesForState);
|
||||
|
||||
// Add extra class names, if any.
|
||||
var extraClassNames = control.getExtraClassNames();
|
||||
if (extraClassNames) {
|
||||
classNames.push.apply(classNames, extraClassNames);
|
||||
}
|
||||
|
||||
// Add composite classes for IE6 support
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
|
||||
classNames.push.apply(classNames,
|
||||
this.getAppliedCombinedClassNames_(classNames));
|
||||
}
|
||||
|
||||
return classNames;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of all the combined class names that should be applied based
|
||||
* on the given list of classes. Checks the result of
|
||||
* {@link getIe6ClassCombinations} for any combinations that have all
|
||||
* members contained in classes. If a combination matches, the members are
|
||||
* joined with an underscore (in order), and added to the return array.
|
||||
*
|
||||
* If opt_includedClass is provided, return only the combined classes that have
|
||||
* all members contained in classes AND include opt_includedClass as well.
|
||||
* opt_includedClass is added to classes as well.
|
||||
* @param {goog.array.ArrayLike<string>} classes Array-like thing of classes to
|
||||
* return matching combined classes for.
|
||||
* @param {?string=} opt_includedClass If provided, get only the combined
|
||||
* classes that include this one.
|
||||
* @return {!Array<string>} Array of combined class names that should be
|
||||
* applied.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
|
||||
classes, opt_includedClass) {
|
||||
var toAdd = [];
|
||||
if (opt_includedClass) {
|
||||
classes = classes.concat([opt_includedClass]);
|
||||
}
|
||||
goog.array.forEach(this.getIe6ClassCombinations(), function(combo) {
|
||||
if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&
|
||||
(!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {
|
||||
toAdd.push(combo.join('_'));
|
||||
}
|
||||
});
|
||||
return toAdd;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array
|
||||
* of the appropriate class names representing the given state, suitable to be
|
||||
* applied to the root element of a component rendered using this renderer, or
|
||||
* null if no state-specific classes need to be applied. This default
|
||||
* implementation uses the renderer's {@link getClassForState} method to
|
||||
* generate each state-specific class.
|
||||
* @param {number} state Bit mask of component states.
|
||||
* @return {!Array<string>} Array of CSS class names representing the given
|
||||
* state.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
|
||||
var classNames = [];
|
||||
while (state) {
|
||||
// For each enabled state, push the corresponding CSS class name onto
|
||||
// the classNames array.
|
||||
var mask = state & -state; // Least significant bit
|
||||
classNames.push(this.getClassForState(
|
||||
/** @type {goog.ui.Component.State} */ (mask)));
|
||||
state &= ~mask;
|
||||
}
|
||||
return classNames;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a single {@link goog.ui.Component.State}, and returns the
|
||||
* corresponding CSS class name (null if none).
|
||||
* @param {goog.ui.Component.State} state Component state.
|
||||
* @return {string|undefined} CSS class representing the given state (undefined
|
||||
* if none).
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
|
||||
if (!this.classByState_) {
|
||||
this.createClassByStateMap_();
|
||||
}
|
||||
return this.classByState_[state];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Takes a single CSS class name which may represent a component state, and
|
||||
* returns the corresponding component state (0x00 if none).
|
||||
* @param {string} className CSS class name, possibly representing a component
|
||||
* state.
|
||||
* @return {goog.ui.Component.State} state Component state corresponding
|
||||
* to the given CSS class (0x00 if none).
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
|
||||
if (!this.stateByClass_) {
|
||||
this.createStateByClassMap_();
|
||||
}
|
||||
var state = parseInt(this.stateByClass_[className], 10);
|
||||
return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates the lookup table of states to classes, used during state changes.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
|
||||
var baseClass = this.getStructuralCssClass();
|
||||
|
||||
// This ensures space-separated css classnames are not allowed, which some
|
||||
// ControlRenderers had been doing. See http://b/13694665.
|
||||
var isValidClassName = !goog.string.contains(
|
||||
goog.string.normalizeWhitespace(baseClass), ' ');
|
||||
goog.asserts.assert(isValidClassName,
|
||||
'ControlRenderer has an invalid css class: \'' + baseClass + '\'');
|
||||
|
||||
/**
|
||||
* Map of component states to state-specific structural class names,
|
||||
* used when changing the DOM in response to a state change. Precomputed
|
||||
* and cached on first use to minimize object allocations and string
|
||||
* concatenation.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.classByState_ = goog.object.create(
|
||||
goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),
|
||||
goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),
|
||||
goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),
|
||||
goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),
|
||||
goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),
|
||||
goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),
|
||||
goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates the lookup table of classes to states, used during decoration.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {
|
||||
// We need the classByState_ map so we can transpose it.
|
||||
if (!this.classByState_) {
|
||||
this.createClassByStateMap_();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of state-specific structural class names to component states,
|
||||
* used during element decoration. Precomputed and cached on first use
|
||||
* to minimize object allocations and string concatenation.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.stateByClass_ = goog.object.transpose(this.classByState_);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<!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.
|
||||
-->
|
||||
<!--
|
||||
Author: attila@google.com (Attila Bodis)
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ControlRenderer
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ControlRendererTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sandbox">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
// Copyright 2010 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 Displays and edits the value of a cookie.
|
||||
* Intended only for debugging.
|
||||
*/
|
||||
goog.provide('goog.ui.CookieEditor');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.net.cookies');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.Component');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Displays and edits the value of a cookie.
|
||||
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
|
||||
* @constructor
|
||||
* @extends {goog.ui.Component}
|
||||
* @final
|
||||
*/
|
||||
goog.ui.CookieEditor = function(opt_domHelper) {
|
||||
goog.ui.CookieEditor.base(this, 'constructor', opt_domHelper);
|
||||
};
|
||||
goog.inherits(goog.ui.CookieEditor, goog.ui.Component);
|
||||
|
||||
|
||||
/**
|
||||
* Cookie key.
|
||||
* @type {?string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.cookieKey_;
|
||||
|
||||
|
||||
/**
|
||||
* Text area.
|
||||
* @type {HTMLTextAreaElement}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.textAreaElem_;
|
||||
|
||||
|
||||
/**
|
||||
* Clear button.
|
||||
* @type {HTMLButtonElement}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.clearButtonElem_;
|
||||
|
||||
|
||||
/**
|
||||
* Invalid value warning text.
|
||||
* @type {HTMLSpanElement}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.valueWarningElem_;
|
||||
|
||||
|
||||
/**
|
||||
* Update button.
|
||||
* @type {HTMLButtonElement}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.updateButtonElem_;
|
||||
|
||||
|
||||
// TODO(user): add combobox for user to select different cookies
|
||||
/**
|
||||
* Sets the cookie which this component will edit.
|
||||
* @param {string} cookieKey Cookie key.
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.selectCookie = function(cookieKey) {
|
||||
goog.asserts.assert(goog.net.cookies.isValidName(cookieKey));
|
||||
this.cookieKey_ = cookieKey;
|
||||
if (this.textAreaElem_) {
|
||||
this.textAreaElem_.value = goog.net.cookies.get(cookieKey) || '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CookieEditor.prototype.canDecorate = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CookieEditor.prototype.createDom = function() {
|
||||
// Debug-only, so we don't need i18n.
|
||||
this.clearButtonElem_ = /** @type {!HTMLButtonElement} */ (goog.dom.createDom(
|
||||
goog.dom.TagName.BUTTON, /* attributes */ null, 'Clear'));
|
||||
this.updateButtonElem_ =
|
||||
/** @type {!HTMLButtonElement} */ (goog.dom.createDom(
|
||||
goog.dom.TagName.BUTTON, /* attributes */ null, 'Update'));
|
||||
var value = this.cookieKey_ && goog.net.cookies.get(this.cookieKey_);
|
||||
this.textAreaElem_ = /** @type {!HTMLTextAreaElement} */ (goog.dom.createDom(
|
||||
goog.dom.TagName.TEXTAREA, /* attibutes */ null, value || ''));
|
||||
this.valueWarningElem_ = /** @type {!HTMLSpanElement} */ (goog.dom.createDom(
|
||||
goog.dom.TagName.SPAN, /* attibutes */ {
|
||||
'style': 'display:none;color:red'
|
||||
}, 'Invalid cookie value.'));
|
||||
this.setElementInternal(goog.dom.createDom(goog.dom.TagName.DIV,
|
||||
/* attibutes */ null,
|
||||
this.valueWarningElem_,
|
||||
goog.dom.createDom(goog.dom.TagName.BR),
|
||||
this.textAreaElem_,
|
||||
goog.dom.createDom(goog.dom.TagName.BR),
|
||||
this.clearButtonElem_,
|
||||
this.updateButtonElem_));
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CookieEditor.prototype.enterDocument = function() {
|
||||
goog.ui.CookieEditor.base(this, 'enterDocument');
|
||||
this.getHandler().listen(this.clearButtonElem_,
|
||||
goog.events.EventType.CLICK,
|
||||
this.handleClear_);
|
||||
this.getHandler().listen(this.updateButtonElem_,
|
||||
goog.events.EventType.CLICK,
|
||||
this.handleUpdate_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles user clicking clear button.
|
||||
* @param {!goog.events.Event} e The click event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.handleClear_ = function(e) {
|
||||
if (this.cookieKey_) {
|
||||
goog.net.cookies.remove(this.cookieKey_);
|
||||
}
|
||||
this.textAreaElem_.value = '';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles user clicking update button.
|
||||
* @param {!goog.events.Event} e The click event.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.CookieEditor.prototype.handleUpdate_ = function(e) {
|
||||
if (this.cookieKey_) {
|
||||
var value = this.textAreaElem_.value;
|
||||
if (value) {
|
||||
// Strip line breaks.
|
||||
value = goog.string.stripNewlines(value);
|
||||
}
|
||||
if (goog.net.cookies.isValidValue(value)) {
|
||||
goog.net.cookies.set(this.cookieKey_, value);
|
||||
goog.style.setElementShown(this.valueWarningElem_, false);
|
||||
} else {
|
||||
goog.style.setElementShown(this.valueWarningElem_, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.CookieEditor.prototype.disposeInternal = function() {
|
||||
this.clearButtonElem_ = null;
|
||||
this.cookieKey_ = null;
|
||||
this.textAreaElem_ = null;
|
||||
this.updateButtonElem_ = null;
|
||||
this.valueWarningElem_ = null;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<!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.CookieEditor
|
||||
</title>
|
||||
<script src="../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.CookieEditorTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test_container">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2010 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.CookieEditorTest');
|
||||
goog.setTestOnly('goog.ui.CookieEditorTest');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.net.cookies');
|
||||
goog.require('goog.testing.events');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.CookieEditor');
|
||||
|
||||
var COOKIE_KEY = 'my_fabulous_cookie';
|
||||
var COOKIE_VALUES;
|
||||
|
||||
goog.net.cookies.get = function(key) {
|
||||
return COOKIE_VALUES[key];
|
||||
};
|
||||
|
||||
goog.net.cookies.set = function(key, value) {
|
||||
return COOKIE_VALUES[key] = value;
|
||||
};
|
||||
|
||||
goog.net.cookies.remove = function(key, value) {
|
||||
delete COOKIE_VALUES[key];
|
||||
};
|
||||
|
||||
function setUp() {
|
||||
goog.dom.removeChildren(goog.dom.getElement('test_container'));
|
||||
COOKIE_VALUES = {};
|
||||
}
|
||||
|
||||
function newCookieEditor(opt_cookieValue) {
|
||||
// Set cookie.
|
||||
if (opt_cookieValue) {
|
||||
goog.net.cookies.set(COOKIE_KEY, opt_cookieValue);
|
||||
}
|
||||
|
||||
// Render editor.
|
||||
var editor = new goog.ui.CookieEditor();
|
||||
editor.selectCookie(COOKIE_KEY);
|
||||
editor.render(goog.dom.getElement('test_container'));
|
||||
assertEquals('wrong text area value', opt_cookieValue || '',
|
||||
editor.textAreaElem_.value || '');
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
function testRender() {
|
||||
// Render editor.
|
||||
var editor = newCookieEditor();
|
||||
|
||||
// All expected elements created?
|
||||
var elem = editor.getElement();
|
||||
assertNotNullNorUndefined('missing element', elem);
|
||||
assertNotNullNorUndefined('missing clear button', editor.clearButtonElem_);
|
||||
assertNotNullNorUndefined('missing update button',
|
||||
editor.updateButtonElem_);
|
||||
assertNotNullNorUndefined('missing text area', editor.textAreaElem_);
|
||||
}
|
||||
|
||||
function testEditCookie() {
|
||||
// Render editor.
|
||||
var editor = newCookieEditor();
|
||||
|
||||
// Invalid value.
|
||||
var newValue = 'my bad value;';
|
||||
editor.textAreaElem_.value = newValue;
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.CLICK, editor.updateButtonElem_));
|
||||
assertTrue('unexpected cookie value', !goog.net.cookies.get(COOKIE_KEY));
|
||||
|
||||
// Valid value.
|
||||
newValue = 'my fabulous value';
|
||||
editor.textAreaElem_.value = newValue;
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.CLICK, editor.updateButtonElem_));
|
||||
assertEquals('wrong cookie value', newValue,
|
||||
goog.net.cookies.get(COOKIE_KEY));
|
||||
}
|
||||
|
||||
function testClearCookie() {
|
||||
// Render editor.
|
||||
var value = 'I will be cleared';
|
||||
var editor = newCookieEditor(value);
|
||||
|
||||
// Clear value.
|
||||
goog.testing.events.fireBrowserEvent(new goog.events.Event(
|
||||
goog.events.EventType.CLICK, editor.clearButtonElem_));
|
||||
assertTrue('unexpected cookie value', !goog.net.cookies.get(COOKIE_KEY));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user