menuitemrenderer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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 Renderer for {@link goog.ui.MenuItem}s.
  16. *
  17. * @author attila@google.com (Attila Bodis)
  18. */
  19. goog.provide('goog.ui.MenuItemRenderer');
  20. goog.require('goog.a11y.aria.Role');
  21. goog.require('goog.asserts');
  22. goog.require('goog.dom');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.dom.classlist');
  25. goog.require('goog.ui.Component');
  26. goog.require('goog.ui.ControlRenderer');
  27. /**
  28. * Default renderer for {@link goog.ui.MenuItem}s. Each item has the following
  29. * structure:
  30. *
  31. * <div class="goog-menuitem">
  32. * <div class="goog-menuitem-content">
  33. * ...(menu item contents)...
  34. * </div>
  35. * </div>
  36. *
  37. * @constructor
  38. * @extends {goog.ui.ControlRenderer}
  39. */
  40. goog.ui.MenuItemRenderer = function() {
  41. goog.ui.ControlRenderer.call(this);
  42. /**
  43. * Commonly used CSS class names, cached here for convenience (and to avoid
  44. * unnecessary string concatenation).
  45. * @type {!Array<string>}
  46. * @private
  47. */
  48. this.classNameCache_ = [];
  49. };
  50. goog.inherits(goog.ui.MenuItemRenderer, goog.ui.ControlRenderer);
  51. goog.addSingletonGetter(goog.ui.MenuItemRenderer);
  52. /**
  53. * CSS class name the renderer applies to menu item elements.
  54. * @type {string}
  55. */
  56. goog.ui.MenuItemRenderer.CSS_CLASS = goog.getCssName('goog-menuitem');
  57. /**
  58. * Constants for referencing composite CSS classes.
  59. * @enum {number}
  60. * @private
  61. */
  62. goog.ui.MenuItemRenderer.CompositeCssClassIndex_ = {
  63. HOVER: 0,
  64. CHECKBOX: 1,
  65. CONTENT: 2
  66. };
  67. /**
  68. * Returns the composite CSS class by using the cached value or by constructing
  69. * the value from the base CSS class and the passed index.
  70. * @param {goog.ui.MenuItemRenderer.CompositeCssClassIndex_} index Index for the
  71. * CSS class - could be highlight, checkbox or content in usual cases.
  72. * @return {string} The composite CSS class.
  73. * @private
  74. */
  75. goog.ui.MenuItemRenderer.prototype.getCompositeCssClass_ = function(index) {
  76. var result = this.classNameCache_[index];
  77. if (!result) {
  78. switch (index) {
  79. case goog.ui.MenuItemRenderer.CompositeCssClassIndex_.HOVER:
  80. result = goog.getCssName(this.getStructuralCssClass(), 'highlight');
  81. break;
  82. case goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CHECKBOX:
  83. result = goog.getCssName(this.getStructuralCssClass(), 'checkbox');
  84. break;
  85. case goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CONTENT:
  86. result = goog.getCssName(this.getStructuralCssClass(), 'content');
  87. break;
  88. }
  89. this.classNameCache_[index] = result;
  90. }
  91. return result;
  92. };
  93. /** @override */
  94. goog.ui.MenuItemRenderer.prototype.getAriaRole = function() {
  95. return goog.a11y.aria.Role.MENU_ITEM;
  96. };
  97. /**
  98. * Overrides {@link goog.ui.ControlRenderer#createDom} by adding extra markup
  99. * and stying to the menu item's element if it is selectable or checkable.
  100. * @param {goog.ui.Control} item Menu item to render.
  101. * @return {Element} Root element for the item.
  102. * @override
  103. */
  104. goog.ui.MenuItemRenderer.prototype.createDom = function(item) {
  105. var element = item.getDomHelper().createDom(
  106. goog.dom.TagName.DIV, this.getClassNames(item).join(' '),
  107. this.createContent(item.getContent(), item.getDomHelper()));
  108. this.setEnableCheckBoxStructure(
  109. item, element, item.isSupportedState(goog.ui.Component.State.SELECTED) ||
  110. item.isSupportedState(goog.ui.Component.State.CHECKED));
  111. return element;
  112. };
  113. /** @override */
  114. goog.ui.MenuItemRenderer.prototype.getContentElement = function(element) {
  115. return /** @type {Element} */ (element && element.firstChild);
  116. };
  117. /**
  118. * Overrides {@link goog.ui.ControlRenderer#decorate} by initializing the
  119. * menu item to checkable based on whether the element to be decorated has
  120. * extra stying indicating that it should be.
  121. * @param {goog.ui.Control} item Menu item instance to decorate the element.
  122. * @param {Element} element Element to decorate.
  123. * @return {Element} Decorated element.
  124. * @override
  125. */
  126. goog.ui.MenuItemRenderer.prototype.decorate = function(item, element) {
  127. goog.asserts.assert(element);
  128. if (!this.hasContentStructure(element)) {
  129. element.appendChild(
  130. this.createContent(element.childNodes, item.getDomHelper()));
  131. }
  132. if (goog.dom.classlist.contains(element, goog.getCssName('goog-option'))) {
  133. (/** @type {goog.ui.MenuItem} */ (item)).setCheckable(true);
  134. this.setCheckable(item, element, true);
  135. }
  136. return goog.ui.MenuItemRenderer.superClass_.decorate.call(
  137. this, item, element);
  138. };
  139. /**
  140. * Takes a menu item's root element, and sets its content to the given text
  141. * caption or DOM structure. Overrides the superclass immplementation by
  142. * making sure that the checkbox structure (for selectable/checkable menu
  143. * items) is preserved.
  144. * @param {Element} element The item's root element.
  145. * @param {goog.ui.ControlContent} content Text caption or DOM structure to be
  146. * set as the item's content.
  147. * @override
  148. */
  149. goog.ui.MenuItemRenderer.prototype.setContent = function(element, content) {
  150. // Save the checkbox element, if present.
  151. var contentElement = this.getContentElement(element);
  152. var checkBoxElement =
  153. this.hasCheckBoxStructure(element) ? contentElement.firstChild : null;
  154. goog.ui.MenuItemRenderer.superClass_.setContent.call(this, element, content);
  155. if (checkBoxElement && !this.hasCheckBoxStructure(element)) {
  156. // The call to setContent() blew away the checkbox element; reattach it.
  157. contentElement.insertBefore(
  158. checkBoxElement, contentElement.firstChild || null);
  159. }
  160. };
  161. /**
  162. * Returns true if the element appears to have a proper menu item structure by
  163. * checking whether its first child has the appropriate structural class name.
  164. * @param {Element} element Element to check.
  165. * @return {boolean} Whether the element appears to have a proper menu item DOM.
  166. * @protected
  167. */
  168. goog.ui.MenuItemRenderer.prototype.hasContentStructure = function(element) {
  169. var child = goog.dom.getFirstElementChild(element);
  170. var contentClassName = this.getCompositeCssClass_(
  171. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CONTENT);
  172. return !!child && goog.dom.classlist.contains(child, contentClassName);
  173. };
  174. /**
  175. * Wraps the given text caption or existing DOM node(s) in a structural element
  176. * containing the menu item's contents.
  177. * @param {goog.ui.ControlContent} content Menu item contents.
  178. * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
  179. * @return {Element} Menu item content element.
  180. * @protected
  181. */
  182. goog.ui.MenuItemRenderer.prototype.createContent = function(content, dom) {
  183. var contentClassName = this.getCompositeCssClass_(
  184. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CONTENT);
  185. return dom.createDom(goog.dom.TagName.DIV, contentClassName, content);
  186. };
  187. /**
  188. * Enables/disables radio button semantics on the menu item.
  189. * @param {goog.ui.Control} item Menu item to update.
  190. * @param {Element} element Menu item element to update (may be null if the
  191. * item hasn't been rendered yet).
  192. * @param {boolean} selectable Whether the item should be selectable.
  193. */
  194. goog.ui.MenuItemRenderer.prototype.setSelectable = function(
  195. item, element, selectable) {
  196. if (item && element) {
  197. this.setEnableCheckBoxStructure(item, element, selectable);
  198. }
  199. };
  200. /**
  201. * Enables/disables checkbox semantics on the menu item.
  202. * @param {goog.ui.Control} item Menu item to update.
  203. * @param {Element} element Menu item element to update (may be null if the
  204. * item hasn't been rendered yet).
  205. * @param {boolean} checkable Whether the item should be checkable.
  206. */
  207. goog.ui.MenuItemRenderer.prototype.setCheckable = function(
  208. item, element, checkable) {
  209. if (item && element) {
  210. this.setEnableCheckBoxStructure(item, element, checkable);
  211. }
  212. };
  213. /**
  214. * Determines whether the item contains a checkbox element.
  215. * @param {Element} element Menu item root element.
  216. * @return {boolean} Whether the element contains a checkbox element.
  217. * @protected
  218. */
  219. goog.ui.MenuItemRenderer.prototype.hasCheckBoxStructure = function(element) {
  220. var contentElement = this.getContentElement(element);
  221. if (contentElement) {
  222. var child = contentElement.firstChild;
  223. var checkboxClassName = this.getCompositeCssClass_(
  224. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CHECKBOX);
  225. return !!child && goog.dom.isElement(child) &&
  226. goog.dom.classlist.contains(
  227. /** @type {!Element} */ (child), checkboxClassName);
  228. }
  229. return false;
  230. };
  231. /**
  232. * Adds or removes extra markup and CSS styling to the menu item to make it
  233. * selectable or non-selectable, depending on the value of the
  234. * {@code selectable} argument.
  235. * @param {!goog.ui.Control} item Menu item to update.
  236. * @param {!Element} element Menu item element to update.
  237. * @param {boolean} enable Whether to add or remove the checkbox structure.
  238. * @protected
  239. */
  240. goog.ui.MenuItemRenderer.prototype.setEnableCheckBoxStructure = function(
  241. item, element, enable) {
  242. this.setAriaRole(element, item.getPreferredAriaRole());
  243. this.setAriaStates(item, element);
  244. if (enable != this.hasCheckBoxStructure(element)) {
  245. goog.dom.classlist.enable(element, goog.getCssName('goog-option'), enable);
  246. var contentElement = this.getContentElement(element);
  247. if (enable) {
  248. // Insert checkbox structure.
  249. var checkboxClassName = this.getCompositeCssClass_(
  250. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.CHECKBOX);
  251. contentElement.insertBefore(
  252. item.getDomHelper().createDom(
  253. goog.dom.TagName.DIV, checkboxClassName),
  254. contentElement.firstChild || null);
  255. } else {
  256. // Remove checkbox structure.
  257. contentElement.removeChild(contentElement.firstChild);
  258. }
  259. }
  260. };
  261. /**
  262. * Takes a single {@link goog.ui.Component.State}, and returns the
  263. * corresponding CSS class name (null if none). Overrides the superclass
  264. * implementation by using 'highlight' as opposed to 'hover' as the CSS
  265. * class name suffix for the HOVER state, for backwards compatibility.
  266. * @param {goog.ui.Component.State} state Component state.
  267. * @return {string|undefined} CSS class representing the given state
  268. * (undefined if none).
  269. * @override
  270. */
  271. goog.ui.MenuItemRenderer.prototype.getClassForState = function(state) {
  272. switch (state) {
  273. case goog.ui.Component.State.HOVER:
  274. // We use 'highlight' as the suffix, for backwards compatibility.
  275. return this.getCompositeCssClass_(
  276. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.HOVER);
  277. case goog.ui.Component.State.CHECKED:
  278. case goog.ui.Component.State.SELECTED:
  279. // We use 'goog-option-selected' as the class, for backwards
  280. // compatibility.
  281. return goog.getCssName('goog-option-selected');
  282. default:
  283. return goog.ui.MenuItemRenderer.superClass_.getClassForState.call(
  284. this, state);
  285. }
  286. };
  287. /**
  288. * Takes a single CSS class name which may represent a component state, and
  289. * returns the corresponding component state (0x00 if none). Overrides the
  290. * superclass implementation by treating 'goog-option-selected' as special,
  291. * for backwards compatibility.
  292. * @param {string} className CSS class name, possibly representing a component
  293. * state.
  294. * @return {goog.ui.Component.State} state Component state corresponding
  295. * to the given CSS class (0x00 if none).
  296. * @override
  297. */
  298. goog.ui.MenuItemRenderer.prototype.getStateFromClass = function(className) {
  299. var hoverClassName = this.getCompositeCssClass_(
  300. goog.ui.MenuItemRenderer.CompositeCssClassIndex_.HOVER);
  301. switch (className) {
  302. case goog.getCssName('goog-option-selected'):
  303. return goog.ui.Component.State.CHECKED;
  304. case hoverClassName:
  305. return goog.ui.Component.State.HOVER;
  306. default:
  307. return goog.ui.MenuItemRenderer.superClass_.getStateFromClass.call(
  308. this, className);
  309. }
  310. };
  311. /** @override */
  312. goog.ui.MenuItemRenderer.prototype.getCssClass = function() {
  313. return goog.ui.MenuItemRenderer.CSS_CLASS;
  314. };