mirror of
https://github.com/Hopiu/jquery-mobile.git
synced 2026-03-20 16:00:31 +00:00
https://github.com/jquery/jquery-mobile/issues/issue/1036 The css styles the select menu with a width of 80% with a max-width of 350px. This fix checks if width of select menu is less than max-width. If that is the case the select menu is centered on screen. If not it checks if the rigth or left side of the select menu are outside the viewport and if so moves the select menu inside the viewport with a 30px tolerance.
541 lines
14 KiB
JavaScript
541 lines
14 KiB
JavaScript
/*
|
|
* jQuery Mobile Framework : "selectmenu" plugin
|
|
* Copyright (c) jQuery Project
|
|
* Dual licensed under the MIT or GPL Version 2 licenses.
|
|
* http://jquery.org/license
|
|
*/
|
|
(function($, undefined ) {
|
|
$.widget( "mobile.selectmenu", $.mobile.widget, {
|
|
options: {
|
|
theme: null,
|
|
disabled: false,
|
|
icon: 'arrow-d',
|
|
iconpos: 'right',
|
|
inline: null,
|
|
corners: true,
|
|
shadow: true,
|
|
iconshadow: true,
|
|
menuPageTheme: 'b',
|
|
overlayTheme: 'a',
|
|
hidePlaceholderMenuItems: true,
|
|
closeText: 'Close',
|
|
nativeMenu: false
|
|
},
|
|
_create: function(){
|
|
|
|
var self = this,
|
|
|
|
o = this.options,
|
|
|
|
select = this.element
|
|
.wrap( "<div class='ui-select'>" ),
|
|
|
|
selectID = select.attr( "id" ),
|
|
|
|
label = $( "label[for="+ selectID +"]" ).addClass( "ui-select" ),
|
|
|
|
button = ( self.options.nativeMenu ? $( "<div/>" ) : $( "<a>", {
|
|
"href": "#",
|
|
"role": "button",
|
|
"id": buttonId,
|
|
"aria-haspopup": "true",
|
|
"aria-owns": menuId
|
|
}) )
|
|
.text( $( select[0].options.item(select[0].selectedIndex) ).text() )
|
|
.insertBefore( select )
|
|
.buttonMarkup({
|
|
theme: o.theme,
|
|
icon: o.icon,
|
|
iconpos: o.iconpos,
|
|
inline: o.inline,
|
|
corners: o.corners,
|
|
shadow: o.shadow,
|
|
iconshadow: o.iconshadow
|
|
}),
|
|
|
|
//multi select or not
|
|
isMultiple = self.isMultiple = select[0].multiple;
|
|
|
|
//Opera does not properly support opacity on select elements
|
|
//In Mini, it hides the element, but not its text
|
|
//On the desktop,it seems to do the opposite
|
|
//for these reasons, using the nativeMenu option results in a full native select in Opera
|
|
if( o.nativeMenu && window.opera && window.opera.version ){
|
|
select.addClass( "ui-select-nativeonly" );
|
|
}
|
|
|
|
//vars for non-native menus
|
|
if( !o.nativeMenu ){
|
|
var options = select.find("option"),
|
|
|
|
buttonId = selectID + "-button",
|
|
|
|
menuId = selectID + "-menu",
|
|
|
|
thisPage = select.closest( ".ui-page" ),
|
|
|
|
//button theme
|
|
theme = /ui-btn-up-([a-z])/.exec( button.attr("class") )[1],
|
|
|
|
menuPage = $( "<div data-role='dialog' data-theme='"+ o.menuPageTheme +"'>" +
|
|
"<div data-role='header'>" +
|
|
"<div class='ui-title'>" + label.text() + "</div>"+
|
|
"</div>"+
|
|
"<div data-role='content'></div>"+
|
|
"</div>" )
|
|
.appendTo( $.mobile.pageContainer )
|
|
.page(),
|
|
|
|
menuPageContent = menuPage.find( ".ui-content" ),
|
|
|
|
menuPageClose = menuPage.find( ".ui-header a" ),
|
|
|
|
screen = $( "<div>", {"class": "ui-selectmenu-screen ui-screen-hidden"})
|
|
.appendTo( thisPage ),
|
|
|
|
listbox = $( "<div>", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all pop ui-body-" + o.overlayTheme } )
|
|
.insertAfter(screen),
|
|
|
|
list = $( "<ul>", {
|
|
"class": "ui-selectmenu-list",
|
|
"id": menuId,
|
|
"role": "listbox",
|
|
"aria-labelledby": buttonId,
|
|
"data-theme": theme
|
|
})
|
|
.appendTo( listbox ),
|
|
|
|
header = $( "<div>", {
|
|
"class": "ui-header ui-bar-" + theme
|
|
})
|
|
.prependTo( listbox ),
|
|
|
|
headerTitle = $( "<h1>", {
|
|
"class": "ui-title"
|
|
})
|
|
.appendTo( header ),
|
|
|
|
headerClose = $( "<a>", {
|
|
"data-iconpos": "notext",
|
|
"data-icon": "delete",
|
|
"text": o.closeText,
|
|
"href": "#",
|
|
"class": "ui-btn-left"
|
|
})
|
|
.appendTo( header )
|
|
.buttonMarkup(),
|
|
|
|
menuType;
|
|
} //end non native vars
|
|
|
|
// add counter for multi selects
|
|
if( isMultiple ){
|
|
self.buttonCount = $('<span>')
|
|
.addClass( 'ui-li-count ui-btn-up-c ui-btn-corner-all' )
|
|
.hide()
|
|
.appendTo( button );
|
|
}
|
|
|
|
//disable if specified
|
|
if( o.disabled ){ this.disable(); }
|
|
|
|
//events on native select
|
|
select
|
|
.change(function(){
|
|
self.refresh();
|
|
});
|
|
|
|
//expose to other methods
|
|
$.extend(self, {
|
|
select: select,
|
|
optionElems: options,
|
|
selectID: selectID,
|
|
label: label,
|
|
buttonId:buttonId,
|
|
menuId:menuId,
|
|
thisPage:thisPage,
|
|
button:button,
|
|
menuPage:menuPage,
|
|
menuPageContent:menuPageContent,
|
|
screen:screen,
|
|
listbox:listbox,
|
|
list:list,
|
|
menuType:menuType,
|
|
header:header,
|
|
headerClose:headerClose,
|
|
headerTitle:headerTitle,
|
|
placeholder: ''
|
|
});
|
|
|
|
//support for using the native select menu with a custom button
|
|
if( o.nativeMenu ){
|
|
|
|
select
|
|
.appendTo(button)
|
|
.bind( "touchstart mousedown", function( e ){
|
|
//add active class to button
|
|
button.addClass( $.mobile.activeBtnClass );
|
|
})
|
|
.bind( "focus mouseover", function(){
|
|
button.trigger( "mouseover" );
|
|
})
|
|
.bind( "touchmove", function(){
|
|
//remove active class on scroll/touchmove
|
|
button.removeClass( $.mobile.activeBtnClass );
|
|
})
|
|
.bind( "change blur mouseout", function(){
|
|
button
|
|
.trigger( "mouseout" )
|
|
.removeClass( $.mobile.activeBtnClass );
|
|
});
|
|
|
|
button.attr( "tabindex", "-1" );
|
|
} else {
|
|
|
|
//create list from select, update state
|
|
self.refresh();
|
|
|
|
select
|
|
.attr( "tabindex", "-1" )
|
|
.focus(function(){
|
|
$(this).blur();
|
|
button.focus();
|
|
});
|
|
|
|
//button events
|
|
button
|
|
.bind( "touchstart" , function( event ){
|
|
//set startTouches to cached copy of
|
|
$( this ).data( "startTouches", $.extend({}, event.originalEvent.touches[ 0 ]) );
|
|
})
|
|
.bind( $.support.touch ? "touchend" : "mouseup" , function( event ){
|
|
//if it's a scroll, don't open
|
|
if( $( this ).data( "moved" ) ){
|
|
$( this ).removeData( "moved" );
|
|
} else {
|
|
self.open();
|
|
}
|
|
event.preventDefault();
|
|
})
|
|
.bind( "touchmove", function( event ){
|
|
//if touch moved enough, set data moved and don't open menu
|
|
var thisTouches = event.originalEvent.touches[ 0 ],
|
|
startTouches = $( this ).data( "startTouches" ),
|
|
deltaX = Math.abs(thisTouches.pageX - startTouches.pageX),
|
|
deltaY = Math.abs(thisTouches.pageY - startTouches.pageY);
|
|
|
|
if( deltaX > 10 || deltaY > 10 ){
|
|
$( this ).data( "moved", true );
|
|
}
|
|
});
|
|
|
|
|
|
//events for list items
|
|
list.delegate("li:not(.ui-disabled, .ui-li-divider)", "click", function(event){
|
|
// clicking on the list item fires click on the link in listview.js.
|
|
// to prevent this handler from firing twice if the link isn't clicked on,
|
|
// short circuit unless the target is the link
|
|
if( !$(event.target).is("a") ){ return; }
|
|
|
|
// index of option tag to be selected
|
|
var newIndex = list.find( "li:not(.ui-li-divider)" ).index( this ),
|
|
option = self.optionElems.eq( newIndex )[0];
|
|
|
|
// toggle selected status on the tag for multi selects
|
|
option.selected = isMultiple ? !option.selected : true;
|
|
|
|
// toggle checkbox class for multiple selects
|
|
if( isMultiple ){
|
|
$(this)
|
|
.find('.ui-icon')
|
|
.toggleClass('ui-icon-checkbox-on', option.selected)
|
|
.toggleClass('ui-icon-checkbox-off', !option.selected);
|
|
}
|
|
|
|
// trigger change
|
|
select.trigger( "change" );
|
|
|
|
//hide custom select for single selects only
|
|
if( !isMultiple ){
|
|
self.close();
|
|
}
|
|
|
|
event.preventDefault();
|
|
});
|
|
|
|
//events on "screen" overlay + close button
|
|
screen
|
|
.add( headerClose )
|
|
.add( menuPageClose )
|
|
.bind("click", function(event){
|
|
self.close();
|
|
event.preventDefault();
|
|
|
|
// if the dialog's close icon was clicked, prevent the dialog's close
|
|
// handler from firing. selectmenu's should take precedence
|
|
if( $.contains(menuPageClose[0], event.target) ){
|
|
event.stopImmediatePropagation();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
},
|
|
|
|
_buildList: function(){
|
|
var self = this,
|
|
o = this.options,
|
|
placeholder = this.placeholder,
|
|
optgroups = [],
|
|
lis = [],
|
|
dataIcon = self.isMultiple ? "checkbox-off" : "false";
|
|
|
|
self.list.empty().filter('.ui-listview').listview('destroy');
|
|
|
|
//populate menu with options from select element
|
|
self.select.find( "option" ).each(function( i ){
|
|
var $this = $(this),
|
|
$parent = $this.parent(),
|
|
text = $this.text(),
|
|
anchor = "<a href='#'>"+ text +"</a>",
|
|
classes = [],
|
|
extraAttrs = [];
|
|
|
|
// are we inside an optgroup?
|
|
if( $parent.is("optgroup") ){
|
|
var optLabel = $parent.attr("label");
|
|
|
|
// has this optgroup already been built yet?
|
|
if( $.inArray(optLabel, optgroups) === -1 ){
|
|
lis.push( "<li data-role='list-divider'>"+ optLabel +"</li>" );
|
|
optgroups.push( optLabel );
|
|
}
|
|
}
|
|
|
|
//find placeholder text
|
|
if( !this.getAttribute('value') || text.length == 0 || $this.data('placeholder') ){
|
|
if( o.hidePlaceholderMenuItems ){
|
|
classes.push( "ui-selectmenu-placeholder" );
|
|
}
|
|
placeholder = self.placeholder = text;
|
|
}
|
|
|
|
// support disabled option tags
|
|
if( this.disabled ){
|
|
classes.push( "ui-disabled" );
|
|
extraAttrs.push( "aria-disabled='true'" );
|
|
}
|
|
|
|
lis.push( "<li data-icon='"+ dataIcon +"' class='"+ classes.join(" ") + "' " + extraAttrs.join(" ") +">"+ anchor +"</li>" )
|
|
});
|
|
|
|
self.list.html( lis.join(" ") );
|
|
|
|
// hide header close link for single selects
|
|
if( !this.isMultiple ){
|
|
this.headerClose.hide();
|
|
}
|
|
|
|
// hide header if it's not a multiselect and there's no placeholder
|
|
if( !this.isMultiple && !placeholder.length ){
|
|
this.header.hide();
|
|
} else {
|
|
this.headerTitle.text( this.placeholder );
|
|
}
|
|
|
|
//now populated, create listview
|
|
self.list.listview();
|
|
},
|
|
|
|
refresh: function( forceRebuild ){
|
|
var self = this,
|
|
select = this.element,
|
|
isMultiple = this.isMultiple,
|
|
options = this.optionElems = select.find("option"),
|
|
selected = options.filter(":selected"),
|
|
|
|
// return an array of all selected index's
|
|
indicies = selected.map(function(){
|
|
return options.index( this );
|
|
}).get();
|
|
|
|
if( !self.options.nativeMenu && ( forceRebuild || select[0].options.length > self.list.find('li').length )){
|
|
self._buildList();
|
|
}
|
|
|
|
self.button
|
|
.find( ".ui-btn-text" )
|
|
.text(function(){
|
|
if( !isMultiple ){
|
|
return selected.text();
|
|
}
|
|
|
|
return selected.length ?
|
|
selected.map(function(){ return $(this).text(); }).get().join(', ') :
|
|
self.placeholder;
|
|
});
|
|
|
|
// multiple count inside button
|
|
if( isMultiple ){
|
|
self.buttonCount[ selected.length > 1 ? 'show' : 'hide' ]().text( selected.length );
|
|
}
|
|
|
|
if( !self.options.nativeMenu ){
|
|
self.list
|
|
.find( 'li:not(.ui-li-divider)' )
|
|
.removeClass( $.mobile.activeBtnClass )
|
|
.attr( 'aria-selected', false )
|
|
.each(function( i ){
|
|
if( $.inArray(i, indicies) > -1 ){
|
|
var item = $(this).addClass( $.mobile.activeBtnClass );
|
|
|
|
// aria selected attr
|
|
item.find( 'a' ).attr( 'aria-selected', true );
|
|
|
|
// multiple selects: add the "on" checkbox state to the icon
|
|
if( isMultiple ){
|
|
item.find('.ui-icon').removeClass('ui-icon-checkbox-off').addClass('ui-icon-checkbox-on');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
open: function(){
|
|
if( this.options.disabled || this.options.nativeMenu ){ return; }
|
|
|
|
var self = this,
|
|
menuHeight = self.list.parent().outerHeight(),
|
|
menuWidth = self.list.parent().outerWidth(),
|
|
scrollTop = $(window).scrollTop(),
|
|
btnOffset = self.button.offset().top,
|
|
screenHeight = window.innerHeight,
|
|
screenWidth = window.innerWidth,
|
|
dialogUsed = self.list.parents('.ui-dialog').length;
|
|
|
|
//add active class to button
|
|
self.button.addClass( $.mobile.activeBtnClass );
|
|
|
|
function focusMenuItem(){
|
|
self.list.find( ".ui-btn-active" ).focus();
|
|
}
|
|
|
|
// NOTE addresses issue with firefox outerHeight when the parent dialog
|
|
// is display: none. Upstream?
|
|
if( dialogUsed || menuHeight > screenHeight - 80 || !$.support.scrollTop ){
|
|
|
|
//for webos (set lastscroll using button offset)
|
|
if( scrollTop == 0 && btnOffset > screenHeight ){
|
|
self.thisPage.one('pagehide',function(){
|
|
$(this).data('lastScroll', btnOffset);
|
|
});
|
|
}
|
|
|
|
self.menuPage.one('pageshow', function() {
|
|
// silentScroll() is called whenever a page is shown to restore
|
|
// any previous scroll position the page may have had. We need to
|
|
// wait for the "silentscroll" event before setting focus to avoid
|
|
// the browser's "feature" which offsets rendering to make sure
|
|
// whatever has focus is in view.
|
|
$(window).one("silentscroll", function(){ focusMenuItem(); });
|
|
});
|
|
|
|
self.menuType = "page";
|
|
self.menuPageContent.append( self.list );
|
|
$.mobile.changePage(self.menuPage, 'pop', false, true);
|
|
}
|
|
else {
|
|
self.menuType = "overlay";
|
|
|
|
self.screen
|
|
.height( $(document).height() )
|
|
.removeClass('ui-screen-hidden');
|
|
|
|
//try and center the overlay over the button
|
|
var roomtop = btnOffset - scrollTop,
|
|
roombot = scrollTop + screenHeight - btnOffset,
|
|
halfheight = menuHeight / 2,
|
|
maxwidth = parseFloat(self.list.parent().css('max-width')),
|
|
newtop,newleft;
|
|
|
|
if( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ){
|
|
newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight;
|
|
}
|
|
else{
|
|
//30px tolerance off the edges
|
|
newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30;
|
|
}
|
|
|
|
if (menuWidth < maxwidth) {
|
|
newleft = (screenWidth - menuWidth) / 2;
|
|
} else {
|
|
newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2;
|
|
// 30px tolerance off the edges
|
|
if (newleft < 30) {
|
|
newleft = 30;
|
|
} else if ((newleft + menuWidth) > screenWidth) {
|
|
newleft = screenWidth - menuWidth - 30;
|
|
}
|
|
}
|
|
|
|
self.listbox
|
|
.append( self.list )
|
|
.removeClass( "ui-selectmenu-hidden" )
|
|
.css({
|
|
top: newtop,
|
|
left: newleft
|
|
})
|
|
.addClass("in");
|
|
|
|
focusMenuItem();
|
|
}
|
|
|
|
// wait before the dialog can be closed
|
|
setTimeout(function(){
|
|
self.isOpen = true;
|
|
}, 400);
|
|
},
|
|
|
|
close: function(){
|
|
if( this.options.disabled || !this.isOpen || this.options.nativeMenu ){ return; }
|
|
var self = this;
|
|
|
|
function focusButton(){
|
|
setTimeout(function(){
|
|
self.button.focus();
|
|
|
|
//remove active class from button
|
|
self.button.removeClass( $.mobile.activeBtnClass );
|
|
}, 40);
|
|
|
|
self.listbox.removeAttr('style').append( self.list );
|
|
}
|
|
|
|
if(self.menuType == "page"){
|
|
$.mobile.changePage([self.menuPage,self.thisPage], 'pop', true, false);
|
|
self.menuPage.one("pagehide", focusButton);
|
|
}
|
|
else{
|
|
self.screen.addClass( "ui-screen-hidden" );
|
|
self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass("in");
|
|
focusButton();
|
|
}
|
|
|
|
// allow the dialog to be closed again
|
|
this.isOpen = false;
|
|
},
|
|
|
|
disable: function(){
|
|
this.element.attr("disabled",true);
|
|
this.button.addClass('ui-disabled').attr("aria-disabled", true);
|
|
return this._setOption( "disabled", true );
|
|
},
|
|
|
|
enable: function(){
|
|
this.element.attr("disabled",false);
|
|
this.button.removeClass('ui-disabled').attr("aria-disabled", false);
|
|
return this._setOption( "disabled", false );
|
|
}
|
|
});
|
|
})( jQuery );
|
|
|