containerrenderer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 Base class for container renderers.
  16. *
  17. * @author attila@google.com (Attila Bodis)
  18. */
  19. goog.provide('goog.ui.ContainerRenderer');
  20. goog.require('goog.a11y.aria');
  21. goog.require('goog.array');
  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.style');
  28. goog.require('goog.ui.registry');
  29. goog.require('goog.userAgent');
  30. /**
  31. * Default renderer for {@link goog.ui.Container}. Can be used as-is, but
  32. * subclasses of Container will probably want to use renderers specifically
  33. * tailored for them by extending this class.
  34. * @param {string=} opt_ariaRole Optional ARIA role used for the element.
  35. * @constructor
  36. */
  37. goog.ui.ContainerRenderer = function(opt_ariaRole) {
  38. // By default, the ARIA role is unspecified.
  39. /** @private {string|undefined} */
  40. this.ariaRole_ = opt_ariaRole;
  41. };
  42. goog.addSingletonGetter(goog.ui.ContainerRenderer);
  43. /**
  44. * Constructs a new renderer and sets the CSS class that the renderer will use
  45. * as the base CSS class to apply to all elements rendered by that renderer.
  46. * An example to use this function using a menu is:
  47. *
  48. * <pre>
  49. * var myCustomRenderer = goog.ui.ContainerRenderer.getCustomRenderer(
  50. * goog.ui.MenuRenderer, 'my-special-menu');
  51. * var newMenu = new goog.ui.Menu(opt_domHelper, myCustomRenderer);
  52. * </pre>
  53. *
  54. * Your styles for the menu can now be:
  55. * <pre>
  56. * .my-special-menu { }
  57. * </pre>
  58. *
  59. * <em>instead</em> of
  60. * <pre>
  61. * .CSS_MY_SPECIAL_MENU .goog-menu { }
  62. * </pre>
  63. *
  64. * You would want to use this functionality when you want an instance of a
  65. * component to have specific styles different than the other components of the
  66. * same type in your application. This avoids using descendant selectors to
  67. * apply the specific styles to this component.
  68. *
  69. * @param {Function} ctor The constructor of the renderer you want to create.
  70. * @param {string} cssClassName The name of the CSS class for this renderer.
  71. * @return {goog.ui.ContainerRenderer} An instance of the desired renderer with
  72. * its getCssClass() method overridden to return the supplied custom CSS
  73. * class name.
  74. */
  75. goog.ui.ContainerRenderer.getCustomRenderer = function(ctor, cssClassName) {
  76. var renderer = new ctor();
  77. /**
  78. * Returns the CSS class to be applied to the root element of components
  79. * rendered using this renderer.
  80. * @return {string} Renderer-specific CSS class.
  81. */
  82. renderer.getCssClass = function() { return cssClassName; };
  83. return renderer;
  84. };
  85. /**
  86. * Default CSS class to be applied to the root element of containers rendered
  87. * by this renderer.
  88. * @type {string}
  89. */
  90. goog.ui.ContainerRenderer.CSS_CLASS = goog.getCssName('goog-container');
  91. /**
  92. * Returns the ARIA role to be applied to the container.
  93. * See http://wiki/Main/ARIA for more info.
  94. * @return {undefined|string} ARIA role.
  95. */
  96. goog.ui.ContainerRenderer.prototype.getAriaRole = function() {
  97. return this.ariaRole_;
  98. };
  99. /**
  100. * Enables or disables the tab index of the element. Only elements with a
  101. * valid tab index can receive focus.
  102. * @param {Element} element Element whose tab index is to be changed.
  103. * @param {boolean} enable Whether to add or remove the element's tab index.
  104. */
  105. goog.ui.ContainerRenderer.prototype.enableTabIndex = function(element, enable) {
  106. if (element) {
  107. element.tabIndex = enable ? 0 : -1;
  108. }
  109. };
  110. /**
  111. * Creates and returns the container's root element. The default
  112. * simply creates a DIV and applies the renderer's own CSS class name to it.
  113. * To be overridden in subclasses.
  114. * @param {goog.ui.Container} container Container to render.
  115. * @return {Element} Root element for the container.
  116. */
  117. goog.ui.ContainerRenderer.prototype.createDom = function(container) {
  118. return container.getDomHelper().createDom(
  119. goog.dom.TagName.DIV, this.getClassNames(container).join(' '));
  120. };
  121. /**
  122. * Returns the DOM element into which child components are to be rendered,
  123. * or null if the container hasn't been rendered yet.
  124. * @param {Element} element Root element of the container whose content element
  125. * is to be returned.
  126. * @return {Element} Element to contain child elements (null if none).
  127. */
  128. goog.ui.ContainerRenderer.prototype.getContentElement = function(element) {
  129. return element;
  130. };
  131. /**
  132. * Default implementation of {@code canDecorate}; returns true if the element
  133. * is a DIV, false otherwise.
  134. * @param {Element} element Element to decorate.
  135. * @return {boolean} Whether the renderer can decorate the element.
  136. */
  137. goog.ui.ContainerRenderer.prototype.canDecorate = function(element) {
  138. return element.tagName == 'DIV';
  139. };
  140. /**
  141. * Default implementation of {@code decorate} for {@link goog.ui.Container}s.
  142. * Decorates the element with the container, and attempts to decorate its child
  143. * elements. Returns the decorated element.
  144. * @param {goog.ui.Container} container Container to decorate the element.
  145. * @param {Element} element Element to decorate.
  146. * @return {!Element} Decorated element.
  147. */
  148. goog.ui.ContainerRenderer.prototype.decorate = function(container, element) {
  149. // Set the container's ID to the decorated element's DOM ID, if any.
  150. if (element.id) {
  151. container.setId(element.id);
  152. }
  153. // Configure the container's state based on the CSS class names it has.
  154. var baseClass = this.getCssClass();
  155. var hasBaseClass = false;
  156. var classNames = goog.dom.classlist.get(element);
  157. if (classNames) {
  158. goog.array.forEach(classNames, function(className) {
  159. if (className == baseClass) {
  160. hasBaseClass = true;
  161. } else if (className) {
  162. this.setStateFromClassName(container, className, baseClass);
  163. }
  164. }, this);
  165. }
  166. if (!hasBaseClass) {
  167. // Make sure the container's root element has the renderer's own CSS class.
  168. goog.dom.classlist.add(element, baseClass);
  169. }
  170. // Decorate the element's children, if applicable. This should happen after
  171. // the container's own state has been initialized, since how children are
  172. // decorated may depend on the state of the container.
  173. this.decorateChildren(container, this.getContentElement(element));
  174. return element;
  175. };
  176. /**
  177. * Sets the container's state based on the given CSS class name, encountered
  178. * during decoration. CSS class names that don't represent container states
  179. * are ignored. Considered protected; subclasses should override this method
  180. * to support more states and CSS class names.
  181. * @param {goog.ui.Container} container Container to update.
  182. * @param {string} className CSS class name.
  183. * @param {string} baseClass Base class name used as the root of state-specific
  184. * class names (typically the renderer's own class name).
  185. * @protected
  186. * @suppress {missingRequire} goog.ui.Container
  187. */
  188. goog.ui.ContainerRenderer.prototype.setStateFromClassName = function(
  189. container, className, baseClass) {
  190. if (className == goog.getCssName(baseClass, 'disabled')) {
  191. container.setEnabled(false);
  192. } else if (className == goog.getCssName(baseClass, 'horizontal')) {
  193. container.setOrientation(goog.ui.Container.Orientation.HORIZONTAL);
  194. } else if (className == goog.getCssName(baseClass, 'vertical')) {
  195. container.setOrientation(goog.ui.Container.Orientation.VERTICAL);
  196. }
  197. };
  198. /**
  199. * Takes a container and an element that may contain child elements, decorates
  200. * the child elements, and adds the corresponding components to the container
  201. * as child components. Any non-element child nodes (e.g. empty text nodes
  202. * introduced by line breaks in the HTML source) are removed from the element.
  203. * @param {goog.ui.Container} container Container whose children are to be
  204. * discovered.
  205. * @param {Element} element Element whose children are to be decorated.
  206. * @param {Element=} opt_firstChild the first child to be decorated.
  207. */
  208. goog.ui.ContainerRenderer.prototype.decorateChildren = function(
  209. container, element, opt_firstChild) {
  210. if (element) {
  211. var node = opt_firstChild || element.firstChild, next;
  212. // Tag soup HTML may result in a DOM where siblings have different parents.
  213. while (node && node.parentNode == element) {
  214. // Get the next sibling here, since the node may be replaced or removed.
  215. next = node.nextSibling;
  216. if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  217. // Decorate element node.
  218. var child = this.getDecoratorForChild(/** @type {!Element} */ (node));
  219. if (child) {
  220. // addChild() may need to look at the element.
  221. child.setElementInternal(/** @type {!Element} */ (node));
  222. // If the container is disabled, mark the child disabled too. See
  223. // bug 1263729. Note that this must precede the call to addChild().
  224. if (!container.isEnabled()) {
  225. child.setEnabled(false);
  226. }
  227. container.addChild(child);
  228. child.decorate(/** @type {!Element} */ (node));
  229. }
  230. } else if (!node.nodeValue || goog.string.trim(node.nodeValue) == '') {
  231. // Remove empty text node, otherwise madness ensues (e.g. controls that
  232. // use goog-inline-block will flicker and shift on hover on Gecko).
  233. element.removeChild(node);
  234. }
  235. node = next;
  236. }
  237. }
  238. };
  239. /**
  240. * Inspects the element, and creates an instance of {@link goog.ui.Control} or
  241. * an appropriate subclass best suited to decorate it. Returns the control (or
  242. * null if no suitable class was found). This default implementation uses the
  243. * element's CSS class to find the appropriate control class to instantiate.
  244. * May be overridden in subclasses.
  245. * @param {Element} element Element to decorate.
  246. * @return {goog.ui.Control?} A new control suitable to decorate the element
  247. * (null if none).
  248. */
  249. goog.ui.ContainerRenderer.prototype.getDecoratorForChild = function(element) {
  250. return /** @type {goog.ui.Control} */ (
  251. goog.ui.registry.getDecorator(element));
  252. };
  253. /**
  254. * Initializes the container's DOM when the container enters the document.
  255. * Called from {@link goog.ui.Container#enterDocument}.
  256. * @param {goog.ui.Container} container Container whose DOM is to be initialized
  257. * as it enters the document.
  258. */
  259. goog.ui.ContainerRenderer.prototype.initializeDom = function(container) {
  260. var elem = container.getElement();
  261. goog.asserts.assert(elem, 'The container DOM element cannot be null.');
  262. // Make sure the container's element isn't selectable. On Gecko, recursively
  263. // marking each child element unselectable is expensive and unnecessary, so
  264. // only mark the root element unselectable.
  265. goog.style.setUnselectable(elem, true, goog.userAgent.GECKO);
  266. // IE doesn't support outline:none, so we have to use the hideFocus property.
  267. if (goog.userAgent.IE) {
  268. elem.hideFocus = true;
  269. }
  270. // Set the ARIA role.
  271. var ariaRole = this.getAriaRole();
  272. if (ariaRole) {
  273. goog.a11y.aria.setRole(elem, ariaRole);
  274. }
  275. };
  276. /**
  277. * Returns the element within the container's DOM that should receive keyboard
  278. * focus (null if none). The default implementation returns the container's
  279. * root element.
  280. * @param {goog.ui.Container} container Container whose key event target is
  281. * to be returned.
  282. * @return {Element} Key event target (null if none).
  283. */
  284. goog.ui.ContainerRenderer.prototype.getKeyEventTarget = function(container) {
  285. return container.getElement();
  286. };
  287. /**
  288. * Returns the CSS class to be applied to the root element of containers
  289. * rendered using this renderer.
  290. * @return {string} Renderer-specific CSS class.
  291. */
  292. goog.ui.ContainerRenderer.prototype.getCssClass = function() {
  293. return goog.ui.ContainerRenderer.CSS_CLASS;
  294. };
  295. /**
  296. * Returns all CSS class names applicable to the given container, based on its
  297. * state. The array of class names returned includes the renderer's own CSS
  298. * class, followed by a CSS class indicating the container's orientation,
  299. * followed by any state-specific CSS classes.
  300. * @param {goog.ui.Container} container Container whose CSS classes are to be
  301. * returned.
  302. * @return {!Array<string>} Array of CSS class names applicable to the
  303. * container.
  304. */
  305. goog.ui.ContainerRenderer.prototype.getClassNames = function(container) {
  306. var baseClass = this.getCssClass();
  307. var isHorizontal =
  308. container.getOrientation() == goog.ui.Container.Orientation.HORIZONTAL;
  309. var classNames = [
  310. baseClass, (isHorizontal ? goog.getCssName(baseClass, 'horizontal') :
  311. goog.getCssName(baseClass, 'vertical'))
  312. ];
  313. if (!container.isEnabled()) {
  314. classNames.push(goog.getCssName(baseClass, 'disabled'));
  315. }
  316. return classNames;
  317. };
  318. /**
  319. * Returns the default orientation of containers rendered or decorated by this
  320. * renderer. The base class implementation returns {@code VERTICAL}.
  321. * @return {goog.ui.Container.Orientation} Default orientation for containers
  322. * created or decorated by this renderer.
  323. * @suppress {missingRequire} goog.ui.Container
  324. */
  325. goog.ui.ContainerRenderer.prototype.getDefaultOrientation = function() {
  326. return goog.ui.Container.Orientation.VERTICAL;
  327. };