Add vectortile branch

This commit is contained in:
Andreas Hocevar
2015-09-10 15:23:57 +02:00
parent 57ee7f52fd
commit 962473c3ab
3201 changed files with 978141 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
// 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.
*
* @author adamwos@google.com (Adam Wos)
* @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,113 @@
// 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.
*
* This class makes use of goog.html.legacyconversions and provides no
* HTML-type-safe alternative. As such, it is not compatible with
* code that sets goog.html.legacyconversions.ALLOW_LEGACY_CONVERSIONS to
* false.
*
* @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,144 @@
// 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.dom.safe');
goog.require('goog.html.legacyconversions');
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.
*
* This class makes use of goog.html.legacyconversions and provides no
* HTML-type-safe alternative. As such, it is not compatible with
* code that sets goog.html.legacyconversions.ALLOW_LEGACY_CONVERSIONS to
* false.
*
* @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) {
// requestMatchingRows() sets innerHTML directly from unsanitized/unescaped
// server-data, with no form of type-safety. Because requestMatchingRows is
// used polymorphically (for example, from
// goog.ui.ac.AutoComplete.prototype.setToken) it is undesirable to have
// Conformance legacyconversions rule for it. Doing so would cause the
// respective check rule fire from all such places which polymorphically
// call requestMatchingRows(); such calls are safe as long as they're not to
// RichRemoteArrayMatcher.
goog.html.legacyconversions.throwIfConversionsDisallowed();
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) {
goog.dom.safe.setInnerHtml(node,
goog.html.legacyconversions.safeHtmlFromString(
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,288 @@
// 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.dom.TagName');
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(goog.dom.TagName.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(
goog.dom.TagName.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,202 @@
// 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.dom.TagName');
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(goog.dom.TagName.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;
font-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 {HTMLElement}
* @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 {HTMLElement} */ (obj);
if (this.selectedElement_) {
goog.dom.classlist.remove(this.selectedElement_,
this.selectedItemClassName_);
}
this.selectedElement_ = elt;
var el = /** @type {HTMLElement} */ (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,174 @@
// 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.dom.InputType');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.InputHandler');
goog.require('goog.i18n.bidi');
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(goog.dom.TagName.INPUT,
{'type': goog.dom.InputType.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();
if (element) {
var text = this.getValue();
goog.i18n.bidi.setElementDirByTextDirectionality(element, text);
}
};
/**
* 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,490 @@
// 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.dom.safe');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.legacyconversions');
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.string.Const');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.Popup');
goog.scope(function() {
var SafeHtml = goog.html.SafeHtml;
/**
* 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|!goog.html.SafeHtml|Element} message HTML or an element
* to display inside the bubble. If possible pass a SafeHtml; string
* is supported for backwards-compatibility only and uses
* goog.html.legacyconversions.
* @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);
if (goog.isString(message)) {
message = goog.html.legacyconversions.safeHtmlFromString(message);
}
/**
* The HTML string or element to display inside the bubble.
*
* @type {!goog.html.SafeHtml|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;
/** @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();
goog.dom.safe.setInnerHtml(/** @type {!Element} */ (element),
this.computeHtmlForCorner_(corner));
if (!(this.message_ instanceof SafeHtml)) {
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);
goog.dom.safe.setInnerHtml(element, goog.html.SafeHtml.EMPTY);
}
};
/**
* 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 {!goog.html.SafeHtml} 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 (this.message_ instanceof SafeHtml) {
message = this.message_;
} else {
message = SafeHtml.create('div', {'id': this.messageId_});
}
var tableRows = goog.html.SafeHtml.concat(
SafeHtml.create('tr', {},
SafeHtml.create('td', {'colspan': 4, 'class': bubbleTopClass})),
SafeHtml.create('tr', {}, SafeHtml.concat(
SafeHtml.create('td', {'class': this.config_.cssBubbleLeft }),
SafeHtml.create('td',
{'class': this.config_.cssBubbleFont, 'style':
goog.string.Const.from('padding:0 4px;background:white')},
message),
SafeHtml.create('td',
{'id': this.closeButtonId_,
'class': this.config_.cssCloseButton }),
SafeHtml.create('td', {'class': this.config_.cssBubbleRight }))),
SafeHtml.create('tr', {},
SafeHtml.create('td', {'colspan': 4, 'class': bubbleBottomClass})));
return SafeHtml.create('table',
{'border': 0, 'cellspacing': 0, 'cellpadding': 0,
'width': this.config_.bubbleWidth,
'style': goog.string.Const.from('z-index:1')},
tableRows);
};
/**
* 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
];
}); // goog.scope
@@ -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,243 @@
// 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.TagName');
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', goog.dom.TagName.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,925 @@
// 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.TagName');
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 {HTMLElement}
* @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(
goog.dom.TagName.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_ = /** @type {!HTMLElement} */ (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(
goog.dom.TagName.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(
goog.dom.TagName.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(
goog.dom.TagName.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(goog.dom.TagName.DIV,
{id: 'zoom', className: goog.getCssName('goog-char-picker-char-zoom')});
this.charNameEl_ = this.getDomHelper().createDom(goog.dom.TagName.DIV,
{id: 'charName', className: goog.getCssName('goog-char-picker-name')});
this.unicodeEl_ = this.getDomHelper().createDom(goog.dom.TagName.DIV,
{id: 'unicode', className: goog.getCssName('goog-char-picker-unicode')});
var card = this.getDomHelper().createDom(goog.dom.TagName.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 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,277 @@
// 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()) {
var wasFocused = this.isFocused();
this.exitDocument();
this.label_ = label;
this.enterDocument();
if (wasFocused) {
this.getElementStrict().focus();
}
} 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,470 @@
// 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.TagName');
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(goog.dom.TagName.DIV);
var label2 = goog.dom.createElement(goog.dom.TagName.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(goog.dom.TagName.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 testLabel_setAgain() {
var label = goog.dom.createElement(goog.dom.TagName.DIV);
document.body.appendChild(label);
try {
checkbox.setChecked(false);
checkbox.setLabel(label);
checkbox.render(label);
checkbox.getElement().focus();
checkbox.setLabel(label);
assertEquals('checkbox should not have lost focus', checkbox.getElement(),
document.activeElement);
} 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,191 @@
// 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.TagName');
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(
goog.dom.TagName.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,65 @@
// 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.TagName');
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(goog.dom.TagName.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,148 @@
// 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.TagName');
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(goog.dom.TagName.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,72 @@
// 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.dom.TagName');
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(goog.dom.TagName.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(goog.dom.TagName.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,179 @@
// 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.dom.TagName');
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(goog.dom.TagName.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,170 @@
// 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.dom.TagName');
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(goog.dom.TagName.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,993 @@
// 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.InputType');
goog.require('goog.dom.TagName');
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(goog.dom.TagName.INPUT, {
name: this.fieldName_,
type: goog.dom.InputType.TEXT,
autocomplete: 'off'
});
this.button_ = this.getDomHelper().createDom(goog.dom.TagName.SPAN,
goog.getCssName('goog-combobox-button'));
this.setElementInternal(this.getDomHelper().createDom(goog.dom.TagName.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(goog.dom.TagName.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(goog.dom.TagName.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(goog.dom.TagName.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(goog.dom.TagName.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(goog.dom.TagName.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(goog.dom.TagName.A));
b.setElementInternal(goog.dom.createElement(goog.dom.TagName.B));
c.setElementInternal(goog.dom.createElement(goog.dom.TagName.C));
d.setElementInternal(goog.dom.createElement(goog.dom.TagName.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(goog.dom.TagName.DIV);
parent.setElementInternal(
goog.dom.createDom(goog.dom.TagName.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(goog.dom.TagName.DIV);
parent.setElementInternal(
goog.dom.createDom(goog.dom.TagName.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,614 @@
// 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.TagName');
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(goog.dom.TagName.UL);
this.forEachChild(function(child) {
child.createDom();
var childEl = child.getElement();
ul.appendChild(
this.getDomHelper().createDom(goog.dom.TagName.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(goog.dom.TagName.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,375 @@
// 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.TagName');
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(goog.dom.TagName.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,226 @@
// 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.dom.TagName');
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(goog.dom.TagName.SPAN, {
id: 'noTabIndex'
}, 'Test'));
sandbox.appendChild(goog.dom.createDom(goog.dom.TagName.DIV, {
id: 'container',
'class': 'goog-container-horizontal'
}, goog.dom.createDom(goog.dom.TagName.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(goog.dom.TagName.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,949 @@
// 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.TagName');
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(
goog.dom.TagName.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