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( " " ); 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 } ) );