balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

selectmenu.js (16053B)


      1 /*!
      2  * jQuery UI Selectmenu 1.12.1
      3  * http://jqueryui.com
      4  *
      5  * Copyright jQuery Foundation and other contributors
      6  * Released under the MIT license.
      7  * http://jquery.org/license
      8  */
      9 
     10 //>>label: Selectmenu
     11 //>>group: Widgets
     12 // jscs:disable maximumLineLength
     13 //>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select.
     14 // jscs:enable maximumLineLength
     15 //>>docs: http://api.jqueryui.com/selectmenu/
     16 //>>demos: http://jqueryui.com/selectmenu/
     17 //>>css.structure: ../../themes/base/core.css
     18 //>>css.structure: ../../themes/base/selectmenu.css, ../../themes/base/button.css
     19 //>>css.theme: ../../themes/base/theme.css
     20 
     21 ( function( factory ) {
     22 	if ( typeof define === "function" && define.amd ) {
     23 
     24 		// AMD. Register as an anonymous module.
     25 		define( [
     26 			"jquery",
     27 			"./menu",
     28 			"./core"
     29 		], factory );
     30 	} else {
     31 
     32 		// Browser globals
     33 		factory( jQuery );
     34 	}
     35 }( function( $ ) {
     36 
     37 return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
     38 	version: "1.12.1",
     39 	defaultElement: "<select>",
     40 	options: {
     41 		appendTo: null,
     42 		classes: {
     43 			"ui-selectmenu-button-open": "ui-corner-top",
     44 			"ui-selectmenu-button-closed": "ui-corner-all"
     45 		},
     46 		disabled: null,
     47 		icons: {
     48 			button: "ui-icon-triangle-1-s"
     49 		},
     50 		position: {
     51 			my: "left top",
     52 			at: "left bottom",
     53 			collision: "none"
     54 		},
     55 		width: false,
     56 
     57 		// Callbacks
     58 		change: null,
     59 		close: null,
     60 		focus: null,
     61 		open: null,
     62 		select: null
     63 	},
     64 
     65 	_create: function() {
     66 		var selectmenuId = this.element.uniqueId().attr( "id" );
     67 		this.ids = {
     68 			element: selectmenuId,
     69 			button: selectmenuId + "-button",
     70 			menu: selectmenuId + "-menu"
     71 		};
     72 
     73 		this._drawButton();
     74 		this._drawMenu();
     75 		this._bindFormResetHandler();
     76 
     77 		this._rendered = false;
     78 		this.menuItems = $();
     79 	},
     80 
     81 	_drawButton: function() {
     82 		var icon,
     83 			that = this,
     84 			item = this._parseOption(
     85 				this.element.find( "option:selected" ),
     86 				this.element[ 0 ].selectedIndex
     87 			);
     88 
     89 		// Associate existing label with the new button
     90 		this.labels = this.element.labels().attr( "for", this.ids.button );
     91 		this._on( this.labels, {
     92 			click: function( event ) {
     93 				this.button.focus();
     94 				event.preventDefault();
     95 			}
     96 		} );
     97 
     98 		// Hide original select element
     99 		this.element.hide();
    100 
    101 		// Create button
    102 		this.button = $( "<span>", {
    103 			tabindex: this.options.disabled ? -1 : 0,
    104 			id: this.ids.button,
    105 			role: "combobox",
    106 			"aria-expanded": "false",
    107 			"aria-autocomplete": "list",
    108 			"aria-owns": this.ids.menu,
    109 			"aria-haspopup": "true",
    110 			title: this.element.attr( "title" )
    111 		} )
    112 			.insertAfter( this.element );
    113 
    114 		this._addClass( this.button, "ui-selectmenu-button ui-selectmenu-button-closed",
    115 			"ui-button ui-widget" );
    116 
    117 		icon = $( "<span>" ).appendTo( this.button );
    118 		this._addClass( icon, "ui-selectmenu-icon", "ui-icon " + this.options.icons.button );
    119 		this.buttonItem = this._renderButtonItem( item )
    120 			.appendTo( this.button );
    121 
    122 		if ( this.options.width !== false ) {
    123 			this._resizeButton();
    124 		}
    125 
    126 		this._on( this.button, this._buttonEvents );
    127 		this.button.one( "focusin", function() {
    128 
    129 			// Delay rendering the menu items until the button receives focus.
    130 			// The menu may have already been rendered via a programmatic open.
    131 			if ( !that._rendered ) {
    132 				that._refreshMenu();
    133 			}
    134 		} );
    135 	},
    136 
    137 	_drawMenu: function() {
    138 		var that = this;
    139 
    140 		// Create menu
    141 		this.menu = $( "<ul>", {
    142 			"aria-hidden": "true",
    143 			"aria-labelledby": this.ids.button,
    144 			id: this.ids.menu
    145 		} );
    146 
    147 		// Wrap menu
    148 		this.menuWrap = $( "<div>" ).append( this.menu );
    149 		this._addClass( this.menuWrap, "ui-selectmenu-menu", "ui-front" );
    150 		this.menuWrap.appendTo( this._appendTo() );
    151 
    152 		// Initialize menu widget
    153 		this.menuInstance = this.menu
    154 			.menu( {
    155 				classes: {
    156 					"ui-menu": "ui-corner-bottom"
    157 				},
    158 				role: "listbox",
    159 				select: function( event, ui ) {
    160 					event.preventDefault();
    161 
    162 					// Support: IE8
    163 					// If the item was selected via a click, the text selection
    164 					// will be destroyed in IE
    165 					that._setSelection();
    166 
    167 					that._select( ui.item.data( "ui-selectmenu-item" ), event );
    168 				},
    169 				focus: function( event, ui ) {
    170 					var item = ui.item.data( "ui-selectmenu-item" );
    171 
    172 					// Prevent inital focus from firing and check if its a newly focused item
    173 					if ( that.focusIndex != null && item.index !== that.focusIndex ) {
    174 						that._trigger( "focus", event, { item: item } );
    175 						if ( !that.isOpen ) {
    176 							that._select( item, event );
    177 						}
    178 					}
    179 					that.focusIndex = item.index;
    180 
    181 					that.button.attr( "aria-activedescendant",
    182 						that.menuItems.eq( item.index ).attr( "id" ) );
    183 				}
    184 			} )
    185 			.menu( "instance" );
    186 
    187 		// Don't close the menu on mouseleave
    188 		this.menuInstance._off( this.menu, "mouseleave" );
    189 
    190 		// Cancel the menu's collapseAll on document click
    191 		this.menuInstance._closeOnDocumentClick = function() {
    192 			return false;
    193 		};
    194 
    195 		// Selects often contain empty items, but never contain dividers
    196 		this.menuInstance._isDivider = function() {
    197 			return false;
    198 		};
    199 	},
    200 
    201 	refresh: function() {
    202 		this._refreshMenu();
    203 		this.buttonItem.replaceWith(
    204 			this.buttonItem = this._renderButtonItem(
    205 
    206 				// Fall back to an empty object in case there are no options
    207 				this._getSelectedItem().data( "ui-selectmenu-item" ) || {}
    208 			)
    209 		);
    210 		if ( this.options.width === null ) {
    211 			this._resizeButton();
    212 		}
    213 	},
    214 
    215 	_refreshMenu: function() {
    216 		var item,
    217 			options = this.element.find( "option" );
    218 
    219 		this.menu.empty();
    220 
    221 		this._parseOptions( options );
    222 		this._renderMenu( this.menu, this.items );
    223 
    224 		this.menuInstance.refresh();
    225 		this.menuItems = this.menu.find( "li" )
    226 			.not( ".ui-selectmenu-optgroup" )
    227 				.find( ".ui-menu-item-wrapper" );
    228 
    229 		this._rendered = true;
    230 
    231 		if ( !options.length ) {
    232 			return;
    233 		}
    234 
    235 		item = this._getSelectedItem();
    236 
    237 		// Update the menu to have the correct item focused
    238 		this.menuInstance.focus( null, item );
    239 		this._setAria( item.data( "ui-selectmenu-item" ) );
    240 
    241 		// Set disabled state
    242 		this._setOption( "disabled", this.element.prop( "disabled" ) );
    243 	},
    244 
    245 	open: function( event ) {
    246 		if ( this.options.disabled ) {
    247 			return;
    248 		}
    249 
    250 		// If this is the first time the menu is being opened, render the items
    251 		if ( !this._rendered ) {
    252 			this._refreshMenu();
    253 		} else {
    254 
    255 			// Menu clears focus on close, reset focus to selected item
    256 			this._removeClass( this.menu.find( ".ui-state-active" ), null, "ui-state-active" );
    257 			this.menuInstance.focus( null, this._getSelectedItem() );
    258 		}
    259 
    260 		// If there are no options, don't open the menu
    261 		if ( !this.menuItems.length ) {
    262 			return;
    263 		}
    264 
    265 		this.isOpen = true;
    266 		this._toggleAttr();
    267 		this._resizeMenu();
    268 		this._position();
    269 
    270 		this._on( this.document, this._documentClick );
    271 
    272 		this._trigger( "open", event );
    273 	},
    274 
    275 	_position: function() {
    276 		this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) );
    277 	},
    278 
    279 	close: function( event ) {
    280 		if ( !this.isOpen ) {
    281 			return;
    282 		}
    283 
    284 		this.isOpen = false;
    285 		this._toggleAttr();
    286 
    287 		this.range = null;
    288 		this._off( this.document );
    289 
    290 		this._trigger( "close", event );
    291 	},
    292 
    293 	widget: function() {
    294 		return this.button;
    295 	},
    296 
    297 	menuWidget: function() {
    298 		return this.menu;
    299 	},
    300 
    301 	_renderButtonItem: function( item ) {
    302 		var buttonItem = $( "<span>" );
    303 
    304 		this._setText( buttonItem, item.label );
    305 		this._addClass( buttonItem, "ui-selectmenu-text" );
    306 
    307 		return buttonItem;
    308 	},
    309 
    310 	_renderMenu: function( ul, items ) {
    311 		var that = this,
    312 			currentOptgroup = "";
    313 
    314 		$.each( items, function( index, item ) {
    315 			var li;
    316 
    317 			if ( item.optgroup !== currentOptgroup ) {
    318 				li = $( "<li>", {
    319 					text: item.optgroup
    320 				} );
    321 				that._addClass( li, "ui-selectmenu-optgroup", "ui-menu-divider" +
    322 					( item.element.parent( "optgroup" ).prop( "disabled" ) ?
    323 						" ui-state-disabled" :
    324 						"" ) );
    325 
    326 				li.appendTo( ul );
    327 
    328 				currentOptgroup = item.optgroup;
    329 			}
    330 
    331 			that._renderItemData( ul, item );
    332 		} );
    333 	},
    334 
    335 	_renderItemData: function( ul, item ) {
    336 		return this._renderItem( ul, item ).data( "ui-selectmenu-item", item );
    337 	},
    338 
    339 	_renderItem: function( ul, item ) {
    340 		var li = $( "<li>" ),
    341 			wrapper = $( "<div>", {
    342 				title: item.element.attr( "title" )
    343 			} );
    344 
    345 		if ( item.disabled ) {
    346 			this._addClass( li, null, "ui-state-disabled" );
    347 		}
    348 		this._setText( wrapper, item.label );
    349 
    350 		return li.append( wrapper ).appendTo( ul );
    351 	},
    352 
    353 	_setText: function( element, value ) {
    354 		if ( value ) {
    355 			element.text( value );
    356 		} else {
    357 			element.html( "&#160;" );
    358 		}
    359 	},
    360 
    361 	_move: function( direction, event ) {
    362 		var item, next,
    363 			filter = ".ui-menu-item";
    364 
    365 		if ( this.isOpen ) {
    366 			item = this.menuItems.eq( this.focusIndex ).parent( "li" );
    367 		} else {
    368 			item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
    369 			filter += ":not(.ui-state-disabled)";
    370 		}
    371 
    372 		if ( direction === "first" || direction === "last" ) {
    373 			next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 );
    374 		} else {
    375 			next = item[ direction + "All" ]( filter ).eq( 0 );
    376 		}
    377 
    378 		if ( next.length ) {
    379 			this.menuInstance.focus( event, next );
    380 		}
    381 	},
    382 
    383 	_getSelectedItem: function() {
    384 		return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
    385 	},
    386 
    387 	_toggle: function( event ) {
    388 		this[ this.isOpen ? "close" : "open" ]( event );
    389 	},
    390 
    391 	_setSelection: function() {
    392 		var selection;
    393 
    394 		if ( !this.range ) {
    395 			return;
    396 		}
    397 
    398 		if ( window.getSelection ) {
    399 			selection = window.getSelection();
    400 			selection.removeAllRanges();
    401 			selection.addRange( this.range );
    402 
    403 		// Support: IE8
    404 		} else {
    405 			this.range.select();
    406 		}
    407 
    408 		// Support: IE
    409 		// Setting the text selection kills the button focus in IE, but
    410 		// restoring the focus doesn't kill the selection.
    411 		this.button.focus();
    412 	},
    413 
    414 	_documentClick: {
    415 		mousedown: function( event ) {
    416 			if ( !this.isOpen ) {
    417 				return;
    418 			}
    419 
    420 			if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" +
    421 					$.ui.escapeSelector( this.ids.button ) ).length ) {
    422 				this.close( event );
    423 			}
    424 		}
    425 	},
    426 
    427 	_buttonEvents: {
    428 
    429 		// Prevent text selection from being reset when interacting with the selectmenu (#10144)
    430 		mousedown: function() {
    431 			var selection;
    432 
    433 			if ( window.getSelection ) {
    434 				selection = window.getSelection();
    435 				if ( selection.rangeCount ) {
    436 					this.range = selection.getRangeAt( 0 );
    437 				}
    438 
    439 			// Support: IE8
    440 			} else {
    441 				this.range = document.selection.createRange();
    442 			}
    443 		},
    444 
    445 		click: function( event ) {
    446 			this._setSelection();
    447 			this._toggle( event );
    448 		},
    449 
    450 		keydown: function( event ) {
    451 			var preventDefault = true;
    452 			switch ( event.keyCode ) {
    453 			case $.ui.keyCode.TAB:
    454 			case $.ui.keyCode.ESCAPE:
    455 				this.close( event );
    456 				preventDefault = false;
    457 				break;
    458 			case $.ui.keyCode.ENTER:
    459 				if ( this.isOpen ) {
    460 					this._selectFocusedItem( event );
    461 				}
    462 				break;
    463 			case $.ui.keyCode.UP:
    464 				if ( event.altKey ) {
    465 					this._toggle( event );
    466 				} else {
    467 					this._move( "prev", event );
    468 				}
    469 				break;
    470 			case $.ui.keyCode.DOWN:
    471 				if ( event.altKey ) {
    472 					this._toggle( event );
    473 				} else {
    474 					this._move( "next", event );
    475 				}
    476 				break;
    477 			case $.ui.keyCode.SPACE:
    478 				if ( this.isOpen ) {
    479 					this._selectFocusedItem( event );
    480 				} else {
    481 					this._toggle( event );
    482 				}
    483 				break;
    484 			case $.ui.keyCode.LEFT:
    485 				this._move( "prev", event );
    486 				break;
    487 			case $.ui.keyCode.RIGHT:
    488 				this._move( "next", event );
    489 				break;
    490 			case $.ui.keyCode.HOME:
    491 			case $.ui.keyCode.PAGE_UP:
    492 				this._move( "first", event );
    493 				break;
    494 			case $.ui.keyCode.END:
    495 			case $.ui.keyCode.PAGE_DOWN:
    496 				this._move( "last", event );
    497 				break;
    498 			default:
    499 				this.menu.trigger( event );
    500 				preventDefault = false;
    501 			}
    502 
    503 			if ( preventDefault ) {
    504 				event.preventDefault();
    505 			}
    506 		}
    507 	},
    508 
    509 	_selectFocusedItem: function( event ) {
    510 		var item = this.menuItems.eq( this.focusIndex ).parent( "li" );
    511 		if ( !item.hasClass( "ui-state-disabled" ) ) {
    512 			this._select( item.data( "ui-selectmenu-item" ), event );
    513 		}
    514 	},
    515 
    516 	_select: function( item, event ) {
    517 		var oldIndex = this.element[ 0 ].selectedIndex;
    518 
    519 		// Change native select element
    520 		this.element[ 0 ].selectedIndex = item.index;
    521 		this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) );
    522 		this._setAria( item );
    523 		this._trigger( "select", event, { item: item } );
    524 
    525 		if ( item.index !== oldIndex ) {
    526 			this._trigger( "change", event, { item: item } );
    527 		}
    528 
    529 		this.close( event );
    530 	},
    531 
    532 	_setAria: function( item ) {
    533 		var id = this.menuItems.eq( item.index ).attr( "id" );
    534 
    535 		this.button.attr( {
    536 			"aria-labelledby": id,
    537 			"aria-activedescendant": id
    538 		} );
    539 		this.menu.attr( "aria-activedescendant", id );
    540 	},
    541 
    542 	_setOption: function( key, value ) {
    543 		if ( key === "icons" ) {
    544 			var icon = this.button.find( "span.ui-icon" );
    545 			this._removeClass( icon, null, this.options.icons.button )
    546 				._addClass( icon, null, value.button );
    547 		}
    548 
    549 		this._super( key, value );
    550 
    551 		if ( key === "appendTo" ) {
    552 			this.menuWrap.appendTo( this._appendTo() );
    553 		}
    554 
    555 		if ( key === "width" ) {
    556 			this._resizeButton();
    557 		}
    558 	},
    559 
    560 	_setOptionDisabled: function( value ) {
    561 		this._super( value );
    562 
    563 		this.menuInstance.option( "disabled", value );
    564 		this.button.attr( "aria-disabled", value );
    565 		this._toggleClass( this.button, null, "ui-state-disabled", value );
    566 
    567 		this.element.prop( "disabled", value );
    568 		if ( value ) {
    569 			this.button.attr( "tabindex", -1 );
    570 			this.close();
    571 		} else {
    572 			this.button.attr( "tabindex", 0 );
    573 		}
    574 	},
    575 
    576 	_appendTo: function() {
    577 		var element = this.options.appendTo;
    578 
    579 		if ( element ) {
    580 			element = element.jquery || element.nodeType ?
    581 				$( element ) :
    582 				this.document.find( element ).eq( 0 );
    583 		}
    584 
    585 		if ( !element || !element[ 0 ] ) {
    586 			element = this.element.closest( ".ui-front, dialog" );
    587 		}
    588 
    589 		if ( !element.length ) {
    590 			element = this.document[ 0 ].body;
    591 		}
    592 
    593 		return element;
    594 	},
    595 
    596 	_toggleAttr: function() {
    597 		this.button.attr( "aria-expanded", this.isOpen );
    598 
    599 		// We can't use two _toggleClass() calls here, because we need to make sure
    600 		// we always remove classes first and add them second, otherwise if both classes have the
    601 		// same theme class, it will be removed after we add it.
    602 		this._removeClass( this.button, "ui-selectmenu-button-" +
    603 			( this.isOpen ? "closed" : "open" ) )
    604 			._addClass( this.button, "ui-selectmenu-button-" +
    605 				( this.isOpen ? "open" : "closed" ) )
    606 			._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen );
    607 
    608 		this.menu.attr( "aria-hidden", !this.isOpen );
    609 	},
    610 
    611 	_resizeButton: function() {
    612 		var width = this.options.width;
    613 
    614 		// For `width: false`, just remove inline style and stop
    615 		if ( width === false ) {
    616 			this.button.css( "width", "" );
    617 			return;
    618 		}
    619 
    620 		// For `width: null`, match the width of the original element
    621 		if ( width === null ) {
    622 			width = this.element.show().outerWidth();
    623 			this.element.hide();
    624 		}
    625 
    626 		this.button.outerWidth( width );
    627 	},
    628 
    629 	_resizeMenu: function() {
    630 		this.menu.outerWidth( Math.max(
    631 			this.button.outerWidth(),
    632 
    633 			// Support: IE10
    634 			// IE10 wraps long text (possibly a rounding bug)
    635 			// so we add 1px to avoid the wrapping
    636 			this.menu.width( "" ).outerWidth() + 1
    637 		) );
    638 	},
    639 
    640 	_getCreateOptions: function() {
    641 		var options = this._super();
    642 
    643 		options.disabled = this.element.prop( "disabled" );
    644 
    645 		return options;
    646 	},
    647 
    648 	_parseOptions: function( options ) {
    649 		var that = this,
    650 			data = [];
    651 		options.each( function( index, item ) {
    652 			data.push( that._parseOption( $( item ), index ) );
    653 		} );
    654 		this.items = data;
    655 	},
    656 
    657 	_parseOption: function( option, index ) {
    658 		var optgroup = option.parent( "optgroup" );
    659 
    660 		return {
    661 			element: option,
    662 			index: index,
    663 			value: option.val(),
    664 			label: option.text(),
    665 			optgroup: optgroup.attr( "label" ) || "",
    666 			disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" )
    667 		};
    668 	},
    669 
    670 	_destroy: function() {
    671 		this._unbindFormResetHandler();
    672 		this.menuWrap.remove();
    673 		this.button.remove();
    674 		this.element.show();
    675 		this.element.removeUniqueId();
    676 		this.labels.attr( "for", this.ids.element );
    677 	}
    678 } ] );
    679 
    680 } ) );