tweakui.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. // Copyright 2009 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview A UI for editing tweak settings / clicking tweak actions.
  16. *
  17. * @author agrieve@google.com (Andrew Grieve)
  18. */
  19. goog.provide('goog.tweak.EntriesPanel');
  20. goog.provide('goog.tweak.TweakUi');
  21. goog.require('goog.array');
  22. goog.require('goog.asserts');
  23. goog.require('goog.dom');
  24. goog.require('goog.dom.TagName');
  25. goog.require('goog.dom.safe');
  26. goog.require('goog.html.SafeHtml');
  27. goog.require('goog.html.SafeStyleSheet');
  28. goog.require('goog.object');
  29. goog.require('goog.string.Const');
  30. goog.require('goog.style');
  31. goog.require('goog.tweak');
  32. goog.require('goog.tweak.BaseEntry');
  33. goog.require('goog.tweak.BooleanGroup');
  34. goog.require('goog.tweak.BooleanInGroupSetting');
  35. goog.require('goog.tweak.BooleanSetting');
  36. goog.require('goog.tweak.ButtonAction');
  37. goog.require('goog.tweak.NumericSetting');
  38. goog.require('goog.tweak.StringSetting');
  39. goog.require('goog.ui.Zippy');
  40. goog.require('goog.userAgent');
  41. /**
  42. * A UI for editing tweak settings / clicking tweak actions.
  43. * @param {!goog.tweak.Registry} registry The registry to render.
  44. * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
  45. * @constructor
  46. * @final
  47. */
  48. goog.tweak.TweakUi = function(registry, opt_domHelper) {
  49. /**
  50. * The registry to create a UI from.
  51. * @type {!goog.tweak.Registry}
  52. * @private
  53. */
  54. this.registry_ = registry;
  55. /**
  56. * The element to display when the UI is visible.
  57. * @type {goog.tweak.EntriesPanel|undefined}
  58. * @private
  59. */
  60. this.entriesPanel_;
  61. /**
  62. * The DomHelper to render with.
  63. * @type {!goog.dom.DomHelper}
  64. * @private
  65. */
  66. this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
  67. // Listen for newly registered entries (happens with lazy-loaded modules).
  68. registry.addOnRegisterListener(goog.bind(this.onNewRegisteredEntry_, this));
  69. };
  70. /**
  71. * The CSS class name unique to the root tweak panel div.
  72. * @type {string}
  73. * @private
  74. */
  75. goog.tweak.TweakUi.ROOT_PANEL_CLASS_ = goog.getCssName('goog-tweak-root');
  76. /**
  77. * The CSS class name unique to the tweak entry div.
  78. * @type {string}
  79. * @private
  80. */
  81. goog.tweak.TweakUi.ENTRY_CSS_CLASS_ = goog.getCssName('goog-tweak-entry');
  82. /**
  83. * The CSS classes for each tweak entry div.
  84. * @type {string}
  85. * @private
  86. */
  87. goog.tweak.TweakUi.ENTRY_CSS_CLASSES_ = goog.tweak.TweakUi.ENTRY_CSS_CLASS_ +
  88. ' ' + goog.getCssName('goog-inline-block');
  89. /**
  90. * The CSS classes for each namespace tweak entry div.
  91. * @type {string}
  92. * @private
  93. */
  94. goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ =
  95. goog.tweak.TweakUi.ENTRY_CSS_CLASS_;
  96. /**
  97. * Marker that the style sheet has already been installed.
  98. * @type {string}
  99. * @private
  100. */
  101. goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ = '__closure_tweak_installed_';
  102. /**
  103. * CSS used by TweakUI.
  104. * @type {!goog.html.SafeStyleSheet}
  105. * @private
  106. */
  107. goog.tweak.TweakUi.CSS_STYLES_ = (function() {
  108. var MOBILE = goog.userAgent.MOBILE;
  109. var IE = goog.userAgent.IE;
  110. var ROOT_PANEL_CLASS = '.' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
  111. var GOOG_INLINE_BLOCK_CLASS = '.' + goog.getCssName('goog-inline-block');
  112. var ret = [goog.html.SafeStyleSheet.createRule(
  113. ROOT_PANEL_CLASS, {'background': '#ffc', 'padding': '0 4px'})];
  114. // Make this work even if the user hasn't included common.css.
  115. if (!IE) {
  116. ret.push(goog.html.SafeStyleSheet.createRule(
  117. GOOG_INLINE_BLOCK_CLASS, {'display': 'inline-block'}));
  118. }
  119. // Space things out vertically for touch UIs.
  120. if (MOBILE) {
  121. ret.push(goog.html.SafeStyleSheet.createRule(
  122. ROOT_PANEL_CLASS + ',' + ROOT_PANEL_CLASS + ' fieldset',
  123. {'line-height': '2em'}));
  124. }
  125. return goog.html.SafeStyleSheet.concat(ret);
  126. })();
  127. /**
  128. * Creates a TweakUi if tweaks are enabled.
  129. * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
  130. * @return {!Element|undefined} The root UI element or undefined if tweaks are
  131. * not enabled.
  132. */
  133. goog.tweak.TweakUi.create = function(opt_domHelper) {
  134. var registry = goog.tweak.getRegistry();
  135. if (registry) {
  136. var ui = new goog.tweak.TweakUi(registry, opt_domHelper);
  137. ui.render();
  138. return ui.getRootElement();
  139. }
  140. };
  141. /**
  142. * Creates a TweakUi inside of a show/hide link.
  143. * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
  144. * @return {!Element|undefined} The root UI element or undefined if tweaks are
  145. * not enabled.
  146. */
  147. goog.tweak.TweakUi.createCollapsible = function(opt_domHelper) {
  148. var registry = goog.tweak.getRegistry();
  149. if (registry) {
  150. var dh = opt_domHelper || goog.dom.getDomHelper();
  151. // The following strings are for internal debugging only. No translation
  152. // necessary. Do NOT wrap goog.getMsg() around these strings.
  153. var showLink =
  154. dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, 'Show Tweaks');
  155. var hideLink =
  156. dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, 'Hide Tweaks');
  157. var ret = dh.createDom(goog.dom.TagName.DIV, null, showLink);
  158. var lazyCreate = function() {
  159. // Lazily render the UI.
  160. var ui = new goog.tweak.TweakUi(
  161. /** @type {!goog.tweak.Registry} */ (registry), dh);
  162. ui.render();
  163. // Put the hide link on the same line as the "Show Descriptions" link.
  164. // Set the style lazily because we can.
  165. hideLink.style.marginRight = '10px';
  166. var tweakElem = ui.getRootElement();
  167. tweakElem.insertBefore(hideLink, tweakElem.firstChild);
  168. ret.appendChild(tweakElem);
  169. return tweakElem;
  170. };
  171. new goog.ui.Zippy(showLink, lazyCreate, false /* expanded */, hideLink);
  172. return ret;
  173. }
  174. };
  175. /**
  176. * Compares the given entries. Orders alphabetically and groups buttons and
  177. * expandable groups.
  178. * @param {!goog.tweak.BaseEntry} a The first entry to compare.
  179. * @param {!goog.tweak.BaseEntry} b The second entry to compare.
  180. * @return {number} Refer to goog.array.defaultCompare.
  181. * @private
  182. */
  183. goog.tweak.TweakUi.entryCompare_ = function(a, b) {
  184. return (
  185. goog.array.defaultCompare(
  186. a instanceof goog.tweak.NamespaceEntry_,
  187. b instanceof goog.tweak.NamespaceEntry_) ||
  188. goog.array.defaultCompare(
  189. a instanceof goog.tweak.BooleanGroup,
  190. b instanceof goog.tweak.BooleanGroup) ||
  191. goog.array.defaultCompare(
  192. a instanceof goog.tweak.ButtonAction,
  193. b instanceof goog.tweak.ButtonAction) ||
  194. goog.array.defaultCompare(a.label, b.label) ||
  195. goog.array.defaultCompare(a.getId(), b.getId()));
  196. };
  197. /**
  198. * @param {!goog.tweak.BaseEntry} entry The entry.
  199. * @return {boolean} Returns whether the given entry contains sub-entries.
  200. * @private
  201. */
  202. goog.tweak.TweakUi.isGroupEntry_ = function(entry) {
  203. return entry instanceof goog.tweak.NamespaceEntry_ ||
  204. entry instanceof goog.tweak.BooleanGroup;
  205. };
  206. /**
  207. * Returns the list of entries from the given boolean group.
  208. * @param {!goog.tweak.BooleanGroup} group The group to get the entries from.
  209. * @return {!Array<!goog.tweak.BaseEntry>} The sorted entries.
  210. * @private
  211. */
  212. goog.tweak.TweakUi.extractBooleanGroupEntries_ = function(group) {
  213. var ret = goog.object.getValues(group.getChildEntries());
  214. ret.sort(goog.tweak.TweakUi.entryCompare_);
  215. return ret;
  216. };
  217. /**
  218. * @param {!goog.tweak.BaseEntry} entry The entry.
  219. * @return {string} Returns the namespace for the entry, or '' if it is not
  220. * namespaced.
  221. * @private
  222. */
  223. goog.tweak.TweakUi.extractNamespace_ = function(entry) {
  224. var namespaceMatch = /.+(?=\.)/.exec(entry.getId());
  225. return namespaceMatch ? namespaceMatch[0] : '';
  226. };
  227. /**
  228. * @param {!goog.tweak.BaseEntry} entry The entry.
  229. * @return {string} Returns the part of the label after the last period, unless
  230. * the label has been explicly set (it is different from the ID).
  231. * @private
  232. */
  233. goog.tweak.TweakUi.getNamespacedLabel_ = function(entry) {
  234. var label = entry.label;
  235. if (label == entry.getId()) {
  236. label = label.substr(label.lastIndexOf('.') + 1);
  237. }
  238. return label;
  239. };
  240. /**
  241. * @return {!Element} The root element. Must not be called before render().
  242. */
  243. goog.tweak.TweakUi.prototype.getRootElement = function() {
  244. goog.asserts.assert(
  245. this.entriesPanel_, 'TweakUi.getRootElement called before render().');
  246. return this.entriesPanel_.getRootElement();
  247. };
  248. /**
  249. * Reloads the page with query parameters set by the UI.
  250. * @private
  251. */
  252. goog.tweak.TweakUi.prototype.restartWithAppliedTweaks_ = function() {
  253. var queryString = this.registry_.makeUrlQuery();
  254. var wnd = this.domHelper_.getWindow();
  255. if (queryString != wnd.location.search) {
  256. wnd.location.search = queryString;
  257. } else {
  258. wnd.location.reload();
  259. }
  260. };
  261. /**
  262. * Installs the required CSS styles.
  263. * @private
  264. */
  265. goog.tweak.TweakUi.prototype.installStyles_ = function() {
  266. // Use an marker to install the styles only once per document.
  267. // Styles are injected via JS instead of in a separate style sheet so that
  268. // they are automatically excluded when tweaks are stripped out.
  269. var doc = this.domHelper_.getDocument();
  270. if (!(goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ in doc)) {
  271. goog.style.installSafeStyleSheet(goog.tweak.TweakUi.CSS_STYLES_, doc);
  272. doc[goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_] = true;
  273. }
  274. };
  275. /**
  276. * Creates the element to display when the UI is visible.
  277. * @return {!Element} The root element.
  278. */
  279. goog.tweak.TweakUi.prototype.render = function() {
  280. this.installStyles_();
  281. var dh = this.domHelper_;
  282. // The submit button
  283. var submitButton = dh.createDom(
  284. goog.dom.TagName.BUTTON, {style: 'font-weight:bold'}, 'Apply Tweaks');
  285. submitButton.onclick = goog.bind(this.restartWithAppliedTweaks_, this);
  286. var rootPanel = new goog.tweak.EntriesPanel([], dh);
  287. var rootPanelDiv = rootPanel.render(submitButton);
  288. rootPanelDiv.className += ' ' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
  289. this.entriesPanel_ = rootPanel;
  290. var entries = this.registry_.extractEntries(
  291. true /* excludeChildEntries */, false /* excludeNonSettings */);
  292. for (var i = 0, entry; entry = entries[i]; i++) {
  293. this.insertEntry_(entry);
  294. }
  295. return rootPanelDiv;
  296. };
  297. /**
  298. * Updates the UI with the given entry.
  299. * @param {!goog.tweak.BaseEntry} entry The newly registered entry.
  300. * @private
  301. */
  302. goog.tweak.TweakUi.prototype.onNewRegisteredEntry_ = function(entry) {
  303. if (this.entriesPanel_) {
  304. this.insertEntry_(entry);
  305. }
  306. };
  307. /**
  308. * Updates the UI with the given entry.
  309. * @param {!goog.tweak.BaseEntry} entry The newly registered entry.
  310. * @private
  311. */
  312. goog.tweak.TweakUi.prototype.insertEntry_ = function(entry) {
  313. var panel = this.entriesPanel_;
  314. var namespace = goog.tweak.TweakUi.extractNamespace_(entry);
  315. if (namespace) {
  316. // Find the NamespaceEntry that the entry belongs to.
  317. var namespaceEntryId = goog.tweak.NamespaceEntry_.ID_PREFIX + namespace;
  318. var nsPanel = panel.childPanels[namespaceEntryId];
  319. if (nsPanel) {
  320. panel = nsPanel;
  321. } else {
  322. entry = new goog.tweak.NamespaceEntry_(namespace, [entry]);
  323. }
  324. }
  325. if (entry instanceof goog.tweak.BooleanInGroupSetting) {
  326. var group = entry.getGroup();
  327. // BooleanGroup entries are always registered before their
  328. // BooleanInGroupSettings.
  329. panel = panel.childPanels[group.getId()];
  330. }
  331. goog.asserts.assert(panel, 'Missing panel for entry %s', entry.getId());
  332. panel.insertEntry(entry);
  333. };
  334. /**
  335. * The body of the tweaks UI and also used for BooleanGroup.
  336. * @param {!Array<!goog.tweak.BaseEntry>} entries The entries to show in the
  337. * panel.
  338. * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
  339. * @constructor
  340. * @final
  341. */
  342. goog.tweak.EntriesPanel = function(entries, opt_domHelper) {
  343. /**
  344. * The entries to show in the panel.
  345. * @type {!Array<!goog.tweak.BaseEntry>} entries
  346. * @private
  347. */
  348. this.entries_ = entries;
  349. var self = this;
  350. /**
  351. * The bound onclick handler for the help question marks.
  352. * @this {Element}
  353. * @private
  354. */
  355. this.boundHelpOnClickHandler_ = function() {
  356. self.onHelpClick_(this.parentNode);
  357. };
  358. /**
  359. * The element that contains the UI.
  360. * @type {Element}
  361. * @private
  362. */
  363. this.rootElem_;
  364. /**
  365. * The element that contains all of the settings and the endElement.
  366. * @type {Element}
  367. * @private
  368. */
  369. this.mainPanel_;
  370. /**
  371. * Flips between true/false each time the "Toggle Descriptions" link is
  372. * clicked.
  373. * @type {boolean}
  374. * @private
  375. */
  376. this.showAllDescriptionsState_;
  377. /**
  378. * The DomHelper to render with.
  379. * @type {!goog.dom.DomHelper}
  380. * @private
  381. */
  382. this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
  383. /**
  384. * Map of tweak ID -> EntriesPanel for child panels (BooleanGroups).
  385. * @type {!Object<!goog.tweak.EntriesPanel>}
  386. */
  387. this.childPanels = {};
  388. };
  389. /**
  390. * @return {!Element} Returns the expanded element. Must not be called before
  391. * render().
  392. */
  393. goog.tweak.EntriesPanel.prototype.getRootElement = function() {
  394. goog.asserts.assert(
  395. this.rootElem_, 'EntriesPanel.getRootElement called before render().');
  396. return /** @type {!Element} */ (this.rootElem_);
  397. };
  398. /**
  399. * Creates and returns the expanded element.
  400. * The markup looks like:
  401. *
  402. * <div>
  403. * <a>Show Descriptions</a>
  404. * <div>
  405. * ...
  406. * {endElement}
  407. * </div>
  408. * </div>
  409. *
  410. * @param {Element|DocumentFragment=} opt_endElement Element to insert after all
  411. * tweak entries.
  412. * @return {!Element} The root element for the panel.
  413. */
  414. goog.tweak.EntriesPanel.prototype.render = function(opt_endElement) {
  415. var dh = this.domHelper_;
  416. var entries = this.entries_;
  417. var ret = dh.createDom(goog.dom.TagName.DIV);
  418. var showAllDescriptionsLink = dh.createDom(
  419. goog.dom.TagName.A, {
  420. href: 'javascript:;',
  421. onclick: goog.bind(this.toggleAllDescriptions, this)
  422. },
  423. 'Toggle all Descriptions');
  424. ret.appendChild(showAllDescriptionsLink);
  425. // Add all of the entries.
  426. var mainPanel = dh.createElement(goog.dom.TagName.DIV);
  427. this.mainPanel_ = mainPanel;
  428. for (var i = 0, entry; entry = entries[i]; i++) {
  429. mainPanel.appendChild(this.createEntryElem_(entry));
  430. }
  431. if (opt_endElement) {
  432. mainPanel.appendChild(opt_endElement);
  433. }
  434. ret.appendChild(mainPanel);
  435. this.rootElem_ = ret;
  436. return /** @type {!Element} */ (ret);
  437. };
  438. /**
  439. * Inserts the given entry into the panel.
  440. * @param {!goog.tweak.BaseEntry} entry The entry to insert.
  441. */
  442. goog.tweak.EntriesPanel.prototype.insertEntry = function(entry) {
  443. var insertIndex =
  444. -goog.array.binarySearch(
  445. this.entries_, entry, goog.tweak.TweakUi.entryCompare_) -
  446. 1;
  447. goog.asserts.assert(
  448. insertIndex >= 0, 'insertEntry failed for %s', entry.getId());
  449. goog.array.insertAt(this.entries_, entry, insertIndex);
  450. this.mainPanel_.insertBefore(
  451. this.createEntryElem_(entry),
  452. // IE doesn't like 'undefined' here.
  453. this.mainPanel_.childNodes[insertIndex] || null);
  454. };
  455. /**
  456. * Creates and returns a form element for the given entry.
  457. * @param {!goog.tweak.BaseEntry} entry The entry.
  458. * @return {!Element} The root DOM element for the entry.
  459. * @private
  460. */
  461. goog.tweak.EntriesPanel.prototype.createEntryElem_ = function(entry) {
  462. var dh = this.domHelper_;
  463. var isGroupEntry = goog.tweak.TweakUi.isGroupEntry_(entry);
  464. var classes = isGroupEntry ? goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ :
  465. goog.tweak.TweakUi.ENTRY_CSS_CLASSES_;
  466. // Containers should not use label tags or else all descendent inputs will be
  467. // connected on desktop browsers.
  468. var containerNodeName =
  469. isGroupEntry ? goog.dom.TagName.SPAN : goog.dom.TagName.LABEL;
  470. var ret = dh.createDom(
  471. goog.dom.TagName.DIV, classes,
  472. dh.createDom(
  473. containerNodeName, {
  474. // Make the hover text the description.
  475. title: entry.description,
  476. style: 'color:' + (entry.isRestartRequired() ? '' : 'blue')
  477. },
  478. this.createTweakEntryDom_(entry)),
  479. // Add the expandable help question mark.
  480. this.createHelpElem_(entry));
  481. return ret;
  482. };
  483. /**
  484. * Click handler for the help link.
  485. * @param {Node} entryDiv The div that contains the tweak.
  486. * @private
  487. */
  488. goog.tweak.EntriesPanel.prototype.onHelpClick_ = function(entryDiv) {
  489. this.showDescription_(entryDiv, !entryDiv.style.display);
  490. };
  491. /**
  492. * Twiddle the DOM so that the entry within the given span is shown/hidden.
  493. * @param {Node} entryDiv The div that contains the tweak.
  494. * @param {boolean} show True to show, false to hide.
  495. * @private
  496. */
  497. goog.tweak.EntriesPanel.prototype.showDescription_ = function(entryDiv, show) {
  498. var descriptionElem = entryDiv.lastChild.lastChild;
  499. goog.style.setElementShown(/** @type {Element} */ (descriptionElem), show);
  500. entryDiv.style.display = show ? 'block' : '';
  501. };
  502. /**
  503. * Creates and returns a help element for the given entry.
  504. * @param {goog.tweak.BaseEntry} entry The entry.
  505. * @return {!Element} The root element of the created DOM.
  506. * @private
  507. */
  508. goog.tweak.EntriesPanel.prototype.createHelpElem_ = function(entry) {
  509. // The markup looks like:
  510. // <span onclick=...><b>?</b><span>{description}</span></span>
  511. var ret = this.domHelper_.createElement(goog.dom.TagName.SPAN);
  512. goog.dom.safe.setInnerHtml(
  513. ret,
  514. goog.html.SafeHtml.concat(
  515. goog.html.SafeHtml.create(
  516. 'b', {'style': goog.string.Const.from('padding:0 1em 0 .5em')},
  517. '?'),
  518. goog.html.SafeHtml.create(
  519. 'span',
  520. {'style': goog.string.Const.from('display:none;color:#666')})));
  521. ret.onclick = this.boundHelpOnClickHandler_;
  522. // IE<9 doesn't support lastElementChild.
  523. var descriptionElem = /** @type {!Element} */ (ret.lastChild);
  524. if (entry.isRestartRequired()) {
  525. goog.dom.setTextContent(descriptionElem, entry.description);
  526. } else {
  527. goog.dom.safe.setInnerHtml(
  528. descriptionElem,
  529. goog.html.SafeHtml.concat(
  530. goog.html.SafeHtml.htmlEscape(entry.description),
  531. goog.html.SafeHtml.create(
  532. 'span', {'style': goog.string.Const.from('color: blue')},
  533. '(no restart required)')));
  534. }
  535. return ret;
  536. };
  537. /**
  538. * Show all entry descriptions (has the same effect as clicking on all ?'s).
  539. */
  540. goog.tweak.EntriesPanel.prototype.toggleAllDescriptions = function() {
  541. var show = !this.showAllDescriptionsState_;
  542. this.showAllDescriptionsState_ = show;
  543. var entryDivs = this.domHelper_.getElementsByTagNameAndClass(
  544. goog.dom.TagName.DIV, goog.tweak.TweakUi.ENTRY_CSS_CLASS_,
  545. this.rootElem_);
  546. for (var i = 0, div; div = entryDivs[i]; i++) {
  547. this.showDescription_(div, show);
  548. }
  549. };
  550. /**
  551. * Creates the DOM element to control the given enum setting.
  552. * @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
  553. * setting.
  554. * @param {string} label The label for the entry.
  555. * @param {!Function} onchangeFunc onchange event handler.
  556. * @return {!DocumentFragment} The DOM element.
  557. * @private
  558. */
  559. goog.tweak.EntriesPanel.prototype.createComboBoxDom_ = function(
  560. tweak, label, onchangeFunc) {
  561. // The markup looks like:
  562. // Label: <select><option></option></select>
  563. var dh = this.domHelper_;
  564. var ret = dh.getDocument().createDocumentFragment();
  565. ret.appendChild(dh.createTextNode(label + ': '));
  566. var selectElem = dh.createElement(goog.dom.TagName.SELECT);
  567. var values = tweak.getValidValues();
  568. for (var i = 0, il = values.length; i < il; ++i) {
  569. var optionElem = dh.createElement(goog.dom.TagName.OPTION);
  570. optionElem.text = String(values[i]);
  571. // Setting the option tag's value is required for selectElem.value to work
  572. // properly.
  573. optionElem.value = String(values[i]);
  574. selectElem.appendChild(optionElem);
  575. }
  576. ret.appendChild(selectElem);
  577. // Set the value and add a callback.
  578. selectElem.value = String(tweak.getNewValue());
  579. selectElem.onchange = onchangeFunc;
  580. tweak.addCallback(function() {
  581. selectElem.value = String(tweak.getNewValue());
  582. });
  583. return ret;
  584. };
  585. /**
  586. * Creates the DOM element to control the given boolean setting.
  587. * @param {!goog.tweak.BooleanSetting} tweak The setting.
  588. * @param {string} label The label for the entry.
  589. * @return {!DocumentFragment} The DOM elements.
  590. * @private
  591. */
  592. goog.tweak.EntriesPanel.prototype.createBooleanSettingDom_ = function(
  593. tweak, label) {
  594. var dh = this.domHelper_;
  595. var ret = dh.getDocument().createDocumentFragment();
  596. var checkbox = dh.createDom(goog.dom.TagName.INPUT, {type: 'checkbox'});
  597. ret.appendChild(checkbox);
  598. ret.appendChild(dh.createTextNode(label));
  599. // Needed on IE6 to ensure the textbox doesn't get cleared
  600. // when added to the DOM.
  601. checkbox.defaultChecked = tweak.getNewValue();
  602. checkbox.checked = tweak.getNewValue();
  603. checkbox.onchange = function() { tweak.setValue(checkbox.checked); };
  604. tweak.addCallback(function() { checkbox.checked = tweak.getNewValue(); });
  605. return ret;
  606. };
  607. /**
  608. * Creates the DOM for a BooleanGroup or NamespaceEntry.
  609. * @param {!goog.tweak.BooleanGroup|!goog.tweak.NamespaceEntry_} entry The
  610. * entry.
  611. * @param {string} label The label for the entry.
  612. * @param {!Array<goog.tweak.BaseEntry>} childEntries The child entries.
  613. * @return {!DocumentFragment} The DOM element.
  614. * @private
  615. */
  616. goog.tweak.EntriesPanel.prototype.createSubPanelDom_ = function(
  617. entry, label, childEntries) {
  618. var dh = this.domHelper_;
  619. var toggleLink =
  620. dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, label + ' \xBB');
  621. var toggleLink2 =
  622. dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, '\xAB ' + label);
  623. toggleLink2.style.marginRight = '10px';
  624. var innerUi = new goog.tweak.EntriesPanel(childEntries, dh);
  625. this.childPanels[entry.getId()] = innerUi;
  626. var elem = innerUi.render();
  627. // Move the toggle descriptions link into the legend.
  628. var descriptionsLink = elem.firstChild;
  629. var childrenElem = dh.createDom(
  630. goog.dom.TagName.FIELDSET, goog.getCssName('goog-inline-block'),
  631. dh.createDom(
  632. goog.dom.TagName.LEGEND, null, toggleLink2, descriptionsLink),
  633. elem);
  634. new goog.ui.Zippy(
  635. toggleLink, childrenElem, false /* expanded */, toggleLink2);
  636. var ret = dh.getDocument().createDocumentFragment();
  637. ret.appendChild(toggleLink);
  638. ret.appendChild(childrenElem);
  639. return ret;
  640. };
  641. /**
  642. * Creates the DOM element to control the given string setting.
  643. * @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
  644. * setting.
  645. * @param {string} label The label for the entry.
  646. * @param {!Function} onchangeFunc onchange event handler.
  647. * @return {!DocumentFragment} The DOM element.
  648. * @private
  649. */
  650. goog.tweak.EntriesPanel.prototype.createTextBoxDom_ = function(
  651. tweak, label, onchangeFunc) {
  652. var dh = this.domHelper_;
  653. var ret = dh.getDocument().createDocumentFragment();
  654. ret.appendChild(dh.createTextNode(label + ': '));
  655. var textBox = dh.createDom(goog.dom.TagName.INPUT, {
  656. value: String(tweak.getNewValue()),
  657. // TODO(agrieve): Make size configurable or autogrow.
  658. size: 5,
  659. onblur: onchangeFunc
  660. });
  661. ret.appendChild(textBox);
  662. tweak.addCallback(function() {
  663. textBox.value = String(tweak.getNewValue());
  664. });
  665. return ret;
  666. };
  667. /**
  668. * Creates the DOM element to control the given button action.
  669. * @param {!goog.tweak.ButtonAction} tweak The action.
  670. * @param {string} label The label for the entry.
  671. * @return {!Element} The DOM element.
  672. * @private
  673. */
  674. goog.tweak.EntriesPanel.prototype.createButtonActionDom_ = function(
  675. tweak, label) {
  676. return this.domHelper_.createDom(
  677. goog.dom.TagName.BUTTON, {onclick: goog.bind(tweak.fireCallbacks, tweak)},
  678. label);
  679. };
  680. /**
  681. * Creates the DOM element to control the given entry.
  682. * @param {!goog.tweak.BaseEntry} entry The entry.
  683. * @return {!Element|!DocumentFragment} The DOM element.
  684. * @private
  685. */
  686. goog.tweak.EntriesPanel.prototype.createTweakEntryDom_ = function(entry) {
  687. var label = goog.tweak.TweakUi.getNamespacedLabel_(entry);
  688. if (entry instanceof goog.tweak.BooleanSetting) {
  689. return this.createBooleanSettingDom_(entry, label);
  690. } else if (entry instanceof goog.tweak.BooleanGroup) {
  691. var childEntries = goog.tweak.TweakUi.extractBooleanGroupEntries_(entry);
  692. return this.createSubPanelDom_(entry, label, childEntries);
  693. } else if (entry instanceof goog.tweak.StringSetting) {
  694. /** @this {Element} */
  695. var setValueFunc = function() { entry.setValue(this.value); };
  696. return entry.getValidValues() ?
  697. this.createComboBoxDom_(entry, label, setValueFunc) :
  698. this.createTextBoxDom_(entry, label, setValueFunc);
  699. } else if (entry instanceof goog.tweak.NumericSetting) {
  700. setValueFunc = function() {
  701. // Reset the value if it's not a number.
  702. if (isNaN(this.value)) {
  703. this.value = entry.getNewValue();
  704. } else {
  705. entry.setValue(+this.value);
  706. }
  707. };
  708. return entry.getValidValues() ?
  709. this.createComboBoxDom_(entry, label, setValueFunc) :
  710. this.createTextBoxDom_(entry, label, setValueFunc);
  711. } else if (entry instanceof goog.tweak.NamespaceEntry_) {
  712. return this.createSubPanelDom_(entry, entry.label, entry.entries);
  713. }
  714. goog.asserts.assertInstanceof(
  715. entry, goog.tweak.ButtonAction, 'invalid entry: %s', entry);
  716. return this.createButtonActionDom_(
  717. /** @type {!goog.tweak.ButtonAction} */ (entry), label);
  718. };
  719. /**
  720. * Entries used to represent the collapsible namespace links. These entries are
  721. * never registered with the TweakRegistry, but are contained within the
  722. * collection of entries within TweakPanels.
  723. * @param {string} namespace The namespace for the entry.
  724. * @param {!Array<!goog.tweak.BaseEntry>} entries Entries within the namespace.
  725. * @constructor
  726. * @extends {goog.tweak.BaseEntry}
  727. * @private
  728. */
  729. goog.tweak.NamespaceEntry_ = function(namespace, entries) {
  730. goog.tweak.BaseEntry.call(
  731. this, goog.tweak.NamespaceEntry_.ID_PREFIX + namespace,
  732. 'Tweaks within the ' + namespace + ' namespace.');
  733. /**
  734. * Entries within this namespace.
  735. * @type {!Array<!goog.tweak.BaseEntry>}
  736. */
  737. this.entries = entries;
  738. this.label = namespace;
  739. };
  740. goog.inherits(goog.tweak.NamespaceEntry_, goog.tweak.BaseEntry);
  741. /**
  742. * Prefix for the IDs of namespace entries used to ensure that they do not
  743. * conflict with regular entries.
  744. * @type {string}
  745. */
  746. goog.tweak.NamespaceEntry_.ID_PREFIX = '!';