// 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.dom.TagName'); goog.require('goog.dom.safe'); goog.require('goog.html.SafeHtml'); goog.require('goog.html.SafeStyleSheet'); goog.require('goog.object'); goog.require('goog.string.Const'); 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 {!goog.html.SafeStyleSheet} * @private */ goog.tweak.TweakUi.CSS_STYLES_ = (function() { var MOBILE = goog.userAgent.MOBILE; var IE = goog.userAgent.IE; var ROOT_PANEL_CLASS = '.' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_; var GOOG_INLINE_BLOCK_CLASS = '.' + goog.getCssName('goog-inline-block'); var ret = [goog.html.SafeStyleSheet.createRule( ROOT_PANEL_CLASS, {'background': '#ffc', 'padding': '0 4px'})]; // Make this work even if the user hasn't included common.css. if (!IE) { ret.push(goog.html.SafeStyleSheet.createRule( GOOG_INLINE_BLOCK_CLASS, {'display': 'inline-block'})); } // Space things out vertically for touch UIs. if (MOBILE) { ret.push(goog.html.SafeStyleSheet.createRule( ROOT_PANEL_CLASS + ',' + ROOT_PANEL_CLASS + ' fieldset', {'line-height': '2em'})); } return goog.html.SafeStyleSheet.concat(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(goog.dom.TagName.A, {href: 'javascript:;'}, 'Show Tweaks'); var hideLink = dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, 'Hide Tweaks'); var ret = dh.createDom(goog.dom.TagName.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} 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.installSafeStyleSheet(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( goog.dom.TagName.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} 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} 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} */ 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: * *
* Show Descriptions *
* ... * {endElement} *
*
* * @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(goog.dom.TagName.DIV); var showAllDescriptionsLink = dh.createDom( goog.dom.TagName.A, { href: 'javascript:;', onclick: goog.bind(this.toggleAllDescriptions, this) }, 'Toggle all Descriptions'); ret.appendChild(showAllDescriptionsLink); // Add all of the entries. var mainPanel = dh.createElement(goog.dom.TagName.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 ? goog.dom.TagName.SPAN : goog.dom.TagName.LABEL; var ret = dh.createDom( goog.dom.TagName.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: // ?{description} var ret = this.domHelper_.createElement(goog.dom.TagName.SPAN); goog.dom.safe.setInnerHtml( ret, goog.html.SafeHtml.concat( goog.html.SafeHtml.create( 'b', {'style': goog.string.Const.from('padding:0 1em 0 .5em')}, '?'), goog.html.SafeHtml.create( 'span', {'style': goog.string.Const.from('display:none;color:#666')}))); ret.onclick = this.boundHelpOnClickHandler_; // IE<9 doesn't support lastElementChild. var descriptionElem = /** @type {!Element} */ (ret.lastChild); if (entry.isRestartRequired()) { goog.dom.setTextContent(descriptionElem, entry.description); } else { goog.dom.safe.setInnerHtml( descriptionElem, goog.html.SafeHtml.concat( goog.html.SafeHtml.htmlEscape(entry.description), goog.html.SafeHtml.create( 'span', {'style': goog.string.Const.from('color: blue')}, '(no restart required)'))); } 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( goog.dom.TagName.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: var dh = this.domHelper_; var ret = dh.getDocument().createDocumentFragment(); ret.appendChild(dh.createTextNode(label + ': ')); var selectElem = dh.createElement(goog.dom.TagName.SELECT); var values = tweak.getValidValues(); for (var i = 0, il = values.length; i < il; ++i) { var optionElem = dh.createElement(goog.dom.TagName.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 = String(tweak.getNewValue()); selectElem.onchange = onchangeFunc; tweak.addCallback(function() { selectElem.value = String(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(goog.dom.TagName.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} 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(goog.dom.TagName.A, {href: 'javascript:;'}, label + ' \xBB'); var toggleLink2 = dh.createDom(goog.dom.TagName.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( goog.dom.TagName.FIELDSET, goog.getCssName('goog-inline-block'), dh.createDom( goog.dom.TagName.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(goog.dom.TagName.INPUT, { value: String(tweak.getNewValue()), // TODO(agrieve): Make size configurable or autogrow. size: 5, onblur: onchangeFunc }); ret.appendChild(textBox); tweak.addCallback(function() { textBox.value = String(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( goog.dom.TagName.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} 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} */ 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 = '!';