Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/third_party/closure/goog/ui/container.js
4049 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 containers that host {@link goog.ui.Control}s,
9
* such as menus and toolbars. Provides default keyboard and mouse event
10
* handling and child management, based on a generalized version of
11
* {@link goog.ui.Menu}.
12
*
13
* @see ../demos/container.html
14
*/
15
// TODO(attila): Fix code/logic duplication between this and goog.ui.Control.
16
// TODO(attila): Maybe pull common stuff all the way up into Component...?
17
18
goog.provide('goog.ui.Container');
19
goog.provide('goog.ui.Container.EventType');
20
goog.provide('goog.ui.Container.Orientation');
21
22
goog.require('goog.a11y.aria');
23
goog.require('goog.a11y.aria.State');
24
goog.require('goog.asserts');
25
goog.require('goog.dom');
26
goog.require('goog.events.EventType');
27
goog.require('goog.events.KeyCodes');
28
goog.require('goog.events.KeyHandler');
29
goog.require('goog.object');
30
goog.require('goog.style');
31
goog.require('goog.ui.Component');
32
goog.require('goog.ui.ComponentUtil');
33
goog.require('goog.ui.ContainerRenderer');
34
goog.require('goog.ui.Control');
35
goog.requireType('goog.events.BrowserEvent');
36
goog.requireType('goog.events.Event');
37
goog.requireType('goog.events.KeyEvent');
38
39
40
41
/**
42
* Base class for containers. Extends {@link goog.ui.Component} by adding
43
* the following:
44
* <ul>
45
* <li>a {@link goog.events.KeyHandler}, to simplify keyboard handling,
46
* <li>a pluggable <em>renderer</em> framework, to simplify the creation of
47
* containers without the need to subclass this class,
48
* <li>methods to manage child controls hosted in the container,
49
* <li>default mouse and keyboard event handling methods.
50
* </ul>
51
* @param {?goog.ui.Container.Orientation=} opt_orientation Container
52
* orientation; defaults to `VERTICAL`.
53
* @param {goog.ui.ContainerRenderer=} opt_renderer Renderer used to render or
54
* decorate the container; defaults to {@link goog.ui.ContainerRenderer}.
55
* @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for document
56
* interaction.
57
* @extends {goog.ui.Component}
58
* @constructor
59
*/
60
goog.ui.Container = function(opt_orientation, opt_renderer, opt_domHelper) {
61
'use strict';
62
goog.ui.Component.call(this, opt_domHelper);
63
this.renderer_ = opt_renderer || goog.ui.ContainerRenderer.getInstance();
64
this.orientation_ = opt_orientation || this.renderer_.getDefaultOrientation();
65
};
66
goog.inherits(goog.ui.Container, goog.ui.Component);
67
68
69
/**
70
* Container-specific events.
71
* @enum {string}
72
*/
73
goog.ui.Container.EventType = {
74
/**
75
* Dispatched after a goog.ui.Container becomes visible. Non-cancellable.
76
* NOTE(user): This event really shouldn't exist, because the
77
* goog.ui.Component.EventType.SHOW event should behave like this one. But the
78
* SHOW event for containers has been behaving as other components'
79
* BEFORE_SHOW event for a long time, and too much code relies on that old
80
* behavior to fix it now.
81
*/
82
AFTER_SHOW: 'aftershow',
83
84
/**
85
* Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.
86
*/
87
AFTER_HIDE: 'afterhide'
88
};
89
90
91
/**
92
* Container orientation constants.
93
* @enum {string}
94
*/
95
goog.ui.Container.Orientation = {
96
HORIZONTAL: 'horizontal',
97
VERTICAL: 'vertical'
98
};
99
100
101
/**
102
* Allows an alternative element to be set to receive key events, otherwise
103
* defers to the renderer's element choice.
104
* @type {?Element|undefined}
105
* @private
106
*/
107
goog.ui.Container.prototype.keyEventTarget_ = null;
108
109
110
/**
111
* Keyboard event handler.
112
* @type {goog.events.KeyHandler?}
113
* @private
114
*/
115
goog.ui.Container.prototype.keyHandler_ = null;
116
117
118
/**
119
* Renderer for the container. Defaults to {@link goog.ui.ContainerRenderer}.
120
* @type {goog.ui.ContainerRenderer?}
121
* @private
122
*/
123
goog.ui.Container.prototype.renderer_ = null;
124
125
126
/**
127
* Container orientation; determines layout and default keyboard navigation.
128
* @type {?goog.ui.Container.Orientation}
129
* @private
130
*/
131
goog.ui.Container.prototype.orientation_ = null;
132
133
134
/**
135
* Whether the container is set to be visible. Defaults to true.
136
* @type {boolean}
137
* @private
138
*/
139
goog.ui.Container.prototype.visible_ = true;
140
141
142
/**
143
* Whether the container is enabled and reacting to keyboard and mouse events.
144
* Defaults to true.
145
* @type {boolean}
146
* @private
147
*/
148
goog.ui.Container.prototype.enabled_ = true;
149
150
151
/**
152
* Whether the container supports keyboard focus. Defaults to true. Focusable
153
* containers have a `tabIndex` and can be navigated to via the keyboard.
154
* @type {boolean}
155
* @private
156
*/
157
goog.ui.Container.prototype.focusable_ = true;
158
159
160
/**
161
* The 0-based index of the currently highlighted control in the container
162
* (-1 if none).
163
* @type {number}
164
* @private
165
*/
166
goog.ui.Container.prototype.highlightedIndex_ = -1;
167
168
169
/**
170
* The currently open (expanded) control in the container (null if none).
171
* @type {goog.ui.Control?}
172
* @private
173
*/
174
goog.ui.Container.prototype.openItem_ = null;
175
176
177
/**
178
* Whether the mouse button is held down. Defaults to false. This flag is set
179
* when the user mouses down over the container, and remains set until they
180
* release the mouse button.
181
* @type {boolean}
182
* @private
183
*/
184
goog.ui.Container.prototype.mouseButtonPressed_ = false;
185
186
187
/**
188
* Whether focus of child components should be allowed. Only effective if
189
* focusable_ is set to false.
190
* @type {boolean}
191
* @private
192
*/
193
goog.ui.Container.prototype.allowFocusableChildren_ = false;
194
195
196
/**
197
* Whether highlighting a child component should also open it.
198
* @type {boolean}
199
* @private
200
*/
201
goog.ui.Container.prototype.openFollowsHighlight_ = true;
202
203
204
/**
205
* Map of DOM IDs to child controls. Each key is the DOM ID of a child
206
* control's root element; each value is a reference to the child control
207
* itself. Used for looking up the child control corresponding to a DOM
208
* node in O(1) time.
209
* @type {?Object}
210
* @private
211
*/
212
goog.ui.Container.prototype.childElementIdMap_ = null;
213
214
215
// Event handler and renderer management.
216
217
218
/**
219
* Returns the DOM element on which the container is listening for keyboard
220
* events (null if none).
221
* @return {Element} Element on which the container is listening for key
222
* events.
223
*/
224
goog.ui.Container.prototype.getKeyEventTarget = function() {
225
'use strict';
226
// Delegate to renderer, unless we've set an explicit target.
227
return this.keyEventTarget_ || this.renderer_.getKeyEventTarget(this);
228
};
229
230
231
/**
232
* Attaches an element on which to listen for key events.
233
* @param {Element|undefined} element The element to attach, or null/undefined
234
* to attach to the default element.
235
*/
236
goog.ui.Container.prototype.setKeyEventTarget = function(element) {
237
'use strict';
238
if (this.focusable_) {
239
var oldTarget = this.getKeyEventTarget();
240
var inDocument = this.isInDocument();
241
242
this.keyEventTarget_ = element;
243
var newTarget = this.getKeyEventTarget();
244
245
if (inDocument) {
246
// Unlisten for events on the old key target. Requires us to reset
247
// key target state temporarily.
248
this.keyEventTarget_ = oldTarget;
249
this.enableFocusHandling_(false);
250
this.keyEventTarget_ = element;
251
252
// Listen for events on the new key target.
253
this.getKeyHandler().attach(newTarget);
254
this.enableFocusHandling_(true);
255
}
256
} else {
257
throw new Error(
258
'Can\'t set key event target for container ' +
259
'that doesn\'t support keyboard focus!');
260
}
261
};
262
263
264
/**
265
* Returns the keyboard event handler for this container, lazily created the
266
* first time this method is called. The keyboard event handler listens for
267
* keyboard events on the container's key event target, as determined by its
268
* renderer.
269
* @return {!goog.events.KeyHandler} Keyboard event handler for this container.
270
*/
271
goog.ui.Container.prototype.getKeyHandler = function() {
272
'use strict';
273
return this.keyHandler_ ||
274
(this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));
275
};
276
277
278
/**
279
* Returns the renderer used by this container to render itself or to decorate
280
* an existing element.
281
* @return {goog.ui.ContainerRenderer} Renderer used by the container.
282
*/
283
goog.ui.Container.prototype.getRenderer = function() {
284
'use strict';
285
return this.renderer_;
286
};
287
288
289
/**
290
* Registers the given renderer with the container. Changing renderers after
291
* the container has already been rendered or decorated is an error.
292
* @param {goog.ui.ContainerRenderer} renderer Renderer used by the container.
293
*/
294
goog.ui.Container.prototype.setRenderer = function(renderer) {
295
'use strict';
296
if (this.getElement()) {
297
// Too late.
298
throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
299
}
300
301
this.renderer_ = renderer;
302
};
303
304
305
// Standard goog.ui.Component implementation.
306
307
308
/**
309
* Creates the container's DOM.
310
* @override
311
*/
312
goog.ui.Container.prototype.createDom = function() {
313
'use strict';
314
// Delegate to renderer.
315
this.setElementInternal(this.renderer_.createDom(this));
316
};
317
318
319
/**
320
* Returns the DOM element into which child components are to be rendered,
321
* or null if the container itself hasn't been rendered yet. Overrides
322
* {@link goog.ui.Component#getContentElement} by delegating to the renderer.
323
* @return {Element} Element to contain child elements (null if none).
324
* @override
325
*/
326
goog.ui.Container.prototype.getContentElement = function() {
327
'use strict';
328
// Delegate to renderer.
329
return this.renderer_.getContentElement(this.getElement());
330
};
331
332
333
/**
334
* Returns true if the given element can be decorated by this container.
335
* Overrides {@link goog.ui.Component#canDecorate}.
336
* @param {Element} element Element to decorate.
337
* @return {boolean} True iff the element can be decorated.
338
* @override
339
*/
340
goog.ui.Container.prototype.canDecorate = function(element) {
341
'use strict';
342
// Delegate to renderer.
343
return this.renderer_.canDecorate(element);
344
};
345
346
347
/**
348
* Decorates the given element with this container. Overrides {@link
349
* goog.ui.Component#decorateInternal}. Considered protected.
350
* @param {Element} element Element to decorate.
351
* @override
352
*/
353
goog.ui.Container.prototype.decorateInternal = function(element) {
354
'use strict';
355
// Delegate to renderer.
356
this.setElementInternal(this.renderer_.decorate(this, element));
357
// Check whether the decorated element is explicitly styled to be invisible.
358
if (element.style.display == 'none') {
359
this.visible_ = false;
360
}
361
};
362
363
364
/**
365
* Configures the container after its DOM has been rendered, and sets up event
366
* handling. Overrides {@link goog.ui.Component#enterDocument}.
367
* @override
368
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
369
*/
370
goog.ui.Container.prototype.enterDocument = function() {
371
'use strict';
372
goog.ui.Container.superClass_.enterDocument.call(this);
373
374
this.forEachChild(function(child) {
375
'use strict';
376
if (child.isInDocument()) {
377
this.registerChildId_(child);
378
}
379
}, this);
380
381
var elem = this.getElement();
382
383
// Call the renderer's initializeDom method to initialize the container's DOM.
384
this.renderer_.initializeDom(this);
385
386
// Initialize visibility (opt_force = true, so we don't dispatch events).
387
this.setVisible(this.visible_, true);
388
389
var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);
390
391
// Handle events dispatched by child controls.
392
this.getHandler()
393
.listen(this, goog.ui.Component.EventType.ENTER, this.handleEnterItem)
394
.listen(
395
this, goog.ui.Component.EventType.HIGHLIGHT, this.handleHighlightItem)
396
.listen(
397
this, goog.ui.Component.EventType.UNHIGHLIGHT,
398
this.handleUnHighlightItem)
399
.listen(this, goog.ui.Component.EventType.OPEN, this.handleOpenItem)
400
.listen(this, goog.ui.Component.EventType.CLOSE, this.handleCloseItem)
401
402
// Handle mouse events.
403
.listen(elem, MouseEventType.MOUSEDOWN, this.handleMouseDown)
404
.listen(
405
goog.dom.getOwnerDocument(elem),
406
[MouseEventType.MOUSEUP, MouseEventType.MOUSECANCEL],
407
this.handleDocumentMouseUp)
408
409
// Handle mouse events on behalf of controls in the container.
410
.listen(
411
elem,
412
[
413
MouseEventType.MOUSEDOWN, MouseEventType.MOUSEUP,
414
MouseEventType.MOUSECANCEL, goog.events.EventType.MOUSEOVER,
415
goog.events.EventType.MOUSEOUT, goog.events.EventType.CONTEXTMENU
416
],
417
this.handleChildMouseEvents);
418
419
if (this.pointerEventsEnabled()) {
420
// Prevent pointer events from capturing the target element so they behave
421
// more like mouse events.
422
this.getHandler().listen(
423
elem, goog.events.EventType.GOTPOINTERCAPTURE,
424
this.preventPointerCapture_);
425
}
426
427
// If the container is focusable, set up keyboard event handling.
428
if (this.isFocusable()) {
429
this.enableFocusHandling_(true);
430
}
431
};
432
433
434
/**
435
* @param {!goog.events.BrowserEvent} e Event to handle.
436
* @private
437
*/
438
goog.ui.Container.prototype.preventPointerCapture_ = function(e) {
439
'use strict';
440
var elem = /** @type {!Element} */ (e.target);
441
if (!!elem.releasePointerCapture) {
442
elem.releasePointerCapture(e.pointerId);
443
}
444
};
445
446
447
/**
448
* Sets up listening for events applicable to focusable containers.
449
* @param {boolean} enable Whether to enable or disable focus handling.
450
* @private
451
*/
452
goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {
453
'use strict';
454
var handler = this.getHandler();
455
var keyTarget = this.getKeyEventTarget();
456
if (enable) {
457
handler.listen(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
458
.listen(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
459
.listen(
460
this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
461
this.handleKeyEvent);
462
} else {
463
handler.unlisten(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
464
.unlisten(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
465
.unlisten(
466
this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
467
this.handleKeyEvent);
468
}
469
};
470
471
472
/**
473
* Cleans up the container before its DOM is removed from the document, and
474
* removes event handlers. Overrides {@link goog.ui.Component#exitDocument}.
475
* @override
476
*/
477
goog.ui.Container.prototype.exitDocument = function() {
478
'use strict';
479
// {@link #setHighlightedIndex} has to be called before
480
// {@link goog.ui.Component#exitDocument}, otherwise it has no effect.
481
this.setHighlightedIndex(-1);
482
483
if (this.openItem_) {
484
this.openItem_.setOpen(false);
485
}
486
487
this.mouseButtonPressed_ = false;
488
489
goog.ui.Container.superClass_.exitDocument.call(this);
490
};
491
492
493
/** @override */
494
goog.ui.Container.prototype.disposeInternal = function() {
495
'use strict';
496
goog.ui.Container.superClass_.disposeInternal.call(this);
497
498
if (this.keyHandler_) {
499
this.keyHandler_.dispose();
500
this.keyHandler_ = null;
501
}
502
503
this.keyEventTarget_ = null;
504
this.childElementIdMap_ = null;
505
this.openItem_ = null;
506
this.renderer_ = null;
507
};
508
509
510
// Default event handlers.
511
512
513
/**
514
* Handles ENTER events raised by child controls when they are navigated to.
515
* @param {goog.events.Event} e ENTER event to handle.
516
* @return {boolean} Whether to prevent handleMouseOver from handling
517
* the event.
518
*/
519
goog.ui.Container.prototype.handleEnterItem = function(e) {
520
'use strict';
521
// Allow the Control to highlight itself.
522
return true;
523
};
524
525
526
/**
527
* Handles HIGHLIGHT events dispatched by items in the container when
528
* they are highlighted.
529
* @param {goog.events.Event} e Highlight event to handle.
530
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
531
*/
532
goog.ui.Container.prototype.handleHighlightItem = function(e) {
533
'use strict';
534
var index = this.indexOfChild(/** @type {goog.ui.Control} */ (e.target));
535
if (index > -1 && index != this.highlightedIndex_) {
536
var item = this.getHighlighted();
537
if (item) {
538
// Un-highlight previously highlighted item.
539
item.setHighlighted(false);
540
}
541
542
this.highlightedIndex_ = index;
543
item = this.getHighlighted();
544
545
if (this.isMouseButtonPressed()) {
546
// Activate item when mouse button is pressed, to allow MacOS-style
547
// dragging to choose menu items. Although this should only truly
548
// happen if the highlight is due to mouse movements, there is little
549
// harm in doing it for keyboard or programmatic highlights.
550
item.setActive(true);
551
}
552
553
// Update open item if open item needs follow highlight.
554
if (this.openFollowsHighlight_ && this.openItem_ &&
555
item != this.openItem_) {
556
if (item.isSupportedState(goog.ui.Component.State.OPENED)) {
557
item.setOpen(true);
558
} else {
559
this.openItem_.setOpen(false);
560
}
561
}
562
}
563
564
var element = this.getElement();
565
goog.asserts.assert(
566
element, 'The DOM element for the container cannot be null.');
567
if (e.target.getElement() != null) {
568
goog.a11y.aria.setState(
569
element, goog.a11y.aria.State.ACTIVEDESCENDANT,
570
e.target.getElement().id);
571
}
572
};
573
574
575
/**
576
* Handles UNHIGHLIGHT events dispatched by items in the container when
577
* they are unhighlighted.
578
* @param {goog.events.Event} e Unhighlight event to handle.
579
*/
580
goog.ui.Container.prototype.handleUnHighlightItem = function(e) {
581
'use strict';
582
if (e.target == this.getHighlighted()) {
583
this.highlightedIndex_ = -1;
584
}
585
var element = this.getElement();
586
goog.asserts.assert(
587
element, 'The DOM element for the container cannot be null.');
588
// Setting certain ARIA attributes to empty strings is problematic.
589
// Just remove the attribute instead.
590
goog.a11y.aria.removeState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);
591
};
592
593
594
/**
595
* Handles OPEN events dispatched by items in the container when they are
596
* opened.
597
* @param {goog.events.Event} e Open event to handle.
598
*/
599
goog.ui.Container.prototype.handleOpenItem = function(e) {
600
'use strict';
601
var item = /** @type {goog.ui.Control} */ (e.target);
602
if (item && item != this.openItem_ && item.getParent() == this) {
603
if (this.openItem_) {
604
this.openItem_.setOpen(false);
605
}
606
this.openItem_ = item;
607
}
608
};
609
610
611
/**
612
* Handles CLOSE events dispatched by items in the container when they are
613
* closed.
614
* @param {goog.events.Event} e Close event to handle.
615
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
616
*/
617
goog.ui.Container.prototype.handleCloseItem = function(e) {
618
'use strict';
619
if (e.target == this.openItem_) {
620
this.openItem_ = null;
621
}
622
623
var element = this.getElement();
624
var targetEl = e.target.getElement();
625
// Set the active descendant to the menu item when its submenu is closed and
626
// it is still highlighted. This can sometimes be called when the menuitem is
627
// unhighlighted because the focus moved elsewhere, do nothing at that point.
628
if (element && e.target.isHighlighted() && targetEl) {
629
goog.a11y.aria.setActiveDescendant(element, targetEl);
630
}
631
};
632
633
634
/**
635
* Handles mousedown events over the container. The default implementation
636
* sets the "mouse button pressed" flag and, if the container is focusable,
637
* grabs keyboard focus.
638
* @param {goog.events.BrowserEvent} e Mousedown event to handle.
639
*/
640
goog.ui.Container.prototype.handleMouseDown = function(e) {
641
'use strict';
642
if (this.enabled_) {
643
this.setMouseButtonPressed(true);
644
}
645
646
var keyTarget = this.getKeyEventTarget();
647
if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {
648
// The container is configured to receive keyboard focus.
649
keyTarget.focus();
650
} else {
651
// The control isn't configured to receive keyboard focus; prevent it
652
// from stealing focus or destroying the selection.
653
e.preventDefault();
654
}
655
};
656
657
658
/**
659
* Handles mouseup events over the document. The default implementation
660
* clears the "mouse button pressed" flag.
661
* @param {goog.events.BrowserEvent} e Mouseup event to handle.
662
*/
663
goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {
664
'use strict';
665
this.setMouseButtonPressed(false);
666
};
667
668
669
/**
670
* Handles mouse events originating from nodes belonging to the controls hosted
671
* in the container. Locates the child control based on the DOM node that
672
* dispatched the event, and forwards the event to the control for handling.
673
* @param {goog.events.BrowserEvent} e Mouse event to handle.
674
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
675
*/
676
goog.ui.Container.prototype.handleChildMouseEvents = function(e) {
677
'use strict';
678
var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);
679
680
var control = this.getOwnerControl(/** @type {Node} */ (e.target));
681
if (control) {
682
// Child control identified; forward the event.
683
switch (e.type) {
684
case MouseEventType.MOUSEDOWN:
685
control.handleMouseDown(e);
686
break;
687
case MouseEventType.MOUSEUP:
688
case MouseEventType.MOUSECANCEL:
689
control.handleMouseUp(e);
690
break;
691
case goog.events.EventType.MOUSEOVER:
692
control.handleMouseOver(e);
693
break;
694
case goog.events.EventType.MOUSEOUT:
695
control.handleMouseOut(e);
696
break;
697
case goog.events.EventType.CONTEXTMENU:
698
control.handleContextMenu(e);
699
break;
700
}
701
}
702
};
703
704
705
/**
706
* Returns the child control that owns the given DOM node, or null if no such
707
* control is found.
708
* @param {Node} node DOM node whose owner is to be returned.
709
* @return {goog.ui.Control?} Control hosted in the container to which the node
710
* belongs (if found).
711
* @protected
712
* @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
713
*/
714
goog.ui.Container.prototype.getOwnerControl = function(node) {
715
'use strict';
716
// Ensure that this container actually has child controls before
717
// looking up the owner.
718
if (this.childElementIdMap_) {
719
var elem = this.getElement();
720
// See http://b/2964418 . IE9 appears to evaluate '!=' incorrectly, so
721
// using '!==' instead.
722
// TODO(user): Possibly revert this change if/when IE9 fixes the issue.
723
while (node && node !== elem) {
724
var id = node.id;
725
if (id in this.childElementIdMap_) {
726
return this.childElementIdMap_[id];
727
}
728
node = node.parentNode;
729
}
730
}
731
return null;
732
};
733
734
735
/**
736
* Handles focus events raised when the container's key event target receives
737
* keyboard focus.
738
* @param {goog.events.BrowserEvent} e Focus event to handle.
739
*/
740
goog.ui.Container.prototype.handleFocus = function(e) {
741
// No-op in the base class.
742
};
743
744
745
/**
746
* Handles blur events raised when the container's key event target loses
747
* keyboard focus. The default implementation clears the highlight index.
748
* @param {goog.events.BrowserEvent} e Blur event to handle.
749
*/
750
goog.ui.Container.prototype.handleBlur = function(e) {
751
'use strict';
752
this.setHighlightedIndex(-1);
753
this.setMouseButtonPressed(false);
754
// If the container loses focus, and one of its children is open, close it.
755
if (this.openItem_) {
756
this.openItem_.setOpen(false);
757
}
758
};
759
760
761
/**
762
* Attempts to handle a keyboard event, if the control is enabled, by calling
763
* {@link handleKeyEventInternal}. Considered protected; should only be used
764
* within this package and by subclasses.
765
* @param {goog.events.KeyEvent} e Key event to handle.
766
* @return {boolean} Whether the key event was handled.
767
*/
768
goog.ui.Container.prototype.handleKeyEvent = function(e) {
769
'use strict';
770
if (this.isEnabled() && this.isVisible() &&
771
(this.getChildCount() != 0 || this.keyEventTarget_) &&
772
this.handleKeyEventInternal(e)) {
773
e.preventDefault();
774
e.stopPropagation();
775
return true;
776
}
777
return false;
778
};
779
780
781
/**
782
* Attempts to handle a keyboard event; returns true if the event was handled,
783
* false otherwise. If the container is enabled, and a child is highlighted,
784
* calls the child control's `handleKeyEvent` method to give the control
785
* a chance to handle the event first.
786
* @param {goog.events.KeyEvent} e Key event to handle.
787
* @return {boolean} Whether the event was handled by the container (or one of
788
* its children).
789
*/
790
goog.ui.Container.prototype.handleKeyEventInternal = function(e) {
791
'use strict';
792
// Give the highlighted control the chance to handle the key event.
793
var highlighted = this.getHighlighted();
794
if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&
795
highlighted.handleKeyEvent(e)) {
796
return true;
797
}
798
799
// Give the open control the chance to handle the key event.
800
if (this.openItem_ && this.openItem_ != highlighted &&
801
typeof this.openItem_.handleKeyEvent == 'function' &&
802
this.openItem_.handleKeyEvent(e)) {
803
return true;
804
}
805
806
// Do not handle the key event if any modifier key is pressed.
807
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
808
return false;
809
}
810
811
// Either nothing is highlighted, or the highlighted control didn't handle
812
// the key event, so attempt to handle it here.
813
switch (e.keyCode) {
814
case goog.events.KeyCodes.ESC:
815
if (this.isFocusable()) {
816
this.getKeyEventTarget().blur();
817
} else {
818
return false;
819
}
820
break;
821
822
case goog.events.KeyCodes.HOME:
823
this.highlightFirst();
824
break;
825
826
case goog.events.KeyCodes.END:
827
this.highlightLast();
828
break;
829
830
case goog.events.KeyCodes.UP:
831
if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
832
this.highlightPrevious();
833
} else {
834
return false;
835
}
836
break;
837
838
case goog.events.KeyCodes.LEFT:
839
if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
840
if (this.isRightToLeft()) {
841
this.highlightNext();
842
} else {
843
this.highlightPrevious();
844
}
845
} else {
846
return false;
847
}
848
break;
849
850
case goog.events.KeyCodes.DOWN:
851
if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
852
this.highlightNext();
853
} else {
854
return false;
855
}
856
break;
857
858
case goog.events.KeyCodes.RIGHT:
859
if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
860
if (this.isRightToLeft()) {
861
this.highlightPrevious();
862
} else {
863
this.highlightNext();
864
}
865
} else {
866
return false;
867
}
868
break;
869
870
default:
871
return false;
872
}
873
874
return true;
875
};
876
877
878
// Child component management.
879
880
881
/**
882
* Creates a DOM ID for the child control and registers it to an internal
883
* hash table to be able to find it fast by id.
884
* @param {goog.ui.Component} child The child control. Its root element has
885
* to be created yet.
886
* @private
887
*/
888
goog.ui.Container.prototype.registerChildId_ = function(child) {
889
'use strict';
890
// Map the DOM ID of the control's root element to the control itself.
891
var childElem = child.getElement();
892
893
// If the control's root element doesn't have a DOM ID assign one.
894
var id = childElem.id || (childElem.id = child.getId());
895
896
// Lazily create the child element ID map on first use.
897
if (!this.childElementIdMap_) {
898
this.childElementIdMap_ = {};
899
}
900
this.childElementIdMap_[id] = child;
901
};
902
903
904
/**
905
* Adds the specified control as the last child of this container. See
906
* {@link goog.ui.Container#addChildAt} for detailed semantics.
907
* @param {goog.ui.Component} child The new child control.
908
* @param {boolean=} opt_render Whether the new child should be rendered
909
* immediately after being added (defaults to false).
910
* @override
911
*/
912
goog.ui.Container.prototype.addChild = function(child, opt_render) {
913
'use strict';
914
goog.asserts.assertInstanceof(
915
child, goog.ui.Control, 'The child of a container must be a control');
916
goog.ui.Container.superClass_.addChild.call(this, child, opt_render);
917
};
918
919
920
/**
921
* Overrides {@link goog.ui.Container#getChild} to make it clear that it
922
* only returns {@link goog.ui.Control}s.
923
* @param {string} id Child component ID.
924
* @return {goog.ui.Control} The child with the given ID; null if none.
925
* @override
926
*/
927
goog.ui.Container.prototype.getChild;
928
929
930
/**
931
* Overrides {@link goog.ui.Container#getChildAt} to make it clear that it
932
* only returns {@link goog.ui.Control}s.
933
* @param {number} index 0-based index.
934
* @return {goog.ui.Control} The child with the given ID; null if none.
935
* @override
936
*/
937
goog.ui.Container.prototype.getChildAt;
938
939
940
/**
941
* Adds the control as a child of this container at the given 0-based index.
942
* Overrides {@link goog.ui.Component#addChildAt} by also updating the
943
* container's highlight index. Since {@link goog.ui.Component#addChild} uses
944
* {@link #addChildAt} internally, we only need to override this method.
945
* @param {goog.ui.Component} control New child.
946
* @param {number} index Index at which the new child is to be added.
947
* @param {boolean=} opt_render Whether the new child should be rendered
948
* immediately after being added (defaults to false).
949
* @override
950
*/
951
goog.ui.Container.prototype.addChildAt = function(control, index, opt_render) {
952
'use strict';
953
goog.asserts.assertInstanceof(control, goog.ui.Control);
954
955
// Make sure the child control dispatches HIGHLIGHT, UNHIGHLIGHT, OPEN, and
956
// CLOSE events, and that it doesn't steal keyboard focus.
957
control.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
958
control.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);
959
if (this.isFocusable() || !this.isFocusableChildrenAllowed()) {
960
control.setSupportedState(goog.ui.Component.State.FOCUSED, false);
961
}
962
963
// Disable mouse event handling by child controls.
964
control.setHandleMouseEvents(false);
965
966
var srcIndex =
967
(control.getParent() == this) ? this.indexOfChild(control) : -1;
968
969
// Let the superclass implementation do the work.
970
goog.ui.Container.superClass_.addChildAt.call(
971
this, control, index, opt_render);
972
973
if (control.isInDocument() && this.isInDocument()) {
974
this.registerChildId_(control);
975
}
976
977
this.updateHighlightedIndex_(srcIndex, index);
978
};
979
980
981
/**
982
* Updates the highlighted index when children are added or moved.
983
* @param {number} fromIndex Index of the child before it was moved, or -1 if
984
* the child was added.
985
* @param {number} toIndex Index of the child after it was moved or added.
986
* @private
987
*/
988
goog.ui.Container.prototype.updateHighlightedIndex_ = function(
989
fromIndex, toIndex) {
990
'use strict';
991
if (fromIndex == -1) {
992
fromIndex = this.getChildCount();
993
}
994
if (fromIndex == this.highlightedIndex_) {
995
// The highlighted element itself was moved.
996
this.highlightedIndex_ = Math.min(this.getChildCount() - 1, toIndex);
997
} else if (
998
fromIndex > this.highlightedIndex_ && toIndex <= this.highlightedIndex_) {
999
// The control was added or moved behind the highlighted index.
1000
this.highlightedIndex_++;
1001
} else if (
1002
fromIndex < this.highlightedIndex_ && toIndex > this.highlightedIndex_) {
1003
// The control was moved from before to behind the highlighted index.
1004
this.highlightedIndex_--;
1005
}
1006
};
1007
1008
1009
/**
1010
* Removes a child control. Overrides {@link goog.ui.Component#removeChild} by
1011
* updating the highlight index. Since {@link goog.ui.Component#removeChildAt}
1012
* uses {@link #removeChild} internally, we only need to override this method.
1013
* @param {string|goog.ui.Component} control The ID of the child to remove, or
1014
* the control itself.
1015
* @param {boolean=} opt_unrender Whether to call `exitDocument` on the
1016
* removed control, and detach its DOM from the document (defaults to
1017
* false).
1018
* @return {?goog.ui.Control} The removed control, if any.
1019
* @override
1020
*/
1021
goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {
1022
'use strict';
1023
control = (typeof control === 'string') ? this.getChild(control) : control;
1024
goog.asserts.assertInstanceof(control, goog.ui.Control);
1025
1026
if (control) {
1027
var index = this.indexOfChild(control);
1028
if (index != -1) {
1029
if (index == this.highlightedIndex_) {
1030
control.setHighlighted(false);
1031
this.highlightedIndex_ = -1;
1032
} else if (index < this.highlightedIndex_) {
1033
this.highlightedIndex_--;
1034
}
1035
}
1036
1037
// Remove the mapping from the child element ID map.
1038
var childElem = control.getElement();
1039
if (childElem && childElem.id && this.childElementIdMap_) {
1040
goog.object.remove(this.childElementIdMap_, childElem.id);
1041
}
1042
}
1043
1044
control = /** @type {!goog.ui.Control} */ (
1045
goog.ui.Container.superClass_.removeChild.call(
1046
this, control, opt_unrender));
1047
1048
// Re-enable mouse event handling (in case the control is reused elsewhere).
1049
control.setHandleMouseEvents(true);
1050
1051
return control;
1052
};
1053
1054
1055
// Container state management.
1056
1057
1058
/**
1059
* Returns the container's orientation.
1060
* @return {?goog.ui.Container.Orientation} Container orientation.
1061
*/
1062
goog.ui.Container.prototype.getOrientation = function() {
1063
'use strict';
1064
return this.orientation_;
1065
};
1066
1067
1068
/**
1069
* Sets the container's orientation.
1070
* @param {goog.ui.Container.Orientation} orientation Container orientation.
1071
*/
1072
// TODO(attila): Do we need to support containers with dynamic orientation?
1073
goog.ui.Container.prototype.setOrientation = function(orientation) {
1074
'use strict';
1075
if (this.getElement()) {
1076
// Too late.
1077
throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
1078
}
1079
1080
this.orientation_ = orientation;
1081
};
1082
1083
1084
/**
1085
* Returns true if the container's visibility is set to visible, false if
1086
* it is set to hidden. A container that is set to hidden is guaranteed
1087
* to be hidden from the user, but the reverse isn't necessarily true.
1088
* A container may be set to visible but can otherwise be obscured by another
1089
* element, rendered off-screen, or hidden using direct CSS manipulation.
1090
* @return {boolean} Whether the container is set to be visible.
1091
*/
1092
goog.ui.Container.prototype.isVisible = function() {
1093
'use strict';
1094
return this.visible_;
1095
};
1096
1097
1098
/**
1099
* Shows or hides the container. Does nothing if the container already has
1100
* the requested visibility. Otherwise, dispatches a SHOW or HIDE event as
1101
* appropriate, giving listeners a chance to prevent the visibility change.
1102
* @param {boolean} visible Whether to show or hide the container.
1103
* @param {boolean=} opt_force If true, doesn't check whether the container
1104
* already has the requested visibility, and doesn't dispatch any events.
1105
* @return {boolean} Whether the visibility was changed.
1106
*/
1107
goog.ui.Container.prototype.setVisible = function(visible, opt_force) {
1108
'use strict';
1109
if (opt_force ||
1110
(this.visible_ != visible &&
1111
this.dispatchEvent(
1112
visible ? goog.ui.Component.EventType.SHOW :
1113
goog.ui.Component.EventType.HIDE))) {
1114
this.visible_ = visible;
1115
1116
var elem = this.getElement();
1117
if (elem) {
1118
goog.style.setElementShown(elem, visible);
1119
if (this.isFocusable()) {
1120
// Enable keyboard access only for enabled & visible containers.
1121
this.renderer_.enableTabIndex(
1122
this.getKeyEventTarget(), this.enabled_ && this.visible_);
1123
}
1124
if (!opt_force) {
1125
this.dispatchEvent(
1126
this.visible_ ? goog.ui.Container.EventType.AFTER_SHOW :
1127
goog.ui.Container.EventType.AFTER_HIDE);
1128
}
1129
}
1130
1131
return true;
1132
}
1133
1134
return false;
1135
};
1136
1137
1138
/**
1139
* Returns true if the container is enabled, false otherwise.
1140
* @return {boolean} Whether the container is enabled.
1141
*/
1142
goog.ui.Container.prototype.isEnabled = function() {
1143
'use strict';
1144
return this.enabled_;
1145
};
1146
1147
1148
/**
1149
* Enables/disables the container based on the `enable` argument.
1150
* Dispatches an `ENABLED` or `DISABLED` event prior to changing
1151
* the container's state, which may be caught and canceled to prevent the
1152
* container from changing state. Also enables/disables child controls.
1153
* @param {boolean} enable Whether to enable or disable the container.
1154
*/
1155
goog.ui.Container.prototype.setEnabled = function(enable) {
1156
'use strict';
1157
if (this.enabled_ != enable &&
1158
this.dispatchEvent(
1159
enable ? goog.ui.Component.EventType.ENABLE :
1160
goog.ui.Component.EventType.DISABLE)) {
1161
if (enable) {
1162
// Flag the container as enabled first, then update children. This is
1163
// because controls can't be enabled if their parent is disabled.
1164
this.enabled_ = true;
1165
this.forEachChild(function(child) {
1166
'use strict';
1167
// Enable child control unless it is flagged.
1168
if (child.wasDisabled) {
1169
delete child.wasDisabled;
1170
} else {
1171
child.setEnabled(true);
1172
}
1173
});
1174
} else {
1175
// Disable children first, then flag the container as disabled. This is
1176
// because controls can't be disabled if their parent is already disabled.
1177
this.forEachChild(function(child) {
1178
'use strict';
1179
// Disable child control, or flag it if it's already disabled.
1180
if (child.isEnabled()) {
1181
child.setEnabled(false);
1182
} else {
1183
child.wasDisabled = true;
1184
}
1185
});
1186
this.enabled_ = false;
1187
this.setMouseButtonPressed(false);
1188
}
1189
1190
if (this.isFocusable()) {
1191
// Enable keyboard access only for enabled & visible components.
1192
this.renderer_.enableTabIndex(
1193
this.getKeyEventTarget(), enable && this.visible_);
1194
}
1195
}
1196
};
1197
1198
1199
/**
1200
* Returns true if the container is focusable, false otherwise. The default
1201
* is true. Focusable containers always have a tab index and allocate a key
1202
* handler to handle keyboard events while focused.
1203
* @return {boolean} Whether the component is focusable.
1204
*/
1205
goog.ui.Container.prototype.isFocusable = function() {
1206
'use strict';
1207
return this.focusable_;
1208
};
1209
1210
1211
/**
1212
* Sets whether the container is focusable. The default is true. Focusable
1213
* containers always have a tab index and allocate a key handler to handle
1214
* keyboard events while focused.
1215
* @param {boolean} focusable Whether the component is to be focusable.
1216
*/
1217
goog.ui.Container.prototype.setFocusable = function(focusable) {
1218
'use strict';
1219
if (focusable != this.focusable_ && this.isInDocument()) {
1220
this.enableFocusHandling_(focusable);
1221
}
1222
this.focusable_ = focusable;
1223
if (this.enabled_ && this.visible_) {
1224
this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);
1225
}
1226
};
1227
1228
1229
/**
1230
* Returns true if the container allows children to be focusable, false
1231
* otherwise. Only effective if the container is not focusable.
1232
* @return {boolean} Whether children should be focusable.
1233
*/
1234
goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {
1235
'use strict';
1236
return this.allowFocusableChildren_;
1237
};
1238
1239
1240
/**
1241
* Sets whether the container allows children to be focusable, false
1242
* otherwise. Only effective if the container is not focusable.
1243
* @param {boolean} focusable Whether the children should be focusable.
1244
*/
1245
goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {
1246
'use strict';
1247
this.allowFocusableChildren_ = focusable;
1248
};
1249
1250
1251
/**
1252
* @return {boolean} Whether highlighting a child component should also open it.
1253
*/
1254
goog.ui.Container.prototype.isOpenFollowsHighlight = function() {
1255
'use strict';
1256
return this.openFollowsHighlight_;
1257
};
1258
1259
1260
/**
1261
* Sets whether highlighting a child component should also open it.
1262
* @param {boolean} follow Whether highlighting a child component also opens it.
1263
*/
1264
goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {
1265
'use strict';
1266
this.openFollowsHighlight_ = follow;
1267
};
1268
1269
1270
// Highlight management.
1271
1272
1273
/**
1274
* Returns the index of the currently highlighted item (-1 if none).
1275
* @return {number} Index of the currently highlighted item.
1276
*/
1277
goog.ui.Container.prototype.getHighlightedIndex = function() {
1278
'use strict';
1279
return this.highlightedIndex_;
1280
};
1281
1282
1283
/**
1284
* Highlights the item at the given 0-based index (if any). If another item
1285
* was previously highlighted, it is un-highlighted.
1286
* @param {number} index Index of item to highlight (-1 removes the current
1287
* highlight).
1288
*/
1289
goog.ui.Container.prototype.setHighlightedIndex = function(index) {
1290
'use strict';
1291
var child = this.getChildAt(index);
1292
if (child) {
1293
child.setHighlighted(true);
1294
} else if (this.highlightedIndex_ > -1) {
1295
this.getHighlighted().setHighlighted(false);
1296
}
1297
};
1298
1299
1300
/**
1301
* Highlights the given item if it exists and is a child of the container;
1302
* otherwise un-highlights the currently highlighted item.
1303
* @param {goog.ui.Control} item Item to highlight.
1304
*/
1305
goog.ui.Container.prototype.setHighlighted = function(item) {
1306
'use strict';
1307
this.setHighlightedIndex(this.indexOfChild(item));
1308
};
1309
1310
1311
/**
1312
* Returns the currently highlighted item (if any).
1313
* @return {goog.ui.Control?} Highlighted item (null if none).
1314
*/
1315
goog.ui.Container.prototype.getHighlighted = function() {
1316
'use strict';
1317
return this.getChildAt(this.highlightedIndex_);
1318
};
1319
1320
1321
/**
1322
* Highlights the first highlightable item in the container
1323
*/
1324
goog.ui.Container.prototype.highlightFirst = function() {
1325
'use strict';
1326
this.highlightHelper(function(index, max) {
1327
'use strict';
1328
return (index + 1) % max;
1329
}, this.getChildCount() - 1);
1330
};
1331
1332
1333
/**
1334
* Highlights the last highlightable item in the container.
1335
*/
1336
goog.ui.Container.prototype.highlightLast = function() {
1337
'use strict';
1338
this.highlightHelper(function(index, max) {
1339
'use strict';
1340
index--;
1341
return index < 0 ? max - 1 : index;
1342
}, 0);
1343
};
1344
1345
1346
/**
1347
* Highlights the next highlightable item (or the first if nothing is currently
1348
* highlighted).
1349
*/
1350
goog.ui.Container.prototype.highlightNext = function() {
1351
'use strict';
1352
this.highlightHelper(function(index, max) {
1353
'use strict';
1354
return (index + 1) % max;
1355
}, this.highlightedIndex_);
1356
};
1357
1358
1359
/**
1360
* Highlights the previous highlightable item (or the last if nothing is
1361
* currently highlighted).
1362
*/
1363
goog.ui.Container.prototype.highlightPrevious = function() {
1364
'use strict';
1365
this.highlightHelper(function(index, max) {
1366
'use strict';
1367
index--;
1368
return index < 0 ? max - 1 : index;
1369
}, this.highlightedIndex_);
1370
};
1371
1372
1373
/**
1374
* Helper function that manages the details of moving the highlight among
1375
* child controls in response to keyboard events.
1376
* @param {function(this: goog.ui.Container, number, number) : number} fn
1377
* Function that accepts the current and maximum indices, and returns the
1378
* next index to check.
1379
* @param {number} startIndex Start index.
1380
* @return {boolean} Whether the highlight has changed.
1381
* @protected
1382
*/
1383
goog.ui.Container.prototype.highlightHelper = function(fn, startIndex) {
1384
'use strict';
1385
// If the start index is -1 (meaning there's nothing currently highlighted),
1386
// try starting from the currently open item, if any.
1387
var curIndex =
1388
startIndex < 0 ? this.indexOfChild(this.openItem_) : startIndex;
1389
var numItems = this.getChildCount();
1390
1391
curIndex = fn.call(this, curIndex, numItems);
1392
var visited = 0;
1393
while (visited <= numItems) {
1394
var control = this.getChildAt(curIndex);
1395
if (control && this.canHighlightItem(control)) {
1396
this.setHighlightedIndexFromKeyEvent(curIndex);
1397
return true;
1398
}
1399
visited++;
1400
curIndex = fn.call(this, curIndex, numItems);
1401
}
1402
return false;
1403
};
1404
1405
1406
/**
1407
* Returns whether the given item can be highlighted.
1408
* @param {goog.ui.Control} item The item to check.
1409
* @return {boolean} Whether the item can be highlighted.
1410
* @protected
1411
*/
1412
goog.ui.Container.prototype.canHighlightItem = function(item) {
1413
'use strict';
1414
return item.isVisible() && item.isEnabled() &&
1415
item.isSupportedState(goog.ui.Component.State.HOVER);
1416
};
1417
1418
1419
/**
1420
* Helper method that sets the highlighted index to the given index in response
1421
* to a keyboard event. The base class implementation simply calls the
1422
* {@link #setHighlightedIndex} method, but subclasses can override this
1423
* behavior as needed.
1424
* @param {number} index Index of item to highlight.
1425
* @protected
1426
*/
1427
goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {
1428
'use strict';
1429
this.setHighlightedIndex(index);
1430
};
1431
1432
1433
/**
1434
* Returns the currently open (expanded) control in the container (null if
1435
* none).
1436
* @return {goog.ui.Control?} The currently open control.
1437
*/
1438
goog.ui.Container.prototype.getOpenItem = function() {
1439
'use strict';
1440
return this.openItem_;
1441
};
1442
1443
1444
/**
1445
* Returns true if the mouse button is pressed, false otherwise.
1446
* @return {boolean} Whether the mouse button is pressed.
1447
*/
1448
goog.ui.Container.prototype.isMouseButtonPressed = function() {
1449
'use strict';
1450
return this.mouseButtonPressed_;
1451
};
1452
1453
1454
/**
1455
* Sets or clears the "mouse button pressed" flag.
1456
* @param {boolean} pressed Whether the mouse button is presed.
1457
*/
1458
goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {
1459
'use strict';
1460
this.mouseButtonPressed_ = pressed;
1461
};
1462
1463