Path: blob/trunk/third_party/closure/goog/ui/controlrenderer.js
4504 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview Base class for control renderers.8* TODO(attila): If the renderer framework works well, pull it into Component.9*/1011goog.provide('goog.ui.ControlRenderer');1213goog.require('goog.a11y.aria');14goog.require('goog.a11y.aria.Role');15goog.require('goog.a11y.aria.State');16goog.require('goog.array');17goog.require('goog.asserts');18goog.require('goog.dom');19goog.require('goog.dom.TagName');20goog.require('goog.dom.classlist');21goog.require('goog.object');22goog.require('goog.string');23goog.require('goog.style');24goog.require('goog.ui.Component');25goog.require('goog.ui.ControlContent');26goog.require('goog.userAgent'); // circular27goog.requireType('goog.ui.Control');28293031/**32* Default renderer for {@link goog.ui.Control}s. Can be used as-is, but33* subclasses of Control will probably want to use renderers specifically34* tailored for them by extending this class. Controls that use renderers35* delegate one or more of the following API methods to the renderer:36* <ul>37* <li>`createDom` - renders the DOM for the component38* <li>`canDecorate` - determines whether an element can be decorated39* by the component40* <li>`decorate` - decorates an existing element with the component41* <li>`setState` - updates the appearance of the component based on42* its state43* <li>`getContent` - returns the component's content44* <li>`setContent` - sets the component's content45* </ul>46* Controls are stateful; renderers, on the other hand, should be stateless and47* reusable.48* @constructor49*/50goog.ui.ControlRenderer = function() {};51goog.addSingletonGetter(goog.ui.ControlRenderer);525354/**55* Constructs a new renderer and sets the CSS class that the renderer will use56* as the base CSS class to apply to all elements rendered by that renderer.57* An example to use this function using a color palette:58*59* <pre>60* var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(61* goog.ui.PaletteRenderer, 'my-special-palette');62* var newColorPalette = new goog.ui.ColorPalette(63* colors, myCustomRenderer, opt_domHelper);64* </pre>65*66* Your CSS can look like this now:67* <pre>68* .my-special-palette { }69* .my-special-palette-table { }70* .my-special-palette-cell { }71* etc.72* </pre>73*74* <em>instead</em> of75* <pre>76* .CSS_MY_SPECIAL_PALETTE .goog-palette { }77* .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }78* .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }79* etc.80* </pre>81*82* You would want to use this functionality when you want an instance of a83* component to have specific styles different than the other components of the84* same type in your application. This avoids using descendant selectors to85* apply the specific styles to this component.86*87* @param {Function} ctor The constructor of the renderer you are trying to88* create.89* @param {string} cssClassName The name of the CSS class for this renderer.90* @return {goog.ui.ControlRenderer} An instance of the desired renderer with91* its getCssClass() method overridden to return the supplied custom CSS92* class name.93*/94goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {95'use strict';96var renderer = new ctor();9798/**99* Returns the CSS class to be applied to the root element of components100* rendered using this renderer.101* @return {string} Renderer-specific CSS class.102*/103renderer.getCssClass = function() {104'use strict';105return cssClassName;106};107108return renderer;109};110111112/**113* Default CSS class to be applied to the root element of components rendered114* by this renderer.115* @type {string}116*/117goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');118119120/**121* Array of arrays of CSS classes that we want composite classes added and122* removed for in IE6 and lower as a workaround for lack of multi-class CSS123* selector support.124*125* Subclasses that have accompanying CSS requiring this workaround should define126* their own static IE6_CLASS_COMBINATIONS constant and override127* getIe6ClassCombinations to return it.128*129* For example, if your stylesheet uses the selector .button.collapse-left130* (and is compiled to .button_collapse-left for the IE6 version of the131* stylesheet,) you should include ['button', 'collapse-left'] in this array132* and the class button_collapse-left will be applied to the root element133* whenever both button and collapse-left are applied individually.134*135* Members of each class name combination will be joined with underscores in the136* order that they're defined in the array. You should alphabetize them (for137* compatibility with the CSS compiler) unless you are doing something special.138* @type {Array<Array<string>>}139*/140goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];141142143/**144* Map of component states to corresponding ARIA attributes. Since the mapping145* of component states to ARIA attributes is neither component- nor146* renderer-specific, this is a static property of the renderer class, and is147* initialized on first use.148* @type {Object<goog.ui.Component.State, goog.a11y.aria.State>}149* @private150*/151goog.ui.ControlRenderer.ariaAttributeMap_;152153154/**155* Map of certain ARIA states to ARIA roles that support them. Used for checked156* and selected Component states because they are used on Components with ARIA157* roles that do not support the corresponding ARIA state.158* @private {!Object<goog.a11y.aria.Role, goog.a11y.aria.State>}159* @const160*/161goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_ = goog.object.create(162goog.a11y.aria.Role.BUTTON, goog.a11y.aria.State.PRESSED,163goog.a11y.aria.Role.CHECKBOX, goog.a11y.aria.State.CHECKED,164goog.a11y.aria.Role.MENU_ITEM, goog.a11y.aria.State.SELECTED,165goog.a11y.aria.Role.MENU_ITEM_CHECKBOX, goog.a11y.aria.State.CHECKED,166goog.a11y.aria.Role.MENU_ITEM_RADIO, goog.a11y.aria.State.CHECKED,167goog.a11y.aria.Role.RADIO, goog.a11y.aria.State.CHECKED,168goog.a11y.aria.Role.TAB, goog.a11y.aria.State.SELECTED,169goog.a11y.aria.Role.TREEITEM, goog.a11y.aria.State.SELECTED);170171172/**173* Returns the ARIA role to be applied to the control.174* See http://wiki/Main/ARIA for more info.175* @return {goog.a11y.aria.Role|undefined} ARIA role.176*/177goog.ui.ControlRenderer.prototype.getAriaRole = function() {178'use strict';179// By default, the ARIA role is unspecified.180return undefined;181};182183184/**185* Returns the control's contents wrapped in a DIV, with the renderer's own186* CSS class and additional state-specific classes applied to it.187* @param {goog.ui.Control} control Control to render.188* @return {Element} Root element for the control.189*/190goog.ui.ControlRenderer.prototype.createDom = function(control) {191'use strict';192// Create and return DIV wrapping contents.193var element = control.getDomHelper().createDom(194goog.dom.TagName.DIV, this.getClassNames(control).join(' '),195control.getContent());196197return element;198};199200201/**202* Takes the control's root element and returns the parent element of the203* control's contents. Since by default controls are rendered as a single204* DIV, the default implementation returns the element itself. Subclasses205* with more complex DOM structures must override this method as needed.206* @param {Element} element Root element of the control whose content element207* is to be returned.208* @return {Element} The control's content element.209*/210goog.ui.ControlRenderer.prototype.getContentElement = function(element) {211'use strict';212return element;213};214215216/**217* Updates the control's DOM by adding or removing the specified class name218* to/from its root element. May add additional combined classes as needed in219* IE6 and lower. Because of this, subclasses should use this method when220* modifying class names on the control's root element.221* @param {goog.ui.Control|Element} control Control instance (or root element)222* to be updated.223* @param {string} className CSS class name to add or remove.224* @param {boolean} enable Whether to add or remove the class name.225*/226goog.ui.ControlRenderer.prototype.enableClassName = function(227control, className, enable) {228'use strict';229/** @suppress {strictMissingProperties} Added to tighten compiler checks */230var element = /** @type {Element} */ (231control.getElement ? control.getElement() : control);232if (element) {233var classNames = [className];234235goog.dom.classlist.enableAll(element, classNames, enable);236}237};238239240/**241* Updates the control's DOM by adding or removing the specified extra class242* name to/from its element.243* @param {goog.ui.Control} control Control to be updated.244* @param {string} className CSS class name to add or remove.245* @param {boolean} enable Whether to add or remove the class name.246*/247goog.ui.ControlRenderer.prototype.enableExtraClassName = function(248control, className, enable) {249'use strict';250// The base class implementation is trivial; subclasses should override as251// needed.252this.enableClassName(control, className, enable);253};254255256/**257* Returns true if this renderer can decorate the element, false otherwise.258* The default implementation always returns true.259* @param {Element} element Element to decorate.260* @return {boolean} Whether the renderer can decorate the element.261*/262goog.ui.ControlRenderer.prototype.canDecorate = function(element) {263'use strict';264return true;265};266267268/**269* Default implementation of `decorate` for {@link goog.ui.Control}s.270* Initializes the control's ID, content, and state based on the ID of the271* element, its child nodes, and its CSS classes, respectively. Returns the272* element.273* @param {goog.ui.Control} control Control instance to decorate the element.274* @param {Element} element Element to decorate.275* @return {Element} Decorated element.276*/277goog.ui.ControlRenderer.prototype.decorate = function(control, element) {278'use strict';279// Set the control's ID to the decorated element's DOM ID, if any.280if (element.id) {281control.setId(element.id);282}283284// Set the control's content to the decorated element's content.285var contentElem = this.getContentElement(element);286if (contentElem && contentElem.firstChild) {287control.setContentInternal(288contentElem.firstChild.nextSibling ?289goog.array.clone(contentElem.childNodes) :290contentElem.firstChild);291} else {292control.setContentInternal(null);293}294295// Initialize the control's state based on the decorated element's CSS class.296// This implementation is optimized to minimize object allocations, string297// comparisons, and DOM access.298var state = 0x00;299var rendererClassName = this.getCssClass();300var structuralClassName = this.getStructuralCssClass();301var hasRendererClassName = false;302var hasStructuralClassName = false;303var hasCombinedClassName = false;304var classNames = goog.array.toArray(goog.dom.classlist.get(element));305classNames.forEach(function(className) {306'use strict';307if (!hasRendererClassName && className == rendererClassName) {308hasRendererClassName = true;309if (structuralClassName == rendererClassName) {310hasStructuralClassName = true;311}312} else if (!hasStructuralClassName && className == structuralClassName) {313hasStructuralClassName = true;314} else {315state |= this.getStateFromClass(className);316}317if (this.getStateFromClass(className) == goog.ui.Component.State.DISABLED) {318goog.asserts.assertElement(contentElem);319if (goog.dom.isFocusableTabIndex(contentElem)) {320goog.dom.setFocusableTabIndex(contentElem, false);321}322}323}, this);324control.setStateInternal(state);325326// Make sure the element has the renderer's CSS classes applied, as well as327// any extra class names set on the control.328if (!hasRendererClassName) {329classNames.push(rendererClassName);330if (structuralClassName == rendererClassName) {331hasStructuralClassName = true;332}333}334if (!hasStructuralClassName) {335classNames.push(structuralClassName);336}337var extraClassNames = control.getExtraClassNames();338if (extraClassNames) {339classNames.push.apply(classNames, extraClassNames);340}341342// Only write to the DOM if new class names had to be added to the element.343if (!hasRendererClassName || !hasStructuralClassName || extraClassNames ||344hasCombinedClassName) {345goog.dom.classlist.set(element, classNames.join(' '));346}347348return element;349};350351352/**353* Initializes the control's DOM by configuring properties that can only be set354* after the DOM has entered the document. This implementation sets up BiDi355* and keyboard focus. Called from {@link goog.ui.Control#enterDocument}.356* @param {goog.ui.Control} control Control whose DOM is to be initialized357* as it enters the document.358*/359goog.ui.ControlRenderer.prototype.initializeDom = function(control) {360'use strict';361// Initialize render direction (BiDi). We optimize the left-to-right render362// direction by assuming that elements are left-to-right by default, and only363// updating their styling if they are explicitly set to right-to-left.364if (control.isRightToLeft()) {365this.setRightToLeft(control.getElement(), true);366}367368// Initialize keyboard focusability (tab index). We assume that components369// aren't focusable by default (i.e have no tab index), and only touch the370// DOM if the component is focusable, enabled, and visible, and therefore371// needs a tab index.372if (control.isEnabled()) {373this.setFocusable(control, control.isVisible());374}375};376377378/**379* Sets the element's ARIA role.380* @param {Element} element Element to update.381* @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.382*/383goog.ui.ControlRenderer.prototype.setAriaRole = function(384element, opt_preferredRole) {385'use strict';386var ariaRole = opt_preferredRole || this.getAriaRole();387if (ariaRole) {388goog.asserts.assert(389element, 'The element passed as a first parameter cannot be null.');390var currentRole = goog.a11y.aria.getRole(element);391if (ariaRole == currentRole) {392return;393}394goog.a11y.aria.setRole(element, ariaRole);395}396};397398399/**400* Sets the element's ARIA attributes, including distinguishing between401* universally supported ARIA properties and ARIA states that are only402* supported by certain ARIA roles. Only attributes which are initialized to be403* true will be set.404* @param {!goog.ui.Control} control Control whose ARIA state will be updated.405* @param {!Element} element Element whose ARIA state is to be updated.406*/407goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {408'use strict';409goog.asserts.assert(control);410goog.asserts.assert(element);411412var ariaLabel = control.getAriaLabel();413if (ariaLabel != null) {414this.setAriaLabel(element, ariaLabel);415}416417if (!control.isVisible()) {418goog.a11y.aria.setState(419element, goog.a11y.aria.State.HIDDEN, !control.isVisible());420}421if (!control.isEnabled()) {422this.updateAriaState(423element, goog.ui.Component.State.DISABLED, !control.isEnabled());424}425if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {426this.updateAriaState(427element, goog.ui.Component.State.SELECTED, control.isSelected());428}429if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {430this.updateAriaState(431element, goog.ui.Component.State.CHECKED, control.isChecked());432}433if (control.isSupportedState(goog.ui.Component.State.OPENED)) {434this.updateAriaState(435element, goog.ui.Component.State.OPENED, control.isOpen());436}437};438439440/**441* Sets the element's ARIA label. This should be overriden by subclasses that442* don't apply the role directly on control.element_.443* @param {!Element} element Element whose ARIA label is to be updated.444* @param {string} ariaLabel Label to add to the element.445*/446goog.ui.ControlRenderer.prototype.setAriaLabel = function(element, ariaLabel) {447'use strict';448goog.a11y.aria.setLabel(element, ariaLabel);449};450451452/**453* Allows or disallows text selection within the control's DOM.454* @param {Element} element The control's root element.455* @param {boolean} allow Whether the element should allow text selection.456*/457goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(458element, allow) {459'use strict';460// On all browsers other than IE and Opera, it isn't necessary to recursively461// apply unselectable styling to the element's children.462goog.style.setUnselectable(element, !allow, !goog.userAgent.IE);463};464465466/**467* Applies special styling to/from the control's element if it is rendered468* right-to-left, and removes it if it is rendered left-to-right.469* @param {Element} element The control's root element.470* @param {boolean} rightToLeft Whether the component is rendered471* right-to-left.472*/473goog.ui.ControlRenderer.prototype.setRightToLeft = function(474element, rightToLeft) {475'use strict';476this.enableClassName(477element, goog.getCssName(this.getStructuralCssClass(), 'rtl'),478rightToLeft);479};480481482/**483* Returns true if the control's key event target supports keyboard focus484* (based on its `tabIndex` attribute), false otherwise.485* @param {goog.ui.Control} control Control whose key event target is to be486* checked.487* @return {boolean} Whether the control's key event target is focusable.488*/489goog.ui.ControlRenderer.prototype.isFocusable = function(control) {490'use strict';491var keyTarget;492if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&493(keyTarget = control.getKeyEventTarget())) {494return goog.dom.isFocusableTabIndex(keyTarget);495}496return false;497};498499500/**501* Updates the control's key event target to make it focusable or non-focusable502* via its `tabIndex` attribute. Does nothing if the control doesn't503* support the `FOCUSED` state, or if it has no key event target.504* @param {goog.ui.Control} control Control whose key event target is to be505* updated.506* @param {boolean} focusable Whether to enable keyboard focus support on the507* control's key event target.508*/509goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {510'use strict';511var keyTarget;512if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&513(keyTarget = control.getKeyEventTarget())) {514if (!focusable && control.isFocused()) {515// Blur before hiding. Note that IE calls onblur handlers asynchronously.516try {517keyTarget.blur();518} catch (e) {519// TODO(user): Find out why this fails on IE.520}521// The blur event dispatched by the key event target element when blur()522// was called on it should have been handled by the control's handleBlur()523// method, so at this point the control should no longer be focused.524// However, blur events are unreliable on IE and FF3, so if at this point525// the control is still focused, we trigger its handleBlur() method526// programmatically.527if (control.isFocused()) {528control.handleBlur(null);529}530}531// Don't overwrite existing tab index values unless needed.532if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {533goog.dom.setFocusableTabIndex(keyTarget, focusable);534}535}536};537538539/**540* Shows or hides the element.541* @param {Element} element Element to update.542* @param {boolean} visible Whether to show the element.543*/544goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {545'use strict';546// The base class implementation is trivial; subclasses should override as547// needed. It should be possible to do animated reveals, for example.548goog.style.setElementShown(element, visible);549if (element) {550goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);551}552};553554555/**556* Updates the appearance of the control in response to a state change.557* @param {goog.ui.Control} control Control instance to update.558* @param {goog.ui.Component.State} state State to enable or disable.559* @param {boolean} enable Whether the control is entering or exiting the state.560*/561goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {562'use strict';563var element = control.getElement();564if (element) {565var className = this.getClassForState(state);566if (className) {567this.enableClassName(control, className, enable);568}569this.updateAriaState(element, state, enable);570}571};572573574/**575* Updates the element's ARIA (accessibility) attributes , including576* distinguishing between universally supported ARIA properties and ARIA states577* that are only supported by certain ARIA roles.578* @param {Element} element Element whose ARIA state is to be updated.579* @param {goog.ui.Component.State} state Component state being enabled or580* disabled.581* @param {boolean} enable Whether the state is being enabled or disabled.582* @protected583*/584goog.ui.ControlRenderer.prototype.updateAriaState = function(585element, state, enable) {586'use strict';587// Ensure the ARIA attribute map exists.588if (!goog.ui.ControlRenderer.ariaAttributeMap_) {589goog.ui.ControlRenderer.ariaAttributeMap_ = goog.object.create(590goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,591goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,592goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,593goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);594}595goog.asserts.assert(596element, 'The element passed as a first parameter cannot be null.');597var ariaAttr = goog.ui.ControlRenderer.getAriaStateForAriaRole_(598element, goog.ui.ControlRenderer.ariaAttributeMap_[state]);599if (ariaAttr) {600goog.a11y.aria.setState(element, ariaAttr, enable);601}602};603604605/**606* Returns the appropriate ARIA attribute based on ARIA role if the ARIA607* attribute is an ARIA state.608* @param {!Element} element The element from which to get the ARIA role for609* matching ARIA state.610* @param {goog.a11y.aria.State} attr The ARIA attribute to check to see if it611* can be applied to the given ARIA role.612* @return {goog.a11y.aria.State} An ARIA attribute that can be applied to the613* given ARIA role.614* @private615*/616goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {617'use strict';618var role = goog.a11y.aria.getRole(element);619if (!role) {620return attr;621}622role = /** @type {goog.a11y.aria.Role} */ (role);623var matchAttr = goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_[role] || attr;624return goog.ui.ControlRenderer.isAriaState_(attr) ? matchAttr : attr;625};626627628/**629* Determines if the given ARIA attribute is an ARIA property or ARIA state.630* @param {goog.a11y.aria.State} attr The ARIA attribute to classify.631* @return {boolean} If the ARIA attribute is an ARIA state.632* @private633*/634goog.ui.ControlRenderer.isAriaState_ = function(attr) {635'use strict';636return attr == goog.a11y.aria.State.CHECKED ||637attr == goog.a11y.aria.State.SELECTED;638};639640641/**642* Takes a control's root element, and sets its content to the given text643* caption or DOM structure. The default implementation replaces the children644* of the given element. Renderers that create more complex DOM structures645* must override this method accordingly.646* @param {Element} element The control's root element.647* @param {goog.ui.ControlContent} content Text caption or DOM structure to be648* set as the control's content. The DOM nodes will not be cloned, they649* will only moved under the content element of the control.650*/651goog.ui.ControlRenderer.prototype.setContent = function(element, content) {652'use strict';653var contentElem = this.getContentElement(element);654if (contentElem) {655goog.dom.removeChildren(contentElem);656if (content) {657if (typeof content === 'string') {658goog.dom.setTextContent(contentElem, content);659} else {660var childHandler = function(child) {661'use strict';662if (child) {663var doc = goog.dom.getOwnerDocument(contentElem);664contentElem.appendChild(665typeof child === 'string' ? doc.createTextNode(child) : child);666}667};668if (Array.isArray(content)) {669// Array of nodes.670content.forEach(childHandler);671} else if (goog.isArrayLike(content) && !('nodeType' in content)) {672// NodeList. The second condition filters out TextNode which also has673// length attribute but is not array like. The nodes have to be cloned674// because childHandler removes them from the list during iteration.675goog.array.clone(/** @type {!NodeList<?>} */ (content))676.forEach(childHandler);677} else {678// Node or string.679childHandler(content);680}681}682}683}684};685686687/**688* Returns the element within the component's DOM that should receive keyboard689* focus (null if none). The default implementation returns the control's root690* element.691* @param {goog.ui.Control} control Control whose key event target is to be692* returned.693* @return {Element} The key event target.694*/695goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {696'use strict';697return control.getElement();698};699700701// CSS class name management.702703704/**705* Returns the CSS class name to be applied to the root element of all706* components rendered or decorated using this renderer. The class name707* is expected to uniquely identify the renderer class, i.e. no two708* renderer classes are expected to share the same CSS class name.709* @return {string} Renderer-specific CSS class name.710*/711goog.ui.ControlRenderer.prototype.getCssClass = function() {712'use strict';713return goog.ui.ControlRenderer.CSS_CLASS;714};715716717/**718* Returns an array of combinations of classes to apply combined class names for719* in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This720* method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be721* compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS722* static constant instead.723* @return {!Array<Array<string>>} Array of class name combinations.724*/725goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {726'use strict';727return [];728};729730731/**732* Returns the name of a DOM structure-specific CSS class to be applied to the733* root element of all components rendered or decorated using this renderer.734* Unlike the class name returned by {@link #getCssClass}, the structural class735* name may be shared among different renderers that generate similar DOM736* structures. The structural class name also serves as the basis of derived737* class names used to identify and style structural elements of the control's738* DOM, as well as the basis for state-specific class names. The default739* implementation returns the same class name as {@link #getCssClass}, but740* subclasses are expected to override this method as needed.741* @return {string} DOM structure-specific CSS class name (same as the renderer-742* specific CSS class name by default).743*/744goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {745'use strict';746return this.getCssClass();747};748749750/**751* Returns all CSS class names applicable to the given control, based on its752* state. The return value is an array of strings containing753* <ol>754* <li>the renderer-specific CSS class returned by {@link #getCssClass},755* followed by756* <li>the structural CSS class returned by {@link getStructuralCssClass} (if757* different from the renderer-specific CSS class), followed by758* <li>any state-specific classes returned by {@link #getClassNamesForState},759* followed by760* <li>any extra classes returned by the control's `getExtraClassNames`761* method and762* <li>for IE6 and lower, additional combined classes from763* {@link getAppliedCombinedClassNames_}.764* </ol>765* Since all controls have at least one renderer-specific CSS class name, this766* method is guaranteed to return an array of at least one element.767* @param {goog.ui.Control} control Control whose CSS classes are to be768* returned.769* @return {!Array<string>} Array of CSS class names applicable to the control.770* @protected771*/772goog.ui.ControlRenderer.prototype.getClassNames = function(control) {773'use strict';774var cssClass = this.getCssClass();775776// Start with the renderer-specific class name.777var classNames = [cssClass];778779// Add structural class name, if different.780var structuralCssClass = this.getStructuralCssClass();781if (structuralCssClass != cssClass) {782classNames.push(structuralCssClass);783}784785// Add state-specific class names, if any.786var classNamesForState = this.getClassNamesForState(control.getState());787classNames.push.apply(classNames, classNamesForState);788789// Add extra class names, if any.790var extraClassNames = control.getExtraClassNames();791if (extraClassNames) {792classNames.push.apply(classNames, extraClassNames);793}794795return classNames;796};797798799/**800* Returns an array of all the combined class names that should be applied based801* on the given list of classes. Checks the result of802* {@link getIe6ClassCombinations} for any combinations that have all803* members contained in classes. If a combination matches, the members are804* joined with an underscore (in order), and added to the return array.805*806* If opt_includedClass is provided, return only the combined classes that have807* all members contained in classes AND include opt_includedClass as well.808* opt_includedClass is added to classes as well.809* @param {IArrayLike<string>} classes Array-like thing of classes to810* return matching combined classes for.811* @param {?string=} opt_includedClass If provided, get only the combined812* classes that include this one.813* @return {!Array<string>} Array of combined class names that should be814* applied.815* @private816*/817goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(818classes, opt_includedClass) {819'use strict';820var toAdd = [];821if (opt_includedClass) {822classes = [].concat(classes, [opt_includedClass]);823}824this.getIe6ClassCombinations().forEach(function(combo) {825'use strict';826if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&827(!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {828toAdd.push(combo.join('_'));829}830});831return toAdd;832};833834835/**836* Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array837* of the appropriate class names representing the given state, suitable to be838* applied to the root element of a component rendered using this renderer, or839* null if no state-specific classes need to be applied. This default840* implementation uses the renderer's {@link getClassForState} method to841* generate each state-specific class.842* @param {number} state Bit mask of component states.843* @return {!Array<string>} Array of CSS class names representing the given844* state.845* @protected846*/847goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {848'use strict';849var classNames = [];850while (state) {851// For each enabled state, push the corresponding CSS class name onto852// the classNames array.853var mask = state & -state; // Least significant bit854classNames.push(855this.getClassForState(856/** @type {goog.ui.Component.State} */ (mask)));857state &= ~mask;858}859return classNames;860};861862863/**864* Takes a single {@link goog.ui.Component.State}, and returns the865* corresponding CSS class name (null if none).866* @param {goog.ui.Component.State} state Component state.867* @return {string|undefined} CSS class representing the given state (undefined868* if none).869* @protected870*/871goog.ui.ControlRenderer.prototype.getClassForState = function(state) {872'use strict';873if (!this.classByState_) {874this.createClassByStateMap_();875}876return this.classByState_[state];877};878879880/**881* Takes a single CSS class name which may represent a component state, and882* returns the corresponding component state (0x00 if none).883* @param {string} className CSS class name, possibly representing a component884* state.885* @return {goog.ui.Component.State} state Component state corresponding886* to the given CSS class (0x00 if none).887* @protected888*/889goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {890'use strict';891if (!this.stateByClass_) {892this.createStateByClassMap_();893}894var state = parseInt(this.stateByClass_[className], 10);895return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);896};897898899/**900* Creates the lookup table of states to classes, used during state changes.901* @private902*/903goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {904'use strict';905var baseClass = this.getStructuralCssClass();906907// This ensures space-separated css classnames are not allowed, which some908// ControlRenderers had been doing. See http://b/13694665.909var isValidClassName =910!goog.string.contains(goog.string.normalizeWhitespace(baseClass), ' ');911goog.asserts.assert(912isValidClassName,913'ControlRenderer has an invalid css class: \'' + baseClass + '\'');914915/**916* Map of component states to state-specific structural class names,917* used when changing the DOM in response to a state change. Precomputed918* and cached on first use to minimize object allocations and string919* concatenation.920* @type {Object}921* @private922*/923this.classByState_ = goog.object.create(924goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),925goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),926goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),927goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),928goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),929goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),930goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));931};932933934/**935* Creates the lookup table of classes to states, used during decoration.936* @private937*/938goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {939'use strict';940// We need the classByState_ map so we can transpose it.941if (!this.classByState_) {942this.createClassByStateMap_();943}944945/**946* Map of state-specific structural class names to component states,947* used during element decoration. Precomputed and cached on first use948* to minimize object allocations and string concatenation.949* @type {Object}950* @private951*/952this.stateByClass_ = goog.object.transpose(this.classByState_);953};954955956