2020-10-07 12:33:25 -04:00
/ * !
2024-05-27 17:14:15 -04:00
* jQuery UI Selectmenu 1.13 . 3
* https : //jqueryui.com
2020-10-07 12:33:25 -04:00
*
2024-05-27 17:14:15 -04:00
* Copyright OpenJS Foundation and other contributors
2020-10-07 12:33:25 -04:00
* Released under the MIT license .
2024-05-27 17:14:15 -04:00
* https : //jquery.org/license
2020-10-07 12:33:25 -04:00
* /
//>>label: Selectmenu
//>>group: Widgets
2021-09-09 20:02:59 -04:00
/* eslint-disable max-len */
2020-10-07 12:33:25 -04:00
//>>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.
2021-09-09 20:02:59 -04:00
/* eslint-enable max-len */
2024-05-27 17:14:15 -04:00
//>>docs: https://api.jqueryui.com/selectmenu/
//>>demos: https://jqueryui.com/selectmenu/
2020-10-07 12:33:25 -04:00
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/selectmenu.css, ../../themes/base/button.css
//>>css.theme: ../../themes/base/theme.css
( function ( factory ) {
2021-09-09 20:02:59 -04:00
"use strict" ;
2020-10-07 12:33:25 -04:00
if ( typeof define === "function" && define . amd ) {
// AMD. Register as an anonymous module.
define ( [
"jquery" ,
"./menu" ,
2024-05-27 17:14:15 -04:00
"../form-reset-mixin" ,
"../keycode" ,
"../labels" ,
"../position" ,
"../unique-id" ,
"../version" ,
"../widget"
2020-10-07 12:33:25 -04:00
] , factory ) ;
} else {
// Browser globals
factory ( jQuery ) ;
}
2021-09-09 20:02:59 -04:00
} ) ( function ( $ ) {
"use strict" ;
2020-10-07 12:33:25 -04:00
return $ . widget ( "ui.selectmenu" , [ $ . ui . formResetMixin , {
2024-05-27 17:14:15 -04:00
version : "1.13.3" ,
2020-10-07 12:33:25 -04:00
defaultElement : "<select>" ,
options : {
appendTo : null ,
classes : {
"ui-selectmenu-button-open" : "ui-corner-top" ,
"ui-selectmenu-button-closed" : "ui-corner-all"
} ,
disabled : null ,
icons : {
button : "ui-icon-triangle-1-s"
} ,
position : {
my : "left top" ,
at : "left bottom" ,
collision : "none"
} ,
width : false ,
// Callbacks
change : null ,
close : null ,
focus : null ,
open : null ,
select : null
} ,
_create : function ( ) {
var selectmenuId = this . element . uniqueId ( ) . attr ( "id" ) ;
this . ids = {
element : selectmenuId ,
button : selectmenuId + "-button" ,
menu : selectmenuId + "-menu"
} ;
this . _drawButton ( ) ;
this . _drawMenu ( ) ;
this . _bindFormResetHandler ( ) ;
this . _rendered = false ;
this . menuItems = $ ( ) ;
} ,
_drawButton : function ( ) {
var icon ,
that = this ,
item = this . _parseOption (
this . element . find ( "option:selected" ) ,
this . element [ 0 ] . selectedIndex
) ;
// Associate existing label with the new button
this . labels = this . element . labels ( ) . attr ( "for" , this . ids . button ) ;
this . _on ( this . labels , {
click : function ( event ) {
2021-09-09 20:02:59 -04:00
this . button . trigger ( "focus" ) ;
2020-10-07 12:33:25 -04:00
event . preventDefault ( ) ;
}
} ) ;
// Hide original select element
this . element . hide ( ) ;
// Create button
this . button = $ ( "<span>" , {
tabindex : this . options . disabled ? - 1 : 0 ,
id : this . ids . button ,
role : "combobox" ,
"aria-expanded" : "false" ,
"aria-autocomplete" : "list" ,
"aria-owns" : this . ids . menu ,
"aria-haspopup" : "true" ,
title : this . element . attr ( "title" )
} )
. insertAfter ( this . element ) ;
this . _addClass ( this . button , "ui-selectmenu-button ui-selectmenu-button-closed" ,
"ui-button ui-widget" ) ;
icon = $ ( "<span>" ) . appendTo ( this . button ) ;
this . _addClass ( icon , "ui-selectmenu-icon" , "ui-icon " + this . options . icons . button ) ;
this . buttonItem = this . _renderButtonItem ( item )
. appendTo ( this . button ) ;
if ( this . options . width !== false ) {
this . _resizeButton ( ) ;
}
this . _on ( this . button , this . _buttonEvents ) ;
this . button . one ( "focusin" , function ( ) {
// Delay rendering the menu items until the button receives focus.
// The menu may have already been rendered via a programmatic open.
if ( ! that . _rendered ) {
that . _refreshMenu ( ) ;
}
} ) ;
} ,
_drawMenu : function ( ) {
var that = this ;
// Create menu
this . menu = $ ( "<ul>" , {
"aria-hidden" : "true" ,
"aria-labelledby" : this . ids . button ,
id : this . ids . menu
} ) ;
// Wrap menu
this . menuWrap = $ ( "<div>" ) . append ( this . menu ) ;
this . _addClass ( this . menuWrap , "ui-selectmenu-menu" , "ui-front" ) ;
this . menuWrap . appendTo ( this . _appendTo ( ) ) ;
// Initialize menu widget
this . menuInstance = this . menu
. menu ( {
classes : {
"ui-menu" : "ui-corner-bottom"
} ,
role : "listbox" ,
select : function ( event , ui ) {
event . preventDefault ( ) ;
// Support: IE8
// If the item was selected via a click, the text selection
// will be destroyed in IE
that . _setSelection ( ) ;
that . _select ( ui . item . data ( "ui-selectmenu-item" ) , event ) ;
} ,
focus : function ( event , ui ) {
var item = ui . item . data ( "ui-selectmenu-item" ) ;
// Prevent inital focus from firing and check if its a newly focused item
if ( that . focusIndex != null && item . index !== that . focusIndex ) {
that . _trigger ( "focus" , event , { item : item } ) ;
if ( ! that . isOpen ) {
that . _select ( item , event ) ;
}
}
that . focusIndex = item . index ;
that . button . attr ( "aria-activedescendant" ,
that . menuItems . eq ( item . index ) . attr ( "id" ) ) ;
}
} )
. menu ( "instance" ) ;
// Don't close the menu on mouseleave
this . menuInstance . _off ( this . menu , "mouseleave" ) ;
// Cancel the menu's collapseAll on document click
this . menuInstance . _closeOnDocumentClick = function ( ) {
return false ;
} ;
// Selects often contain empty items, but never contain dividers
this . menuInstance . _isDivider = function ( ) {
return false ;
} ;
} ,
refresh : function ( ) {
this . _refreshMenu ( ) ;
this . buttonItem . replaceWith (
this . buttonItem = this . _renderButtonItem (
// Fall back to an empty object in case there are no options
this . _getSelectedItem ( ) . data ( "ui-selectmenu-item" ) || { }
)
) ;
if ( this . options . width === null ) {
this . _resizeButton ( ) ;
}
} ,
_refreshMenu : function ( ) {
var item ,
options = this . element . find ( "option" ) ;
this . menu . empty ( ) ;
this . _parseOptions ( options ) ;
this . _renderMenu ( this . menu , this . items ) ;
this . menuInstance . refresh ( ) ;
this . menuItems = this . menu . find ( "li" )
. not ( ".ui-selectmenu-optgroup" )
2022-09-19 14:04:09 -04:00
. find ( ".ui-menu-item-wrapper" ) ;
2020-10-07 12:33:25 -04:00
this . _rendered = true ;
if ( ! options . length ) {
return ;
}
item = this . _getSelectedItem ( ) ;
// Update the menu to have the correct item focused
this . menuInstance . focus ( null , item ) ;
this . _setAria ( item . data ( "ui-selectmenu-item" ) ) ;
// Set disabled state
this . _setOption ( "disabled" , this . element . prop ( "disabled" ) ) ;
} ,
open : function ( event ) {
if ( this . options . disabled ) {
return ;
}
// If this is the first time the menu is being opened, render the items
if ( ! this . _rendered ) {
this . _refreshMenu ( ) ;
} else {
// Menu clears focus on close, reset focus to selected item
this . _removeClass ( this . menu . find ( ".ui-state-active" ) , null , "ui-state-active" ) ;
this . menuInstance . focus ( null , this . _getSelectedItem ( ) ) ;
}
// If there are no options, don't open the menu
if ( ! this . menuItems . length ) {
return ;
}
this . isOpen = true ;
this . _toggleAttr ( ) ;
this . _resizeMenu ( ) ;
this . _position ( ) ;
this . _on ( this . document , this . _documentClick ) ;
this . _trigger ( "open" , event ) ;
} ,
_position : function ( ) {
this . menuWrap . position ( $ . extend ( { of : this . button } , this . options . position ) ) ;
} ,
close : function ( event ) {
if ( ! this . isOpen ) {
return ;
}
this . isOpen = false ;
this . _toggleAttr ( ) ;
this . range = null ;
this . _off ( this . document ) ;
this . _trigger ( "close" , event ) ;
} ,
widget : function ( ) {
return this . button ;
} ,
menuWidget : function ( ) {
return this . menu ;
} ,
_renderButtonItem : function ( item ) {
var buttonItem = $ ( "<span>" ) ;
this . _setText ( buttonItem , item . label ) ;
this . _addClass ( buttonItem , "ui-selectmenu-text" ) ;
return buttonItem ;
} ,
_renderMenu : function ( ul , items ) {
var that = this ,
currentOptgroup = "" ;
$ . each ( items , function ( index , item ) {
var li ;
if ( item . optgroup !== currentOptgroup ) {
li = $ ( "<li>" , {
text : item . optgroup
} ) ;
that . _addClass ( li , "ui-selectmenu-optgroup" , "ui-menu-divider" +
( item . element . parent ( "optgroup" ) . prop ( "disabled" ) ?
" ui-state-disabled" :
"" ) ) ;
li . appendTo ( ul ) ;
currentOptgroup = item . optgroup ;
}
that . _renderItemData ( ul , item ) ;
} ) ;
} ,
_renderItemData : function ( ul , item ) {
return this . _renderItem ( ul , item ) . data ( "ui-selectmenu-item" , item ) ;
} ,
_renderItem : function ( ul , item ) {
var li = $ ( "<li>" ) ,
wrapper = $ ( "<div>" , {
title : item . element . attr ( "title" )
} ) ;
if ( item . disabled ) {
this . _addClass ( li , null , "ui-state-disabled" ) ;
}
2024-05-27 17:14:15 -04:00
if ( item . hidden ) {
li . prop ( "hidden" , true ) ;
} else {
this . _setText ( wrapper , item . label ) ;
}
2020-10-07 12:33:25 -04:00
return li . append ( wrapper ) . appendTo ( ul ) ;
} ,
_setText : function ( element , value ) {
if ( value ) {
element . text ( value ) ;
} else {
element . html ( " " ) ;
}
} ,
_move : function ( direction , event ) {
var item , next ,
filter = ".ui-menu-item" ;
if ( this . isOpen ) {
item = this . menuItems . eq ( this . focusIndex ) . parent ( "li" ) ;
} else {
item = this . menuItems . eq ( this . element [ 0 ] . selectedIndex ) . parent ( "li" ) ;
filter += ":not(.ui-state-disabled)" ;
}
if ( direction === "first" || direction === "last" ) {
next = item [ direction === "first" ? "prevAll" : "nextAll" ] ( filter ) . eq ( - 1 ) ;
} else {
next = item [ direction + "All" ] ( filter ) . eq ( 0 ) ;
}
if ( next . length ) {
this . menuInstance . focus ( event , next ) ;
}
} ,
_getSelectedItem : function ( ) {
return this . menuItems . eq ( this . element [ 0 ] . selectedIndex ) . parent ( "li" ) ;
} ,
_toggle : function ( event ) {
this [ this . isOpen ? "close" : "open" ] ( event ) ;
} ,
_setSelection : function ( ) {
var selection ;
if ( ! this . range ) {
return ;
}
if ( window . getSelection ) {
selection = window . getSelection ( ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( this . range ) ;
2022-09-19 14:04:09 -04:00
// Support: IE8
2020-10-07 12:33:25 -04:00
} else {
this . range . select ( ) ;
}
// Support: IE
// Setting the text selection kills the button focus in IE, but
// restoring the focus doesn't kill the selection.
2022-09-19 14:04:09 -04:00
this . button . trigger ( "focus" ) ;
2020-10-07 12:33:25 -04:00
} ,
_documentClick : {
mousedown : function ( event ) {
if ( ! this . isOpen ) {
return ;
}
if ( ! $ ( event . target ) . closest ( ".ui-selectmenu-menu, #" +
2021-09-09 20:02:59 -04:00
$ . escapeSelector ( this . ids . button ) ) . length ) {
2020-10-07 12:33:25 -04:00
this . close ( event ) ;
}
}
} ,
_buttonEvents : {
// Prevent text selection from being reset when interacting with the selectmenu (#10144)
mousedown : function ( ) {
var selection ;
if ( window . getSelection ) {
selection = window . getSelection ( ) ;
if ( selection . rangeCount ) {
this . range = selection . getRangeAt ( 0 ) ;
}
2022-09-19 14:04:09 -04:00
// Support: IE8
2020-10-07 12:33:25 -04:00
} else {
this . range = document . selection . createRange ( ) ;
}
} ,
click : function ( event ) {
this . _setSelection ( ) ;
this . _toggle ( event ) ;
} ,
keydown : function ( event ) {
var preventDefault = true ;
switch ( event . keyCode ) {
2022-09-19 14:04:09 -04:00
case $ . ui . keyCode . TAB :
case $ . ui . keyCode . ESCAPE :
this . close ( event ) ;
preventDefault = false ;
break ;
case $ . ui . keyCode . ENTER :
if ( this . isOpen ) {
this . _selectFocusedItem ( event ) ;
}
break ;
case $ . ui . keyCode . UP :
if ( event . altKey ) {
this . _toggle ( event ) ;
} else {
2020-10-07 12:33:25 -04:00
this . _move ( "prev" , event ) ;
2022-09-19 14:04:09 -04:00
}
break ;
case $ . ui . keyCode . DOWN :
if ( event . altKey ) {
this . _toggle ( event ) ;
} else {
2020-10-07 12:33:25 -04:00
this . _move ( "next" , event ) ;
2022-09-19 14:04:09 -04:00
}
break ;
case $ . ui . keyCode . SPACE :
if ( this . isOpen ) {
this . _selectFocusedItem ( event ) ;
} else {
this . _toggle ( event ) ;
}
break ;
case $ . ui . keyCode . LEFT :
this . _move ( "prev" , event ) ;
break ;
case $ . ui . keyCode . RIGHT :
this . _move ( "next" , event ) ;
break ;
case $ . ui . keyCode . HOME :
case $ . ui . keyCode . PAGE _UP :
this . _move ( "first" , event ) ;
break ;
case $ . ui . keyCode . END :
case $ . ui . keyCode . PAGE _DOWN :
this . _move ( "last" , event ) ;
break ;
default :
this . menu . trigger ( event ) ;
preventDefault = false ;
2020-10-07 12:33:25 -04:00
}
if ( preventDefault ) {
event . preventDefault ( ) ;
}
}
} ,
_selectFocusedItem : function ( event ) {
var item = this . menuItems . eq ( this . focusIndex ) . parent ( "li" ) ;
if ( ! item . hasClass ( "ui-state-disabled" ) ) {
this . _select ( item . data ( "ui-selectmenu-item" ) , event ) ;
}
} ,
_select : function ( item , event ) {
var oldIndex = this . element [ 0 ] . selectedIndex ;
// Change native select element
this . element [ 0 ] . selectedIndex = item . index ;
this . buttonItem . replaceWith ( this . buttonItem = this . _renderButtonItem ( item ) ) ;
this . _setAria ( item ) ;
this . _trigger ( "select" , event , { item : item } ) ;
if ( item . index !== oldIndex ) {
this . _trigger ( "change" , event , { item : item } ) ;
}
this . close ( event ) ;
} ,
_setAria : function ( item ) {
var id = this . menuItems . eq ( item . index ) . attr ( "id" ) ;
this . button . attr ( {
"aria-labelledby" : id ,
"aria-activedescendant" : id
} ) ;
this . menu . attr ( "aria-activedescendant" , id ) ;
} ,
_setOption : function ( key , value ) {
if ( key === "icons" ) {
var icon = this . button . find ( "span.ui-icon" ) ;
this . _removeClass ( icon , null , this . options . icons . button )
. _addClass ( icon , null , value . button ) ;
}
this . _super ( key , value ) ;
if ( key === "appendTo" ) {
this . menuWrap . appendTo ( this . _appendTo ( ) ) ;
}
if ( key === "width" ) {
this . _resizeButton ( ) ;
}
} ,
_setOptionDisabled : function ( value ) {
this . _super ( value ) ;
this . menuInstance . option ( "disabled" , value ) ;
this . button . attr ( "aria-disabled" , value ) ;
this . _toggleClass ( this . button , null , "ui-state-disabled" , value ) ;
this . element . prop ( "disabled" , value ) ;
if ( value ) {
this . button . attr ( "tabindex" , - 1 ) ;
this . close ( ) ;
} else {
this . button . attr ( "tabindex" , 0 ) ;
}
} ,
_appendTo : function ( ) {
var element = this . options . appendTo ;
if ( element ) {
element = element . jquery || element . nodeType ?
$ ( element ) :
this . document . find ( element ) . eq ( 0 ) ;
}
if ( ! element || ! element [ 0 ] ) {
element = this . element . closest ( ".ui-front, dialog" ) ;
}
if ( ! element . length ) {
element = this . document [ 0 ] . body ;
}
return element ;
} ,
_toggleAttr : function ( ) {
this . button . attr ( "aria-expanded" , this . isOpen ) ;
// We can't use two _toggleClass() calls here, because we need to make sure
// we always remove classes first and add them second, otherwise if both classes have the
// same theme class, it will be removed after we add it.
this . _removeClass ( this . button , "ui-selectmenu-button-" +
( this . isOpen ? "closed" : "open" ) )
. _addClass ( this . button , "ui-selectmenu-button-" +
( this . isOpen ? "open" : "closed" ) )
. _toggleClass ( this . menuWrap , "ui-selectmenu-open" , null , this . isOpen ) ;
this . menu . attr ( "aria-hidden" , ! this . isOpen ) ;
} ,
_resizeButton : function ( ) {
var width = this . options . width ;
// For `width: false`, just remove inline style and stop
if ( width === false ) {
this . button . css ( "width" , "" ) ;
return ;
}
// For `width: null`, match the width of the original element
if ( width === null ) {
width = this . element . show ( ) . outerWidth ( ) ;
this . element . hide ( ) ;
}
this . button . outerWidth ( width ) ;
} ,
_resizeMenu : function ( ) {
this . menu . outerWidth ( Math . max (
this . button . outerWidth ( ) ,
// Support: IE10
// IE10 wraps long text (possibly a rounding bug)
// so we add 1px to avoid the wrapping
this . menu . width ( "" ) . outerWidth ( ) + 1
) ) ;
} ,
_getCreateOptions : function ( ) {
var options = this . _super ( ) ;
options . disabled = this . element . prop ( "disabled" ) ;
return options ;
} ,
_parseOptions : function ( options ) {
var that = this ,
data = [ ] ;
options . each ( function ( index , item ) {
data . push ( that . _parseOption ( $ ( item ) , index ) ) ;
} ) ;
this . items = data ;
} ,
_parseOption : function ( option , index ) {
var optgroup = option . parent ( "optgroup" ) ;
return {
element : option ,
index : index ,
value : option . val ( ) ,
label : option . text ( ) ,
2024-05-27 17:14:15 -04:00
hidden : optgroup . prop ( "hidden" ) || option . prop ( "hidden" ) ,
2020-10-07 12:33:25 -04:00
optgroup : optgroup . attr ( "label" ) || "" ,
disabled : optgroup . prop ( "disabled" ) || option . prop ( "disabled" )
} ;
} ,
_destroy : function ( ) {
this . _unbindFormResetHandler ( ) ;
this . menuWrap . remove ( ) ;
this . button . remove ( ) ;
this . element . show ( ) ;
this . element . removeUniqueId ( ) ;
this . labels . attr ( "for" , this . ids . element ) ;
}
} ] ) ;
2021-09-09 20:02:59 -04:00
} ) ;