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