1 // Copyright 2008 The Closure Library Authors. All Rights Reserved.
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
7 // http://www.apache.org/licenses/LICENSE-2.0
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.
16 * @fileoverview Base class for control renderers.
17 * TODO(attila): If the renderer framework works well, pull it into Component.
19 * @author attila@google.com (Attila Bodis)
22 goog.provide('goog.ui.ControlRenderer');
24 goog.require('goog.a11y.aria');
25 goog.require('goog.a11y.aria.Role');
26 goog.require('goog.a11y.aria.State');
27 goog.require('goog.array');
28 goog.require('goog.asserts');
29 goog.require('goog.dom');
30 goog.require('goog.dom.classlist');
31 goog.require('goog.object');
32 goog.require('goog.string');
33 goog.require('goog.style');
34 goog.require('goog.ui.Component');
35 goog.require('goog.userAgent');
40 * Default renderer for {@link goog.ui.Control}s. Can be used as-is, but
41 * subclasses of Control will probably want to use renderers specifically
42 * tailored for them by extending this class. Controls that use renderers
43 * delegate one or more of the following API methods to the renderer:
45 * <li>{@code createDom} - renders the DOM for the component
46 * <li>{@code canDecorate} - determines whether an element can be decorated
48 * <li>{@code decorate} - decorates an existing element with the component
49 * <li>{@code setState} - updates the appearance of the component based on
51 * <li>{@code getContent} - returns the component's content
52 * <li>{@code setContent} - sets the component's content
54 * Controls are stateful; renderers, on the other hand, should be stateless and
58 goog.ui.ControlRenderer = function() {
60 goog.addSingletonGetter(goog.ui.ControlRenderer);
61 goog.tagUnsealableClass(goog.ui.ControlRenderer);
65 * Constructs a new renderer and sets the CSS class that the renderer will use
66 * as the base CSS class to apply to all elements rendered by that renderer.
67 * An example to use this function using a color palette:
70 * var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(
71 * goog.ui.PaletteRenderer, 'my-special-palette');
72 * var newColorPalette = new goog.ui.ColorPalette(
73 * colors, myCustomRenderer, opt_domHelper);
76 * Your CSS can look like this now:
78 * .my-special-palette { }
79 * .my-special-palette-table { }
80 * .my-special-palette-cell { }
86 * .CSS_MY_SPECIAL_PALETTE .goog-palette { }
87 * .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
88 * .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
92 * You would want to use this functionality when you want an instance of a
93 * component to have specific styles different than the other components of the
94 * same type in your application. This avoids using descendant selectors to
95 * apply the specific styles to this component.
97 * @param {Function} ctor The constructor of the renderer you are trying to
99 * @param {string} cssClassName The name of the CSS class for this renderer.
100 * @return {goog.ui.ControlRenderer} An instance of the desired renderer with
101 * its getCssClass() method overridden to return the supplied custom CSS
104 goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
105 var renderer = new ctor();
108 * Returns the CSS class to be applied to the root element of components
109 * rendered using this renderer.
110 * @return {string} Renderer-specific CSS class.
112 renderer.getCssClass = function() {
121 * Default CSS class to be applied to the root element of components rendered
125 goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');
129 * Array of arrays of CSS classes that we want composite classes added and
130 * removed for in IE6 and lower as a workaround for lack of multi-class CSS
133 * Subclasses that have accompanying CSS requiring this workaround should define
134 * their own static IE6_CLASS_COMBINATIONS constant and override
135 * getIe6ClassCombinations to return it.
137 * For example, if your stylesheet uses the selector .button.collapse-left
138 * (and is compiled to .button_collapse-left for the IE6 version of the
139 * stylesheet,) you should include ['button', 'collapse-left'] in this array
140 * and the class button_collapse-left will be applied to the root element
141 * whenever both button and collapse-left are applied individually.
143 * Members of each class name combination will be joined with underscores in the
144 * order that they're defined in the array. You should alphabetize them (for
145 * compatibility with the CSS compiler) unless you are doing something special.
146 * @type {Array.<Array.<string>>}
148 goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];
152 * Map of component states to corresponding ARIA attributes. Since the mapping
153 * of component states to ARIA attributes is neither component- nor
154 * renderer-specific, this is a static property of the renderer class, and is
155 * initialized on first use.
156 * @type {Object.<goog.ui.Component.State, goog.a11y.aria.State>}
160 goog.ui.ControlRenderer.ARIA_ATTRIBUTE_MAP_;
164 * Map of certain ARIA states to ARIA roles that support them. Used for checked
165 * and selected Component states because they are used on Components with ARIA
166 * roles that do not support the corresponding ARIA state.
167 * @private {!Object.<goog.a11y.aria.Role, goog.a11y.aria.State>}
170 goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_ = goog.object.create(
171 goog.a11y.aria.Role.BUTTON, goog.a11y.aria.State.PRESSED,
172 goog.a11y.aria.Role.CHECKBOX, goog.a11y.aria.State.CHECKED,
173 goog.a11y.aria.Role.MENU_ITEM, goog.a11y.aria.State.SELECTED,
174 goog.a11y.aria.Role.MENU_ITEM_CHECKBOX, goog.a11y.aria.State.CHECKED,
175 goog.a11y.aria.Role.MENU_ITEM_RADIO, goog.a11y.aria.State.CHECKED,
176 goog.a11y.aria.Role.RADIO, goog.a11y.aria.State.CHECKED,
177 goog.a11y.aria.Role.TAB, goog.a11y.aria.State.SELECTED,
178 goog.a11y.aria.Role.TREEITEM, goog.a11y.aria.State.SELECTED);
182 * Returns the ARIA role to be applied to the control.
183 * See http://wiki/Main/ARIA for more info.
184 * @return {goog.a11y.aria.Role|undefined} ARIA role.
186 goog.ui.ControlRenderer.prototype.getAriaRole = function() {
187 // By default, the ARIA role is unspecified.
193 * Returns the control's contents wrapped in a DIV, with the renderer's own
194 * CSS class and additional state-specific classes applied to it.
195 * @param {goog.ui.Control} control Control to render.
196 * @return {Element} Root element for the control.
198 goog.ui.ControlRenderer.prototype.createDom = function(control) {
199 // Create and return DIV wrapping contents.
200 var element = control.getDomHelper().createDom(
201 'div', this.getClassNames(control).join(' '), control.getContent());
203 this.setAriaStates(control, element);
209 * Takes the control's root element and returns the parent element of the
210 * control's contents. Since by default controls are rendered as a single
211 * DIV, the default implementation returns the element itself. Subclasses
212 * with more complex DOM structures must override this method as needed.
213 * @param {Element} element Root element of the control whose content element
215 * @return {Element} The control's content element.
217 goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
223 * Updates the control's DOM by adding or removing the specified class name
224 * to/from its root element. May add additional combined classes as needed in
225 * IE6 and lower. Because of this, subclasses should use this method when
226 * modifying class names on the control's root element.
227 * @param {goog.ui.Control|Element} control Control instance (or root element)
229 * @param {string} className CSS class name to add or remove.
230 * @param {boolean} enable Whether to add or remove the class name.
232 goog.ui.ControlRenderer.prototype.enableClassName = function(control,
234 var element = /** @type {Element} */ (
235 control.getElement ? control.getElement() : control);
237 var classNames = [className];
239 // For IE6, we need to enable any combined classes involving this class
241 // TODO(user): Remove this as IE6 is no longer in use.
242 if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
243 classNames = this.getAppliedCombinedClassNames_(
244 goog.dom.classlist.get(element), className);
245 classNames.push(className);
248 goog.dom.classlist.enableAll(element, classNames, enable);
254 * Updates the control's DOM by adding or removing the specified extra class
255 * name to/from its element.
256 * @param {goog.ui.Control} control Control to be updated.
257 * @param {string} className CSS class name to add or remove.
258 * @param {boolean} enable Whether to add or remove the class name.
260 goog.ui.ControlRenderer.prototype.enableExtraClassName = function(control,
262 // The base class implementation is trivial; subclasses should override as
264 this.enableClassName(control, className, enable);
269 * Returns true if this renderer can decorate the element, false otherwise.
270 * The default implementation always returns true.
271 * @param {Element} element Element to decorate.
272 * @return {boolean} Whether the renderer can decorate the element.
274 goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
280 * Default implementation of {@code decorate} for {@link goog.ui.Control}s.
281 * Initializes the control's ID, content, and state based on the ID of the
282 * element, its child nodes, and its CSS classes, respectively. Returns the
284 * @param {goog.ui.Control} control Control instance to decorate the element.
285 * @param {Element} element Element to decorate.
286 * @return {Element} Decorated element.
288 goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
289 // Set the control's ID to the decorated element's DOM ID, if any.
291 control.setId(element.id);
294 // Set the control's content to the decorated element's content.
295 var contentElem = this.getContentElement(element);
296 if (contentElem && contentElem.firstChild) {
297 control.setContentInternal(contentElem.firstChild.nextSibling ?
298 goog.array.clone(contentElem.childNodes) : contentElem.firstChild);
300 control.setContentInternal(null);
303 // Initialize the control's state based on the decorated element's CSS class.
304 // This implementation is optimized to minimize object allocations, string
305 // comparisons, and DOM access.
307 var rendererClassName = this.getCssClass();
308 var structuralClassName = this.getStructuralCssClass();
309 var hasRendererClassName = false;
310 var hasStructuralClassName = false;
311 var hasCombinedClassName = false;
312 var classNames = goog.array.toArray(goog.dom.classlist.get(element));
313 goog.array.forEach(classNames, function(className) {
314 if (!hasRendererClassName && className == rendererClassName) {
315 hasRendererClassName = true;
316 if (structuralClassName == rendererClassName) {
317 hasStructuralClassName = true;
319 } else if (!hasStructuralClassName && className == structuralClassName) {
320 hasStructuralClassName = true;
322 state |= this.getStateFromClass(className);
325 control.setStateInternal(state);
327 // Make sure the element has the renderer's CSS classes applied, as well as
328 // any extra class names set on the control.
329 if (!hasRendererClassName) {
330 classNames.push(rendererClassName);
331 if (structuralClassName == rendererClassName) {
332 hasStructuralClassName = true;
335 if (!hasStructuralClassName) {
336 classNames.push(structuralClassName);
338 var extraClassNames = control.getExtraClassNames();
339 if (extraClassNames) {
340 classNames.push.apply(classNames, extraClassNames);
343 // For IE6, rewrite all classes on the decorated element if any combined
345 if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
346 var combinedClasses = this.getAppliedCombinedClassNames_(
348 if (combinedClasses.length > 0) {
349 classNames.push.apply(classNames, combinedClasses);
350 hasCombinedClassName = true;
354 // Only write to the DOM if new class names had to be added to the element.
355 if (!hasRendererClassName || !hasStructuralClassName ||
356 extraClassNames || hasCombinedClassName) {
357 goog.dom.classlist.set(element, classNames.join(' '));
360 this.setAriaStates(control, element);
366 * Initializes the control's DOM by configuring properties that can only be set
367 * after the DOM has entered the document. This implementation sets up BiDi
368 * and keyboard focus. Called from {@link goog.ui.Control#enterDocument}.
369 * @param {goog.ui.Control} control Control whose DOM is to be initialized
370 * as it enters the document.
372 goog.ui.ControlRenderer.prototype.initializeDom = function(control) {
373 // Initialize render direction (BiDi). We optimize the left-to-right render
374 // direction by assuming that elements are left-to-right by default, and only
375 // updating their styling if they are explicitly set to right-to-left.
376 if (control.isRightToLeft()) {
377 this.setRightToLeft(control.getElement(), true);
380 // Initialize keyboard focusability (tab index). We assume that components
381 // aren't focusable by default (i.e have no tab index), and only touch the
382 // DOM if the component is focusable, enabled, and visible, and therefore
383 // needs a tab index.
384 if (control.isEnabled()) {
385 this.setFocusable(control, control.isVisible());
391 * Sets the element's ARIA role.
392 * @param {Element} element Element to update.
393 * @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.
395 goog.ui.ControlRenderer.prototype.setAriaRole = function(element,
397 var ariaRole = opt_preferredRole || this.getAriaRole();
399 goog.asserts.assert(element,
400 'The element passed as a first parameter cannot be null.');
401 var currentRole = goog.a11y.aria.getRole(element);
402 if (ariaRole == currentRole) {
405 goog.a11y.aria.setRole(element, ariaRole);
411 * Sets the element's ARIA attributes, including distinguishing between
412 * universally supported ARIA properties and ARIA states that are only
413 * supported by certain ARIA roles. Only attributes which are initialized to be
415 * @param {!goog.ui.Control} control Control whose ARIA state will be updated.
416 * @param {!Element} element Element whose ARIA state is to be updated.
418 goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
419 goog.asserts.assert(control);
420 goog.asserts.assert(element);
422 if (!control.isVisible()) {
423 goog.a11y.aria.setState(
424 element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
426 if (!control.isEnabled()) {
427 this.updateAriaState(
428 element, goog.ui.Component.State.DISABLED, !control.isEnabled());
430 if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
431 this.updateAriaState(
432 element, goog.ui.Component.State.SELECTED, control.isSelected());
434 if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
435 this.updateAriaState(
436 element, goog.ui.Component.State.CHECKED, control.isChecked());
438 if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
439 this.updateAriaState(
440 element, goog.ui.Component.State.OPENED, control.isOpen());
446 * Allows or disallows text selection within the control's DOM.
447 * @param {Element} element The control's root element.
448 * @param {boolean} allow Whether the element should allow text selection.
450 goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(element,
452 // On all browsers other than IE and Opera, it isn't necessary to recursively
453 // apply unselectable styling to the element's children.
454 goog.style.setUnselectable(element, !allow,
455 !goog.userAgent.IE && !goog.userAgent.OPERA);
460 * Applies special styling to/from the control's element if it is rendered
461 * right-to-left, and removes it if it is rendered left-to-right.
462 * @param {Element} element The control's root element.
463 * @param {boolean} rightToLeft Whether the component is rendered
466 goog.ui.ControlRenderer.prototype.setRightToLeft = function(element,
468 this.enableClassName(element,
469 goog.getCssName(this.getStructuralCssClass(), 'rtl'), rightToLeft);
474 * Returns true if the control's key event target supports keyboard focus
475 * (based on its {@code tabIndex} attribute), false otherwise.
476 * @param {goog.ui.Control} control Control whose key event target is to be
478 * @return {boolean} Whether the control's key event target is focusable.
480 goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
482 if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
483 (keyTarget = control.getKeyEventTarget())) {
484 return goog.dom.isFocusableTabIndex(keyTarget);
491 * Updates the control's key event target to make it focusable or non-focusable
492 * via its {@code tabIndex} attribute. Does nothing if the control doesn't
493 * support the {@code FOCUSED} state, or if it has no key event target.
494 * @param {goog.ui.Control} control Control whose key event target is to be
496 * @param {boolean} focusable Whether to enable keyboard focus support on the
497 * control's key event target.
499 goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
501 if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
502 (keyTarget = control.getKeyEventTarget())) {
503 if (!focusable && control.isFocused()) {
504 // Blur before hiding. Note that IE calls onblur handlers asynchronously.
508 // TODO(user|user): Find out why this fails on IE.
510 // The blur event dispatched by the key event target element when blur()
511 // was called on it should have been handled by the control's handleBlur()
512 // method, so at this point the control should no longer be focused.
513 // However, blur events are unreliable on IE and FF3, so if at this point
514 // the control is still focused, we trigger its handleBlur() method
516 if (control.isFocused()) {
517 control.handleBlur(null);
520 // Don't overwrite existing tab index values unless needed.
521 if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
522 goog.dom.setFocusableTabIndex(keyTarget, focusable);
529 * Shows or hides the element.
530 * @param {Element} element Element to update.
531 * @param {boolean} visible Whether to show the element.
533 goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {
534 // The base class implementation is trivial; subclasses should override as
535 // needed. It should be possible to do animated reveals, for example.
536 goog.style.setElementShown(element, visible);
538 goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
544 * Updates the appearance of the control in response to a state change.
545 * @param {goog.ui.Control} control Control instance to update.
546 * @param {goog.ui.Component.State} state State to enable or disable.
547 * @param {boolean} enable Whether the control is entering or exiting the state.
549 goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
550 var element = control.getElement();
552 var className = this.getClassForState(state);
554 this.enableClassName(control, className, enable);
556 this.updateAriaState(element, state, enable);
562 * Updates the element's ARIA (accessibility) attributes , including
563 * distinguishing between universally supported ARIA properties and ARIA states
564 * that are only supported by certain ARIA roles.
565 * @param {Element} element Element whose ARIA state is to be updated.
566 * @param {goog.ui.Component.State} state Component state being enabled or
568 * @param {boolean} enable Whether the state is being enabled or disabled.
571 goog.ui.ControlRenderer.prototype.updateAriaState = function(element, state,
573 // Ensure the ARIA attribute map exists.
574 if (!goog.ui.ControlRenderer.ARIA_ATTRIBUTE_MAP_) {
575 goog.ui.ControlRenderer.ARIA_ATTRIBUTE_MAP_ = goog.object.create(
576 goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,
577 goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,
578 goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,
579 goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);
581 goog.asserts.assert(element,
582 'The element passed as a first parameter cannot be null.');
583 var ariaAttr = goog.ui.ControlRenderer.getAriaStateForAriaRole_(
584 element, goog.ui.ControlRenderer.ARIA_ATTRIBUTE_MAP_[state]);
586 goog.a11y.aria.setState(element, ariaAttr, enable);
592 * Returns the appropriate ARIA attribute based on ARIA role if the ARIA
593 * attribute is an ARIA state.
594 * @param {!Element} element The element from which to get the ARIA role for
595 * matching ARIA state.
596 * @param {goog.a11y.aria.State} attr The ARIA attribute to check to see if it
597 * can be applied to the given ARIA role.
598 * @return {goog.a11y.aria.State} An ARIA attribute that can be applied to the
602 goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {
603 var role = goog.a11y.aria.getRole(element);
607 role = /** @type {goog.a11y.aria.Role} */ (role);
608 var matchAttr = goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_[role] || attr;
609 return goog.ui.ControlRenderer.isAriaState_(attr) ? matchAttr : attr;
614 * Determines if the given ARIA attribute is an ARIA property or ARIA state.
615 * @param {goog.a11y.aria.State} attr The ARIA attribute to classify.
616 * @return {boolean} If the ARIA attribute is an ARIA state.
619 goog.ui.ControlRenderer.isAriaState_ = function(attr) {
620 return attr == goog.a11y.aria.State.CHECKED ||
621 attr == goog.a11y.aria.State.SELECTED;
626 * Takes a control's root element, and sets its content to the given text
627 * caption or DOM structure. The default implementation replaces the children
628 * of the given element. Renderers that create more complex DOM structures
629 * must override this method accordingly.
630 * @param {Element} element The control's root element.
631 * @param {goog.ui.ControlContent} content Text caption or DOM structure to be
632 * set as the control's content. The DOM nodes will not be cloned, they
633 * will only moved under the content element of the control.
635 goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
636 var contentElem = this.getContentElement(element);
638 goog.dom.removeChildren(contentElem);
640 if (goog.isString(content)) {
641 goog.dom.setTextContent(contentElem, content);
643 var childHandler = function(child) {
645 var doc = goog.dom.getOwnerDocument(contentElem);
646 contentElem.appendChild(goog.isString(child) ?
647 doc.createTextNode(child) : child);
650 if (goog.isArray(content)) {
652 goog.array.forEach(content, childHandler);
653 } else if (goog.isArrayLike(content) && !('nodeType' in content)) {
654 // NodeList. The second condition filters out TextNode which also has
655 // length attribute but is not array like. The nodes have to be cloned
656 // because childHandler removes them from the list during iteration.
657 goog.array.forEach(goog.array.clone(/** @type {NodeList} */(content)),
661 childHandler(content);
670 * Returns the element within the component's DOM that should receive keyboard
671 * focus (null if none). The default implementation returns the control's root
673 * @param {goog.ui.Control} control Control whose key event target is to be
675 * @return {Element} The key event target.
677 goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
678 return control.getElement();
682 // CSS class name management.
686 * Returns the CSS class name to be applied to the root element of all
687 * components rendered or decorated using this renderer. The class name
688 * is expected to uniquely identify the renderer class, i.e. no two
689 * renderer classes are expected to share the same CSS class name.
690 * @return {string} Renderer-specific CSS class name.
692 goog.ui.ControlRenderer.prototype.getCssClass = function() {
693 return goog.ui.ControlRenderer.CSS_CLASS;
698 * Returns an array of combinations of classes to apply combined class names for
699 * in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This
700 * method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be
701 * compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS
702 * static constant instead.
703 * @return {Array.<Array.<string>>} Array of class name combinations.
705 goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
711 * Returns the name of a DOM structure-specific CSS class to be applied to the
712 * root element of all components rendered or decorated using this renderer.
713 * Unlike the class name returned by {@link #getCssClass}, the structural class
714 * name may be shared among different renderers that generate similar DOM
715 * structures. The structural class name also serves as the basis of derived
716 * class names used to identify and style structural elements of the control's
717 * DOM, as well as the basis for state-specific class names. The default
718 * implementation returns the same class name as {@link #getCssClass}, but
719 * subclasses are expected to override this method as needed.
720 * @return {string} DOM structure-specific CSS class name (same as the renderer-
721 * specific CSS class name by default).
723 goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
724 return this.getCssClass();
729 * Returns all CSS class names applicable to the given control, based on its
730 * state. The return value is an array of strings containing
732 * <li>the renderer-specific CSS class returned by {@link #getCssClass},
734 * <li>the structural CSS class returned by {@link getStructuralCssClass} (if
735 * different from the renderer-specific CSS class), followed by
736 * <li>any state-specific classes returned by {@link #getClassNamesForState},
738 * <li>any extra classes returned by the control's {@code getExtraClassNames}
740 * <li>for IE6 and lower, additional combined classes from
741 * {@link getAppliedCombinedClassNames_}.
743 * Since all controls have at least one renderer-specific CSS class name, this
744 * method is guaranteed to return an array of at least one element.
745 * @param {goog.ui.Control} control Control whose CSS classes are to be
747 * @return {!Array.<string>} Array of CSS class names applicable to the control.
750 goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
751 var cssClass = this.getCssClass();
753 // Start with the renderer-specific class name.
754 var classNames = [cssClass];
756 // Add structural class name, if different.
757 var structuralCssClass = this.getStructuralCssClass();
758 if (structuralCssClass != cssClass) {
759 classNames.push(structuralCssClass);
762 // Add state-specific class names, if any.
763 var classNamesForState = this.getClassNamesForState(control.getState());
764 classNames.push.apply(classNames, classNamesForState);
766 // Add extra class names, if any.
767 var extraClassNames = control.getExtraClassNames();
768 if (extraClassNames) {
769 classNames.push.apply(classNames, extraClassNames);
772 // Add composite classes for IE6 support
773 if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
774 classNames.push.apply(classNames,
775 this.getAppliedCombinedClassNames_(classNames));
783 * Returns an array of all the combined class names that should be applied based
784 * on the given list of classes. Checks the result of
785 * {@link getIe6ClassCombinations} for any combinations that have all
786 * members contained in classes. If a combination matches, the members are
787 * joined with an underscore (in order), and added to the return array.
789 * If opt_includedClass is provided, return only the combined classes that have
790 * all members contained in classes AND include opt_includedClass as well.
791 * opt_includedClass is added to classes as well.
792 * @param {goog.array.ArrayLike.<string>} classes Array-like thing of classes to
793 * return matching combined classes for.
794 * @param {?string=} opt_includedClass If provided, get only the combined
795 * classes that include this one.
796 * @return {!Array.<string>} Array of combined class names that should be
800 goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
801 classes, opt_includedClass) {
803 if (opt_includedClass) {
804 classes = classes.concat([opt_includedClass]);
806 goog.array.forEach(this.getIe6ClassCombinations(), function(combo) {
807 if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&
808 (!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {
809 toAdd.push(combo.join('_'));
817 * Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array
818 * of the appropriate class names representing the given state, suitable to be
819 * applied to the root element of a component rendered using this renderer, or
820 * null if no state-specific classes need to be applied. This default
821 * implementation uses the renderer's {@link getClassForState} method to
822 * generate each state-specific class.
823 * @param {number} state Bit mask of component states.
824 * @return {!Array.<string>} Array of CSS class names representing the given
828 goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
831 // For each enabled state, push the corresponding CSS class name onto
832 // the classNames array.
833 var mask = state & -state; // Least significant bit
834 classNames.push(this.getClassForState(
835 /** @type {goog.ui.Component.State} */ (mask)));
843 * Takes a single {@link goog.ui.Component.State}, and returns the
844 * corresponding CSS class name (null if none).
845 * @param {goog.ui.Component.State} state Component state.
846 * @return {string|undefined} CSS class representing the given state (undefined
850 goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
851 if (!this.classByState_) {
852 this.createClassByStateMap_();
854 return this.classByState_[state];
859 * Takes a single CSS class name which may represent a component state, and
860 * returns the corresponding component state (0x00 if none).
861 * @param {string} className CSS class name, possibly representing a component
863 * @return {goog.ui.Component.State} state Component state corresponding
864 * to the given CSS class (0x00 if none).
867 goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
868 if (!this.stateByClass_) {
869 this.createStateByClassMap_();
871 var state = parseInt(this.stateByClass_[className], 10);
872 return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
877 * Creates the lookup table of states to classes, used during state changes.
880 goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
881 var baseClass = this.getStructuralCssClass();
883 // This ensures space-separated css classnames are not allowed, which some
884 // ControlRenderers had been doing. See http://b/13694665.
885 var isValidClassName = !goog.string.contains(
886 goog.string.normalizeWhitespace(baseClass), ' ');
887 goog.asserts.assert(isValidClassName,
888 'ControlRenderer has an invalid css class: \'' + baseClass + '\'');
891 * Map of component states to state-specific structural class names,
892 * used when changing the DOM in response to a state change. Precomputed
893 * and cached on first use to minimize object allocations and string
898 this.classByState_ = goog.object.create(
899 goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),
900 goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),
901 goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),
902 goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),
903 goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),
904 goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),
905 goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));
910 * Creates the lookup table of classes to states, used during decoration.
913 goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {
914 // We need the classByState_ map so we can transpose it.
915 if (!this.classByState_) {
916 this.createClassByStateMap_();
920 * Map of state-specific structural class names to component states,
921 * used during element decoration. Precomputed and cached on first use
922 * to minimize object allocations and string concatenation.
926 this.stateByClass_ = goog.object.transpose(this.classByState_);