controlrenderer.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  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 control renderers.
  16. * TODO(attila): If the renderer framework works well, pull it into Component.
  17. *
  18. * @author attila@google.com (Attila Bodis)
  19. */
  20. goog.provide('goog.ui.ControlRenderer');
  21. goog.require('goog.a11y.aria');
  22. goog.require('goog.a11y.aria.Role');
  23. goog.require('goog.a11y.aria.State');
  24. goog.require('goog.array');
  25. goog.require('goog.asserts');
  26. goog.require('goog.dom');
  27. goog.require('goog.dom.TagName');
  28. goog.require('goog.dom.classlist');
  29. goog.require('goog.object');
  30. goog.require('goog.string');
  31. goog.require('goog.style');
  32. goog.require('goog.ui.Component');
  33. goog.require('goog.ui.ControlContent');
  34. goog.require('goog.userAgent');
  35. goog.forwardDeclare('goog.ui.Control'); // circular
  36. /**
  37. * Default renderer for {@link goog.ui.Control}s. Can be used as-is, but
  38. * subclasses of Control will probably want to use renderers specifically
  39. * tailored for them by extending this class. Controls that use renderers
  40. * delegate one or more of the following API methods to the renderer:
  41. * <ul>
  42. * <li>{@code createDom} - renders the DOM for the component
  43. * <li>{@code canDecorate} - determines whether an element can be decorated
  44. * by the component
  45. * <li>{@code decorate} - decorates an existing element with the component
  46. * <li>{@code setState} - updates the appearance of the component based on
  47. * its state
  48. * <li>{@code getContent} - returns the component's content
  49. * <li>{@code setContent} - sets the component's content
  50. * </ul>
  51. * Controls are stateful; renderers, on the other hand, should be stateless and
  52. * reusable.
  53. * @constructor
  54. */
  55. goog.ui.ControlRenderer = function() {};
  56. goog.addSingletonGetter(goog.ui.ControlRenderer);
  57. goog.tagUnsealableClass(goog.ui.ControlRenderer);
  58. /**
  59. * Constructs a new renderer and sets the CSS class that the renderer will use
  60. * as the base CSS class to apply to all elements rendered by that renderer.
  61. * An example to use this function using a color palette:
  62. *
  63. * <pre>
  64. * var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(
  65. * goog.ui.PaletteRenderer, 'my-special-palette');
  66. * var newColorPalette = new goog.ui.ColorPalette(
  67. * colors, myCustomRenderer, opt_domHelper);
  68. * </pre>
  69. *
  70. * Your CSS can look like this now:
  71. * <pre>
  72. * .my-special-palette { }
  73. * .my-special-palette-table { }
  74. * .my-special-palette-cell { }
  75. * etc.
  76. * </pre>
  77. *
  78. * <em>instead</em> of
  79. * <pre>
  80. * .CSS_MY_SPECIAL_PALETTE .goog-palette { }
  81. * .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
  82. * .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
  83. * etc.
  84. * </pre>
  85. *
  86. * You would want to use this functionality when you want an instance of a
  87. * component to have specific styles different than the other components of the
  88. * same type in your application. This avoids using descendant selectors to
  89. * apply the specific styles to this component.
  90. *
  91. * @param {Function} ctor The constructor of the renderer you are trying to
  92. * create.
  93. * @param {string} cssClassName The name of the CSS class for this renderer.
  94. * @return {goog.ui.ControlRenderer} An instance of the desired renderer with
  95. * its getCssClass() method overridden to return the supplied custom CSS
  96. * class name.
  97. */
  98. goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
  99. var renderer = new ctor();
  100. /**
  101. * Returns the CSS class to be applied to the root element of components
  102. * rendered using this renderer.
  103. * @return {string} Renderer-specific CSS class.
  104. */
  105. renderer.getCssClass = function() { return cssClassName; };
  106. return renderer;
  107. };
  108. /**
  109. * Default CSS class to be applied to the root element of components rendered
  110. * by this renderer.
  111. * @type {string}
  112. */
  113. goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');
  114. /**
  115. * Array of arrays of CSS classes that we want composite classes added and
  116. * removed for in IE6 and lower as a workaround for lack of multi-class CSS
  117. * selector support.
  118. *
  119. * Subclasses that have accompanying CSS requiring this workaround should define
  120. * their own static IE6_CLASS_COMBINATIONS constant and override
  121. * getIe6ClassCombinations to return it.
  122. *
  123. * For example, if your stylesheet uses the selector .button.collapse-left
  124. * (and is compiled to .button_collapse-left for the IE6 version of the
  125. * stylesheet,) you should include ['button', 'collapse-left'] in this array
  126. * and the class button_collapse-left will be applied to the root element
  127. * whenever both button and collapse-left are applied individually.
  128. *
  129. * Members of each class name combination will be joined with underscores in the
  130. * order that they're defined in the array. You should alphabetize them (for
  131. * compatibility with the CSS compiler) unless you are doing something special.
  132. * @type {Array<Array<string>>}
  133. */
  134. goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];
  135. /**
  136. * Map of component states to corresponding ARIA attributes. Since the mapping
  137. * of component states to ARIA attributes is neither component- nor
  138. * renderer-specific, this is a static property of the renderer class, and is
  139. * initialized on first use.
  140. * @type {Object<goog.ui.Component.State, goog.a11y.aria.State>}
  141. * @private
  142. */
  143. goog.ui.ControlRenderer.ariaAttributeMap_;
  144. /**
  145. * Map of certain ARIA states to ARIA roles that support them. Used for checked
  146. * and selected Component states because they are used on Components with ARIA
  147. * roles that do not support the corresponding ARIA state.
  148. * @private {!Object<goog.a11y.aria.Role, goog.a11y.aria.State>}
  149. * @const
  150. */
  151. goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_ = goog.object.create(
  152. goog.a11y.aria.Role.BUTTON, goog.a11y.aria.State.PRESSED,
  153. goog.a11y.aria.Role.CHECKBOX, goog.a11y.aria.State.CHECKED,
  154. goog.a11y.aria.Role.MENU_ITEM, goog.a11y.aria.State.SELECTED,
  155. goog.a11y.aria.Role.MENU_ITEM_CHECKBOX, goog.a11y.aria.State.CHECKED,
  156. goog.a11y.aria.Role.MENU_ITEM_RADIO, goog.a11y.aria.State.CHECKED,
  157. goog.a11y.aria.Role.RADIO, goog.a11y.aria.State.CHECKED,
  158. goog.a11y.aria.Role.TAB, goog.a11y.aria.State.SELECTED,
  159. goog.a11y.aria.Role.TREEITEM, goog.a11y.aria.State.SELECTED);
  160. /**
  161. * Returns the ARIA role to be applied to the control.
  162. * See http://wiki/Main/ARIA for more info.
  163. * @return {goog.a11y.aria.Role|undefined} ARIA role.
  164. */
  165. goog.ui.ControlRenderer.prototype.getAriaRole = function() {
  166. // By default, the ARIA role is unspecified.
  167. return undefined;
  168. };
  169. /**
  170. * Returns the control's contents wrapped in a DIV, with the renderer's own
  171. * CSS class and additional state-specific classes applied to it.
  172. * @param {goog.ui.Control} control Control to render.
  173. * @return {Element} Root element for the control.
  174. */
  175. goog.ui.ControlRenderer.prototype.createDom = function(control) {
  176. // Create and return DIV wrapping contents.
  177. var element = control.getDomHelper().createDom(
  178. goog.dom.TagName.DIV, this.getClassNames(control).join(' '),
  179. control.getContent());
  180. return element;
  181. };
  182. /**
  183. * Takes the control's root element and returns the parent element of the
  184. * control's contents. Since by default controls are rendered as a single
  185. * DIV, the default implementation returns the element itself. Subclasses
  186. * with more complex DOM structures must override this method as needed.
  187. * @param {Element} element Root element of the control whose content element
  188. * is to be returned.
  189. * @return {Element} The control's content element.
  190. */
  191. goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
  192. return element;
  193. };
  194. /**
  195. * Updates the control's DOM by adding or removing the specified class name
  196. * to/from its root element. May add additional combined classes as needed in
  197. * IE6 and lower. Because of this, subclasses should use this method when
  198. * modifying class names on the control's root element.
  199. * @param {goog.ui.Control|Element} control Control instance (or root element)
  200. * to be updated.
  201. * @param {string} className CSS class name to add or remove.
  202. * @param {boolean} enable Whether to add or remove the class name.
  203. */
  204. goog.ui.ControlRenderer.prototype.enableClassName = function(
  205. control, className, enable) {
  206. var element = /** @type {Element} */ (
  207. control.getElement ? control.getElement() : control);
  208. if (element) {
  209. var classNames = [className];
  210. // For IE6, we need to enable any combined classes involving this class
  211. // as well.
  212. // TODO(user): Remove this as IE6 is no longer in use.
  213. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
  214. classNames = this.getAppliedCombinedClassNames_(
  215. goog.dom.classlist.get(element), className);
  216. classNames.push(className);
  217. }
  218. goog.dom.classlist.enableAll(element, classNames, enable);
  219. }
  220. };
  221. /**
  222. * Updates the control's DOM by adding or removing the specified extra class
  223. * name to/from its element.
  224. * @param {goog.ui.Control} control Control to be updated.
  225. * @param {string} className CSS class name to add or remove.
  226. * @param {boolean} enable Whether to add or remove the class name.
  227. */
  228. goog.ui.ControlRenderer.prototype.enableExtraClassName = function(
  229. control, className, enable) {
  230. // The base class implementation is trivial; subclasses should override as
  231. // needed.
  232. this.enableClassName(control, className, enable);
  233. };
  234. /**
  235. * Returns true if this renderer can decorate the element, false otherwise.
  236. * The default implementation always returns true.
  237. * @param {Element} element Element to decorate.
  238. * @return {boolean} Whether the renderer can decorate the element.
  239. */
  240. goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
  241. return true;
  242. };
  243. /**
  244. * Default implementation of {@code decorate} for {@link goog.ui.Control}s.
  245. * Initializes the control's ID, content, and state based on the ID of the
  246. * element, its child nodes, and its CSS classes, respectively. Returns the
  247. * element.
  248. * @param {goog.ui.Control} control Control instance to decorate the element.
  249. * @param {Element} element Element to decorate.
  250. * @return {Element} Decorated element.
  251. */
  252. goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
  253. // Set the control's ID to the decorated element's DOM ID, if any.
  254. if (element.id) {
  255. control.setId(element.id);
  256. }
  257. // Set the control's content to the decorated element's content.
  258. var contentElem = this.getContentElement(element);
  259. if (contentElem && contentElem.firstChild) {
  260. control.setContentInternal(
  261. contentElem.firstChild.nextSibling ?
  262. goog.array.clone(contentElem.childNodes) :
  263. contentElem.firstChild);
  264. } else {
  265. control.setContentInternal(null);
  266. }
  267. // Initialize the control's state based on the decorated element's CSS class.
  268. // This implementation is optimized to minimize object allocations, string
  269. // comparisons, and DOM access.
  270. var state = 0x00;
  271. var rendererClassName = this.getCssClass();
  272. var structuralClassName = this.getStructuralCssClass();
  273. var hasRendererClassName = false;
  274. var hasStructuralClassName = false;
  275. var hasCombinedClassName = false;
  276. var classNames = goog.array.toArray(goog.dom.classlist.get(element));
  277. goog.array.forEach(classNames, function(className) {
  278. if (!hasRendererClassName && className == rendererClassName) {
  279. hasRendererClassName = true;
  280. if (structuralClassName == rendererClassName) {
  281. hasStructuralClassName = true;
  282. }
  283. } else if (!hasStructuralClassName && className == structuralClassName) {
  284. hasStructuralClassName = true;
  285. } else {
  286. state |= this.getStateFromClass(className);
  287. }
  288. if (this.getStateFromClass(className) == goog.ui.Component.State.DISABLED) {
  289. goog.asserts.assertElement(contentElem);
  290. if (goog.dom.isFocusableTabIndex(contentElem)) {
  291. goog.dom.setFocusableTabIndex(contentElem, false);
  292. }
  293. }
  294. }, this);
  295. control.setStateInternal(state);
  296. // Make sure the element has the renderer's CSS classes applied, as well as
  297. // any extra class names set on the control.
  298. if (!hasRendererClassName) {
  299. classNames.push(rendererClassName);
  300. if (structuralClassName == rendererClassName) {
  301. hasStructuralClassName = true;
  302. }
  303. }
  304. if (!hasStructuralClassName) {
  305. classNames.push(structuralClassName);
  306. }
  307. var extraClassNames = control.getExtraClassNames();
  308. if (extraClassNames) {
  309. classNames.push.apply(classNames, extraClassNames);
  310. }
  311. // For IE6, rewrite all classes on the decorated element if any combined
  312. // classes apply.
  313. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
  314. var combinedClasses = this.getAppliedCombinedClassNames_(classNames);
  315. if (combinedClasses.length > 0) {
  316. classNames.push.apply(classNames, combinedClasses);
  317. hasCombinedClassName = true;
  318. }
  319. }
  320. // Only write to the DOM if new class names had to be added to the element.
  321. if (!hasRendererClassName || !hasStructuralClassName || extraClassNames ||
  322. hasCombinedClassName) {
  323. goog.dom.classlist.set(element, classNames.join(' '));
  324. }
  325. return element;
  326. };
  327. /**
  328. * Initializes the control's DOM by configuring properties that can only be set
  329. * after the DOM has entered the document. This implementation sets up BiDi
  330. * and keyboard focus. Called from {@link goog.ui.Control#enterDocument}.
  331. * @param {goog.ui.Control} control Control whose DOM is to be initialized
  332. * as it enters the document.
  333. */
  334. goog.ui.ControlRenderer.prototype.initializeDom = function(control) {
  335. // Initialize render direction (BiDi). We optimize the left-to-right render
  336. // direction by assuming that elements are left-to-right by default, and only
  337. // updating their styling if they are explicitly set to right-to-left.
  338. if (control.isRightToLeft()) {
  339. this.setRightToLeft(control.getElement(), true);
  340. }
  341. // Initialize keyboard focusability (tab index). We assume that components
  342. // aren't focusable by default (i.e have no tab index), and only touch the
  343. // DOM if the component is focusable, enabled, and visible, and therefore
  344. // needs a tab index.
  345. if (control.isEnabled()) {
  346. this.setFocusable(control, control.isVisible());
  347. }
  348. };
  349. /**
  350. * Sets the element's ARIA role.
  351. * @param {Element} element Element to update.
  352. * @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.
  353. */
  354. goog.ui.ControlRenderer.prototype.setAriaRole = function(
  355. element, opt_preferredRole) {
  356. var ariaRole = opt_preferredRole || this.getAriaRole();
  357. if (ariaRole) {
  358. goog.asserts.assert(
  359. element, 'The element passed as a first parameter cannot be null.');
  360. var currentRole = goog.a11y.aria.getRole(element);
  361. if (ariaRole == currentRole) {
  362. return;
  363. }
  364. goog.a11y.aria.setRole(element, ariaRole);
  365. }
  366. };
  367. /**
  368. * Sets the element's ARIA attributes, including distinguishing between
  369. * universally supported ARIA properties and ARIA states that are only
  370. * supported by certain ARIA roles. Only attributes which are initialized to be
  371. * true will be set.
  372. * @param {!goog.ui.Control} control Control whose ARIA state will be updated.
  373. * @param {!Element} element Element whose ARIA state is to be updated.
  374. */
  375. goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
  376. goog.asserts.assert(control);
  377. goog.asserts.assert(element);
  378. var ariaLabel = control.getAriaLabel();
  379. if (goog.isDefAndNotNull(ariaLabel)) {
  380. this.setAriaLabel(element, ariaLabel);
  381. }
  382. if (!control.isVisible()) {
  383. goog.a11y.aria.setState(
  384. element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
  385. }
  386. if (!control.isEnabled()) {
  387. this.updateAriaState(
  388. element, goog.ui.Component.State.DISABLED, !control.isEnabled());
  389. }
  390. if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
  391. this.updateAriaState(
  392. element, goog.ui.Component.State.SELECTED, control.isSelected());
  393. }
  394. if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
  395. this.updateAriaState(
  396. element, goog.ui.Component.State.CHECKED, control.isChecked());
  397. }
  398. if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
  399. this.updateAriaState(
  400. element, goog.ui.Component.State.OPENED, control.isOpen());
  401. }
  402. };
  403. /**
  404. * Sets the element's ARIA label. This should be overriden by subclasses that
  405. * don't apply the role directly on control.element_.
  406. * @param {!Element} element Element whose ARIA label is to be updated.
  407. * @param {string} ariaLabel Label to add to the element.
  408. */
  409. goog.ui.ControlRenderer.prototype.setAriaLabel = function(element, ariaLabel) {
  410. goog.a11y.aria.setLabel(element, ariaLabel);
  411. };
  412. /**
  413. * Allows or disallows text selection within the control's DOM.
  414. * @param {Element} element The control's root element.
  415. * @param {boolean} allow Whether the element should allow text selection.
  416. */
  417. goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(
  418. element, allow) {
  419. // On all browsers other than IE and Opera, it isn't necessary to recursively
  420. // apply unselectable styling to the element's children.
  421. goog.style.setUnselectable(
  422. element, !allow, !goog.userAgent.IE && !goog.userAgent.OPERA);
  423. };
  424. /**
  425. * Applies special styling to/from the control's element if it is rendered
  426. * right-to-left, and removes it if it is rendered left-to-right.
  427. * @param {Element} element The control's root element.
  428. * @param {boolean} rightToLeft Whether the component is rendered
  429. * right-to-left.
  430. */
  431. goog.ui.ControlRenderer.prototype.setRightToLeft = function(
  432. element, rightToLeft) {
  433. this.enableClassName(
  434. element, goog.getCssName(this.getStructuralCssClass(), 'rtl'),
  435. rightToLeft);
  436. };
  437. /**
  438. * Returns true if the control's key event target supports keyboard focus
  439. * (based on its {@code tabIndex} attribute), false otherwise.
  440. * @param {goog.ui.Control} control Control whose key event target is to be
  441. * checked.
  442. * @return {boolean} Whether the control's key event target is focusable.
  443. */
  444. goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
  445. var keyTarget;
  446. if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
  447. (keyTarget = control.getKeyEventTarget())) {
  448. return goog.dom.isFocusableTabIndex(keyTarget);
  449. }
  450. return false;
  451. };
  452. /**
  453. * Updates the control's key event target to make it focusable or non-focusable
  454. * via its {@code tabIndex} attribute. Does nothing if the control doesn't
  455. * support the {@code FOCUSED} state, or if it has no key event target.
  456. * @param {goog.ui.Control} control Control whose key event target is to be
  457. * updated.
  458. * @param {boolean} focusable Whether to enable keyboard focus support on the
  459. * control's key event target.
  460. */
  461. goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
  462. var keyTarget;
  463. if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
  464. (keyTarget = control.getKeyEventTarget())) {
  465. if (!focusable && control.isFocused()) {
  466. // Blur before hiding. Note that IE calls onblur handlers asynchronously.
  467. try {
  468. keyTarget.blur();
  469. } catch (e) {
  470. // TODO(user|user): Find out why this fails on IE.
  471. }
  472. // The blur event dispatched by the key event target element when blur()
  473. // was called on it should have been handled by the control's handleBlur()
  474. // method, so at this point the control should no longer be focused.
  475. // However, blur events are unreliable on IE and FF3, so if at this point
  476. // the control is still focused, we trigger its handleBlur() method
  477. // programmatically.
  478. if (control.isFocused()) {
  479. control.handleBlur(null);
  480. }
  481. }
  482. // Don't overwrite existing tab index values unless needed.
  483. if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
  484. goog.dom.setFocusableTabIndex(keyTarget, focusable);
  485. }
  486. }
  487. };
  488. /**
  489. * Shows or hides the element.
  490. * @param {Element} element Element to update.
  491. * @param {boolean} visible Whether to show the element.
  492. */
  493. goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {
  494. // The base class implementation is trivial; subclasses should override as
  495. // needed. It should be possible to do animated reveals, for example.
  496. goog.style.setElementShown(element, visible);
  497. if (element) {
  498. goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
  499. }
  500. };
  501. /**
  502. * Updates the appearance of the control in response to a state change.
  503. * @param {goog.ui.Control} control Control instance to update.
  504. * @param {goog.ui.Component.State} state State to enable or disable.
  505. * @param {boolean} enable Whether the control is entering or exiting the state.
  506. */
  507. goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
  508. var element = control.getElement();
  509. if (element) {
  510. var className = this.getClassForState(state);
  511. if (className) {
  512. this.enableClassName(control, className, enable);
  513. }
  514. this.updateAriaState(element, state, enable);
  515. }
  516. };
  517. /**
  518. * Updates the element's ARIA (accessibility) attributes , including
  519. * distinguishing between universally supported ARIA properties and ARIA states
  520. * that are only supported by certain ARIA roles.
  521. * @param {Element} element Element whose ARIA state is to be updated.
  522. * @param {goog.ui.Component.State} state Component state being enabled or
  523. * disabled.
  524. * @param {boolean} enable Whether the state is being enabled or disabled.
  525. * @protected
  526. */
  527. goog.ui.ControlRenderer.prototype.updateAriaState = function(
  528. element, state, enable) {
  529. // Ensure the ARIA attribute map exists.
  530. if (!goog.ui.ControlRenderer.ariaAttributeMap_) {
  531. goog.ui.ControlRenderer.ariaAttributeMap_ = goog.object.create(
  532. goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,
  533. goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,
  534. goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,
  535. goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);
  536. }
  537. goog.asserts.assert(
  538. element, 'The element passed as a first parameter cannot be null.');
  539. var ariaAttr = goog.ui.ControlRenderer.getAriaStateForAriaRole_(
  540. element, goog.ui.ControlRenderer.ariaAttributeMap_[state]);
  541. if (ariaAttr) {
  542. goog.a11y.aria.setState(element, ariaAttr, enable);
  543. }
  544. };
  545. /**
  546. * Returns the appropriate ARIA attribute based on ARIA role if the ARIA
  547. * attribute is an ARIA state.
  548. * @param {!Element} element The element from which to get the ARIA role for
  549. * matching ARIA state.
  550. * @param {goog.a11y.aria.State} attr The ARIA attribute to check to see if it
  551. * can be applied to the given ARIA role.
  552. * @return {goog.a11y.aria.State} An ARIA attribute that can be applied to the
  553. * given ARIA role.
  554. * @private
  555. */
  556. goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {
  557. var role = goog.a11y.aria.getRole(element);
  558. if (!role) {
  559. return attr;
  560. }
  561. role = /** @type {goog.a11y.aria.Role} */ (role);
  562. var matchAttr = goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_[role] || attr;
  563. return goog.ui.ControlRenderer.isAriaState_(attr) ? matchAttr : attr;
  564. };
  565. /**
  566. * Determines if the given ARIA attribute is an ARIA property or ARIA state.
  567. * @param {goog.a11y.aria.State} attr The ARIA attribute to classify.
  568. * @return {boolean} If the ARIA attribute is an ARIA state.
  569. * @private
  570. */
  571. goog.ui.ControlRenderer.isAriaState_ = function(attr) {
  572. return attr == goog.a11y.aria.State.CHECKED ||
  573. attr == goog.a11y.aria.State.SELECTED;
  574. };
  575. /**
  576. * Takes a control's root element, and sets its content to the given text
  577. * caption or DOM structure. The default implementation replaces the children
  578. * of the given element. Renderers that create more complex DOM structures
  579. * must override this method accordingly.
  580. * @param {Element} element The control's root element.
  581. * @param {goog.ui.ControlContent} content Text caption or DOM structure to be
  582. * set as the control's content. The DOM nodes will not be cloned, they
  583. * will only moved under the content element of the control.
  584. */
  585. goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
  586. var contentElem = this.getContentElement(element);
  587. if (contentElem) {
  588. goog.dom.removeChildren(contentElem);
  589. if (content) {
  590. if (goog.isString(content)) {
  591. goog.dom.setTextContent(contentElem, content);
  592. } else {
  593. var childHandler = function(child) {
  594. if (child) {
  595. var doc = goog.dom.getOwnerDocument(contentElem);
  596. contentElem.appendChild(
  597. goog.isString(child) ? doc.createTextNode(child) : child);
  598. }
  599. };
  600. if (goog.isArray(content)) {
  601. // Array of nodes.
  602. goog.array.forEach(content, childHandler);
  603. } else if (goog.isArrayLike(content) && !('nodeType' in content)) {
  604. // NodeList. The second condition filters out TextNode which also has
  605. // length attribute but is not array like. The nodes have to be cloned
  606. // because childHandler removes them from the list during iteration.
  607. goog.array.forEach(
  608. goog.array.clone(/** @type {!NodeList<?>} */ (content)),
  609. childHandler);
  610. } else {
  611. // Node or string.
  612. childHandler(content);
  613. }
  614. }
  615. }
  616. }
  617. };
  618. /**
  619. * Returns the element within the component's DOM that should receive keyboard
  620. * focus (null if none). The default implementation returns the control's root
  621. * element.
  622. * @param {goog.ui.Control} control Control whose key event target is to be
  623. * returned.
  624. * @return {Element} The key event target.
  625. */
  626. goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
  627. return control.getElement();
  628. };
  629. // CSS class name management.
  630. /**
  631. * Returns the CSS class name to be applied to the root element of all
  632. * components rendered or decorated using this renderer. The class name
  633. * is expected to uniquely identify the renderer class, i.e. no two
  634. * renderer classes are expected to share the same CSS class name.
  635. * @return {string} Renderer-specific CSS class name.
  636. */
  637. goog.ui.ControlRenderer.prototype.getCssClass = function() {
  638. return goog.ui.ControlRenderer.CSS_CLASS;
  639. };
  640. /**
  641. * Returns an array of combinations of classes to apply combined class names for
  642. * in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This
  643. * method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be
  644. * compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS
  645. * static constant instead.
  646. * @return {Array<Array<string>>} Array of class name combinations.
  647. */
  648. goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
  649. return [];
  650. };
  651. /**
  652. * Returns the name of a DOM structure-specific CSS class to be applied to the
  653. * root element of all components rendered or decorated using this renderer.
  654. * Unlike the class name returned by {@link #getCssClass}, the structural class
  655. * name may be shared among different renderers that generate similar DOM
  656. * structures. The structural class name also serves as the basis of derived
  657. * class names used to identify and style structural elements of the control's
  658. * DOM, as well as the basis for state-specific class names. The default
  659. * implementation returns the same class name as {@link #getCssClass}, but
  660. * subclasses are expected to override this method as needed.
  661. * @return {string} DOM structure-specific CSS class name (same as the renderer-
  662. * specific CSS class name by default).
  663. */
  664. goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
  665. return this.getCssClass();
  666. };
  667. /**
  668. * Returns all CSS class names applicable to the given control, based on its
  669. * state. The return value is an array of strings containing
  670. * <ol>
  671. * <li>the renderer-specific CSS class returned by {@link #getCssClass},
  672. * followed by
  673. * <li>the structural CSS class returned by {@link getStructuralCssClass} (if
  674. * different from the renderer-specific CSS class), followed by
  675. * <li>any state-specific classes returned by {@link #getClassNamesForState},
  676. * followed by
  677. * <li>any extra classes returned by the control's {@code getExtraClassNames}
  678. * method and
  679. * <li>for IE6 and lower, additional combined classes from
  680. * {@link getAppliedCombinedClassNames_}.
  681. * </ol>
  682. * Since all controls have at least one renderer-specific CSS class name, this
  683. * method is guaranteed to return an array of at least one element.
  684. * @param {goog.ui.Control} control Control whose CSS classes are to be
  685. * returned.
  686. * @return {!Array<string>} Array of CSS class names applicable to the control.
  687. * @protected
  688. */
  689. goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
  690. var cssClass = this.getCssClass();
  691. // Start with the renderer-specific class name.
  692. var classNames = [cssClass];
  693. // Add structural class name, if different.
  694. var structuralCssClass = this.getStructuralCssClass();
  695. if (structuralCssClass != cssClass) {
  696. classNames.push(structuralCssClass);
  697. }
  698. // Add state-specific class names, if any.
  699. var classNamesForState = this.getClassNamesForState(control.getState());
  700. classNames.push.apply(classNames, classNamesForState);
  701. // Add extra class names, if any.
  702. var extraClassNames = control.getExtraClassNames();
  703. if (extraClassNames) {
  704. classNames.push.apply(classNames, extraClassNames);
  705. }
  706. // Add composite classes for IE6 support
  707. if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
  708. classNames.push.apply(
  709. classNames, this.getAppliedCombinedClassNames_(classNames));
  710. }
  711. return classNames;
  712. };
  713. /**
  714. * Returns an array of all the combined class names that should be applied based
  715. * on the given list of classes. Checks the result of
  716. * {@link getIe6ClassCombinations} for any combinations that have all
  717. * members contained in classes. If a combination matches, the members are
  718. * joined with an underscore (in order), and added to the return array.
  719. *
  720. * If opt_includedClass is provided, return only the combined classes that have
  721. * all members contained in classes AND include opt_includedClass as well.
  722. * opt_includedClass is added to classes as well.
  723. * @param {IArrayLike<string>} classes Array-like thing of classes to
  724. * return matching combined classes for.
  725. * @param {?string=} opt_includedClass If provided, get only the combined
  726. * classes that include this one.
  727. * @return {!Array<string>} Array of combined class names that should be
  728. * applied.
  729. * @private
  730. */
  731. goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
  732. classes, opt_includedClass) {
  733. var toAdd = [];
  734. if (opt_includedClass) {
  735. classes = goog.array.concat(classes, [opt_includedClass]);
  736. }
  737. goog.array.forEach(this.getIe6ClassCombinations(), function(combo) {
  738. if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&
  739. (!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {
  740. toAdd.push(combo.join('_'));
  741. }
  742. });
  743. return toAdd;
  744. };
  745. /**
  746. * Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array
  747. * of the appropriate class names representing the given state, suitable to be
  748. * applied to the root element of a component rendered using this renderer, or
  749. * null if no state-specific classes need to be applied. This default
  750. * implementation uses the renderer's {@link getClassForState} method to
  751. * generate each state-specific class.
  752. * @param {number} state Bit mask of component states.
  753. * @return {!Array<string>} Array of CSS class names representing the given
  754. * state.
  755. * @protected
  756. */
  757. goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
  758. var classNames = [];
  759. while (state) {
  760. // For each enabled state, push the corresponding CSS class name onto
  761. // the classNames array.
  762. var mask = state & -state; // Least significant bit
  763. classNames.push(
  764. this.getClassForState(
  765. /** @type {goog.ui.Component.State} */ (mask)));
  766. state &= ~mask;
  767. }
  768. return classNames;
  769. };
  770. /**
  771. * Takes a single {@link goog.ui.Component.State}, and returns the
  772. * corresponding CSS class name (null if none).
  773. * @param {goog.ui.Component.State} state Component state.
  774. * @return {string|undefined} CSS class representing the given state (undefined
  775. * if none).
  776. * @protected
  777. */
  778. goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
  779. if (!this.classByState_) {
  780. this.createClassByStateMap_();
  781. }
  782. return this.classByState_[state];
  783. };
  784. /**
  785. * Takes a single CSS class name which may represent a component state, and
  786. * returns the corresponding component state (0x00 if none).
  787. * @param {string} className CSS class name, possibly representing a component
  788. * state.
  789. * @return {goog.ui.Component.State} state Component state corresponding
  790. * to the given CSS class (0x00 if none).
  791. * @protected
  792. */
  793. goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
  794. if (!this.stateByClass_) {
  795. this.createStateByClassMap_();
  796. }
  797. var state = parseInt(this.stateByClass_[className], 10);
  798. return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
  799. };
  800. /**
  801. * Creates the lookup table of states to classes, used during state changes.
  802. * @private
  803. */
  804. goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
  805. var baseClass = this.getStructuralCssClass();
  806. // This ensures space-separated css classnames are not allowed, which some
  807. // ControlRenderers had been doing. See http://b/13694665.
  808. var isValidClassName =
  809. !goog.string.contains(goog.string.normalizeWhitespace(baseClass), ' ');
  810. goog.asserts.assert(
  811. isValidClassName,
  812. 'ControlRenderer has an invalid css class: \'' + baseClass + '\'');
  813. /**
  814. * Map of component states to state-specific structural class names,
  815. * used when changing the DOM in response to a state change. Precomputed
  816. * and cached on first use to minimize object allocations and string
  817. * concatenation.
  818. * @type {Object}
  819. * @private
  820. */
  821. this.classByState_ = goog.object.create(
  822. goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),
  823. goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),
  824. goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),
  825. goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),
  826. goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),
  827. goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),
  828. goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));
  829. };
  830. /**
  831. * Creates the lookup table of classes to states, used during decoration.
  832. * @private
  833. */
  834. goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {
  835. // We need the classByState_ map so we can transpose it.
  836. if (!this.classByState_) {
  837. this.createClassByStateMap_();
  838. }
  839. /**
  840. * Map of state-specific structural class names to component states,
  841. * used during element decoration. Precomputed and cached on first use
  842. * to minimize object allocations and string concatenation.
  843. * @type {Object}
  844. * @private
  845. */
  846. this.stateByClass_ = goog.object.transpose(this.classByState_);
  847. };