Path: blob/trunk/third_party/closure/goog/ui/select.js
4504 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview A class that supports single selection from a dropdown menu,8* with semantics similar to the native HTML <code><select></code>9* element.10*11* @see ../demos/select.html12*/1314goog.provide('goog.ui.Select');1516goog.require('goog.a11y.aria');17goog.require('goog.a11y.aria.Role');18goog.require('goog.a11y.aria.State');19goog.require('goog.events.EventType');20goog.require('goog.ui.Component');21goog.require('goog.ui.IdGenerator');22goog.require('goog.ui.MenuButton');23goog.require('goog.ui.MenuItem');24goog.require('goog.ui.MenuRenderer');25goog.require('goog.ui.SelectionModel');26goog.require('goog.ui.registry');27goog.requireType('goog.dom.DomHelper');28goog.requireType('goog.events.Event');29goog.requireType('goog.ui.ButtonRenderer');30goog.requireType('goog.ui.Control');31goog.requireType('goog.ui.ControlContent');32goog.requireType('goog.ui.Menu');33goog.requireType('goog.ui.MenuSeparator');34353637/**38* A selection control. Extends {@link goog.ui.MenuButton} by composing a39* menu with a selection model, and automatically updating the button's caption40* based on the current selection.41*42* Select fires the following events:43* CHANGE - after selection changes.44*45* @param {goog.ui.ControlContent=} opt_caption Default caption or existing DOM46* structure to display as the button's caption when nothing is selected.47* Defaults to no caption.48* @param {goog.ui.Menu=} opt_menu Menu containing selection options.49* @param {goog.ui.ButtonRenderer=} opt_renderer Renderer used to render or50* decorate the control; defaults to {@link goog.ui.MenuButtonRenderer}.51* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for52* document interaction.53* @param {!goog.ui.MenuRenderer=} opt_menuRenderer Renderer used to render or54* decorate the menu; defaults to {@link goog.ui.MenuRenderer}.55* @constructor56* @extends {goog.ui.MenuButton}57*/58goog.ui.Select = function(59opt_caption, opt_menu, opt_renderer, opt_domHelper, opt_menuRenderer) {60'use strict';61goog.ui.Select.base(62this, 'constructor', opt_caption, opt_menu, opt_renderer, opt_domHelper,63opt_menuRenderer ||64new goog.ui.MenuRenderer(goog.a11y.aria.Role.LISTBOX));65/**66* Default caption to show when no option is selected.67* @private {goog.ui.ControlContent}68*/69this.defaultCaption_ = this.getContent();7071/**72* The initial value of the aria label of the content element. This will be73* null until the caption is first populated and will be non-null thereafter.74* @private {?string}75*/76this.initialAriaLabel_ = null;7778this.setPreferredAriaRole(goog.a11y.aria.Role.LISTBOX);79};80goog.inherits(goog.ui.Select, goog.ui.MenuButton);818283/**84* The selection model controlling the items in the menu.85* @type {?goog.ui.SelectionModel}86* @private87*/88goog.ui.Select.prototype.selectionModel_ = null;899091/** @override */92goog.ui.Select.prototype.enterDocument = function() {93'use strict';94goog.ui.Select.superClass_.enterDocument.call(this);95this.updateCaption();96this.listenToSelectionModelEvents_();97};9899100/**101* Decorates the given element with this control. Overrides the superclass102* implementation by initializing the default caption on the select button.103* @param {Element} element Element to decorate.104* @override105*/106goog.ui.Select.prototype.decorateInternal = function(element) {107'use strict';108goog.ui.Select.superClass_.decorateInternal.call(this, element);109var caption = this.getCaption();110if (caption) {111// Initialize the default caption.112this.setDefaultCaption(caption);113} else if (!this.getSelectedItem()) {114// If there is no default caption and no selected item, select the first115// option (this is technically an arbitrary choice, but what most people116// would expect to happen).117this.setSelectedIndex(0);118}119};120121122/** @override */123goog.ui.Select.prototype.disposeInternal = function() {124'use strict';125goog.ui.Select.superClass_.disposeInternal.call(this);126127if (this.selectionModel_) {128this.selectionModel_.dispose();129this.selectionModel_ = null;130}131132this.defaultCaption_ = null;133};134135136/**137* Handles {@link goog.ui.Component.EventType.ACTION} events dispatched by138* the menu item clicked by the user. Updates the selection model, calls139* the superclass implementation to hide the menu, stops the propagation of140* the event, and dispatches an ACTION event on behalf of the select control141* itself. Overrides {@link goog.ui.MenuButton#handleMenuAction}.142* @param {goog.events.Event} e Action event to handle.143* @override144*/145goog.ui.Select.prototype.handleMenuAction = function(e) {146'use strict';147this.setSelectedItem(/** @type {goog.ui.MenuItem} */ (e.target));148goog.ui.Select.base(this, 'handleMenuAction', e);149150// NOTE(chrishenry): We should not stop propagation and then fire151// our own ACTION event. Fixing this without breaking anyone152// relying on this event is hard though.153e.stopPropagation();154this.dispatchEvent(goog.ui.Component.EventType.ACTION);155};156157158/**159* Handles {@link goog.events.EventType.SELECT} events raised by the160* selection model when the selection changes. Updates the contents of the161* select button.162* @param {goog.events.Event} e Selection event to handle.163*/164goog.ui.Select.prototype.handleSelectionChange = function(e) {165'use strict';166var item = this.getSelectedItem();167goog.ui.Select.superClass_.setValue.call(this, item && item.getValue());168this.updateCaption();169};170171172/**173* Replaces the menu currently attached to the control (if any) with the given174* argument, and updates the selection model. Does nothing if the new menu is175* the same as the old one. Overrides {@link goog.ui.MenuButton#setMenu}.176* @param {goog.ui.Menu} menu New menu to be attached to the menu button.177* @return {goog.ui.Menu|undefined} Previous menu (undefined if none).178* @override179*/180goog.ui.Select.prototype.setMenu = function(menu) {181'use strict';182// Call superclass implementation to replace the menu.183var oldMenu = goog.ui.Select.superClass_.setMenu.call(this, menu);184185// Do nothing unless the new menu is different from the current one.186if (menu != oldMenu) {187// Clear the old selection model (if any).188if (this.selectionModel_) {189this.selectionModel_.clear();190}191192// Initialize new selection model (unless the new menu is null).193if (menu) {194if (this.selectionModel_) {195menu.forEachChild(function(child, index) {196'use strict';197this.setCorrectAriaRole_(198/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (child));199this.selectionModel_.addItem(child);200}, this);201} else {202this.createSelectionModel_(menu);203}204}205}206207return oldMenu;208};209210211/**212* Returns the default caption to be shown when no option is selected.213* @return {goog.ui.ControlContent} Default caption.214*/215goog.ui.Select.prototype.getDefaultCaption = function() {216'use strict';217return this.defaultCaption_;218};219220221/**222* Sets the default caption to the given string or DOM structure.223* @param {goog.ui.ControlContent} caption Default caption to be shown224* when no option is selected.225*/226goog.ui.Select.prototype.setDefaultCaption = function(caption) {227'use strict';228this.defaultCaption_ = caption;229this.updateCaption();230};231232233/**234* Adds a new menu item at the end of the menu.235* @param {goog.ui.Control} item Menu item to add to the menu.236* @override237*/238goog.ui.Select.prototype.addItem = function(item) {239'use strict';240this.setCorrectAriaRole_(241/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (item));242goog.ui.Select.superClass_.addItem.call(this, item);243244if (this.selectionModel_) {245this.selectionModel_.addItem(item);246} else {247this.createSelectionModel_(this.getMenu());248}249this.updateAriaActiveDescendant_();250};251252253/**254* Adds a new menu item at a specific index in the menu.255* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item Menu item to add to the256* menu.257* @param {number} index Index at which to insert the menu item.258* @override259*/260goog.ui.Select.prototype.addItemAt = function(item, index) {261'use strict';262this.setCorrectAriaRole_(263/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (item));264goog.ui.Select.superClass_.addItemAt.call(this, item, index);265266if (this.selectionModel_) {267this.selectionModel_.addItemAt(item, index);268} else {269this.createSelectionModel_(this.getMenu());270}271};272273274/**275* Removes an item from the menu and disposes it.276* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item The menu item to remove.277* @override278*/279goog.ui.Select.prototype.removeItem = function(item) {280'use strict';281goog.ui.Select.superClass_.removeItem.call(this, item);282if (this.selectionModel_) {283this.selectionModel_.removeItem(item);284}285};286287288/**289* Removes a menu item at a given index in the menu and disposes it.290* @param {number} index Index of item.291* @override292*/293goog.ui.Select.prototype.removeItemAt = function(index) {294'use strict';295goog.ui.Select.superClass_.removeItemAt.call(this, index);296if (this.selectionModel_) {297this.selectionModel_.removeItemAt(index);298}299};300301302/**303* Selects the specified option (assumed to be in the select menu), and304* deselects the previously selected option, if any. A null argument clears305* the selection.306* @param {goog.ui.MenuItem} item Option to be selected (null to clear307* the selection).308*/309goog.ui.Select.prototype.setSelectedItem = function(item) {310'use strict';311if (this.selectionModel_) {312var prevItem = this.getSelectedItem();313this.selectionModel_.setSelectedItem(item);314315if (item != prevItem) {316this.dispatchEvent(goog.ui.Component.EventType.CHANGE);317}318}319};320321322/**323* Selects the option at the specified index, or clears the selection if the324* index is out of bounds.325* @param {number} index Index of the option to be selected.326*/327goog.ui.Select.prototype.setSelectedIndex = function(index) {328'use strict';329if (this.selectionModel_) {330this.setSelectedItem(/** @type {goog.ui.MenuItem} */331(this.selectionModel_.getItemAt(index)));332}333};334335336/**337* Selects the first option found with an associated value equal to the338* argument, or clears the selection if no such option is found. A null339* argument also clears the selection. Overrides {@link340* goog.ui.Button#setValue}.341* @param {*} value Value of the option to be selected (null to clear342* the selection).343* @override344*/345goog.ui.Select.prototype.setValue = function(value) {346'use strict';347if (value != null && this.selectionModel_) {348for (var i = 0, item; item = this.selectionModel_.getItemAt(i); i++) {349if (item && typeof item.getValue == 'function' &&350item.getValue() == value) {351this.setSelectedItem(/** @type {!goog.ui.MenuItem} */ (item));352return;353}354}355}356357this.setSelectedItem(null);358};359360361/**362* Gets the value associated with the currently selected option (null if none).363*364* Note that unlike {@link goog.ui.Button#getValue} which this method overrides,365* the "value" of a Select instance is the value of its selected menu item, not366* its own value. This makes a difference because the "value" of a Button is367* reset to the value of the element it decorates when it's added to the DOM368* (via ButtonRenderer), whereas the value of the selected item is unaffected.369* So while setValue() has no effect on a Button before it is added to the DOM,370* it will make a persistent change to a Select instance (which is consistent371* with any changes made by {@link goog.ui.Select#setSelectedItem} and372* {@link goog.ui.Select#setSelectedIndex}).373*374* @override375*/376goog.ui.Select.prototype.getValue = function() {377'use strict';378var selectedItem = this.getSelectedItem();379return selectedItem ? selectedItem.getValue() : null;380};381382383/**384* Returns the currently selected option.385* @return {goog.ui.MenuItem} The currently selected option (null if none).386*/387goog.ui.Select.prototype.getSelectedItem = function() {388'use strict';389return this.selectionModel_ ?390/** @type {goog.ui.MenuItem} */ (this.selectionModel_.getSelectedItem()) :391null;392};393394395/**396* Returns the index of the currently selected option.397* @return {number} 0-based index of the currently selected option (-1 if none).398*/399goog.ui.Select.prototype.getSelectedIndex = function() {400'use strict';401return this.selectionModel_ ? this.selectionModel_.getSelectedIndex() : -1;402};403404405/**406* @return {goog.ui.SelectionModel} The selection model.407* @protected408*/409goog.ui.Select.prototype.getSelectionModel = function() {410'use strict';411return this.selectionModel_;412};413414415/**416* Creates a new selection model and sets up an event listener to handle417* {@link goog.events.EventType.SELECT} events dispatched by it.418* @param {goog.ui.Component=} opt_component If provided, will add the419* component's children as items to the selection model.420* @private421*/422goog.ui.Select.prototype.createSelectionModel_ = function(opt_component) {423'use strict';424this.selectionModel_ = new goog.ui.SelectionModel();425if (opt_component) {426opt_component.forEachChild(function(child, index) {427'use strict';428this.setCorrectAriaRole_(429/** @type {goog.ui.MenuItem|goog.ui.MenuSeparator} */ (child));430this.selectionModel_.addItem(child);431}, this);432}433this.listenToSelectionModelEvents_();434};435436437/**438* Subscribes to events dispatched by the selection model.439* @private440*/441goog.ui.Select.prototype.listenToSelectionModelEvents_ = function() {442'use strict';443if (this.selectionModel_) {444this.getHandler().listen(445this.selectionModel_, goog.events.EventType.SELECT,446this.handleSelectionChange);447}448};449450451/**452* Updates the caption to be shown in the select button. If no option is453* selected and a default caption is set, sets the caption to the default454* caption; otherwise to the empty string.455* @protected456*/457goog.ui.Select.prototype.updateCaption = function() {458'use strict';459var item = this.getSelectedItem();460this.setContent(item ? item.getCaption() : this.defaultCaption_);461462var contentElement = this.getRenderer().getContentElement(this.getElement());463// Despite the ControlRenderer interface indicating the return value is464// {Element}, many renderers cast element.firstChild to {Element} when it is465// really {Node}. Checking tagName verifies this is an {!Element}.466if (contentElement && this.getDomHelper().isElement(contentElement)) {467if (this.initialAriaLabel_ == null) {468this.initialAriaLabel_ = goog.a11y.aria.getLabel(contentElement);469}470var itemElement = item ? item.getElement() : null;471goog.a11y.aria.setLabel(472contentElement, itemElement ? goog.a11y.aria.getLabel(itemElement) :473this.initialAriaLabel_);474this.updateAriaActiveDescendant_();475}476};477478479/**480* Updates the aria active descendant attribute.481* @private482*/483goog.ui.Select.prototype.updateAriaActiveDescendant_ = function() {484'use strict';485var renderer = this.getRenderer();486if (renderer) {487var contentElement = renderer.getContentElement(this.getElement());488if (contentElement) {489var buttonElement = this.getElementStrict();490if (!contentElement.id) {491contentElement.id = goog.ui.IdGenerator.getInstance().getNextUniqueId();492}493goog.a11y.aria.setRole(contentElement, goog.a11y.aria.Role.OPTION);494// Set 'aria-selected' to true since the content element represents the495// currently selected option.496goog.a11y.aria.setState(497contentElement, goog.a11y.aria.State.SELECTED, true);498goog.a11y.aria.setState(499buttonElement, goog.a11y.aria.State.ACTIVEDESCENDANT,500contentElement.id);501if (this.selectionModel_) {502// We can't use selectionmodel's getItemCount here because we need to503// skip separators.504var items = this.selectionModel_.getItems();505goog.a11y.aria.setState(506contentElement, goog.a11y.aria.State.SETSIZE,507this.getNumMenuItems_(items));508// Set a human-readable selection index, excluding menu separators.509var index = this.selectionModel_.getSelectedIndex();510goog.a11y.aria.setState(511contentElement, goog.a11y.aria.State.POSINSET,512index >= 0 ? this.getNumMenuItems_(items.slice(0, index + 1)) : 0);513}514}515}516};517518519/**520* Gets the number of menu items in the array.521* @param {!Array<?Object>} items The items.522* @return {number}523* @private524*/525goog.ui.Select.prototype.getNumMenuItems_ = function(items) {526'use strict';527return items528.filter(function(item) {529'use strict';530return item instanceof goog.ui.MenuItem;531})532.length;533};534535536/**537* Sets the correct ARIA role for the menu item or separator.538* @param {goog.ui.MenuItem|goog.ui.MenuSeparator} item The item to set.539* @private540*/541goog.ui.Select.prototype.setCorrectAriaRole_ = function(item) {542'use strict';543item.setPreferredAriaRole(544item instanceof goog.ui.MenuItem ? goog.a11y.aria.Role.OPTION :545goog.a11y.aria.Role.SEPARATOR);546};547548549/**550* Opens or closes the menu. Overrides {@link goog.ui.MenuButton#setOpen} by551* highlighting the currently selected option on open.552* @param {boolean} open Whether to open or close the menu.553* @param {goog.events.Event=} opt_e Mousedown event that caused the menu to554* be opened.555* @override556*/557goog.ui.Select.prototype.setOpen = function(open, opt_e) {558'use strict';559goog.ui.Select.superClass_.setOpen.call(this, open, opt_e);560561if (this.isOpen()) {562this.getMenu().setHighlightedIndex(this.getSelectedIndex());563} else {564this.updateAriaActiveDescendant_();565}566};567568569// Register a decorator factory function for goog.ui.Selects.570goog.ui.registry.setDecoratorByClassName(571goog.getCssName('goog-select'), function() {572'use strict';573// Select defaults to using MenuButtonRenderer, since it shares its L&F.574return new goog.ui.Select(null);575});576577578