custombuttonrenderer.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. // Copyright 2008 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 custom button renderer that uses CSS voodoo to render a
  16. * button-like object with fake rounded corners.
  17. *
  18. * @author attila@google.com (Attila Bodis)
  19. */
  20. goog.provide('goog.ui.CustomButtonRenderer');
  21. goog.require('goog.a11y.aria.Role');
  22. goog.require('goog.asserts');
  23. goog.require('goog.dom.NodeType');
  24. goog.require('goog.dom.TagName');
  25. goog.require('goog.dom.classlist');
  26. goog.require('goog.string');
  27. goog.require('goog.ui.ButtonRenderer');
  28. goog.require('goog.ui.INLINE_BLOCK_CLASSNAME');
  29. /**
  30. * Custom renderer for {@link goog.ui.Button}s. Custom buttons can contain
  31. * almost arbitrary HTML content, will flow like inline elements, but can be
  32. * styled like block-level elements.
  33. *
  34. * @constructor
  35. * @extends {goog.ui.ButtonRenderer}
  36. */
  37. goog.ui.CustomButtonRenderer = function() {
  38. goog.ui.ButtonRenderer.call(this);
  39. };
  40. goog.inherits(goog.ui.CustomButtonRenderer, goog.ui.ButtonRenderer);
  41. goog.addSingletonGetter(goog.ui.CustomButtonRenderer);
  42. /**
  43. * Default CSS class to be applied to the root element of components rendered
  44. * by this renderer.
  45. * @type {string}
  46. */
  47. goog.ui.CustomButtonRenderer.CSS_CLASS = goog.getCssName('goog-custom-button');
  48. /**
  49. * Returns the button's contents wrapped in the following DOM structure:
  50. *
  51. * <div class="goog-inline-block goog-custom-button">
  52. * <div class="goog-inline-block goog-custom-button-outer-box">
  53. * <div class="goog-inline-block goog-custom-button-inner-box">
  54. * Contents...
  55. * </div>
  56. * </div>
  57. * </div>
  58. *
  59. * Overrides {@link goog.ui.ButtonRenderer#createDom}.
  60. * @param {goog.ui.Control} control goog.ui.Button to render.
  61. * @return {!Element} Root element for the button.
  62. * @override
  63. */
  64. goog.ui.CustomButtonRenderer.prototype.createDom = function(control) {
  65. var button = /** @type {goog.ui.Button} */ (control);
  66. var classNames = this.getClassNames(button);
  67. var buttonElement = button.getDomHelper().createDom(
  68. goog.dom.TagName.DIV,
  69. goog.ui.INLINE_BLOCK_CLASSNAME + ' ' + classNames.join(' '),
  70. this.createButton(button.getContent(), button.getDomHelper()));
  71. this.setTooltip(buttonElement, /** @type {!string}*/ (button.getTooltip()));
  72. return buttonElement;
  73. };
  74. /**
  75. * Returns the ARIA role to be applied to custom buttons.
  76. * @return {goog.a11y.aria.Role|undefined} ARIA role.
  77. * @override
  78. */
  79. goog.ui.CustomButtonRenderer.prototype.getAriaRole = function() {
  80. return goog.a11y.aria.Role.BUTTON;
  81. };
  82. /**
  83. * Takes the button's root element and returns the parent element of the
  84. * button's contents. Overrides the superclass implementation by taking
  85. * the nested DIV structure of custom buttons into account.
  86. * @param {Element} element Root element of the button whose content
  87. * element is to be returned.
  88. * @return {Element} The button's content element (if any).
  89. * @override
  90. */
  91. goog.ui.CustomButtonRenderer.prototype.getContentElement = function(element) {
  92. return element && element.firstChild &&
  93. /** @type {Element} */ (element.firstChild.firstChild);
  94. };
  95. /**
  96. * Takes a text caption or existing DOM structure, and returns the content
  97. * wrapped in a pseudo-rounded-corner box. Creates the following DOM structure:
  98. *
  99. * <div class="goog-inline-block goog-custom-button-outer-box">
  100. * <div class="goog-inline-block goog-custom-button-inner-box">
  101. * Contents...
  102. * </div>
  103. * </div>
  104. *
  105. * Used by both {@link #createDom} and {@link #decorate}. To be overridden
  106. * by subclasses.
  107. * @param {goog.ui.ControlContent} content Text caption or DOM structure to wrap
  108. * in a box.
  109. * @param {goog.dom.DomHelper} dom DOM helper, used for document interaction.
  110. * @return {Element} Pseudo-rounded-corner box containing the content.
  111. */
  112. goog.ui.CustomButtonRenderer.prototype.createButton = function(content, dom) {
  113. return dom.createDom(
  114. goog.dom.TagName.DIV, goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
  115. goog.getCssName(this.getCssClass(), 'outer-box'),
  116. dom.createDom(
  117. goog.dom.TagName.DIV, goog.ui.INLINE_BLOCK_CLASSNAME + ' ' +
  118. goog.getCssName(this.getCssClass(), 'inner-box'),
  119. content));
  120. };
  121. /**
  122. * Returns true if this renderer can decorate the element. Overrides
  123. * {@link goog.ui.ButtonRenderer#canDecorate} by returning true if the
  124. * element is a DIV, false otherwise.
  125. * @param {Element} element Element to decorate.
  126. * @return {boolean} Whether the renderer can decorate the element.
  127. * @override
  128. */
  129. goog.ui.CustomButtonRenderer.prototype.canDecorate = function(element) {
  130. return element.tagName == goog.dom.TagName.DIV;
  131. };
  132. /**
  133. * Check if the button's element has a box structure.
  134. * @param {goog.ui.Button} button Button instance whose structure is being
  135. * checked.
  136. * @param {Element} element Element of the button.
  137. * @return {boolean} Whether the element has a box structure.
  138. * @protected
  139. */
  140. goog.ui.CustomButtonRenderer.prototype.hasBoxStructure = function(
  141. button, element) {
  142. var outer = button.getDomHelper().getFirstElementChild(element);
  143. var outerClassName = goog.getCssName(this.getCssClass(), 'outer-box');
  144. if (outer && goog.dom.classlist.contains(outer, outerClassName)) {
  145. var inner = button.getDomHelper().getFirstElementChild(outer);
  146. var innerClassName = goog.getCssName(this.getCssClass(), 'inner-box');
  147. if (inner && goog.dom.classlist.contains(inner, innerClassName)) {
  148. // We have a proper box structure.
  149. return true;
  150. }
  151. }
  152. return false;
  153. };
  154. /**
  155. * Takes an existing element and decorates it with the custom button control.
  156. * Initializes the control's ID, content, tooltip, value, and state based
  157. * on the ID of the element, its child nodes, and its CSS classes, respectively.
  158. * Returns the element. Overrides {@link goog.ui.ButtonRenderer#decorate}.
  159. * @param {goog.ui.Control} control Button instance to decorate the element.
  160. * @param {Element} element Element to decorate.
  161. * @return {Element} Decorated element.
  162. * @override
  163. */
  164. goog.ui.CustomButtonRenderer.prototype.decorate = function(control, element) {
  165. goog.asserts.assert(element);
  166. var button = /** @type {goog.ui.Button} */ (control);
  167. // Trim text nodes in the element's child node list; otherwise madness
  168. // ensues (i.e. on Gecko, buttons will flicker and shift when moused over).
  169. goog.ui.CustomButtonRenderer.trimTextNodes_(element, true);
  170. goog.ui.CustomButtonRenderer.trimTextNodes_(element, false);
  171. // Create the buttom dom if it has not been created.
  172. if (!this.hasBoxStructure(button, element)) {
  173. element.appendChild(
  174. this.createButton(element.childNodes, button.getDomHelper()));
  175. }
  176. goog.dom.classlist.addAll(
  177. element, [goog.ui.INLINE_BLOCK_CLASSNAME, this.getCssClass()]);
  178. return goog.ui.CustomButtonRenderer.superClass_.decorate.call(
  179. this, button, element);
  180. };
  181. /**
  182. * Returns the CSS class to be applied to the root element of components
  183. * rendered using this renderer.
  184. * @return {string} Renderer-specific CSS class.
  185. * @override
  186. */
  187. goog.ui.CustomButtonRenderer.prototype.getCssClass = function() {
  188. return goog.ui.CustomButtonRenderer.CSS_CLASS;
  189. };
  190. /**
  191. * Takes an element and removes leading or trailing whitespace from the start
  192. * or the end of its list of child nodes. The Boolean argument determines
  193. * whether to trim from the start or the end of the node list. Empty text
  194. * nodes are removed, and the first non-empty text node is trimmed from the
  195. * left or the right as appropriate. For example,
  196. *
  197. * <div class="goog-inline-block">
  198. * #text ""
  199. * #text "\n Hello "
  200. * <span>...</span>
  201. * #text " World! \n"
  202. * #text ""
  203. * </div>
  204. *
  205. * becomes
  206. *
  207. * <div class="goog-inline-block">
  208. * #text "Hello "
  209. * <span>...</span>
  210. * #text " World!"
  211. * </div>
  212. *
  213. * This is essential for Gecko, where leading/trailing whitespace messes with
  214. * the layout of elements with -moz-inline-box (used in goog-inline-block), and
  215. * optional but harmless for non-Gecko.
  216. *
  217. * @param {Element} element Element whose child node list is to be trimmed.
  218. * @param {boolean} fromStart Whether to trim from the start or from the end.
  219. * @private
  220. */
  221. goog.ui.CustomButtonRenderer.trimTextNodes_ = function(element, fromStart) {
  222. if (element) {
  223. var node = fromStart ? element.firstChild : element.lastChild, next;
  224. // Tag soup HTML may result in a DOM where siblings have different parents.
  225. while (node && node.parentNode == element) {
  226. // Get the next/previous sibling here, since the node may be removed.
  227. next = fromStart ? node.nextSibling : node.previousSibling;
  228. if (node.nodeType == goog.dom.NodeType.TEXT) {
  229. // Found a text node.
  230. var text = node.nodeValue;
  231. if (goog.string.trim(text) == '') {
  232. // Found an empty text node; remove it.
  233. element.removeChild(node);
  234. } else {
  235. // Found a non-empty text node; trim from the start/end, then exit.
  236. node.nodeValue = fromStart ? goog.string.trimLeft(text) :
  237. goog.string.trimRight(text);
  238. break;
  239. }
  240. } else {
  241. // Found a non-text node; done.
  242. break;
  243. }
  244. node = next;
  245. }
  246. }
  247. };