Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/ui/select.js
4504 views
1
/**
2
* @license
3
* Copyright The Closure Library Authors.
4
* SPDX-License-Identifier: Apache-2.0
5
*/
6
7
/**
8
* @fileoverview A class that supports single selection from a dropdown menu,
9
* with semantics similar to the native HTML <code>&lt;select&gt;</code>
10
* element.
11
*
12
* @see ../demos/select.html
13
*/
14
15
goog.provide('goog.ui.Select');
16
17
goog.require('goog.a11y.aria');
18
goog.require('goog.a11y.aria.Role');
19
goog.require('goog.a11y.aria.State');
20
goog.require('goog.events.EventType');
21
goog.require('goog.ui.Component');
22
goog.require('goog.ui.IdGenerator');
23
goog.require('goog.ui.MenuButton');
24
goog.require('goog.ui.MenuItem');
25
goog.require('goog.ui.MenuRenderer');
26
goog.require('goog.ui.SelectionModel');
27
goog.require('goog.ui.registry');
28
goog.requireType('goog.dom.DomHelper');
29
goog.requireType('goog.events.Event');
30
goog.requireType('goog.ui.ButtonRenderer');
31
goog.requireType('goog.ui.Control');
32
goog.requireType('goog.ui.ControlContent');
33
goog.requireType('goog.ui.Menu');
34
goog.requireType('goog.ui.MenuSeparator');
35
36
37
38
/**
39
* A selection control. Extends {@link goog.ui.MenuButton} by composing a
40
* menu with a selection model, and automatically updating the button's caption
41
* based on the current selection.
42
*
43
* Select fires the following events:
44
* CHANGE - after selection changes.
45
*
46
* @param {goog.ui.ControlContent=} opt_caption Default caption or existing DOM
47
* structure to display as the button's caption when nothing is selected.
48
* Defaults to no caption.
49
* @param {goog.ui.Menu=} opt_menu Menu containing selection options.
50
* @param {goog.ui.ButtonRenderer=} opt_renderer Renderer used to render or
51
* decorate the control; defaults to {@link goog.ui.MenuButtonRenderer}.
52
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
53
* document interaction.
54
* @param {!goog.ui.MenuRenderer=} opt_menuRenderer Renderer used to render or
55
* decorate the menu; defaults to {@link goog.ui.MenuRenderer}.
56
* @constructor
57
* @extends {goog.ui.MenuButton}
58
*/
59
goog.ui.Select = function(
60
opt_caption, opt_menu, opt_renderer, opt_domHelper, opt_menuRenderer) {
61
'use strict';
62
goog.ui.Select.base(
63
this, 'constructor', opt_caption, opt_menu, opt_renderer, opt_domHelper,
64
opt_menuRenderer ||
65
new goog.ui.MenuRenderer(goog.a11y.aria.Role.LISTBOX));
66
/**
67
* Default caption to show when no option is selected.
68
* @private {goog.ui.ControlContent}
69
*/
70
this.defaultCaption_ = this.getContent();
71
72
/**
73
* The initial value of the aria label of the content element. This will be
74
* null until the caption is first populated and will be non-null thereafter.
75
* @private {?string}
76
*/
77
this.initialAriaLabel_ = null;
78
79
this.setPreferredAriaRole(goog.a11y.aria.Role.LISTBOX);
80
};
81
goog.inherits(goog.ui.Select, goog.ui.MenuButton);
82
83
84
/**
85
* The selection model controlling the items in the menu.
86
* @type {?goog.ui.SelectionModel}
87
* @private
88
*/
89
goog.ui.Select.prototype.selectionModel_ = null;
90
91
92
/** @override */
93
goog.ui.Select.prototype.enterDocument = function() {
94
'use strict';
95
goog.ui.Select.superClass_.enterDocument.call(this);
96
this.updateCaption();
97
this.listenToSelectionModelEvents_();
98
};
99
100
101
/**
102
* Decorates the given element with this control. Overrides the superclass
103
* implementation by initializing the default caption on the select button.
104
* @param {Element} element Element to decorate.
105
* @override
106
*/
107
goog.ui.Select.prototype.decorateInternal = function(element) {
108
'use strict';
109
goog.ui.Select.superClass_.decorateInternal.call(this, element);
110
var caption = this.getCaption();
111
if (caption) {
112
// Initialize the default caption.
113
this.setDefaultCaption(caption);
114
} else if (!this.getSelectedItem()) {
115
// If there is no default caption and no selected item, select the first
116
// option (this is technically an arbitrary choice, but what most people
117
// would expect to happen).
118
this.setSelectedIndex(0);
119
}
120
};
121
122
123
/** @override */
124
goog.ui.Select.prototype.disposeInternal = function() {
125
'use strict';
126
goog.ui.Select.superClass_.disposeInternal.call(this);
127
128
if (this.selectionModel_) {
129
this.selectionModel_.dispose();
130
this.selectionModel_ = null;
131
}
132
133
this.defaultCaption_ = null;
134
};
135
136
137
/**
138
* Handles {@link goog.ui.Component.EventType.ACTION} events dispatched by
139
* the menu item clicked by the user. Updates the selection model, calls
140
* the superclass implementation to hide the menu, stops the propagation of
141
* the event, and dispatches an ACTION event on behalf of the select control
142
* itself. Overrides {@link goog.ui.MenuButton#handleMenuAction}.
143
* @param {goog.events.Event} e Action event to handle.
144
* @override
145
*/
146
goog.ui.Select.prototype.handleMenuAction = function(e) {
147
'use strict';
148
this.setSelectedItem(/** @type {goog.ui.MenuItem} */ (e.target));
149
goog.ui.Select.base(this, 'handleMenuAction', e);
150
151
// NOTE(chrishenry): We should not stop propagation and then fire
152
// our own ACTION event. Fixing this without breaking anyone
153
// relying on this event is hard though.
154
e.stopPropagation();
155
this.dispatchEvent(goog.ui.Component.EventType.ACTION);
156
};
157
158
159
/**
160
* Handles {@link goog.events.EventType.SELECT} events raised by the
161
* selection model when the selection changes. Updates the contents of the
162
* select button.
163
* @param {goog.events.Event} e Selection event to handle.
164
*/
165
goog.ui.Select.prototype.handleSelectionChange = function(e) {
166
'use strict';
167
var item = this.getSelectedItem();
168
goog.ui.Select.superClass_.setValue.call(this, item && item.getValue());
169
this.updateCaption();
170
};
171
172
173
/**
174
* Replaces the menu currently attached to the control (if any) with the given
175
* argument, and updates the selection model. Does nothing if the new menu is
176
* the same as the old one. Overrides {@link goog.ui.MenuButton#setMenu}.
177
* @param {goog.ui.Menu} menu New menu to be attached to the menu button.
178
* @return {goog.ui.Menu|undefined} Previous menu (undefined if none).
179
* @override
180
*/
181
goog.ui.Select.prototype.setMenu = function(menu) {
182
'use strict';
183
// Call superclass implementation to replace the menu.
184
var oldMenu = goog.ui.Select.superClass_.setMenu.call(this, menu);
185
186
// Do nothing unless the new menu is different from the current one.
187
if (menu != oldMenu) {
188
// Clear the old selection model (if any).
189
if (this.selectionModel_) {
190
this.selectionModel_.clear();
191
}
192
193
// Initialize new selection model (unless the new menu is null).
194
if (menu) {
195
if (this.selectionModel_) {
196
menu.forEachChild(function(child, index) {
197
'use strict';
198
this.setCorrectAriaRole_(
199
/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (child));
200
this.selectionModel_.addItem(child);
201
}, this);
202
} else {
203
this.createSelectionModel_(menu);
204
}
205
}
206
}
207
208
return oldMenu;
209
};
210
211
212
/**
213
* Returns the default caption to be shown when no option is selected.
214
* @return {goog.ui.ControlContent} Default caption.
215
*/
216
goog.ui.Select.prototype.getDefaultCaption = function() {
217
'use strict';
218
return this.defaultCaption_;
219
};
220
221
222
/**
223
* Sets the default caption to the given string or DOM structure.
224
* @param {goog.ui.ControlContent} caption Default caption to be shown
225
* when no option is selected.
226
*/
227
goog.ui.Select.prototype.setDefaultCaption = function(caption) {
228
'use strict';
229
this.defaultCaption_ = caption;
230
this.updateCaption();
231
};
232
233
234
/**
235
* Adds a new menu item at the end of the menu.
236
* @param {goog.ui.Control} item Menu item to add to the menu.
237
* @override
238
*/
239
goog.ui.Select.prototype.addItem = function(item) {
240
'use strict';
241
this.setCorrectAriaRole_(
242
/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (item));
243
goog.ui.Select.superClass_.addItem.call(this, item);
244
245
if (this.selectionModel_) {
246
this.selectionModel_.addItem(item);
247
} else {
248
this.createSelectionModel_(this.getMenu());
249
}
250
this.updateAriaActiveDescendant_();
251
};
252
253
254
/**
255
* Adds a new menu item at a specific index in the menu.
256
* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu item to add to the
257
* menu.
258
* @param {number} index Index at which to insert the menu item.
259
* @override
260
*/
261
goog.ui.Select.prototype.addItemAt = function(item, index) {
262
'use strict';
263
this.setCorrectAriaRole_(
264
/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (item));
265
goog.ui.Select.superClass_.addItemAt.call(this, item, index);
266
267
if (this.selectionModel_) {
268
this.selectionModel_.addItemAt(item, index);
269
} else {
270
this.createSelectionModel_(this.getMenu());
271
}
272
};
273
274
275
/**
276
* Removes an item from the menu and disposes it.
277
* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item The menu item to remove.
278
* @override
279
*/
280
goog.ui.Select.prototype.removeItem = function(item) {
281
'use strict';
282
goog.ui.Select.superClass_.removeItem.call(this, item);
283
if (this.selectionModel_) {
284
this.selectionModel_.removeItem(item);
285
}
286
};
287
288
289
/**
290
* Removes a menu item at a given index in the menu and disposes it.
291
* @param {number} index Index of item.
292
* @override
293
*/
294
goog.ui.Select.prototype.removeItemAt = function(index) {
295
'use strict';
296
goog.ui.Select.superClass_.removeItemAt.call(this, index);
297
if (this.selectionModel_) {
298
this.selectionModel_.removeItemAt(index);
299
}
300
};
301
302
303
/**
304
* Selects the specified option (assumed to be in the select menu), and
305
* deselects the previously selected option, if any. A null argument clears
306
* the selection.
307
* @param {goog.ui.MenuItem} item Option to be selected (null to clear
308
* the selection).
309
*/
310
goog.ui.Select.prototype.setSelectedItem = function(item) {
311
'use strict';
312
if (this.selectionModel_) {
313
var prevItem = this.getSelectedItem();
314
this.selectionModel_.setSelectedItem(item);
315
316
if (item != prevItem) {
317
this.dispatchEvent(goog.ui.Component.EventType.CHANGE);
318
}
319
}
320
};
321
322
323
/**
324
* Selects the option at the specified index, or clears the selection if the
325
* index is out of bounds.
326
* @param {number} index Index of the option to be selected.
327
*/
328
goog.ui.Select.prototype.setSelectedIndex = function(index) {
329
'use strict';
330
if (this.selectionModel_) {
331
this.setSelectedItem(/** @type {goog.ui.MenuItem} */
332
(this.selectionModel_.getItemAt(index)));
333
}
334
};
335
336
337
/**
338
* Selects the first option found with an associated value equal to the
339
* argument, or clears the selection if no such option is found. A null
340
* argument also clears the selection. Overrides {@link
341
* goog.ui.Button#setValue}.
342
* @param {*} value Value of the option to be selected (null to clear
343
* the selection).
344
* @override
345
*/
346
goog.ui.Select.prototype.setValue = function(value) {
347
'use strict';
348
if (value != null && this.selectionModel_) {
349
for (var i = 0, item; item = this.selectionModel_.getItemAt(i); i++) {
350
if (item && typeof item.getValue == 'function' &&
351
item.getValue() == value) {
352
this.setSelectedItem(/** @type {!goog.ui.MenuItem} */ (item));
353
return;
354
}
355
}
356
}
357
358
this.setSelectedItem(null);
359
};
360
361
362
/**
363
* Gets the value associated with the currently selected option (null if none).
364
*
365
* Note that unlike {@link goog.ui.Button#getValue} which this method overrides,
366
* the "value" of a Select instance is the value of its selected menu item, not
367
* its own value. This makes a difference because the "value" of a Button is
368
* reset to the value of the element it decorates when it's added to the DOM
369
* (via ButtonRenderer), whereas the value of the selected item is unaffected.
370
* So while setValue() has no effect on a Button before it is added to the DOM,
371
* it will make a persistent change to a Select instance (which is consistent
372
* with any changes made by {@link goog.ui.Select#setSelectedItem} and
373
* {@link goog.ui.Select#setSelectedIndex}).
374
*
375
* @override
376
*/
377
goog.ui.Select.prototype.getValue = function() {
378
'use strict';
379
var selectedItem = this.getSelectedItem();
380
return selectedItem ? selectedItem.getValue() : null;
381
};
382
383
384
/**
385
* Returns the currently selected option.
386
* @return {goog.ui.MenuItem} The currently selected option (null if none).
387
*/
388
goog.ui.Select.prototype.getSelectedItem = function() {
389
'use strict';
390
return this.selectionModel_ ?
391
/** @type {goog.ui.MenuItem} */ (this.selectionModel_.getSelectedItem()) :
392
null;
393
};
394
395
396
/**
397
* Returns the index of the currently selected option.
398
* @return {number} 0-based index of the currently selected option (-1 if none).
399
*/
400
goog.ui.Select.prototype.getSelectedIndex = function() {
401
'use strict';
402
return this.selectionModel_ ? this.selectionModel_.getSelectedIndex() : -1;
403
};
404
405
406
/**
407
* @return {goog.ui.SelectionModel} The selection model.
408
* @protected
409
*/
410
goog.ui.Select.prototype.getSelectionModel = function() {
411
'use strict';
412
return this.selectionModel_;
413
};
414
415
416
/**
417
* Creates a new selection model and sets up an event listener to handle
418
* {@link goog.events.EventType.SELECT} events dispatched by it.
419
* @param {goog.ui.Component=} opt_component If provided, will add the
420
* component's children as items to the selection model.
421
* @private
422
*/
423
goog.ui.Select.prototype.createSelectionModel_ = function(opt_component) {
424
'use strict';
425
this.selectionModel_ = new goog.ui.SelectionModel();
426
if (opt_component) {
427
opt_component.forEachChild(function(child, index) {
428
'use strict';
429
this.setCorrectAriaRole_(
430
/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (child));
431
this.selectionModel_.addItem(child);
432
}, this);
433
}
434
this.listenToSelectionModelEvents_();
435
};
436
437
438
/**
439
* Subscribes to events dispatched by the selection model.
440
* @private
441
*/
442
goog.ui.Select.prototype.listenToSelectionModelEvents_ = function() {
443
'use strict';
444
if (this.selectionModel_) {
445
this.getHandler().listen(
446
this.selectionModel_, goog.events.EventType.SELECT,
447
this.handleSelectionChange);
448
}
449
};
450
451
452
/**
453
* Updates the caption to be shown in the select button. If no option is
454
* selected and a default caption is set, sets the caption to the default
455
* caption; otherwise to the empty string.
456
* @protected
457
*/
458
goog.ui.Select.prototype.updateCaption = function() {
459
'use strict';
460
var item = this.getSelectedItem();
461
this.setContent(item ? item.getCaption() : this.defaultCaption_);
462
463
var contentElement = this.getRenderer().getContentElement(this.getElement());
464
// Despite the ControlRenderer interface indicating the return value is
465
// {Element}, many renderers cast element.firstChild to {Element} when it is
466
// really {Node}. Checking tagName verifies this is an {!Element}.
467
if (contentElement && this.getDomHelper().isElement(contentElement)) {
468
if (this.initialAriaLabel_ == null) {
469
this.initialAriaLabel_ = goog.a11y.aria.getLabel(contentElement);
470
}
471
var itemElement = item ? item.getElement() : null;
472
goog.a11y.aria.setLabel(
473
contentElement, itemElement ? goog.a11y.aria.getLabel(itemElement) :
474
this.initialAriaLabel_);
475
this.updateAriaActiveDescendant_();
476
}
477
};
478
479
480
/**
481
* Updates the aria active descendant attribute.
482
* @private
483
*/
484
goog.ui.Select.prototype.updateAriaActiveDescendant_ = function() {
485
'use strict';
486
var renderer = this.getRenderer();
487
if (renderer) {
488
var contentElement = renderer.getContentElement(this.getElement());
489
if (contentElement) {
490
var buttonElement = this.getElementStrict();
491
if (!contentElement.id) {
492
contentElement.id = goog.ui.IdGenerator.getInstance().getNextUniqueId();
493
}
494
goog.a11y.aria.setRole(contentElement, goog.a11y.aria.Role.OPTION);
495
// Set 'aria-selected' to true since the content element represents the
496
// currently selected option.
497
goog.a11y.aria.setState(
498
contentElement, goog.a11y.aria.State.SELECTED, true);
499
goog.a11y.aria.setState(
500
buttonElement, goog.a11y.aria.State.ACTIVEDESCENDANT,
501
contentElement.id);
502
if (this.selectionModel_) {
503
// We can't use selectionmodel's getItemCount here because we need to
504
// skip separators.
505
var items = this.selectionModel_.getItems();
506
goog.a11y.aria.setState(
507
contentElement, goog.a11y.aria.State.SETSIZE,
508
this.getNumMenuItems_(items));
509
// Set a human-readable selection index, excluding menu separators.
510
var index = this.selectionModel_.getSelectedIndex();
511
goog.a11y.aria.setState(
512
contentElement, goog.a11y.aria.State.POSINSET,
513
index >= 0 ? this.getNumMenuItems_(items.slice(0, index + 1)) : 0);
514
}
515
}
516
}
517
};
518
519
520
/**
521
* Gets the number of menu items in the array.
522
* @param {!Array<?Object>} items The items.
523
* @return {number}
524
* @private
525
*/
526
goog.ui.Select.prototype.getNumMenuItems_ = function(items) {
527
'use strict';
528
return items
529
.filter(function(item) {
530
'use strict';
531
return item instanceof goog.ui.MenuItem;
532
})
533
.length;
534
};
535
536
537
/**
538
* Sets the correct ARIA role for the menu item or separator.
539
* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item The item to set.
540
* @private
541
*/
542
goog.ui.Select.prototype.setCorrectAriaRole_ = function(item) {
543
'use strict';
544
item.setPreferredAriaRole(
545
item instanceof goog.ui.MenuItem ? goog.a11y.aria.Role.OPTION :
546
goog.a11y.aria.Role.SEPARATOR);
547
};
548
549
550
/**
551
* Opens or closes the menu. Overrides {@link goog.ui.MenuButton#setOpen} by
552
* highlighting the currently selected option on open.
553
* @param {boolean} open Whether to open or close the menu.
554
* @param {goog.events.Event=} opt_e Mousedown event that caused the menu to
555
* be opened.
556
* @override
557
*/
558
goog.ui.Select.prototype.setOpen = function(open, opt_e) {
559
'use strict';
560
goog.ui.Select.superClass_.setOpen.call(this, open, opt_e);
561
562
if (this.isOpen()) {
563
this.getMenu().setHighlightedIndex(this.getSelectedIndex());
564
} else {
565
this.updateAriaActiveDescendant_();
566
}
567
};
568
569
570
// Register a decorator factory function for goog.ui.Selects.
571
goog.ui.registry.setDecoratorByClassName(
572
goog.getCssName('goog-select'), function() {
573
'use strict';
574
// Select defaults to using MenuButtonRenderer, since it shares its L&F.
575
return new goog.ui.Select(null);
576
});
577
578