jquery-mobile/js/jquery.mobile.forms.select.js

590 lines
15 KiB
JavaScript
Raw Normal View History

2010-09-10 22:23:13 +00:00
/*
2010-11-10 00:55:52 +00:00
* jQuery Mobile Framework : "selectmenu" plugin
2010-09-10 22:23:13 +00:00
* 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,
2010-11-11 03:54:57 +00:00
icon: 'arrow-d',
iconpos: 'right',
inline: null,
corners: true,
shadow: true,
iconshadow: true,
menuPageTheme: 'b',
overlayTheme: 'a',
2010-12-07 20:45:20 +00:00
hidePlaceholderMenuItems: true,
closeText: 'Close',
nativeMenu: true
},
_create: function(){
2011-04-20 05:53:53 +00:00
var self = this,
2010-11-11 03:54:57 +00:00
o = this.options,
select = this.element
2010-12-07 20:45:20 +00:00
.wrap( "<div class='ui-select'>" ),
2011-03-13 06:56:36 +00:00
selectID = select.attr( "id" ),
2011-05-02 08:00:36 +00:00
label = $( 'label[for="'+ selectID +'"]' ).addClass( "ui-select" ),
2011-04-20 05:53:53 +00:00
2011-03-21 14:21:53 +00:00
//IE throws an exception at options.item() function when
//there is no selected item
2011-04-20 05:53:53 +00:00
//select first in this case
2011-03-21 14:21:53 +00:00
selectedIndex = select[0].selectedIndex == -1 ? 0 : select[0].selectedIndex,
2011-04-20 05:53:53 +00:00
button = ( self.options.nativeMenu ? $( "<div/>" ) : $( "<a>", {
"href": "#",
"role": "button",
"id": buttonId,
"aria-haspopup": "true",
"aria-owns": menuId
}) )
2011-03-21 14:21:53 +00:00
.text( $( select[0].options.item( selectedIndex ) ).text() )
.insertBefore( select )
.buttonMarkup({
theme: o.theme,
2010-11-11 03:54:57 +00:00
icon: o.icon,
iconpos: o.iconpos,
inline: o.inline,
corners: o.corners,
shadow: o.shadow,
iconshadow: o.iconshadow
}),
2011-03-13 06:56:36 +00:00
//multi select or not
isMultiple = self.isMultiple = select[0].multiple;
2011-03-13 06:56:36 +00:00
//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" );
2011-03-13 06:56:36 +00:00
}
//vars for non-native menus
if( !o.nativeMenu ){
var options = select.find("option"),
2011-03-13 06:56:36 +00:00
buttonId = selectID + "-button",
2011-03-13 06:56:36 +00:00
menuId = selectID + "-menu",
2011-03-13 06:56:36 +00:00
thisPage = select.closest( ".ui-page" ),
2011-03-13 06:56:36 +00:00
//button theme
theme = /ui-btn-up-([a-z])/.exec( button.attr("class") )[1],
2011-03-13 06:56:36 +00:00
menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ o.menuPageTheme +"'>" +
"<div data-" + $.mobile.ns + "role='header'>" +
"<div class='ui-title'>" + label.text() + "</div>"+
"</div>"+
"<div data-" + $.mobile.ns + "role='content'></div>"+
"</div>" )
.appendTo( $.mobile.pageContainer )
.page(),
2011-03-13 06:56:36 +00:00
menuPageContent = menuPage.find( ".ui-content" ),
2011-03-13 06:56:36 +00:00
menuPageClose = menuPage.find( ".ui-header a" ),
2011-03-13 06:56:36 +00:00
screen = $( "<div>", {"class": "ui-selectmenu-screen ui-screen-hidden"})
.appendTo( thisPage ),
2011-03-13 06:56:36 +00:00
listbox = $( "<div>", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all pop ui-body-" + o.overlayTheme } )
.insertAfter(screen),
2011-03-13 06:56:36 +00:00
list = $( "<ul>", {
"class": "ui-selectmenu-list",
"id": menuId,
"role": "listbox",
2011-02-20 18:43:02 +00:00
"aria-labelledby": buttonId
})
2011-02-20 18:43:02 +00:00
.attr( "data-" + $.mobile.ns + "theme", theme )
.appendTo( listbox ),
2011-03-13 06:56:36 +00:00
header = $( "<div>", {
"class": "ui-header ui-bar-" + theme
})
.prependTo( listbox ),
2011-03-13 06:56:36 +00:00
headerTitle = $( "<h1>", {
"class": "ui-title"
})
.appendTo( header ),
2011-03-13 06:56:36 +00:00
headerClose = $( "<a>", {
"text": o.closeText,
"href": "#",
"class": "ui-btn-left"
})
2011-02-20 18:43:02 +00:00
.attr( "data-" + $.mobile.ns + "iconpos", "notext" )
.attr( "data-" + $.mobile.ns + "icon", "delete" )
.appendTo( header )
.buttonMarkup(),
2011-03-13 06:56:36 +00:00
menuType;
2011-03-13 06:56:36 +00:00
} //end non native vars
2010-12-07 20:45:20 +00:00
// 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();
});
2011-03-13 06:56:36 +00:00
2010-12-07 20:45:20 +00:00
//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: ''
2011-03-13 06:56:36 +00:00
});
//support for using the native select menu with a custom button
if( o.nativeMenu ){
2011-01-27 05:35:00 +00:00
select
.appendTo(button)
.bind( "vmousedown", function( e ){
//add active class to button
button.addClass( $.mobile.activeBtnClass );
})
.bind( "focus vmouseover", function(){
button.trigger( "vmouseover" );
})
.bind( "vmousemove", function(){
//remove active class on scroll/touchmove
button.removeClass( $.mobile.activeBtnClass );
})
.bind( "change blur vmouseout", function(){
button
.trigger( "vmouseout" )
.removeClass( $.mobile.activeBtnClass );
});
} else {
2011-03-13 06:56:36 +00:00
//create list from select, update state
self.refresh();
2011-01-26 06:50:55 +00:00
select
.attr( "tabindex", "-1" )
2011-01-26 06:50:55 +00:00
.focus(function(){
$(this).blur();
button.focus();
2011-04-20 05:53:53 +00:00
});
2011-01-26 06:50:55 +00:00
//button events
button
.bind( "vclick keydown" , function( event ){
2011-04-20 05:53:53 +00:00
if( event.type == "vclick" ||
event.keyCode && ( event.keyCode === $.mobile.keyCode.ENTER || event.keyCode === $.mobile.keyCode.SPACE ) ){
self.open();
event.preventDefault();
}
2011-01-26 06:50:55 +00:00
});
2011-03-13 06:56:36 +00:00
//events for list items
list
.attr( "role", "listbox" )
.delegate( ".ui-li>a", "focusin", function() {
$( this ).attr( "tabindex", "0" );
})
.delegate( ".ui-li>a", "focusout", function() {
$( this ).attr( "tabindex", "-1" );
})
.delegate("li:not(.ui-disabled, .ui-li-divider)", "vclick", function(event){
2011-03-13 06:56:36 +00:00
// index of option tag to be selected
var oldIndex = select[0].selectedIndex,
newIndex = list.find( "li:not(.ui-li-divider)" ).index( this ),
option = self.optionElems.eq( newIndex )[0];
2011-03-13 06:56:36 +00:00
// toggle selected status on the tag for multi selects
option.selected = isMultiple ? !option.selected : true;
2011-03-13 06:56:36 +00:00
// 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);
}
2011-03-13 06:56:36 +00:00
// trigger change if value changed
if( oldIndex !== newIndex ){
select.trigger( "change" );
}
2011-03-13 06:56:36 +00:00
//hide custom select for single selects only
if( !isMultiple ){
self.close();
}
2011-03-13 06:56:36 +00:00
event.preventDefault();
})
//keyboard events for menu items
.keydown(function( e ) {
var target = $( e.target ),
li = target.closest( "li" );
2011-04-20 05:53:53 +00:00
// switch logic based on which key was pressed
switch ( e.keyCode ) {
// up or left arrow keys
case 38:
var prev = li.prev();
2011-04-20 05:53:53 +00:00
// if there's a previous option, focus it
if ( prev.length ) {
target
.blur()
.attr( "tabindex", "-1" );
2011-04-20 05:53:53 +00:00
prev.find( "a" ).first().focus();
2011-04-20 05:53:53 +00:00
}
return false;
break;
2011-04-20 05:53:53 +00:00
// down or right arrow keys
case 40:
var next = li.next();
2011-04-20 05:53:53 +00:00
// if there's a next option, focus it
if ( next.length ) {
target
.blur()
.attr( "tabindex", "-1" );
2011-04-20 05:53:53 +00:00
next.find( "a" ).first().focus();
2011-04-20 05:53:53 +00:00
}
return false;
break;
2011-04-20 05:53:53 +00:00
// if enter or space is pressed, trigger click
case 13:
case 32:
target.trigger( "vclick" );
2011-04-20 05:53:53 +00:00
return false;
2011-04-20 05:53:53 +00:00
break;
}
2011-04-20 05:53:53 +00:00
});
2011-03-13 06:56:36 +00:00
//events on "screen" overlay
screen.bind("vclick", function( event ){
self.close();
});
2011-04-20 05:53:53 +00:00
//close button on small overlays
self.headerClose.click(function(){
if( self.menuType == "overlay" ){
self.close();
return false;
}
})
2011-03-13 06:56:36 +00:00
}
},
_buildList: function(){
var self = this,
2010-12-07 23:05:26 +00:00
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 ){
2010-12-03 15:57:21 +00:00
var $this = $(this),
2010-12-07 23:05:26 +00:00
$parent = $this.parent(),
text = $this.text(),
anchor = "<a href='#'>"+ text +"</a>",
classes = [],
extraAttrs = [];
2010-12-03 15:57:21 +00:00
// are we inside an optgroup?
if( $parent.is("optgroup") ){
var optLabel = $parent.attr("label");
2010-12-03 15:57:21 +00:00
// has this optgroup already been built yet?
if( $.inArray(optLabel, optgroups) === -1 ){
lis.push( "<li data-" + $.mobile.ns + "role='list-divider'>"+ optLabel +"</li>" );
2010-12-03 15:57:21 +00:00
optgroups.push( optLabel );
}
}
2011-03-13 06:56:36 +00:00
//find placeholder text
if( !this.getAttribute('value') || text.length == 0 || $this.jqmData('placeholder') ){
2010-12-07 23:05:26 +00:00
if( o.hidePlaceholderMenuItems ){
classes.push( "ui-selectmenu-placeholder" );
2010-12-07 23:05:26 +00:00
}
placeholder = self.placeholder = text;
2010-12-07 20:45:20 +00:00
}
// support disabled option tags
if( this.disabled ){
classes.push( "ui-disabled" );
extraAttrs.push( "aria-disabled='true'" );
}
lis.push( "<li data-" + $.mobile.ns + "icon='"+ dataIcon +"' class='"+ classes.join(" ") + "' " + extraAttrs.join(" ") +">"+ anchor +"</li>" )
});
2011-03-13 06:56:36 +00:00
self.list.html( lis.join(" ") );
2011-04-20 05:53:53 +00:00
self.list.find( "li" )
.attr({ "role": "option", "tabindex": "-1" })
.first().attr( "tabindex", "0" );
2010-12-07 20:45:20 +00:00
// hide header close link for single selects
if( !this.isMultiple ){
this.headerClose.hide();
}
2010-12-07 20:45:20 +00:00
// hide header if it's not a multiselect and there's no placeholder
2010-12-07 23:05:26 +00:00
if( !this.isMultiple && !placeholder.length ){
2010-12-07 20:45:20 +00:00
this.header.hide();
2010-12-07 23:05:26 +00:00
} else {
this.headerTitle.text( this.placeholder );
2010-12-07 20:45:20 +00:00
}
//now populated, create listview
2010-12-07 20:45:20 +00:00
self.list.listview();
},
refresh: function( forceRebuild ){
var self = this,
select = this.element,
2010-12-07 20:45:20 +00:00
isMultiple = this.isMultiple,
options = this.optionElems = select.find("option"),
selected = options.filter(":selected"),
2010-12-07 20:45:20 +00:00
// return an array of all selected index's
indicies = selected.map(function(){
return options.index( this );
2010-12-07 20:45:20 +00:00
}).get();
if( !self.options.nativeMenu && ( forceRebuild || select[0].options.length != self.list.find('li').length )){
self._buildList();
2010-12-07 20:45:20 +00:00
}
2010-12-07 20:45:20 +00:00
self.button
.find( ".ui-btn-text" )
.text(function(){
if( !isMultiple ){
return selected.text();
}
2010-12-07 20:45:20 +00:00
return selected.length ?
selected.map(function(){ return $(this).text(); }).get().join(', ') :
self.placeholder;
});
2010-12-07 20:45:20 +00:00
// multiple count inside button
if( isMultiple ){
self.buttonCount[ selected.length > 1 ? 'show' : 'hide' ]().text( selected.length );
}
2011-03-13 06:56:36 +00:00
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 );
2011-03-13 06:56:36 +00:00
// aria selected attr
item.find( 'a' ).attr( 'aria-selected', true );
2011-03-13 06:56:36 +00:00
// 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');
}
2010-12-07 20:45:20 +00:00
}
});
2011-03-13 06:56:36 +00:00
}
},
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;
//add active class to button
self.button.addClass( $.mobile.activeBtnClass );
//remove after delay
setTimeout(function(){
self.button.removeClass( $.mobile.activeBtnClass );
}, 300);
function focusMenuItem(){
self.list.find( ".ui-btn-active" ).focus();
}
if( menuHeight > screenHeight - 80 || !$.support.scrollTop ){
//for webos (set lastscroll using button offset)
if( scrollTop == 0 && btnOffset > screenHeight ){
self.thisPage.one('pagehide',function(){
$(this).jqmData('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 );
This commit includes simplifications to the hashchange event handling & changePage logic, which results in a few bug fixes and removal of some previous limitations. Details: - jquery.mobile.core.js no longer creates pages from every page and dialog in the DOM automatically at domready. Instead, pages are created as they are referenced via changePage, which can speed up pageload in multi-page documents, and means local "dialogs" referenced via data-rel="dialog" no longer need a data-role="dialog" attribute when served. - in changePage, "from" is now allowed to be undefined. This simplifies the logic involved in showing the first page, which never had a "from" page, and previously needed a custom pageChange workaround to accommodate that. - The pageshow event is no longer used as a callback for returning false and preventing the $.mobile.activePage from being set to the newly shown page. In other words, a page always becomes $.mobile.activePage once its shown now (the only reason this was optional before was because of a dialog restriction that's no longer true) - the hashchange event logic for showing a particular page is now greatly simplified. It either shows the page referenced in location.hash, or if there's no hash it changes to the first page in the dom. This means every pageshow (including the first one) now uses pageChange internally. - the hashchange event listener is no longer disabled when ajaxEnabled == false. Doing this before prevented local non-ajax page navigation from working properly. To disable hashchange listening, use $.mobile.urlHistory.listeningEnabled. We might consider defining (or moving) this on the $.mobile hash later as well for easier access. - The internal var $.mobile.startPage is now $.mobile.firstPage, because it's not necessarily the page you started on, but merely a reference to the first "page" in the dom. - Back buttons are auto-added to every page after the first one you first visit (this includes generated pages, such as those in a multipage document or nested listviews). Keep in mind that a "back" button does not take the place of a standard "home" link, and when building an app with jQuery Mobile, it's good to make use of both (particularly on deep-linked pages). Fixes #908
2011-01-31 22:05:57 +00:00
$.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,
2011-03-13 07:02:11 +00:00
maxwidth = parseFloat(self.list.parent().css('max-width')),
newtop, newleft;
2011-03-13 06:56:36 +00:00
2011-03-13 06:58:21 +00:00
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;
}
2011-03-13 06:56:36 +00:00
2011-03-13 07:02:11 +00:00
// if the menuwidth is smaller than the screen center is
2011-03-13 06:58:21 +00:00
if (menuWidth < maxwidth) {
newleft = (screenWidth - menuWidth) / 2;
2011-03-13 07:02:11 +00:00
} else { //otherwise insure a >= 30px offset from the left
newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2;
2011-03-13 06:58:21 +00:00
// 30px tolerance off the edges
if (newleft < 30) {
newleft = 30;
} else if ((newleft + menuWidth) > screenWidth) {
newleft = screenWidth - menuWidth - 30;
}
2011-03-13 06:58:21 +00:00
}
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();
}, 40);
self.listbox.removeAttr('style').append( self.list );
}
if(self.menuType == "page"){
// button refocus ensures proper height calculation
// by removing the inline style and ensuring page inclusion
self.menuPage.one("pagehide", focusButton);
// doesn't solve the possible issue with calling change page
// where the objects don't define data urls which prevents dialog key
// stripping - changePage has incoming refactor
window.history.back();
}
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);
2010-11-11 03:43:30 +00:00
this.button.addClass('ui-disabled').attr("aria-disabled", true);
2010-11-11 03:48:31 +00:00
return this._setOption( "disabled", true );
},
enable: function(){
this.element.attr("disabled",false);
2010-11-11 03:43:30 +00:00
this.button.removeClass('ui-disabled').attr("aria-disabled", false);
2010-11-11 03:48:31 +00:00
return this._setOption( "disabled", false );
}
});
})( jQuery );