Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/third_party/closure/goog/ui/submenu.js
4050 views
1
/**
2
* @license
3
* Copyright The Closure Library Authors.
4
* SPDX-License-Identifier: Apache-2.0
5
*/
6
7
/**
8
* @fileoverview A class representing menu items that open a submenu.
9
* @see goog.ui.Menu
10
*
11
* @see ../demos/submenus.html
12
* @see ../demos/submenus2.html
13
*/
14
15
goog.provide('goog.ui.SubMenu');
16
17
goog.require('goog.Timer');
18
goog.require('goog.asserts');
19
goog.require('goog.dom');
20
goog.require('goog.dom.classlist');
21
goog.require('goog.events.KeyCodes');
22
goog.require('goog.positioning.AnchoredViewportPosition');
23
goog.require('goog.positioning.Corner');
24
goog.require('goog.style');
25
goog.require('goog.ui.Component');
26
goog.require('goog.ui.Menu');
27
goog.require('goog.ui.MenuItem');
28
goog.require('goog.ui.SubMenuRenderer');
29
goog.require('goog.ui.registry');
30
goog.requireType('goog.events.BrowserEvent');
31
goog.requireType('goog.events.Event');
32
goog.requireType('goog.events.KeyEvent');
33
goog.requireType('goog.ui.ControlContent');
34
goog.requireType('goog.ui.MenuHeader');
35
goog.requireType('goog.ui.MenuItemRenderer');
36
goog.requireType('goog.ui.MenuSeparator');
37
38
39
40
/**
41
* Class representing a submenu that can be added as an item to other menus.
42
*
43
* @param {goog.ui.ControlContent} content Text caption or DOM structure to
44
* display as the content of the submenu (use to add icons or styling to
45
* menus).
46
* @param {*=} opt_model Data/model associated with the menu item.
47
* @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper used for dom
48
* interactions.
49
* @param {goog.ui.MenuItemRenderer=} opt_renderer Renderer used to render or
50
* decorate the component; defaults to {@link goog.ui.SubMenuRenderer}.
51
* @constructor
52
* @extends {goog.ui.MenuItem}
53
*/
54
goog.ui.SubMenu = function(content, opt_model, opt_domHelper, opt_renderer) {
55
'use strict';
56
goog.ui.MenuItem.call(
57
this, content, opt_model, opt_domHelper,
58
opt_renderer || goog.ui.SubMenuRenderer.getInstance());
59
};
60
goog.inherits(goog.ui.SubMenu, goog.ui.MenuItem);
61
62
63
/**
64
* The delay before opening the sub menu in milliseconds.
65
* @type {number}
66
*/
67
goog.ui.SubMenu.MENU_DELAY_MS = 218;
68
69
70
/**
71
* Timer used to dismiss the submenu when the item becomes unhighlighted.
72
* @type {?number}
73
* @private
74
*/
75
goog.ui.SubMenu.prototype.dismissTimer_ = null;
76
77
78
/**
79
* Timer used to show the submenu on mouseover.
80
* @type {?number}
81
* @private
82
*/
83
goog.ui.SubMenu.prototype.showTimer_ = null;
84
85
86
/**
87
* Whether the submenu believes the menu is visible.
88
* @type {boolean}
89
* @private
90
*/
91
goog.ui.SubMenu.prototype.menuIsVisible_ = false;
92
93
94
/**
95
* The lazily created sub menu.
96
* @type {goog.ui.Menu?}
97
* @private
98
*/
99
goog.ui.SubMenu.prototype.subMenu_ = null;
100
101
102
/**
103
* Whether or not the sub-menu was set explicitly.
104
* @type {boolean}
105
* @private
106
*/
107
goog.ui.SubMenu.prototype.externalSubMenu_ = false;
108
109
110
/**
111
* Whether or not to align the submenu at the end of the parent menu.
112
* If true, the menu expands to the right in LTR languages and to the left
113
* in RTL langauges.
114
* @type {boolean}
115
* @private
116
*/
117
goog.ui.SubMenu.prototype.alignToEnd_ = true;
118
119
120
/**
121
* Whether the position of this submenu may be adjusted to fit
122
* the visible area, as in {@link goog.ui.Popup.positionAtCoordinate}.
123
* @type {boolean}
124
* @private
125
*/
126
goog.ui.SubMenu.prototype.isPositionAdjustable_ = false;
127
128
129
/** @override */
130
goog.ui.SubMenu.prototype.enterDocument = function() {
131
'use strict';
132
goog.ui.SubMenu.superClass_.enterDocument.call(this);
133
134
this.getHandler().listen(
135
this.getParent(), goog.ui.Component.EventType.HIDE, this.onParentHidden_);
136
137
if (this.subMenu_) {
138
this.setMenuListenersEnabled_(this.subMenu_, true);
139
}
140
};
141
142
143
/** @override */
144
goog.ui.SubMenu.prototype.exitDocument = function() {
145
'use strict';
146
this.getHandler().unlisten(
147
this.getParent(), goog.ui.Component.EventType.HIDE, this.onParentHidden_);
148
149
if (this.subMenu_) {
150
this.setMenuListenersEnabled_(this.subMenu_, false);
151
if (!this.externalSubMenu_) {
152
this.subMenu_.exitDocument();
153
goog.dom.removeNode(this.subMenu_.getElement());
154
}
155
}
156
157
goog.ui.SubMenu.superClass_.exitDocument.call(this);
158
};
159
160
161
/** @override */
162
goog.ui.SubMenu.prototype.disposeInternal = function() {
163
'use strict';
164
if (this.subMenu_ && !this.externalSubMenu_) {
165
this.subMenu_.dispose();
166
}
167
this.subMenu_ = null;
168
goog.ui.SubMenu.superClass_.disposeInternal.call(this);
169
};
170
171
172
/**
173
* @override
174
* Dismisses the submenu on a delay, with the result that the user needs less
175
* accuracy when moving to submenus. Alternate implementations could use
176
* geometry instead of a timer.
177
* @param {boolean} highlight Whether item should be highlighted.
178
* @param {boolean=} opt_btnPressed Whether the mouse button is held down.
179
*/
180
goog.ui.SubMenu.prototype.setHighlighted = function(highlight, opt_btnPressed) {
181
'use strict';
182
goog.ui.SubMenu.superClass_.setHighlighted.call(this, highlight);
183
184
if (opt_btnPressed) {
185
this.getMenu().setMouseButtonPressed(true);
186
}
187
188
if (!highlight) {
189
if (this.dismissTimer_) {
190
goog.Timer.clear(this.dismissTimer_);
191
}
192
this.dismissTimer_ =
193
goog.Timer.callOnce(this.dismissSubMenu, this.getMenuDelay(), this);
194
}
195
};
196
197
198
/**
199
* Show the submenu and ensure that all siblings are hidden.
200
*/
201
goog.ui.SubMenu.prototype.showSubMenu = function() {
202
'use strict';
203
// Only show the menu if this item is still selected. This is called on a
204
// timeout, so make sure our parent still exists.
205
var parent = this.getParent();
206
if (parent && parent.getHighlighted() == this) {
207
this.setSubMenuVisible_(true);
208
this.dismissSiblings_();
209
}
210
};
211
212
213
/**
214
* Dismisses the menu and all further submenus.
215
*/
216
goog.ui.SubMenu.prototype.dismissSubMenu = function() {
217
'use strict';
218
// Because setHighlighted calls this function on a timeout, we need to make
219
// sure that the sub menu hasn't been disposed when we come back.
220
var subMenu = this.subMenu_;
221
if (subMenu && subMenu.getParent() == this) {
222
this.setSubMenuVisible_(false);
223
subMenu.forEachChild(function(child) {
224
'use strict';
225
if (typeof child.dismissSubMenu == 'function') {
226
child.dismissSubMenu();
227
}
228
});
229
}
230
};
231
232
233
/**
234
* Clears the show and hide timers for the sub menu.
235
*/
236
goog.ui.SubMenu.prototype.clearTimers = function() {
237
'use strict';
238
if (this.dismissTimer_) {
239
goog.Timer.clear(this.dismissTimer_);
240
}
241
if (this.showTimer_) {
242
goog.Timer.clear(this.showTimer_);
243
}
244
};
245
246
247
/**
248
* Sets the menu item to be visible or invisible.
249
* @param {boolean} visible Whether to show or hide the component.
250
* @param {boolean=} opt_force If true, doesn't check whether the component
251
* already has the requested visibility, and doesn't dispatch any events.
252
* @return {boolean} Whether the visibility was changed.
253
* @override
254
*/
255
goog.ui.SubMenu.prototype.setVisible = function(visible, opt_force) {
256
'use strict';
257
var visibilityChanged =
258
goog.ui.SubMenu.superClass_.setVisible.call(this, visible, opt_force);
259
// For menus that allow menu items to be hidden (i.e. ComboBox) ensure that
260
// the submenu is hidden.
261
if (visibilityChanged && !this.isVisible()) {
262
this.dismissSubMenu();
263
}
264
return visibilityChanged;
265
};
266
267
268
/**
269
* Dismiss all the sub menus of sibling menu items.
270
* @private
271
*/
272
goog.ui.SubMenu.prototype.dismissSiblings_ = function() {
273
'use strict';
274
this.getParent().forEachChild(function(child) {
275
'use strict';
276
if (child != this && typeof child.dismissSubMenu == 'function') {
277
child.dismissSubMenu();
278
child.clearTimers();
279
}
280
}, this);
281
};
282
283
284
/**
285
* Handles a key event that is passed to the menu item from its parent because
286
* it is highlighted. If the arrow keys or enter key is pressed the sub menu
287
* takes control and delegates further key events to its menu until it is
288
* dismissed.
289
* @param {goog.events.KeyEvent} e A key event.
290
* @return {boolean} Whether the event was handled.
291
* @override
292
*/
293
goog.ui.SubMenu.prototype.handleKeyEvent = function(e) {
294
'use strict';
295
var keyCode = e.keyCode;
296
var arrowOpenKeyCode = this.isRightToLeft() ? goog.events.KeyCodes.LEFT :
297
goog.events.KeyCodes.RIGHT;
298
var closeKeyCode = this.isRightToLeft() ? goog.events.KeyCodes.RIGHT :
299
goog.events.KeyCodes.LEFT;
300
301
if (!this.menuIsVisible_) {
302
// Menu item doesn't have keyboard control and the correct key was pressed.
303
// So open take keyboard control and open the sub menu.
304
if (this.isEnabled() &&
305
(keyCode == arrowOpenKeyCode || keyCode == goog.events.KeyCodes.ENTER ||
306
keyCode == this.getMnemonic())) {
307
this.showSubMenu();
308
this.getMenu().highlightFirst();
309
this.clearTimers();
310
311
// The menu item doesn't currently care about the key events so let the
312
// parent menu handle them accordingly .
313
} else {
314
return false;
315
}
316
317
// Menu item has control, so let its menu try to handle the keys (this may
318
// in turn be handled by sub-sub menus).
319
} else if (this.getMenu().handleKeyEvent(e)) {
320
// Nothing to do
321
322
// The menu has control and the key hasn't yet been handled, on left arrow
323
// we turn off key control.
324
} else if (keyCode == closeKeyCode) {
325
this.dismissSubMenu();
326
327
} else {
328
// Submenu didn't handle the key so let the parent decide what to do.
329
return false;
330
}
331
332
e.preventDefault();
333
return true;
334
};
335
336
337
/**
338
* Listens to the sub menus items and ensures that this menu item is selected
339
* while dismissing the others. This handles the case when the user mouses
340
* over other items on their way to the sub menu.
341
* @param {goog.events.Event} e Enter event to handle.
342
* @private
343
*/
344
goog.ui.SubMenu.prototype.onChildEnter_ = function(e) {
345
'use strict';
346
if (this.subMenu_.getParent() == this) {
347
this.clearTimers();
348
this.getParentEventTarget().setHighlighted(this);
349
this.dismissSiblings_();
350
}
351
};
352
353
354
/**
355
* Listens to the parent menu's hide event and ensures that all submenus are
356
* hidden at the same time.
357
* @param {goog.events.Event} e The event.
358
* @private
359
*/
360
goog.ui.SubMenu.prototype.onParentHidden_ = function(e) {
361
'use strict';
362
// Ignore propagated events
363
if (e.target == this.getParentEventTarget()) {
364
// TODO(user): Using an event for this is expensive. Consider having a
365
// generalized interface that the parent menu calls on its children when
366
// it is hidden.
367
this.dismissSubMenu();
368
this.clearTimers();
369
}
370
};
371
372
373
/**
374
* @override
375
* Sets a timer to show the submenu and then dispatches an ENTER event to the
376
* parent menu.
377
* @param {goog.events.BrowserEvent} e Mouse event to handle.
378
*/
379
goog.ui.SubMenu.prototype.handleMouseOver = function(e) {
380
'use strict';
381
if (this.isEnabled()) {
382
this.clearTimers();
383
this.showTimer_ =
384
goog.Timer.callOnce(this.showSubMenu, this.getMenuDelay(), this);
385
}
386
goog.ui.SubMenu.superClass_.handleMouseOver.call(this, e);
387
};
388
389
390
/**
391
* Returns the delay before opening or closing the menu in milliseconds.
392
* @return {number}
393
* @protected
394
*/
395
goog.ui.SubMenu.prototype.getMenuDelay = function() {
396
'use strict';
397
return goog.ui.SubMenu.MENU_DELAY_MS;
398
};
399
400
401
/**
402
* Overrides the default mouseup event handler, so that the ACTION isn't
403
* dispatched for the submenu itself, instead the submenu is shown instantly.
404
* @param {goog.events.Event} e The browser event.
405
* @return {boolean} True if the action was allowed to proceed, false otherwise.
406
* @override
407
*/
408
goog.ui.SubMenu.prototype.performActionInternal = function(e) {
409
'use strict';
410
this.clearTimers();
411
var shouldHandleClick =
412
this.isSupportedState(goog.ui.Component.State.SELECTED) ||
413
this.isSupportedState(goog.ui.Component.State.CHECKED);
414
if (shouldHandleClick) {
415
return goog.ui.SubMenu.superClass_.performActionInternal.call(this, e);
416
} else {
417
this.showSubMenu();
418
return true;
419
}
420
};
421
422
423
/**
424
* Sets the visiblility of the sub menu.
425
* @param {boolean} visible Whether to show menu.
426
* @private
427
*/
428
goog.ui.SubMenu.prototype.setSubMenuVisible_ = function(visible) {
429
'use strict';
430
// Unhighlighting the menuitems if closing the menu so the event handlers can
431
// determine the correct state.
432
if (!visible && this.getMenu()) {
433
this.getMenu().setHighlightedIndex(-1);
434
}
435
436
// Dispatch OPEN event before calling getMenu(), so we can create the menu
437
// lazily on first access.
438
this.dispatchEvent(
439
goog.ui.Component.getStateTransitionEvent(
440
goog.ui.Component.State.OPENED, visible));
441
var subMenu = this.getMenu();
442
if (visible != this.menuIsVisible_) {
443
goog.dom.classlist.enable(
444
goog.asserts.assert(this.getElement()),
445
goog.getCssName('goog-submenu-open'), visible);
446
}
447
if (visible != subMenu.isVisible()) {
448
if (visible) {
449
// Lazy-render menu when first shown, if needed.
450
if (!subMenu.isInDocument()) {
451
subMenu.render();
452
}
453
subMenu.setHighlightedIndex(-1);
454
}
455
subMenu.setVisible(visible);
456
// We must position after the menu is visible, otherwise positioning logic
457
// breaks in RTL.
458
if (visible) {
459
this.positionSubMenu();
460
}
461
}
462
this.menuIsVisible_ = visible;
463
};
464
465
466
/**
467
* Attaches or detaches menu event listeners to/from the given menu. Called
468
* each time a menu is attached to or detached from the submenu.
469
* @param {goog.ui.Menu} menu Menu on which to listen for events.
470
* @param {boolean} attach Whether to attach or detach event listeners.
471
* @private
472
*/
473
goog.ui.SubMenu.prototype.setMenuListenersEnabled_ = function(menu, attach) {
474
'use strict';
475
var handler = this.getHandler();
476
var method = attach ? handler.listen : handler.unlisten;
477
method.call(
478
handler, menu, goog.ui.Component.EventType.ENTER, this.onChildEnter_);
479
};
480
481
482
/**
483
* Sets whether the submenu is aligned at the end of the parent menu.
484
* @param {boolean} alignToEnd True to align to end, false to align to start.
485
*/
486
goog.ui.SubMenu.prototype.setAlignToEnd = function(alignToEnd) {
487
'use strict';
488
if (alignToEnd != this.alignToEnd_) {
489
this.alignToEnd_ = alignToEnd;
490
if (this.isInDocument()) {
491
// Completely re-render the widget.
492
var oldElement = this.getElement();
493
this.exitDocument();
494
495
if (oldElement.nextSibling) {
496
this.renderBefore(/** @type {!Element} */ (oldElement.nextSibling));
497
} else {
498
this.render(/** @type {Element} */ (oldElement.parentNode));
499
}
500
}
501
}
502
};
503
504
505
/**
506
* Determines whether the submenu is aligned at the end of the parent menu.
507
* @return {boolean} True if aligned to the end (the default), false if
508
* aligned to the start.
509
*/
510
goog.ui.SubMenu.prototype.isAlignedToEnd = function() {
511
'use strict';
512
return this.alignToEnd_;
513
};
514
515
516
/**
517
* Positions the submenu. This method should be called if the sub menu is
518
* opened and the menu element's size changes (e.g., when adding/removing items
519
* to an opened sub menu).
520
*/
521
goog.ui.SubMenu.prototype.positionSubMenu = function() {
522
'use strict';
523
var position = new goog.positioning.AnchoredViewportPosition(
524
this.getElement(),
525
this.isAlignedToEnd() ? goog.positioning.Corner.TOP_END :
526
goog.positioning.Corner.TOP_START,
527
this.isPositionAdjustable_);
528
529
// TODO(user): Clean up popup code and have this be a one line call
530
var subMenu = this.getMenu();
531
var el = subMenu.getElement();
532
if (!subMenu.isVisible()) {
533
el.style.visibility = 'hidden';
534
goog.style.setElementShown(el, true);
535
}
536
537
position.reposition(
538
el, this.isAlignedToEnd() ? goog.positioning.Corner.TOP_START :
539
goog.positioning.Corner.TOP_END);
540
541
if (!subMenu.isVisible()) {
542
goog.style.setElementShown(el, false);
543
el.style.visibility = 'visible';
544
}
545
};
546
547
548
// Methods delegated to sub-menu but accessible here for convinience
549
550
551
/**
552
* Adds a new menu item at the end of the menu.
553
* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu
554
* item to add to the menu.
555
*/
556
goog.ui.SubMenu.prototype.addItem = function(item) {
557
'use strict';
558
this.getMenu().addChild(item, true);
559
};
560
561
562
/**
563
* Adds a new menu item at a specific index in the menu.
564
* @param {goog.ui.MenuHeader|goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu
565
* item to add to the menu.
566
* @param {number} n Index at which to insert the menu item.
567
*/
568
goog.ui.SubMenu.prototype.addItemAt = function(item, n) {
569
'use strict';
570
this.getMenu().addChildAt(item, n, true);
571
};
572
573
574
/**
575
* Removes an item from the menu and disposes it.
576
* @param {goog.ui.MenuItem} item The menu item to remove.
577
*/
578
goog.ui.SubMenu.prototype.removeItem = function(item) {
579
'use strict';
580
var child = this.getMenu().removeChild(item, true);
581
if (child) {
582
child.dispose();
583
}
584
};
585
586
587
/**
588
* Removes a menu item at a given index in the menu and disposes it.
589
* @param {number} n Index of item.
590
*/
591
goog.ui.SubMenu.prototype.removeItemAt = function(n) {
592
'use strict';
593
var child = this.getMenu().removeChildAt(n, true);
594
if (child) {
595
child.dispose();
596
}
597
};
598
599
600
/**
601
* Returns a reference to the menu item at a given index.
602
* @param {number} n Index of menu item.
603
* @return {goog.ui.Component} Reference to the menu item.
604
*/
605
goog.ui.SubMenu.prototype.getItemAt = function(n) {
606
'use strict';
607
return this.getMenu().getChildAt(n);
608
};
609
610
611
/**
612
* Returns the number of items in the sub menu (including separators).
613
* @return {number} The number of items in the menu.
614
*/
615
goog.ui.SubMenu.prototype.getItemCount = function() {
616
'use strict';
617
return this.getMenu().getChildCount();
618
};
619
620
621
/**
622
* Returns the menu items contained in the sub menu.
623
* @return {!Array<!goog.ui.MenuItem>} An array of menu items.
624
* @deprecated Use getItemAt/getItemCount instead.
625
*/
626
goog.ui.SubMenu.prototype.getItems = function() {
627
'use strict';
628
return this.getMenu().getItems();
629
};
630
631
632
/**
633
* Gets a reference to the submenu's actual menu.
634
* @return {!goog.ui.Menu} Reference to the object representing the sub menu.
635
*/
636
goog.ui.SubMenu.prototype.getMenu = function() {
637
'use strict';
638
if (!this.subMenu_) {
639
this.setMenu(
640
new goog.ui.Menu(this.getDomHelper()), /* opt_internal */ true);
641
} else if (this.externalSubMenu_ && this.subMenu_.getParent() != this) {
642
// Since it is possible for the same popup menu to be attached to multiple
643
// submenus, we need to ensure that it has the correct parent event target.
644
this.subMenu_.setParent(this);
645
}
646
// Always create the menu DOM, for backward compatibility.
647
if (!this.subMenu_.getElement()) {
648
this.subMenu_.createDom();
649
}
650
return this.subMenu_;
651
};
652
653
654
/**
655
* Sets the submenu to a specific menu.
656
* @param {goog.ui.Menu} menu The menu to show when this item is selected.
657
* @param {boolean=} opt_internal Whether this menu is an "internal" menu, and
658
* should be disposed of when this object is disposed of.
659
*/
660
goog.ui.SubMenu.prototype.setMenu = function(menu, opt_internal) {
661
'use strict';
662
var oldMenu = this.subMenu_;
663
if (menu != oldMenu) {
664
if (oldMenu) {
665
this.dismissSubMenu();
666
if (this.isInDocument()) {
667
this.setMenuListenersEnabled_(oldMenu, false);
668
}
669
}
670
671
this.subMenu_ = menu;
672
this.externalSubMenu_ = !opt_internal;
673
674
if (menu) {
675
menu.setParent(this);
676
// There's no need to dispatch a HIDE event during submenu construction.
677
menu.setVisible(false, /* opt_force */ true);
678
menu.setAllowAutoFocus(false);
679
menu.setFocusable(false);
680
if (this.isInDocument()) {
681
this.setMenuListenersEnabled_(menu, true);
682
}
683
}
684
}
685
};
686
687
688
/**
689
* Returns true if the provided element is to be considered inside the menu for
690
* purposes such as dismissing the menu on an event. This is so submenus can
691
* make use of elements outside their own DOM.
692
* @param {Element} element The element to test for.
693
* @return {boolean} Whether or not the provided element is contained.
694
*/
695
goog.ui.SubMenu.prototype.containsElement = function(element) {
696
'use strict';
697
return this.getMenu().containsElement(element);
698
};
699
700
701
/**
702
* @param {boolean} isAdjustable Whether this submenu is adjustable.
703
*/
704
goog.ui.SubMenu.prototype.setPositionAdjustable = function(isAdjustable) {
705
'use strict';
706
this.isPositionAdjustable_ = !!isAdjustable;
707
};
708
709
710
/**
711
* @return {boolean} Whether this submenu is adjustable.
712
*/
713
goog.ui.SubMenu.prototype.isPositionAdjustable = function() {
714
'use strict';
715
return this.isPositionAdjustable_;
716
};
717
718
719
// Register a decorator factory function for goog.ui.SubMenus.
720
goog.ui.registry.setDecoratorByClassName(
721
goog.getCssName('goog-submenu'), function() {
722
'use strict';
723
return new goog.ui.SubMenu(null);
724
});
725
726