// 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: * *