// 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} 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} * @private */ this.entryMap_ = {}; /** * The map of query params to use when initializing entry settings. * @type {!Object} * @private */ this.parsedQueryParams_ = goog.tweak.Registry.parseQueryParams(queryParams); /** * List of callbacks to call when a new entry is registered. * @type {!Array} * @private */ this.onRegisterListeners_ = []; /** * A map of entry ID -> default value override for overrides set by the * compiler. * @type {!Object} * @private */ this.compilerDefaultValueOverrides_ = compilerOverrides; /** * A map of entry ID -> default value override for overrides set by * goog.tweak.overrideDefaultValue(). * @type {!Object} * @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} 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} 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; };