Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
Author: agrieve@google.com (Andrew Grieve)
-->
<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.tweak.BaseEntry
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.tweak.BaseEntryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,85 @@
// 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.tweak.BaseEntryTest');
goog.setTestOnly('goog.tweak.BaseEntryTest');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');
/** @suppress {extraRequire} needed for createRegistryEntries. */
goog.require('goog.tweak.testhelpers');
var mockControl;
function setUp() {
mockControl = new goog.testing.MockControl();
}
function tearDown() {
goog.tweak.registry_ = null;
mockControl.$verifyAll();
}
function testGetValue_defaultValues() {
createRegistryEntries('');
assertFalse('wrong initial value for bool', boolEntry.getValue());
assertEquals('wrong initial value for enum', 'A', strEnumEntry.getValue());
assertEquals('wrong initial value for str', '', strEntry.getValue());
assertEquals('wrong initial value for bool2', true, boolEntry2.getValue());
assertEquals('wrong initial value for enum2', 1, numEnumEntry.getValue());
assertEquals('wrong initial value for str2', 'foo', strEntry2.getValue());
assertFalse('wrong initial value for BoolOne', boolOneEntry.getValue());
assertTrue('wrong initial value for BoolTwo', boolTwoEntry.getValue());
}
function testGetValue_nonDefaultValues() {
createRegistryEntries('?bool=1&enum=C');
// These have the restartRequired option set.
boolEntry.setValue(false);
strEntry.setValue('foo');
numEntry.setValue(5);
assertTrue('wrong value for boolean', boolEntry.getValue());
assertEquals('wrong value for string', strEntry.getDefaultValue(),
strEntry.getValue());
assertEquals('wrong value for num', numEntry.getDefaultValue(),
numEntry.getValue());
// These do not have the restartRequired option set.
strEnumEntry.setValue('B');
boolOneEntry.setValue(true);
assertEquals('wrong value for strEnum', 'B', strEnumEntry.getValue());
assertEquals('wrong value for boolOne', true, boolOneEntry.getValue());
}
function testCallbacks() {
createRegistryEntries('');
var mockCallback = mockControl.createFunctionMock();
boolEntry.addCallback(mockCallback);
boolOneEntry.addCallback(mockCallback);
strEnumEntry.addCallback(mockCallback);
numEnumEntry.addCallback(mockCallback);
mockCallback(boolEntry);
mockCallback(boolOneEntry);
mockCallback(strEnumEntry);
mockCallback(numEnumEntry);
mockControl.$replayAll();
boolEntry.setValue(true);
boolOneEntry.setValue(true);
strEnumEntry.setValue('C');
numEnumEntry.setValue(3);
}
@@ -0,0 +1,315 @@
// 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 Definition for goog.tweak.Registry.
* Most clients should not use this class directly, but instead use the API
* defined in tweak.js. One possible use case for directly using TweakRegistry
* is to register tweaks that are not known at compile time.
*
* @author agrieve@google.com (Andrew Grieve)
*/
goog.provide('goog.tweak.Registry');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.log');
goog.require('goog.string');
goog.require('goog.tweak.BasePrimitiveSetting');
goog.require('goog.tweak.BaseSetting');
goog.require('goog.tweak.BooleanSetting');
goog.require('goog.tweak.NumericSetting');
goog.require('goog.tweak.StringSetting');
goog.require('goog.uri.utils');
/**
* Singleton that manages all tweaks. This should be instantiated only from
* goog.tweak.getRegistry().
* @param {string} queryParams Value of window.location.search.
* @param {!Object<string|number|boolean>} compilerOverrides Default value
* overrides set by the compiler.
* @constructor
* @final
*/
goog.tweak.Registry = function(queryParams, compilerOverrides) {
/**
* A map of entry id -> entry object
* @type {!Object<!goog.tweak.BaseEntry>}
* @private
*/
this.entryMap_ = {};
/**
* The map of query params to use when initializing entry settings.
* @type {!Object<string>}
* @private
*/
this.parsedQueryParams_ = goog.tweak.Registry.parseQueryParams(queryParams);
/**
* List of callbacks to call when a new entry is registered.
* @type {!Array<!Function>}
* @private
*/
this.onRegisterListeners_ = [];
/**
* A map of entry ID -> default value override for overrides set by the
* compiler.
* @type {!Object<string|number|boolean>}
* @private
*/
this.compilerDefaultValueOverrides_ = compilerOverrides;
/**
* A map of entry ID -> default value override for overrides set by
* goog.tweak.overrideDefaultValue().
* @type {!Object<string|number|boolean>}
* @private
*/
this.defaultValueOverrides_ = {};
};
/**
* The logger for this class.
* @type {goog.log.Logger}
* @private
*/
goog.tweak.Registry.prototype.logger_ =
goog.log.getLogger('goog.tweak.Registry');
/**
* Simple parser for query params. Makes all keys lower-case.
* @param {string} queryParams The part of the url between the ? and the #.
* @return {!Object<string>} map of key->value.
*/
goog.tweak.Registry.parseQueryParams = function(queryParams) {
// Strip off the leading ? and split on &.
var parts = queryParams.substr(1).split('&');
var ret = {};
for (var i = 0, il = parts.length; i < il; ++i) {
var entry = parts[i].split('=');
if (entry[0]) {
ret[goog.string.urlDecode(entry[0]).toLowerCase()] =
goog.string.urlDecode(entry[1] || '');
}
}
return ret;
};
/**
* Registers the given tweak setting/action.
* @param {goog.tweak.BaseEntry} entry The entry.
*/
goog.tweak.Registry.prototype.register = function(entry) {
var id = entry.getId();
var oldBaseEntry = this.entryMap_[id];
if (oldBaseEntry) {
if (oldBaseEntry == entry) {
goog.log.warning(this.logger_, 'Tweak entry registered twice: ' + id);
return;
}
goog.asserts.fail(
'Tweak entry registered twice and with different types: ' + id);
}
// Check for a default value override, either from compiler flags or from a
// call to overrideDefaultValue().
var defaultValueOverride = (id in this.compilerDefaultValueOverrides_) ?
this.compilerDefaultValueOverrides_[id] : this.defaultValueOverrides_[id];
if (goog.isDef(defaultValueOverride)) {
goog.asserts.assertInstanceof(entry, goog.tweak.BasePrimitiveSetting,
'Cannot set the default value of non-primitive setting %s',
entry.label);
entry.setDefaultValue(defaultValueOverride);
}
// Set its value from the query params.
if (entry instanceof goog.tweak.BaseSetting) {
if (entry.getParamName()) {
entry.setInitialQueryParamValue(
this.parsedQueryParams_[entry.getParamName()]);
}
}
this.entryMap_[id] = entry;
// Call all listeners.
for (var i = 0, callback; callback = this.onRegisterListeners_[i]; ++i) {
callback(entry);
}
};
/**
* Adds a callback to be called whenever a new tweak is added.
* @param {!Function} func The callback.
*/
goog.tweak.Registry.prototype.addOnRegisterListener = function(func) {
this.onRegisterListeners_.push(func);
};
/**
* @param {string} id The unique string that identifies this entry.
* @return {boolean} Whether a tweak with the given ID is registered.
*/
goog.tweak.Registry.prototype.hasEntry = function(id) {
return id in this.entryMap_;
};
/**
* Returns the BaseEntry with the given ID. Asserts if it does not exists.
* @param {string} id The unique string that identifies this entry.
* @return {!goog.tweak.BaseEntry} The entry.
*/
goog.tweak.Registry.prototype.getEntry = function(id) {
var ret = this.entryMap_[id];
goog.asserts.assert(ret, 'Tweak not registered: %s', id);
return ret;
};
/**
* Returns the boolean setting with the given ID. Asserts if the ID does not
* refer to a registered entry or if it refers to one of the wrong type.
* @param {string} id The unique string that identifies this entry.
* @return {!goog.tweak.BooleanSetting} The entry.
*/
goog.tweak.Registry.prototype.getBooleanSetting = function(id) {
var entry = this.getEntry(id);
goog.asserts.assertInstanceof(entry, goog.tweak.BooleanSetting,
'getBooleanSetting called on wrong type of BaseSetting');
return /** @type {!goog.tweak.BooleanSetting} */ (entry);
};
/**
* Returns the string setting with the given ID. Asserts if the ID does not
* refer to a registered entry or if it refers to one of the wrong type.
* @param {string} id The unique string that identifies this entry.
* @return {!goog.tweak.StringSetting} The entry.
*/
goog.tweak.Registry.prototype.getStringSetting = function(id) {
var entry = this.getEntry(id);
goog.asserts.assertInstanceof(entry, goog.tweak.StringSetting,
'getStringSetting called on wrong type of BaseSetting');
return /** @type {!goog.tweak.StringSetting} */ (entry);
};
/**
* Returns the numeric setting with the given ID. Asserts if the ID does not
* refer to a registered entry or if it refers to one of the wrong type.
* @param {string} id The unique string that identifies this entry.
* @return {!goog.tweak.NumericSetting} The entry.
*/
goog.tweak.Registry.prototype.getNumericSetting = function(id) {
var entry = this.getEntry(id);
goog.asserts.assertInstanceof(entry, goog.tweak.NumericSetting,
'getNumericSetting called on wrong type of BaseSetting');
return /** @type {!goog.tweak.NumericSetting} */ (entry);
};
/**
* Creates and returns an array of all BaseSetting objects with an associted
* query parameter.
* @param {boolean} excludeChildEntries Exclude BooleanInGroupSettings.
* @param {boolean} excludeNonSettings Exclude entries that are not subclasses
* of BaseSetting.
* @return {!Array<!goog.tweak.BaseSetting>} The settings.
*/
goog.tweak.Registry.prototype.extractEntries =
function(excludeChildEntries, excludeNonSettings) {
var entries = [];
for (var id in this.entryMap_) {
var entry = this.entryMap_[id];
if (entry instanceof goog.tweak.BaseSetting) {
if (excludeChildEntries && !entry.getParamName()) {
continue;
}
} else if (excludeNonSettings) {
continue;
}
entries.push(entry);
}
return entries;
};
/**
* Returns the query part of the URL that will apply all set tweaks.
* @param {string=} opt_existingSearchStr The part of the url between the ? and
* the #. Uses window.location.search if not given.
* @return {string} The query string.
*/
goog.tweak.Registry.prototype.makeUrlQuery =
function(opt_existingSearchStr) {
var existingParams = opt_existingSearchStr == undefined ?
window.location.search : opt_existingSearchStr;
var sortedEntries = this.extractEntries(true /* excludeChildEntries */,
true /* excludeNonSettings */);
// Sort the params so that the urlQuery has stable ordering.
sortedEntries.sort(function(a, b) {
return goog.array.defaultCompare(a.getParamName(), b.getParamName());
});
// Add all values that are not set to their defaults.
var keysAndValues = [];
for (var i = 0, entry; entry = sortedEntries[i]; ++i) {
var encodedValue = entry.getNewValueEncoded();
if (encodedValue != null) {
keysAndValues.push(entry.getParamName(), encodedValue);
}
// Strip all tweak query params from the existing query string. This will
// make the final query string contain only the tweak settings that are set
// to their non-default values and also maintain non-tweak related query
// parameters.
existingParams = goog.uri.utils.removeParam(existingParams,
encodeURIComponent(/** @type {string} */ (entry.getParamName())));
}
var tweakParams = goog.uri.utils.buildQueryData(keysAndValues);
// Decode spaces and commas in order to make the URL more readable.
tweakParams = tweakParams.replace(/%2C/g, ',').replace(/%20/g, '+');
return !tweakParams ? existingParams :
existingParams ? existingParams + '&' + tweakParams :
'?' + tweakParams;
};
/**
* Sets a default value to use for the given tweak instead of the one passed
* to the register* function. This function must be called before the tweak is
* registered.
* @param {string} id The unique string that identifies the entry.
* @param {string|number|boolean} value The replacement value to be used as the
* default value for the setting.
*/
goog.tweak.Registry.prototype.overrideDefaultValue = function(id, value) {
goog.asserts.assert(!this.hasEntry(id),
'goog.tweak.overrideDefaultValue must be called before the tweak is ' +
'registered. Tweak: %s', id);
this.defaultValueOverrides_[id] = value;
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<!--
Author: agrieve@google.com (Andrew Grieve)
-->
<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.tweak.Registry
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.tweak.RegistryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,137 @@
// 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.tweak.RegistryTest');
goog.setTestOnly('goog.tweak.RegistryTest');
goog.require('goog.asserts.AssertionError');
goog.require('goog.testing.jsunit');
goog.require('goog.tweak');
/** @suppress {extraRequire} needed for createRegistryEntries. */
goog.require('goog.tweak.testhelpers');
var registry;
function setUp() {
createRegistryEntries('');
registry = goog.tweak.getRegistry();
}
function tearDown() {
goog.tweak.registry_ = null;
}
function testGetBaseEntry() {
// Initial values
assertEquals('wrong bool1 object', boolEntry,
registry.getBooleanSetting('Bool'));
assertEquals('wrong string object', strEntry,
registry.getStringSetting('Str'));
assertEquals('wrong numeric object', numEntry,
registry.getNumericSetting('Num'));
assertEquals('wrong button object', buttonEntry,
registry.getEntry('Button'));
assertEquals('wrong button object', boolGroup,
registry.getEntry('BoolGroup'));
}
function testInitializeFromQueryParams() {
var testCase = 0;
function assertQuery(queryStr, boolValue, enumValue, strValue, subBoolValue,
subBoolValue2) {
createRegistryEntries(queryStr);
assertEquals('Wrong bool value for query: ' + queryStr, boolValue,
boolEntry.getValue());
assertEquals('Wrong enum value for query: ' + queryStr, enumValue,
strEnumEntry.getValue());
assertEquals('Wrong str value for query: ' + queryStr, strValue,
strEntry.getValue());
assertEquals('Wrong BoolOne value for query: ' + queryStr, subBoolValue,
boolOneEntry.getValue());
assertEquals('Wrong BoolTwo value for query: ' + queryStr, subBoolValue2,
boolTwoEntry.getValue());
}
assertQuery('?dummy=1&bool=&enum=&s=', false, '', '', false, true);
assertQuery('?bool=0&enum=A&s=a', false, 'A', 'a', false, true);
assertQuery('?bool=1&enum=A', true, 'A', '', false, true);
assertQuery('?bool=true&enum=B&s=as+df', true, 'B', 'as df', false, true);
assertQuery('?enum=C', false, 'C', '', false, true);
assertQuery('?enum=C&boolgroup=-booltwo', false, 'C', '', false, false);
assertQuery('?enum=C&boolgroup=b1,-booltwo', false, 'C', '', true, false);
assertQuery('?enum=C&boolgroup=b1', false, 'C', '', true, true);
assertQuery('?s=a+b%20c%26', false, 'A', 'a b c&', false, true);
}
function testMakeUrlQuery() {
assertEquals('All values are default.',
'', registry.makeUrlQuery(''));
assertEquals('All values are default - with existing params.',
'?super=pudu', registry.makeUrlQuery('?super=pudu'));
boolEntry.setValue(true);
numEnumEntry.setValue(2);
strEntry.setValue('f o&o');
assertEquals('Wrong query string 1.',
'?bool=1&enum2=2&s=f+o%26o',
registry.makeUrlQuery('?bool=1'));
assertEquals('Wrong query string 1 - with existing params.',
'?super=pudu&bool=1&enum2=2&s=f+o%26o',
registry.makeUrlQuery('?bool=0&s=g&super=pudu'));
boolOneEntry.setValue(true);
assertEquals('Wrong query string 2.',
'?bool=1&boolgroup=B1&enum2=2&s=f+o%26o',
registry.makeUrlQuery(''));
boolTwoEntry.setValue(false);
assertEquals('Wrong query string 3.',
'?bool=1&boolgroup=B1,-booltwo&enum2=2&s=f+o%26o',
registry.makeUrlQuery(''));
}
function testOverrideDefaultValue_calledBefore() {
registry.overrideDefaultValue('b', false);
registry.overrideDefaultValue('b', true);
goog.tweak.registerBoolean('b', 'b desc');
var bEntry = registry.getEntry('b');
assertTrue('Default value should be true.', bEntry.getDefaultValue());
assertTrue('Value should be true.', bEntry.getValue());
}
function testOverrideDefaultValue_calledAfter() {
var exception = assertThrows('Should assert.', function() {
registry.overrideDefaultValue('Bool2', false);
});
assertTrue('Wrong exception',
exception instanceof goog.asserts.AssertionError);
}
function testCompilerOverrideDefaultValue() {
createRegistryEntries('', { 'b': true });
registry = goog.tweak.getRegistry();
goog.tweak.registerBoolean('b', 'b desc');
var bEntry = registry.getEntry('b');
assertTrue('Default value should be true.', bEntry.getDefaultValue());
assertTrue('Value should be true.', bEntry.getValue());
}
function testCompilerAndJsOverrideDefaultValue() {
createRegistryEntries('', { 'b': false });
registry = goog.tweak.getRegistry();
registry.overrideDefaultValue('b', true);
goog.tweak.registerBoolean('b', 'b desc', true);
var bEntry = registry.getEntry('b');
assertFalse('Default value should be false.', bEntry.getDefaultValue());
assertFalse('Value should be false.', bEntry.getValue());
}
@@ -0,0 +1,123 @@
// 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 Common test functions for tweak unit tests.
*
* @author agrieve@google.com (Andrew Grieve)
* @package
*/
goog.provide('goog.tweak.testhelpers');
goog.setTestOnly();
goog.require('goog.tweak');
goog.require('goog.tweak.BooleanGroup');
goog.require('goog.tweak.BooleanInGroupSetting');
goog.require('goog.tweak.BooleanSetting');
goog.require('goog.tweak.ButtonAction');
goog.require('goog.tweak.NumericSetting');
goog.require('goog.tweak.Registry');
goog.require('goog.tweak.StringSetting');
var boolEntry;
var boolEntry2;
var strEntry;
var strEntry2;
var strEnumEntry;
var numEntry;
var numEnumEntry;
var boolGroup;
var boolOneEntry;
var boolTwoEntry;
var buttonEntry;
/**
* Creates a registry with some entries in it.
* @param {string} queryParams The query parameter string to use for the
* registry.
* @param {!Object<string|number|boolean>=} opt_compilerOverrides Compiler
* overrides.
* @suppress {accessControls} Private state is accessed for test purposes.
*/
function createRegistryEntries(queryParams, opt_compilerOverrides) {
// Initialize the registry with the given query string.
var registry =
new goog.tweak.Registry(queryParams, opt_compilerOverrides || {});
goog.tweak.registry_ = registry;
boolEntry = new goog.tweak.BooleanSetting('Bool', 'The bool1');
registry.register(boolEntry);
boolEntry2 = new goog.tweak.BooleanSetting('Bool2', 'The bool2');
boolEntry2.setDefaultValue(true);
registry.register(boolEntry2);
strEntry = new goog.tweak.StringSetting('Str', 'The str1');
strEntry.setParamName('s');
registry.register(strEntry);
strEntry2 = new goog.tweak.StringSetting('Str2', 'The str2');
strEntry2.setDefaultValue('foo');
registry.register(strEntry2);
strEnumEntry = new goog.tweak.StringSetting('Enum', 'The enum');
strEnumEntry.setValidValues(['A', 'B', 'C']);
strEnumEntry.setRestartRequired(false);
registry.register(strEnumEntry);
numEntry = new goog.tweak.NumericSetting('Num', 'The num');
numEntry.setDefaultValue(99);
registry.register(numEntry);
numEnumEntry = new goog.tweak.NumericSetting('Enum2', 'The 2nd enum');
numEnumEntry.setValidValues([1, 2, 3]);
numEnumEntry.setRestartRequired(false);
numEnumEntry.label = 'Enum the second&';
registry.register(numEnumEntry);
boolGroup = new goog.tweak.BooleanGroup('BoolGroup', 'The bool group');
registry.register(boolGroup);
boolOneEntry = new goog.tweak.BooleanInGroupSetting('BoolOne', 'Desc for 1',
boolGroup);
boolOneEntry.setToken('B1');
boolOneEntry.setRestartRequired(false);
boolGroup.addChild(boolOneEntry);
registry.register(boolOneEntry);
boolTwoEntry = new goog.tweak.BooleanInGroupSetting('BoolTwo', 'Desc for 2',
boolGroup);
boolTwoEntry.setDefaultValue(true);
boolGroup.addChild(boolTwoEntry);
registry.register(boolTwoEntry);
buttonEntry = new goog.tweak.ButtonAction('Button', 'The Btn',
goog.nullFunction);
buttonEntry.label = '<btn>';
registry.register(buttonEntry);
var nsBoolGroup = new goog.tweak.BooleanGroup('foo.bar.BoolGroup',
'Namespaced Bool Group');
registry.register(nsBoolGroup);
var nsBool = new goog.tweak.BooleanInGroupSetting('foo.bar.BoolOne',
'Desc for Namespaced 1', nsBoolGroup);
nsBoolGroup.addChild(nsBool);
registry.register(nsBool);
}
@@ -0,0 +1,301 @@
// 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 Provides facilities for creating and querying tweaks.
* @see http://code.google.com/p/closure-library/wiki/UsingTweaks
*
* @author agrieve@google.com (Andrew Grieve)
*/
goog.provide('goog.tweak');
goog.provide('goog.tweak.ConfigParams');
goog.require('goog.asserts');
goog.require('goog.tweak.BaseSetting');
goog.require('goog.tweak.BooleanGroup');
goog.require('goog.tweak.BooleanInGroupSetting');
goog.require('goog.tweak.BooleanSetting');
goog.require('goog.tweak.ButtonAction');
goog.require('goog.tweak.NumericSetting');
goog.require('goog.tweak.Registry');
goog.require('goog.tweak.StringSetting');
/**
* Calls to this function are overridden by the compiler by the processTweaks
* pass. It returns the overrides to default values for tweaks set by compiler
* options.
* @return {!Object<number|string|boolean>} A map of tweakId -> defaultValue.
* @private
*/
goog.tweak.getCompilerOverrides_ = function() {
return {};
};
/**
* The global reference to the registry, if it exists.
* @type {goog.tweak.Registry}
* @private
*/
goog.tweak.registry_ = null;
/**
* The boolean group set by beginBooleanGroup and cleared by endBooleanGroup.
* @type {goog.tweak.BooleanGroup}
* @private
*/
goog.tweak.activeBooleanGroup_ = null;
/**
* Returns/creates the registry singleton.
* @return {!goog.tweak.Registry} The tweak registry.
*/
goog.tweak.getRegistry = function() {
if (!goog.tweak.registry_) {
var queryString = window.location.search;
var overrides = goog.tweak.getCompilerOverrides_();
goog.tweak.registry_ = new goog.tweak.Registry(queryString, overrides);
}
return goog.tweak.registry_;
};
/**
* Type for configParams.
* TODO(agrieve): Remove |Object when optional fields in struct types are
* implemented.
* @typedef {{
* label:(string|undefined),
* validValues:(!Array<string>|!Array<number>|undefined),
* paramName:(string|undefined),
* restartRequired:(boolean|undefined),
* callback:(Function|undefined),
* token:(string|undefined)
* }|!Object}
*/
goog.tweak.ConfigParams;
/**
* Applies all extra configuration parameters in configParams.
* @param {!goog.tweak.BaseEntry} entry The entry to apply them to.
* @param {!goog.tweak.ConfigParams} configParams Extra configuration
* parameters.
* @private
*/
goog.tweak.applyConfigParams_ = function(entry, configParams) {
if (configParams.label) {
entry.label = configParams.label;
delete configParams.label;
}
if (configParams.validValues) {
goog.asserts.assert(entry instanceof goog.tweak.StringSetting ||
entry instanceof goog.tweak.NumericSetting,
'Cannot set validValues on tweak: %s', entry.getId());
entry.setValidValues(configParams.validValues);
delete configParams.validValues;
}
if (goog.isDef(configParams.paramName)) {
goog.asserts.assertInstanceof(entry, goog.tweak.BaseSetting,
'Cannot set paramName on tweak: %s', entry.getId());
entry.setParamName(configParams.paramName);
delete configParams.paramName;
}
if (goog.isDef(configParams.restartRequired)) {
entry.setRestartRequired(configParams.restartRequired);
delete configParams.restartRequired;
}
if (configParams.callback) {
entry.addCallback(configParams.callback);
delete configParams.callback;
goog.asserts.assert(
!entry.isRestartRequired() || (configParams.restartRequired == false),
'Tweak %s should set restartRequired: false, when adding a callback.',
entry.getId());
}
if (configParams.token) {
goog.asserts.assertInstanceof(entry, goog.tweak.BooleanInGroupSetting,
'Cannot set token on tweak: %s', entry.getId());
entry.setToken(configParams.token);
delete configParams.token;
}
for (var key in configParams) {
goog.asserts.fail('Unknown config options (' + key + '=' +
configParams[key] + ') for tweak ' + entry.getId());
}
};
/**
* Registers a tweak using the given factoryFunc.
* @param {!goog.tweak.BaseEntry} entry The entry to register.
* @param {boolean|string|number=} opt_defaultValue Default value.
* @param {goog.tweak.ConfigParams=} opt_configParams Extra
* configuration parameters.
* @private
*/
goog.tweak.doRegister_ = function(entry, opt_defaultValue, opt_configParams) {
if (opt_configParams) {
goog.tweak.applyConfigParams_(entry, opt_configParams);
}
if (opt_defaultValue != undefined) {
entry.setDefaultValue(opt_defaultValue);
}
if (goog.tweak.activeBooleanGroup_) {
goog.asserts.assertInstanceof(entry, goog.tweak.BooleanInGroupSetting,
'Forgot to end Boolean Group: %s',
goog.tweak.activeBooleanGroup_.getId());
goog.tweak.activeBooleanGroup_.addChild(
/** @type {!goog.tweak.BooleanInGroupSetting} */ (entry));
}
goog.tweak.getRegistry().register(entry);
};
/**
* Creates and registers a group of BooleanSettings that are all set by a
* single query parameter. A call to goog.tweak.endBooleanGroup() must be used
* to close this group. Only goog.tweak.registerBoolean() calls are allowed with
* the beginBooleanGroup()/endBooleanGroup().
* @param {string} id The unique ID for the setting.
* @param {string} description A description of what the setting does.
* @param {goog.tweak.ConfigParams=} opt_configParams Extra configuration
* parameters.
*/
goog.tweak.beginBooleanGroup = function(id, description, opt_configParams) {
var entry = new goog.tweak.BooleanGroup(id, description);
goog.tweak.doRegister_(entry, undefined, opt_configParams);
goog.tweak.activeBooleanGroup_ = entry;
};
/**
* Stops adding boolean entries to the active boolean group.
*/
goog.tweak.endBooleanGroup = function() {
goog.tweak.activeBooleanGroup_ = null;
};
/**
* Creates and registers a BooleanSetting.
* @param {string} id The unique ID for the setting.
* @param {string} description A description of what the setting does.
* @param {boolean=} opt_defaultValue The default value for the setting.
* @param {goog.tweak.ConfigParams=} opt_configParams Extra configuration
* parameters.
*/
goog.tweak.registerBoolean =
function(id, description, opt_defaultValue, opt_configParams) {
// TODO(agrieve): There is a bug in the compiler that causes these calls not
// to be stripped without this outer if. Might be Issue #90.
if (goog.tweak.activeBooleanGroup_) {
var entry = new goog.tweak.BooleanInGroupSetting(id, description,
goog.tweak.activeBooleanGroup_);
} else {
entry = new goog.tweak.BooleanSetting(id, description);
}
goog.tweak.doRegister_(entry, opt_defaultValue, opt_configParams);
};
/**
* Creates and registers a StringSetting.
* @param {string} id The unique ID for the setting.
* @param {string} description A description of what the setting does.
* @param {string=} opt_defaultValue The default value for the setting.
* @param {goog.tweak.ConfigParams=} opt_configParams Extra configuration
* parameters.
*/
goog.tweak.registerString =
function(id, description, opt_defaultValue, opt_configParams) {
goog.tweak.doRegister_(new goog.tweak.StringSetting(id, description),
opt_defaultValue, opt_configParams);
};
/**
* Creates and registers a NumericSetting.
* @param {string} id The unique ID for the setting.
* @param {string} description A description of what the setting does.
* @param {number=} opt_defaultValue The default value for the setting.
* @param {goog.tweak.ConfigParams=} opt_configParams Extra configuration
* parameters.
*/
goog.tweak.registerNumber =
function(id, description, opt_defaultValue, opt_configParams) {
goog.tweak.doRegister_(new goog.tweak.NumericSetting(id, description),
opt_defaultValue, opt_configParams);
};
/**
* Creates and registers a ButtonAction.
* @param {string} id The unique ID for the setting.
* @param {string} description A description of what the action does.
* @param {!Function} callback Function to call when the button is clicked.
* @param {string=} opt_label The button text (instead of the ID).
*/
goog.tweak.registerButton = function(id, description, callback, opt_label) {
var tweak = new goog.tweak.ButtonAction(id, description, callback);
tweak.label = opt_label || tweak.label;
goog.tweak.doRegister_(tweak);
};
/**
* Sets a default value to use for the given tweak instead of the one passed
* to the register* function. This function must be called before the tweak is
* registered.
* @param {string} id The unique string that identifies the entry.
* @param {string|number|boolean} value The new default value for the tweak.
*/
goog.tweak.overrideDefaultValue = function(id, value) {
goog.tweak.getRegistry().overrideDefaultValue(id, value);
};
/**
* Returns the value of the boolean setting with the given ID.
* @param {string} id The unique string that identifies this entry.
* @return {boolean} The value of the tweak.
*/
goog.tweak.getBoolean = function(id) {
return goog.tweak.getRegistry().getBooleanSetting(id).getValue();
};
/**
* Returns the value of the string setting with the given ID,
* @param {string} id The unique string that identifies this entry.
* @return {string} The value of the tweak.
*/
goog.tweak.getString = function(id) {
return goog.tweak.getRegistry().getStringSetting(id).getValue();
};
/**
* Returns the value of the numeric setting with the given ID.
* @param {string} id The unique string that identifies this entry.
* @return {number} The value of the tweak.
*/
goog.tweak.getNumber = function(id) {
return goog.tweak.getRegistry().getNumericSetting(id).getValue();
};
@@ -0,0 +1,826 @@
// 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 A UI for editing tweak settings / clicking tweak actions.
*
* @author agrieve@google.com (Andrew Grieve)
*/
goog.provide('goog.tweak.EntriesPanel');
goog.provide('goog.tweak.TweakUi');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.object');
goog.require('goog.style');
goog.require('goog.tweak');
goog.require('goog.tweak.BaseEntry');
goog.require('goog.tweak.BooleanGroup');
goog.require('goog.tweak.BooleanInGroupSetting');
goog.require('goog.tweak.BooleanSetting');
goog.require('goog.tweak.ButtonAction');
goog.require('goog.tweak.NumericSetting');
goog.require('goog.tweak.StringSetting');
goog.require('goog.ui.Zippy');
goog.require('goog.userAgent');
/**
* A UI for editing tweak settings / clicking tweak actions.
* @param {!goog.tweak.Registry} registry The registry to render.
* @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
* @constructor
* @final
*/
goog.tweak.TweakUi = function(registry, opt_domHelper) {
/**
* The registry to create a UI from.
* @type {!goog.tweak.Registry}
* @private
*/
this.registry_ = registry;
/**
* The element to display when the UI is visible.
* @type {goog.tweak.EntriesPanel|undefined}
* @private
*/
this.entriesPanel_;
/**
* The DomHelper to render with.
* @type {!goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
// Listen for newly registered entries (happens with lazy-loaded modules).
registry.addOnRegisterListener(goog.bind(this.onNewRegisteredEntry_, this));
};
/**
* The CSS class name unique to the root tweak panel div.
* @type {string}
* @private
*/
goog.tweak.TweakUi.ROOT_PANEL_CLASS_ = goog.getCssName('goog-tweak-root');
/**
* The CSS class name unique to the tweak entry div.
* @type {string}
* @private
*/
goog.tweak.TweakUi.ENTRY_CSS_CLASS_ = goog.getCssName('goog-tweak-entry');
/**
* The CSS classes for each tweak entry div.
* @type {string}
* @private
*/
goog.tweak.TweakUi.ENTRY_CSS_CLASSES_ = goog.tweak.TweakUi.ENTRY_CSS_CLASS_ +
' ' + goog.getCssName('goog-inline-block');
/**
* The CSS classes for each namespace tweak entry div.
* @type {string}
* @private
*/
goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ =
goog.tweak.TweakUi.ENTRY_CSS_CLASS_;
/**
* Marker that the style sheet has already been installed.
* @type {string}
* @private
*/
goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ =
'__closure_tweak_installed_';
/**
* CSS used by TweakUI.
* @type {string}
* @private
*/
goog.tweak.TweakUi.CSS_STYLES_ = (function() {
var MOBILE = goog.userAgent.MOBILE;
var IE = goog.userAgent.IE;
var ENTRY_CLASS = '.' + goog.tweak.TweakUi.ENTRY_CSS_CLASS_;
var ROOT_PANEL_CLASS = '.' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
var GOOG_INLINE_BLOCK_CLASS = '.' + goog.getCssName('goog-inline-block');
var ret = ROOT_PANEL_CLASS + '{background:#ffc; padding:0 4px}';
// Make this work even if the user hasn't included common.css.
if (!IE) {
ret += GOOG_INLINE_BLOCK_CLASS + '{display:inline-block}';
}
// Space things out vertically for touch UIs.
if (MOBILE) {
ret += ROOT_PANEL_CLASS + ',' + ROOT_PANEL_CLASS + ' fieldset{' +
'line-height:2em;' + '}';
}
return ret;
})();
/**
* Creates a TweakUi if tweaks are enabled.
* @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
* @return {!Element|undefined} The root UI element or undefined if tweaks are
* not enabled.
*/
goog.tweak.TweakUi.create = function(opt_domHelper) {
var registry = goog.tweak.getRegistry();
if (registry) {
var ui = new goog.tweak.TweakUi(registry, opt_domHelper);
ui.render();
return ui.getRootElement();
}
};
/**
* Creates a TweakUi inside of a show/hide link.
* @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
* @return {!Element|undefined} The root UI element or undefined if tweaks are
* not enabled.
*/
goog.tweak.TweakUi.createCollapsible = function(opt_domHelper) {
var registry = goog.tweak.getRegistry();
if (registry) {
var dh = opt_domHelper || goog.dom.getDomHelper();
// The following strings are for internal debugging only. No translation
// necessary. Do NOT wrap goog.getMsg() around these strings.
var showLink = dh.createDom('a', {href: 'javascript:;'}, 'Show Tweaks');
var hideLink = dh.createDom('a', {href: 'javascript:;'}, 'Hide Tweaks');
var ret = dh.createDom('div', null, showLink);
var lazyCreate = function() {
// Lazily render the UI.
var ui = new goog.tweak.TweakUi(
/** @type {!goog.tweak.Registry} */ (registry), dh);
ui.render();
// Put the hide link on the same line as the "Show Descriptions" link.
// Set the style lazily because we can.
hideLink.style.marginRight = '10px';
var tweakElem = ui.getRootElement();
tweakElem.insertBefore(hideLink, tweakElem.firstChild);
ret.appendChild(tweakElem);
return tweakElem;
};
new goog.ui.Zippy(showLink, lazyCreate, false /* expanded */, hideLink);
return ret;
}
};
/**
* Compares the given entries. Orders alphabetically and groups buttons and
* expandable groups.
* @param {goog.tweak.BaseEntry} a The first entry to compare.
* @param {goog.tweak.BaseEntry} b The second entry to compare.
* @return {number} Refer to goog.array.defaultCompare.
* @private
*/
goog.tweak.TweakUi.entryCompare_ = function(a, b) {
return (
goog.array.defaultCompare(a instanceof goog.tweak.NamespaceEntry_,
b instanceof goog.tweak.NamespaceEntry_) ||
goog.array.defaultCompare(a instanceof goog.tweak.BooleanGroup,
b instanceof goog.tweak.BooleanGroup) ||
goog.array.defaultCompare(a instanceof goog.tweak.ButtonAction,
b instanceof goog.tweak.ButtonAction) ||
goog.array.defaultCompare(a.label, b.label) ||
goog.array.defaultCompare(a.getId(), b.getId()));
};
/**
* @param {!goog.tweak.BaseEntry} entry The entry.
* @return {boolean} Returns whether the given entry contains sub-entries.
* @private
*/
goog.tweak.TweakUi.isGroupEntry_ = function(entry) {
return entry instanceof goog.tweak.NamespaceEntry_ ||
entry instanceof goog.tweak.BooleanGroup;
};
/**
* Returns the list of entries from the given boolean group.
* @param {!goog.tweak.BooleanGroup} group The group to get the entries from.
* @return {!Array<!goog.tweak.BaseEntry>} The sorted entries.
* @private
*/
goog.tweak.TweakUi.extractBooleanGroupEntries_ = function(group) {
var ret = goog.object.getValues(group.getChildEntries());
ret.sort(goog.tweak.TweakUi.entryCompare_);
return ret;
};
/**
* @param {!goog.tweak.BaseEntry} entry The entry.
* @return {string} Returns the namespace for the entry, or '' if it is not
* namespaced.
* @private
*/
goog.tweak.TweakUi.extractNamespace_ = function(entry) {
var namespaceMatch = /.+(?=\.)/.exec(entry.getId());
return namespaceMatch ? namespaceMatch[0] : '';
};
/**
* @param {!goog.tweak.BaseEntry} entry The entry.
* @return {string} Returns the part of the label after the last period, unless
* the label has been explicly set (it is different from the ID).
* @private
*/
goog.tweak.TweakUi.getNamespacedLabel_ = function(entry) {
var label = entry.label;
if (label == entry.getId()) {
label = label.substr(label.lastIndexOf('.') + 1);
}
return label;
};
/**
* @return {!Element} The root element. Must not be called before render().
*/
goog.tweak.TweakUi.prototype.getRootElement = function() {
goog.asserts.assert(this.entriesPanel_,
'TweakUi.getRootElement called before render().');
return this.entriesPanel_.getRootElement();
};
/**
* Reloads the page with query parameters set by the UI.
* @private
*/
goog.tweak.TweakUi.prototype.restartWithAppliedTweaks_ = function() {
var queryString = this.registry_.makeUrlQuery();
var wnd = this.domHelper_.getWindow();
if (queryString != wnd.location.search) {
wnd.location.search = queryString;
} else {
wnd.location.reload();
}
};
/**
* Installs the required CSS styles.
* @private
*/
goog.tweak.TweakUi.prototype.installStyles_ = function() {
// Use an marker to install the styles only once per document.
// Styles are injected via JS instead of in a separate style sheet so that
// they are automatically excluded when tweaks are stripped out.
var doc = this.domHelper_.getDocument();
if (!(goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ in doc)) {
goog.style.installStyles(
goog.tweak.TweakUi.CSS_STYLES_, doc);
doc[goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_] = true;
}
};
/**
* Creates the element to display when the UI is visible.
* @return {!Element} The root element.
*/
goog.tweak.TweakUi.prototype.render = function() {
this.installStyles_();
var dh = this.domHelper_;
// The submit button
var submitButton = dh.createDom('button', {style: 'font-weight:bold'},
'Apply Tweaks');
submitButton.onclick = goog.bind(this.restartWithAppliedTweaks_, this);
var rootPanel = new goog.tweak.EntriesPanel([], dh);
var rootPanelDiv = rootPanel.render(submitButton);
rootPanelDiv.className += ' ' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
this.entriesPanel_ = rootPanel;
var entries = this.registry_.extractEntries(true /* excludeChildEntries */,
false /* excludeNonSettings */);
for (var i = 0, entry; entry = entries[i]; i++) {
this.insertEntry_(entry);
}
return rootPanelDiv;
};
/**
* Updates the UI with the given entry.
* @param {!goog.tweak.BaseEntry} entry The newly registered entry.
* @private
*/
goog.tweak.TweakUi.prototype.onNewRegisteredEntry_ = function(entry) {
if (this.entriesPanel_) {
this.insertEntry_(entry);
}
};
/**
* Updates the UI with the given entry.
* @param {!goog.tweak.BaseEntry} entry The newly registered entry.
* @private
*/
goog.tweak.TweakUi.prototype.insertEntry_ = function(entry) {
var panel = this.entriesPanel_;
var namespace = goog.tweak.TweakUi.extractNamespace_(entry);
if (namespace) {
// Find the NamespaceEntry that the entry belongs to.
var namespaceEntryId = goog.tweak.NamespaceEntry_.ID_PREFIX + namespace;
var nsPanel = panel.childPanels[namespaceEntryId];
if (nsPanel) {
panel = nsPanel;
} else {
entry = new goog.tweak.NamespaceEntry_(namespace, [entry]);
}
}
if (entry instanceof goog.tweak.BooleanInGroupSetting) {
var group = entry.getGroup();
// BooleanGroup entries are always registered before their
// BooleanInGroupSettings.
panel = panel.childPanels[group.getId()];
}
goog.asserts.assert(panel, 'Missing panel for entry %s', entry.getId());
panel.insertEntry(entry);
};
/**
* The body of the tweaks UI and also used for BooleanGroup.
* @param {!Array<!goog.tweak.BaseEntry>} entries The entries to show in the
* panel.
* @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
* @constructor
* @final
*/
goog.tweak.EntriesPanel = function(entries, opt_domHelper) {
/**
* The entries to show in the panel.
* @type {!Array<!goog.tweak.BaseEntry>} entries
* @private
*/
this.entries_ = entries;
var self = this;
/**
* The bound onclick handler for the help question marks.
* @this {Element}
* @private
*/
this.boundHelpOnClickHandler_ = function() {
self.onHelpClick_(this.parentNode);
};
/**
* The element that contains the UI.
* @type {Element}
* @private
*/
this.rootElem_;
/**
* The element that contains all of the settings and the endElement.
* @type {Element}
* @private
*/
this.mainPanel_;
/**
* Flips between true/false each time the "Toggle Descriptions" link is
* clicked.
* @type {boolean}
* @private
*/
this.showAllDescriptionsState_;
/**
* The DomHelper to render with.
* @type {!goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
/**
* Map of tweak ID -> EntriesPanel for child panels (BooleanGroups).
* @type {!Object<!goog.tweak.EntriesPanel>}
*/
this.childPanels = {};
};
/**
* @return {!Element} Returns the expanded element. Must not be called before
* render().
*/
goog.tweak.EntriesPanel.prototype.getRootElement = function() {
goog.asserts.assert(this.rootElem_,
'EntriesPanel.getRootElement called before render().');
return /** @type {!Element} */ (this.rootElem_);
};
/**
* Creates and returns the expanded element.
* The markup looks like:
* <div>
* <a>Show Descriptions</a>
* <div>
* ...
* {endElement}
* </div>
* </div>
* @param {Element|DocumentFragment=} opt_endElement Element to insert after all
* tweak entries.
* @return {!Element} The root element for the panel.
*/
goog.tweak.EntriesPanel.prototype.render = function(opt_endElement) {
var dh = this.domHelper_;
var entries = this.entries_;
var ret = dh.createDom('div');
var showAllDescriptionsLink = dh.createDom('a', {
href: 'javascript:;',
onclick: goog.bind(this.toggleAllDescriptions, this)
}, 'Toggle all Descriptions');
ret.appendChild(showAllDescriptionsLink);
// Add all of the entries.
var mainPanel = dh.createElement('div');
this.mainPanel_ = mainPanel;
for (var i = 0, entry; entry = entries[i]; i++) {
mainPanel.appendChild(this.createEntryElem_(entry));
}
if (opt_endElement) {
mainPanel.appendChild(opt_endElement);
}
ret.appendChild(mainPanel);
this.rootElem_ = ret;
return /** @type {!Element} */ (ret);
};
/**
* Inserts the given entry into the panel.
* @param {!goog.tweak.BaseEntry} entry The entry to insert.
*/
goog.tweak.EntriesPanel.prototype.insertEntry = function(entry) {
var insertIndex = -goog.array.binarySearch(this.entries_, entry,
goog.tweak.TweakUi.entryCompare_) - 1;
goog.asserts.assert(insertIndex >= 0, 'insertEntry failed for %s',
entry.getId());
goog.array.insertAt(this.entries_, entry, insertIndex);
this.mainPanel_.insertBefore(
this.createEntryElem_(entry),
// IE doesn't like 'undefined' here.
this.mainPanel_.childNodes[insertIndex] || null);
};
/**
* Creates and returns a form element for the given entry.
* @param {!goog.tweak.BaseEntry} entry The entry.
* @return {!Element} The root DOM element for the entry.
* @private
*/
goog.tweak.EntriesPanel.prototype.createEntryElem_ = function(entry) {
var dh = this.domHelper_;
var isGroupEntry = goog.tweak.TweakUi.isGroupEntry_(entry);
var classes = isGroupEntry ? goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ :
goog.tweak.TweakUi.ENTRY_CSS_CLASSES_;
// Containers should not use label tags or else all descendent inputs will be
// connected on desktop browsers.
var containerNodeName = isGroupEntry ? 'span' : 'label';
var ret = dh.createDom('div', classes,
dh.createDom(containerNodeName, {
// Make the hover text the description.
title: entry.description,
style: 'color:' + (entry.isRestartRequired() ? '' : 'blue')
}, this.createTweakEntryDom_(entry)),
// Add the expandable help question mark.
this.createHelpElem_(entry));
return ret;
};
/**
* Click handler for the help link.
* @param {Node} entryDiv The div that contains the tweak.
* @private
*/
goog.tweak.EntriesPanel.prototype.onHelpClick_ = function(entryDiv) {
this.showDescription_(entryDiv, !entryDiv.style.display);
};
/**
* Twiddle the DOM so that the entry within the given span is shown/hidden.
* @param {Node} entryDiv The div that contains the tweak.
* @param {boolean} show True to show, false to hide.
* @private
*/
goog.tweak.EntriesPanel.prototype.showDescription_ =
function(entryDiv, show) {
var descriptionElem = entryDiv.lastChild.lastChild;
goog.style.setElementShown(/** @type {Element} */ (descriptionElem), show);
entryDiv.style.display = show ? 'block' : '';
};
/**
* Creates and returns a help element for the given entry.
* @param {goog.tweak.BaseEntry} entry The entry.
* @return {!Element} The root element of the created DOM.
* @private
*/
goog.tweak.EntriesPanel.prototype.createHelpElem_ = function(entry) {
// The markup looks like:
// <span onclick=...><b>?</b><span>{description}</span></span>
var ret = this.domHelper_.createElement('span');
ret.innerHTML = '<b style="padding:0 1em 0 .5em">?</b>' +
'<span style="display:none;color:#666"></span>';
ret.onclick = this.boundHelpOnClickHandler_;
var descriptionElem = ret.lastChild;
goog.dom.setTextContent(/** @type {Element} */ (descriptionElem),
entry.description);
if (!entry.isRestartRequired()) {
descriptionElem.innerHTML +=
' <span style="color:blue">(no restart required)</span>';
}
return ret;
};
/**
* Show all entry descriptions (has the same effect as clicking on all ?'s).
*/
goog.tweak.EntriesPanel.prototype.toggleAllDescriptions = function() {
var show = !this.showAllDescriptionsState_;
this.showAllDescriptionsState_ = show;
var entryDivs = this.domHelper_.getElementsByTagNameAndClass('div',
goog.tweak.TweakUi.ENTRY_CSS_CLASS_, this.rootElem_);
for (var i = 0, div; div = entryDivs[i]; i++) {
this.showDescription_(div, show);
}
};
/**
* Creates the DOM element to control the given enum setting.
* @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
* setting.
* @param {string} label The label for the entry.
* @param {!Function} onchangeFunc onchange event handler.
* @return {!DocumentFragment} The DOM element.
* @private
*/
goog.tweak.EntriesPanel.prototype.createComboBoxDom_ =
function(tweak, label, onchangeFunc) {
// The markup looks like:
// Label: <select><option></option></select>
var dh = this.domHelper_;
var ret = dh.getDocument().createDocumentFragment();
ret.appendChild(dh.createTextNode(label + ': '));
var selectElem = dh.createElement('select');
var values = tweak.getValidValues();
for (var i = 0, il = values.length; i < il; ++i) {
var optionElem = dh.createElement('option');
optionElem.text = String(values[i]);
// Setting the option tag's value is required for selectElem.value to work
// properly.
optionElem.value = String(values[i]);
selectElem.appendChild(optionElem);
}
ret.appendChild(selectElem);
// Set the value and add a callback.
selectElem.value = tweak.getNewValue();
selectElem.onchange = onchangeFunc;
tweak.addCallback(function() {
selectElem.value = tweak.getNewValue();
});
return ret;
};
/**
* Creates the DOM element to control the given boolean setting.
* @param {!goog.tweak.BooleanSetting} tweak The setting.
* @param {string} label The label for the entry.
* @return {!DocumentFragment} The DOM elements.
* @private
*/
goog.tweak.EntriesPanel.prototype.createBooleanSettingDom_ =
function(tweak, label) {
var dh = this.domHelper_;
var ret = dh.getDocument().createDocumentFragment();
var checkbox = dh.createDom('input', {type: 'checkbox'});
ret.appendChild(checkbox);
ret.appendChild(dh.createTextNode(label));
// Needed on IE6 to ensure the textbox doesn't get cleared
// when added to the DOM.
checkbox.defaultChecked = tweak.getNewValue();
checkbox.checked = tweak.getNewValue();
checkbox.onchange = function() {
tweak.setValue(checkbox.checked);
};
tweak.addCallback(function() {
checkbox.checked = tweak.getNewValue();
});
return ret;
};
/**
* Creates the DOM for a BooleanGroup or NamespaceEntry.
* @param {!goog.tweak.BooleanGroup|!goog.tweak.NamespaceEntry_} entry The
* entry.
* @param {string} label The label for the entry.
* @param {!Array<goog.tweak.BaseEntry>} childEntries The child entries.
* @return {!DocumentFragment} The DOM element.
* @private
*/
goog.tweak.EntriesPanel.prototype.createSubPanelDom_ = function(entry, label,
childEntries) {
var dh = this.domHelper_;
var toggleLink = dh.createDom('a', {href: 'javascript:;'},
label + ' \xBB');
var toggleLink2 = dh.createDom('a', {href: 'javascript:;'},
'\xAB ' + label);
toggleLink2.style.marginRight = '10px';
var innerUi = new goog.tweak.EntriesPanel(childEntries, dh);
this.childPanels[entry.getId()] = innerUi;
var elem = innerUi.render();
// Move the toggle descriptions link into the legend.
var descriptionsLink = elem.firstChild;
var childrenElem = dh.createDom('fieldset',
goog.getCssName('goog-inline-block'),
dh.createDom('legend', null, toggleLink2, descriptionsLink),
elem);
new goog.ui.Zippy(toggleLink, childrenElem, false /* expanded */,
toggleLink2);
var ret = dh.getDocument().createDocumentFragment();
ret.appendChild(toggleLink);
ret.appendChild(childrenElem);
return ret;
};
/**
* Creates the DOM element to control the given string setting.
* @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
* setting.
* @param {string} label The label for the entry.
* @param {!Function} onchangeFunc onchange event handler.
* @return {!DocumentFragment} The DOM element.
* @private
*/
goog.tweak.EntriesPanel.prototype.createTextBoxDom_ =
function(tweak, label, onchangeFunc) {
var dh = this.domHelper_;
var ret = dh.getDocument().createDocumentFragment();
ret.appendChild(dh.createTextNode(label + ': '));
var textBox = dh.createDom('input', {
value: tweak.getNewValue(),
// TODO(agrieve): Make size configurable or autogrow.
size: 5,
onblur: onchangeFunc
});
ret.appendChild(textBox);
tweak.addCallback(function() {
textBox.value = tweak.getNewValue();
});
return ret;
};
/**
* Creates the DOM element to control the given button action.
* @param {!goog.tweak.ButtonAction} tweak The action.
* @param {string} label The label for the entry.
* @return {!Element} The DOM element.
* @private
*/
goog.tweak.EntriesPanel.prototype.createButtonActionDom_ =
function(tweak, label) {
return this.domHelper_.createDom('button', {
onclick: goog.bind(tweak.fireCallbacks, tweak)
}, label);
};
/**
* Creates the DOM element to control the given entry.
* @param {!goog.tweak.BaseEntry} entry The entry.
* @return {!Element|!DocumentFragment} The DOM element.
* @private
*/
goog.tweak.EntriesPanel.prototype.createTweakEntryDom_ = function(entry) {
var label = goog.tweak.TweakUi.getNamespacedLabel_(entry);
if (entry instanceof goog.tweak.BooleanSetting) {
return this.createBooleanSettingDom_(entry, label);
} else if (entry instanceof goog.tweak.BooleanGroup) {
var childEntries = goog.tweak.TweakUi.extractBooleanGroupEntries_(entry);
return this.createSubPanelDom_(entry, label, childEntries);
} else if (entry instanceof goog.tweak.StringSetting) {
/** @this {Element} */
var setValueFunc = function() {
entry.setValue(this.value);
};
return entry.getValidValues() ?
this.createComboBoxDom_(entry, label, setValueFunc) :
this.createTextBoxDom_(entry, label, setValueFunc);
} else if (entry instanceof goog.tweak.NumericSetting) {
setValueFunc = function() {
// Reset the value if it's not a number.
if (isNaN(this.value)) {
this.value = entry.getNewValue();
} else {
entry.setValue(+this.value);
}
};
return entry.getValidValues() ?
this.createComboBoxDom_(entry, label, setValueFunc) :
this.createTextBoxDom_(entry, label, setValueFunc);
} else if (entry instanceof goog.tweak.NamespaceEntry_) {
return this.createSubPanelDom_(entry, entry.label, entry.entries);
}
goog.asserts.assertInstanceof(entry, goog.tweak.ButtonAction,
'invalid entry: %s', entry);
return this.createButtonActionDom_(
/** @type {!goog.tweak.ButtonAction} */ (entry), label);
};
/**
* Entries used to represent the collapsible namespace links. These entries are
* never registered with the TweakRegistry, but are contained within the
* collection of entries within TweakPanels.
* @param {string} namespace The namespace for the entry.
* @param {!Array<!goog.tweak.BaseEntry>} entries Entries within the namespace.
* @constructor
* @extends {goog.tweak.BaseEntry}
* @private
*/
goog.tweak.NamespaceEntry_ = function(namespace, entries) {
goog.tweak.BaseEntry.call(this,
goog.tweak.NamespaceEntry_.ID_PREFIX + namespace,
'Tweaks within the ' + namespace + ' namespace.');
/**
* Entries within this namespace.
* @type {!Array<!goog.tweak.BaseEntry>}
*/
this.entries = entries;
this.label = namespace;
};
goog.inherits(goog.tweak.NamespaceEntry_, goog.tweak.BaseEntry);
/**
* Prefix for the IDs of namespace entries used to ensure that they do not
* conflict with regular entries.
* @type {string}
*/
goog.tweak.NamespaceEntry_.ID_PREFIX = '!';
@@ -0,0 +1,27 @@
<!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.
Author: agrieve@google.com (Andrew Grieve)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.tweak.TweakUi
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.tweak.TweakUiTest');
</script>
<link rel="stylesheet" type="text/css" href="../css/common.css" />
</head>
<body>
<div id="root">
</div>
</body>
</html>
@@ -0,0 +1,275 @@
// 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.tweak.TweakUiTest');
goog.setTestOnly('goog.tweak.TweakUiTest');
goog.require('goog.dom');
goog.require('goog.string');
goog.require('goog.testing.jsunit');
goog.require('goog.tweak');
goog.require('goog.tweak.TweakUi');
/** @suppress {extraRequire} needed for createRegistryEntries. */
goog.require('goog.tweak.testhelpers');
var root;
var registry;
var EXPECTED_ENTRIES_COUNT = 14;
function setUp() {
root = document.getElementById('root');
// Make both test cases use the same entries in order to be able to test that
// having two UIs on the same page does not cause trouble.
createRegistryEntries('');
registry = goog.tweak.getRegistry();
}
function tearDown() {
goog.tweak.activeBooleanGroup_ = null;
// When debugging a single test, don't clear out the DOM.
if (window.location.search.indexOf('runTests') == -1) {
root.innerHTML = '';
}
}
function tearDownPage() {
// When debugging a single test, don't clear out the DOM.
if (window.location.search.indexOf('runTests') != -1) {
return;
}
// Create both registries for interactive testing.
createRegistryEntries('');
registry = goog.tweak.getRegistry();
// Add an extra tweak for testing the creation of tweaks after the UI has
// already been rendered.
var entryCounter = 0;
goog.tweak.registerButton('CreateNewTweak', 'Creates a new tweak. Meant ' +
'to simulate a tweak being registered in a lazy-loaded module.',
function() {
goog.tweak.registerBoolean(
'Lazy' + ++entryCounter, 'Lazy-loaded tweak.');
});
goog.tweak.registerButton('CreateNewTweakInNamespace1',
'Creates a new tweak within a namespace. Meant to simulate a tweak ' +
'being registered in a lazy-loaded module.', function() {
goog.tweak.registerString(
'foo.bar.Lazy' + ++entryCounter, 'Lazy-loaded tweak.');
});
goog.tweak.registerButton('CreateNewTweakInNamespace2',
'Creates a new tweak within a namespace. Meant to simulate a tweak ' +
'being registered in a lazy-loaded module.', function() {
goog.tweak.registerNumber(
'foo.bar.baz.Lazy' + ++entryCounter, 'Lazy combo', 3,
{validValues: [1, 2, 3], label: 'Lazy!'});
});
var label = document.createElement('h3');
label.innerHTML = 'TweakUi:';
root.appendChild(label);
createUi(false);
label = document.createElement('h3');
label.innerHTML = 'Collapsible:';
root.appendChild(label);
createUi(true);
}
function createUi(collapsible) {
var tweakUiElem = collapsible ? goog.tweak.TweakUi.createCollapsible() :
goog.tweak.TweakUi.create();
root.appendChild(tweakUiElem);
}
function getAllEntryDivs() {
return goog.dom.getElementsByTagNameAndClass('div',
goog.tweak.TweakUi.ENTRY_CSS_CLASS_);
}
function getEntryDiv(entry) {
var label = goog.tweak.TweakUi.getNamespacedLabel_(entry);
var allDivs = getAllEntryDivs();
var ret;
for (var i = 0, div; div = allDivs[i]; i++) {
var divText = goog.dom.getTextContent(div);
if (goog.string.startsWith(divText, label) &&
goog.string.contains(divText, entry.description)) {
assertFalse('Found multiple divs matching entry ' + entry.getId(), !!ret);
ret = div;
}
}
assertTrue('getEntryDiv failed for ' + entry.getId(), !!ret);
return ret;
}
function getEntryInput(entry) {
var div = getEntryDiv(entry);
return div.getElementsByTagName('input')[0] ||
div.getElementsByTagName('select')[0];
}
function testCreate() {
createUi(false);
assertEquals('Wrong number of entry divs.', EXPECTED_ENTRIES_COUNT,
getAllEntryDivs().length);
assertFalse('checkbox should not be checked 1',
getEntryInput(boolEntry).checked);
assertTrue('checkbox should be checked 2',
getEntryInput(boolEntry2).checked);
// Enusre custom labels are being used.
var html = document.getElementsByTagName('button')[0].innerHTML;
assertTrue('Button label is wrong', html.indexOf('&lt;btn&gt;') > -1);
html = getEntryDiv(numEnumEntry).innerHTML;
assertTrue('Enum2 label is wrong', html.indexOf('second&amp;') > -1);
}
function testToggleBooleanSetting() {
boolEntry.setValue(true);
createUi(false);
assertTrue('checkbox should be checked',
getEntryInput(boolEntry).checked);
boolEntry.setValue(false);
assertFalse('checkbox should not be checked 1',
getEntryInput(boolEntry).checked);
}
function testToggleStringSetting() {
strEntry.setValue('val1');
createUi(false);
assertEquals('Textbox has wrong value 1',
'val1', getEntryInput(strEntry).value);
strEntry.setValue('val2');
assertEquals('Textbox has wrong value 2',
'val2', getEntryInput(strEntry).value);
}
function testToggleStringEnumSetting() {
strEnumEntry.setValue('B');
createUi(false);
assertEquals('wrong value 1',
'B', getEntryInput(strEnumEntry).value);
strEnumEntry.setValue('C');
assertEquals('wrong value 2',
'C', getEntryInput(strEnumEntry).value);
}
function testToggleNumericSetting() {
numEntry.setValue(3);
createUi(false);
assertEquals('wrong value 1',
'3', getEntryInput(numEntry).value);
numEntry.setValue(4);
assertEquals('wrong value 2',
'4', getEntryInput(numEntry).value);
}
function testToggleNumericEnumSetting() {
numEnumEntry.setValue(2);
createUi(false);
assertEquals('wrong value 1',
'2', getEntryInput(numEnumEntry).value);
numEnumEntry.setValue(3);
assertEquals('wrong value 2',
'3', getEntryInput(numEnumEntry).value);
}
function testClickBooleanSetting() {
createUi(false);
var input = getEntryInput(boolEntry);
input.checked = true;
input.onchange();
assertTrue('setting should be true', boolEntry.getNewValue());
input.checked = false;
input.onchange();
assertFalse('setting should be false', boolEntry.getNewValue());
}
function testToggleDescriptions() {
createUi(false);
var toggleLink = root.getElementsByTagName('a')[0];
var heightBefore = root.offsetHeight;
toggleLink.onclick();
assertTrue('Expected div height to grow from toggle descriptions.',
root.offsetHeight > heightBefore);
toggleLink.onclick();
assertEquals('Expected div height to revert from toggle descriptions.',
heightBefore, root.offsetHeight);
}
function assertEntryOrder(entryId1, entryId2) {
var entry1 = registry.getEntry(entryId1);
var entry2 = registry.getEntry(entryId2);
var div1 = getEntryDiv(entry1);
var div2 = getEntryDiv(entry2);
var order = goog.dom.compareNodeOrder(div1, div2);
assertTrue(entry1.getId() + ' should be before ' + entry2.getId(), order < 0);
}
function testAddEntry() {
createUi(false);
goog.tweak.registerBoolean('Lazy1', 'Lazy-loaded tweak.');
goog.tweak.registerBoolean('Lazy2', 'Lazy-loaded tweak.',
/* defaultValue */ false, { restartRequired: false });
goog.tweak.beginBooleanGroup('LazyGroup', 'Lazy-loaded tweak.');
goog.tweak.registerBoolean('Lazy3', 'Lazy-loaded tweak.');
goog.tweak.endBooleanGroup();
assertEquals('Wrong number of entry divs.', EXPECTED_ENTRIES_COUNT + 4,
getAllEntryDivs().length);
assertEntryOrder('Enum2', 'Lazy1');
assertEntryOrder('Lazy1', 'Lazy2');
assertEntryOrder('Lazy2', 'Num');
assertEntryOrder('BoolGroup', 'Lazy3');
}
function testAddNamespacedEntries() {
createUi(false);
goog.tweak.beginBooleanGroup('NS.LazyGroup', 'Lazy-loaded tweak.');
goog.tweak.registerBoolean('NS.InGroup', 'Lazy-loaded tweak.');
goog.tweak.endBooleanGroup();
goog.tweak.registerBoolean('NS.Banana', 'Lazy-loaded tweak.');
goog.tweak.registerBoolean('NS.Apple', 'Lazy-loaded tweak.');
assertEquals('Wrong number of entry divs.', EXPECTED_ENTRIES_COUNT + 5,
getAllEntryDivs().length);
assertEntryOrder('Enum2', 'NS.Apple');
assertEntryOrder('NS.Apple', 'NS.Banana');
assertEntryOrder('NS.Banana', 'NS.InGroup');
}
function testCollapsibleIsLazy() {
if (document.createEvent) {
createUi(true);
assertEquals('Expected no entry divs.', 0, getAllEntryDivs().length);
var showLink = root.getElementsByTagName('a')[0];
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
showLink.dispatchEvent(event);
assertEquals('Wrong number of entry divs.', EXPECTED_ENTRIES_COUNT,
getAllEntryDivs().length);
}
}