Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / third_party / google_input_tools / third_party / closure_library / closure / goog / ui / controlrenderer.js
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 /**
16  * @fileoverview Base class for control renderers.
17  * TODO(attila):  If the renderer framework works well, pull it into Component.
18  *
19  * @author attila@google.com (Attila Bodis)
20  */
21
22 goog.provide('goog.ui.ControlRenderer');
23
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');
36
37
38
39 /**
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:
44  * <ul>
45  *    <li>{@code createDom} - renders the DOM for the component
46  *    <li>{@code canDecorate} - determines whether an element can be decorated
47  *        by the component
48  *    <li>{@code decorate} - decorates an existing element with the component
49  *    <li>{@code setState} - updates the appearance of the component based on
50  *        its state
51  *    <li>{@code getContent} - returns the component's content
52  *    <li>{@code setContent} - sets the component's content
53  * </ul>
54  * Controls are stateful; renderers, on the other hand, should be stateless and
55  * reusable.
56  * @constructor
57  */
58 goog.ui.ControlRenderer = function() {
59 };
60 goog.addSingletonGetter(goog.ui.ControlRenderer);
61 goog.tagUnsealableClass(goog.ui.ControlRenderer);
62
63
64 /**
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:
68  *
69  * <pre>
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);
74  * </pre>
75  *
76  * Your CSS can look like this now:
77  * <pre>
78  * .my-special-palette { }
79  * .my-special-palette-table { }
80  * .my-special-palette-cell { }
81  * etc.
82  * </pre>
83  *
84  * <em>instead</em> of
85  * <pre>
86  * .CSS_MY_SPECIAL_PALETTE .goog-palette { }
87  * .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
88  * .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
89  * etc.
90  * </pre>
91  *
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.
96  *
97  * @param {Function} ctor The constructor of the renderer you are trying to
98  *     create.
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
102  *     class name.
103  */
104 goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
105   var renderer = new ctor();
106
107   /**
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.
111    */
112   renderer.getCssClass = function() {
113     return cssClassName;
114   };
115
116   return renderer;
117 };
118
119
120 /**
121  * Default CSS class to be applied to the root element of components rendered
122  * by this renderer.
123  * @type {string}
124  */
125 goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');
126
127
128 /**
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
131  * selector support.
132  *
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.
136  *
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.
142  *
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>>}
147  */
148 goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];
149
150
151 /**
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>}
157  * @private
158  * @const
159  */
160 goog.ui.ControlRenderer.ARIA_ATTRIBUTE_MAP_;
161
162
163 /**
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>}
168  * @const
169  */
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);
179
180
181 /**
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.
185  */
186 goog.ui.ControlRenderer.prototype.getAriaRole = function() {
187   // By default, the ARIA role is unspecified.
188   return undefined;
189 };
190
191
192 /**
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.
197  */
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());
202
203   this.setAriaStates(control, element);
204   return element;
205 };
206
207
208 /**
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
214  *     is to be returned.
215  * @return {Element} The control's content element.
216  */
217 goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
218   return element;
219 };
220
221
222 /**
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)
228  *     to be updated.
229  * @param {string} className CSS class name to add or remove.
230  * @param {boolean} enable Whether to add or remove the class name.
231  */
232 goog.ui.ControlRenderer.prototype.enableClassName = function(control,
233     className, enable) {
234   var element = /** @type {Element} */ (
235       control.getElement ? control.getElement() : control);
236   if (element) {
237     var classNames = [className];
238
239     // For IE6, we need to enable any combined classes involving this class
240     // as well.
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);
246     }
247
248     goog.dom.classlist.enableAll(element, classNames, enable);
249   }
250 };
251
252
253 /**
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.
259  */
260 goog.ui.ControlRenderer.prototype.enableExtraClassName = function(control,
261     className, enable) {
262   // The base class implementation is trivial; subclasses should override as
263   // needed.
264   this.enableClassName(control, className, enable);
265 };
266
267
268 /**
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.
273  */
274 goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
275   return true;
276 };
277
278
279 /**
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
283  * element.
284  * @param {goog.ui.Control} control Control instance to decorate the element.
285  * @param {Element} element Element to decorate.
286  * @return {Element} Decorated element.
287  */
288 goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
289   // Set the control's ID to the decorated element's DOM ID, if any.
290   if (element.id) {
291     control.setId(element.id);
292   }
293
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);
299   } else {
300     control.setContentInternal(null);
301   }
302
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.
306   var state = 0x00;
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;
318       }
319     } else if (!hasStructuralClassName && className == structuralClassName) {
320       hasStructuralClassName = true;
321     } else {
322       state |= this.getStateFromClass(className);
323     }
324   }, this);
325   control.setStateInternal(state);
326
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;
333     }
334   }
335   if (!hasStructuralClassName) {
336     classNames.push(structuralClassName);
337   }
338   var extraClassNames = control.getExtraClassNames();
339   if (extraClassNames) {
340     classNames.push.apply(classNames, extraClassNames);
341   }
342
343   // For IE6, rewrite all classes on the decorated element if any combined
344   // classes apply.
345   if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) {
346     var combinedClasses = this.getAppliedCombinedClassNames_(
347         classNames);
348     if (combinedClasses.length > 0) {
349       classNames.push.apply(classNames, combinedClasses);
350       hasCombinedClassName = true;
351     }
352   }
353
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(' '));
358   }
359
360   this.setAriaStates(control, element);
361   return element;
362 };
363
364
365 /**
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.
371  */
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);
378   }
379
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());
386   }
387 };
388
389
390 /**
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.
394  */
395 goog.ui.ControlRenderer.prototype.setAriaRole = function(element,
396     opt_preferredRole) {
397   var ariaRole = opt_preferredRole || this.getAriaRole();
398   if (ariaRole) {
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) {
403       return;
404     }
405     goog.a11y.aria.setRole(element, ariaRole);
406   }
407 };
408
409
410 /**
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
414  * true will be set.
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.
417  */
418 goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
419   goog.asserts.assert(control);
420   goog.asserts.assert(element);
421
422   if (!control.isVisible()) {
423     goog.a11y.aria.setState(
424         element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
425   }
426   if (!control.isEnabled()) {
427     this.updateAriaState(
428         element, goog.ui.Component.State.DISABLED, !control.isEnabled());
429   }
430   if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
431     this.updateAriaState(
432         element, goog.ui.Component.State.SELECTED, control.isSelected());
433   }
434   if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
435     this.updateAriaState(
436         element, goog.ui.Component.State.CHECKED, control.isChecked());
437   }
438   if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
439     this.updateAriaState(
440         element, goog.ui.Component.State.OPENED, control.isOpen());
441   }
442 };
443
444
445 /**
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.
449  */
450 goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(element,
451     allow) {
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);
456 };
457
458
459 /**
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
464  *     right-to-left.
465  */
466 goog.ui.ControlRenderer.prototype.setRightToLeft = function(element,
467     rightToLeft) {
468   this.enableClassName(element,
469       goog.getCssName(this.getStructuralCssClass(), 'rtl'), rightToLeft);
470 };
471
472
473 /**
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
477  *     checked.
478  * @return {boolean} Whether the control's key event target is focusable.
479  */
480 goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
481   var keyTarget;
482   if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
483       (keyTarget = control.getKeyEventTarget())) {
484     return goog.dom.isFocusableTabIndex(keyTarget);
485   }
486   return false;
487 };
488
489
490 /**
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
495  *     updated.
496  * @param {boolean} focusable Whether to enable keyboard focus support on the
497  *     control's key event target.
498  */
499 goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
500   var keyTarget;
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.
505       try {
506         keyTarget.blur();
507       } catch (e) {
508         // TODO(user|user):  Find out why this fails on IE.
509       }
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
515       // programmatically.
516       if (control.isFocused()) {
517         control.handleBlur(null);
518       }
519     }
520     // Don't overwrite existing tab index values unless needed.
521     if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
522       goog.dom.setFocusableTabIndex(keyTarget, focusable);
523     }
524   }
525 };
526
527
528 /**
529  * Shows or hides the element.
530  * @param {Element} element Element to update.
531  * @param {boolean} visible Whether to show the element.
532  */
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);
537   if (element) {
538     goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
539   }
540 };
541
542
543 /**
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.
548  */
549 goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
550   var element = control.getElement();
551   if (element) {
552     var className = this.getClassForState(state);
553     if (className) {
554       this.enableClassName(control, className, enable);
555     }
556     this.updateAriaState(element, state, enable);
557   }
558 };
559
560
561 /**
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
567  *     disabled.
568  * @param {boolean} enable Whether the state is being enabled or disabled.
569  * @protected
570  */
571 goog.ui.ControlRenderer.prototype.updateAriaState = function(element, state,
572     enable) {
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);
580   }
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]);
585   if (ariaAttr) {
586     goog.a11y.aria.setState(element, ariaAttr, enable);
587   }
588 };
589
590
591 /**
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
599  * given ARIA role.
600  * @private
601  */
602 goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {
603   var role = goog.a11y.aria.getRole(element);
604   if (!role) {
605     return attr;
606   }
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;
610 };
611
612
613 /**
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.
617  * @private
618  */
619 goog.ui.ControlRenderer.isAriaState_ = function(attr) {
620   return attr == goog.a11y.aria.State.CHECKED ||
621       attr == goog.a11y.aria.State.SELECTED;
622 };
623
624
625 /**
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.
634  */
635 goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
636   var contentElem = this.getContentElement(element);
637   if (contentElem) {
638     goog.dom.removeChildren(contentElem);
639     if (content) {
640       if (goog.isString(content)) {
641         goog.dom.setTextContent(contentElem, content);
642       } else {
643         var childHandler = function(child) {
644           if (child) {
645             var doc = goog.dom.getOwnerDocument(contentElem);
646             contentElem.appendChild(goog.isString(child) ?
647                 doc.createTextNode(child) : child);
648           }
649         };
650         if (goog.isArray(content)) {
651           // Array of nodes.
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)),
658               childHandler);
659         } else {
660           // Node or string.
661           childHandler(content);
662         }
663       }
664     }
665   }
666 };
667
668
669 /**
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
672  * element.
673  * @param {goog.ui.Control} control Control whose key event target is to be
674  *     returned.
675  * @return {Element} The key event target.
676  */
677 goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
678   return control.getElement();
679 };
680
681
682 // CSS class name management.
683
684
685 /**
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.
691  */
692 goog.ui.ControlRenderer.prototype.getCssClass = function() {
693   return goog.ui.ControlRenderer.CSS_CLASS;
694 };
695
696
697 /**
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.
704  */
705 goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
706   return [];
707 };
708
709
710 /**
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).
722  */
723 goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
724   return this.getCssClass();
725 };
726
727
728 /**
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
731  * <ol>
732  *   <li>the renderer-specific CSS class returned by {@link #getCssClass},
733  *       followed by
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},
737  *       followed by
738  *   <li>any extra classes returned by the control's {@code getExtraClassNames}
739  *       method and
740  *   <li>for IE6 and lower, additional combined classes from
741  *       {@link getAppliedCombinedClassNames_}.
742  * </ol>
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
746  *     returned.
747  * @return {!Array.<string>} Array of CSS class names applicable to the control.
748  * @protected
749  */
750 goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
751   var cssClass = this.getCssClass();
752
753   // Start with the renderer-specific class name.
754   var classNames = [cssClass];
755
756   // Add structural class name, if different.
757   var structuralCssClass = this.getStructuralCssClass();
758   if (structuralCssClass != cssClass) {
759     classNames.push(structuralCssClass);
760   }
761
762   // Add state-specific class names, if any.
763   var classNamesForState = this.getClassNamesForState(control.getState());
764   classNames.push.apply(classNames, classNamesForState);
765
766   // Add extra class names, if any.
767   var extraClassNames = control.getExtraClassNames();
768   if (extraClassNames) {
769     classNames.push.apply(classNames, extraClassNames);
770   }
771
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));
776   }
777
778   return classNames;
779 };
780
781
782 /**
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.
788  *
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
797  *     applied.
798  * @private
799  */
800 goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
801     classes, opt_includedClass) {
802   var toAdd = [];
803   if (opt_includedClass) {
804     classes = classes.concat([opt_includedClass]);
805   }
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('_'));
810     }
811   });
812   return toAdd;
813 };
814
815
816 /**
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
825  *     state.
826  * @protected
827  */
828 goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
829   var classNames = [];
830   while (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)));
836     state &= ~mask;
837   }
838   return classNames;
839 };
840
841
842 /**
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
847  *     if none).
848  * @protected
849  */
850 goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
851   if (!this.classByState_) {
852     this.createClassByStateMap_();
853   }
854   return this.classByState_[state];
855 };
856
857
858 /**
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
862  *     state.
863  * @return {goog.ui.Component.State} state Component state corresponding
864  *     to the given CSS class (0x00 if none).
865  * @protected
866  */
867 goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
868   if (!this.stateByClass_) {
869     this.createStateByClassMap_();
870   }
871   var state = parseInt(this.stateByClass_[className], 10);
872   return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
873 };
874
875
876 /**
877  * Creates the lookup table of states to classes, used during state changes.
878  * @private
879  */
880 goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
881   var baseClass = this.getStructuralCssClass();
882
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 + '\'');
889
890   /**
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
894    * concatenation.
895    * @type {Object}
896    * @private
897    */
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'));
906 };
907
908
909 /**
910  * Creates the lookup table of classes to states, used during decoration.
911  * @private
912  */
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_();
917   }
918
919   /**
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.
923    * @type {Object}
924    * @private
925    */
926   this.stateByClass_ = goog.object.transpose(this.classByState_);
927 };