2 * custom "selectmenu" plugin
5 (function( $, undefined ) {
6 var extendSelect = function( widget ){
8 var select = widget.select,
9 selectID = widget.selectID,
11 thisPage = widget.select.closest( ".ui-page" ),
12 screen = $( "<div>", {"class": "ui-selectmenu-screen ui-screen-hidden"} ).appendTo( thisPage ),
13 selectOptions = widget._selectOptions(),
14 isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
15 buttonId = selectID + "-button",
16 menuId = selectID + "-menu",
17 menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
18 "<div data-" + $.mobile.ns + "role='header'>" +
19 "<div class='ui-title'>" + label.getEncodedText() + "</div>"+
21 "<div data-" + $.mobile.ns + "role='content'></div>"+
22 "</div>" ).appendTo( $.mobile.pageContainer ).page(),
24 listbox = $("<div>", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-" + widget.options.overlayTheme + " " + $.mobile.defaultDialogTransition } ).insertAfter(screen),
27 "class": "ui-selectmenu-list",
30 "aria-labelledby": buttonId
31 }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ),
33 header = $( "<div>", {
34 "class": "ui-header ui-bar-" + widget.options.theme
35 }).prependTo( listbox ),
37 headerTitle = $( "<h1>", {
39 }).appendTo( header ),
41 headerClose = $( "<a>", {
42 "text": widget.options.closeText,
44 "class": "ui-btn-left"
45 }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(),
47 menuPageContent = menuPage.find( ".ui-content" ),
49 menuPageClose = menuPage.find( ".ui-header a" );
53 select: widget.select,
61 selectOptions: selectOptions,
62 isMultiple: isMultiple,
63 theme: widget.options.theme,
67 headerTitle: headerTitle,
68 headerClose: headerClose,
69 menuPageContent: menuPageContent,
70 menuPageClose: menuPageClose,
76 // Create list from select, update state
79 self.select.attr( "tabindex", "-1" ).focus(function() {
85 self.button.bind( "vclick keydown" , function( event ) {
86 if ( event.type == "vclick" ||
87 event.keyCode && ( event.keyCode === $.mobile.keyCode.ENTER ||
88 event.keyCode === $.mobile.keyCode.SPACE ) ) {
91 event.preventDefault();
95 // Events for list items
96 self.list.attr( "role", "listbox" )
97 .delegate( ".ui-li>a", "focusin", function() {
98 $( this ).attr( "tabindex", "0" );
100 .delegate( ".ui-li>a", "focusout", function() {
101 $( this ).attr( "tabindex", "-1" );
103 .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
105 // index of option tag to be selected
106 var oldIndex = self.select[ 0 ].selectedIndex,
107 newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
108 option = self._selectOptions().eq( newIndex )[ 0 ];
110 // toggle selected status on the tag for multi selects
111 option.selected = self.isMultiple ? !option.selected : true;
113 // toggle checkbox class for multiple selects
114 if ( self.isMultiple ) {
115 $( this ).find( ".ui-icon" )
116 .toggleClass( "ui-icon-checkbox-on", option.selected )
117 .toggleClass( "ui-icon-checkbox-off", !option.selected );
120 // trigger change if value changed
121 if ( self.isMultiple || oldIndex !== newIndex ) {
122 self.select.trigger( "change" );
125 //hide custom select for single selects only
126 if ( !self.isMultiple ) {
130 event.preventDefault();
132 .keydown(function( event ) { //keyboard events for menu items
133 var target = $( event.target ),
134 li = target.closest( "li" ),
137 // switch logic based on which key was pressed
138 switch ( event.keyCode ) {
139 // up or left arrow keys
143 // if there's a previous option, focus it
147 .attr( "tabindex", "-1" );
149 prev.find( "a" ).first().focus();
155 // down or right arrow keys
159 // if there's a next option, focus it
163 .attr( "tabindex", "-1" );
165 next.find( "a" ).first().focus();
171 // If enter or space is pressed, trigger click
174 target.trigger( "click" );
181 // button refocus ensures proper height calculation
182 // by removing the inline style and ensuring page inclusion
183 self.menuPage.bind( "pagehide", function() {
184 self.list.appendTo( self.listbox );
187 // TODO centralize page removal binding / handling in the page plugin.
188 // Suggestion from @jblas to do refcounting
190 // TODO extremely confusing dependency on the open method where the pagehide.remove
191 // bindings are stripped to prevent the parent page from disappearing. The way
192 // we're keeping pages in the DOM right now sucks
194 // rebind the page remove that was unbound in the open function
195 // to allow for the parent page removal from actions other than the use
196 // of a dialog sized custom select
198 // doing this here provides for the back button on the custom select dialog
199 $.mobile._bindPageRemove.call( self.thisPage );
202 // Events on "screen" overlay
203 self.screen.bind( "vclick", function( event ) {
207 // Close button on small overlays
208 self.headerClose.click( function() {
209 if ( self.menuType == "overlay" ) {
215 // track this dependency so that when the parent page
216 // is removed on pagehide it will also remove the menupage
217 self.thisPage.addDependents( this.menuPage );
220 _isRebuildRequired: function() {
221 var list = this.list.find( "li" ),
222 options = this._selectOptions();
224 // TODO exceedingly naive method to determine difference
225 // ignores value changes etc in favor of a forcedRebuild
226 // from the user in the refresh method
227 return options.text() !== list.text();
230 refresh: function( forceRebuild , foo ){
232 select = this.element,
233 isMultiple = this.isMultiple,
234 options = this._selectOptions(),
235 selected = this.selected(),
236 // return an array of all selected index's
237 indicies = this.selectedIndices();
239 if ( forceRebuild || this._isRebuildRequired() ) {
243 self.setButtonText();
244 self.setButtonCount();
246 self.list.find( "li:not(.ui-li-divider)" )
247 .removeClass( $.mobile.activeBtnClass )
248 .attr( "aria-selected", false )
249 .each(function( i ) {
251 if ( $.inArray( i, indicies ) > -1 ) {
252 var item = $( this );
254 // Aria selected attr
255 item.attr( "aria-selected", true );
257 // Multiple selects: add the "on" checkbox state to the icon
258 if ( self.isMultiple ) {
259 item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
261 item.addClass( $.mobile.activeBtnClass );
268 if ( this.options.disabled || !this.isOpen ) {
274 if ( self.menuType == "page" ) {
275 // doesn't solve the possible issue with calling change page
276 // where the objects don't define data urls which prevents dialog key
277 // stripping - changePage has incoming refactor
278 window.history.back();
280 self.screen.addClass( "ui-screen-hidden" );
281 self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass( "in" );
282 self.list.appendTo( self.listbox );
286 // allow the dialog to be closed again
291 if ( this.options.disabled ) {
296 menuHeight = self.list.parent().outerHeight(),
297 menuWidth = self.list.parent().outerWidth(),
298 activePage = $( ".ui-page-active" ),
299 tOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled,
300 tScrollElem = activePage.is( ".ui-native-fixed" ) ? activePage.find( ".ui-content" ) : activePage;
301 scrollTop = tOverflow ? tScrollElem.scrollTop() : $( window ).scrollTop(),
302 btnOffset = self.button.offset().top,
303 screenHeight = window.innerHeight,
304 screenWidth = window.innerWidth;
306 //add active class to button
307 self.button.addClass( $.mobile.activeBtnClass );
310 setTimeout( function() {
311 self.button.removeClass( $.mobile.activeBtnClass );
314 function focusMenuItem() {
315 self.list.find( $.mobile.activeBtnClass ).focus();
318 if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
319 // prevent the parent page from being removed from the DOM,
320 // otherwise the results of selecting a list item in the dialog
321 // fall into a black hole
322 self.thisPage.unbind( "pagehide.remove" );
324 //for WebOS/Opera Mini (set lastscroll using button offset)
325 if ( scrollTop == 0 && btnOffset > screenHeight ) {
326 self.thisPage.one( "pagehide", function() {
327 $( this ).jqmData( "lastScroll", btnOffset );
331 self.menuPage.one( "pageshow", function() {
332 // silentScroll() is called whenever a page is shown to restore
333 // any previous scroll position the page may have had. We need to
334 // wait for the "silentscroll" event before setting focus to avoid
335 // the browser"s "feature" which offsets rendering to make sure
336 // whatever has focus is in view.
337 $( window ).one( "silentscroll", function() {
344 self.menuType = "page";
345 self.menuPageContent.append( self.list );
346 self.menuPage.find("div .ui-title").text(self.label.text());
347 $.mobile.changePage( self.menuPage, {
348 transition: $.mobile.defaultDialogTransition
351 self.menuType = "overlay";
353 self.screen.height( $(document).height() )
354 .removeClass( "ui-screen-hidden" );
356 // Try and center the overlay over the button
357 var roomtop = btnOffset - scrollTop,
358 roombot = scrollTop + screenHeight - btnOffset,
359 halfheight = menuHeight / 2,
360 maxwidth = parseFloat( self.list.parent().css( "max-width" ) ),
363 if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) {
364 newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight;
366 // 30px tolerance off the edges
367 newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30;
370 // If the menuwidth is smaller than the screen center is
371 if ( menuWidth < maxwidth ) {
372 newleft = ( screenWidth - menuWidth ) / 2;
375 //otherwise insure a >= 30px offset from the left
376 newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2;
378 // 30px tolerance off the edges
379 if ( newleft < 30 ) {
381 } else if ( (newleft + menuWidth) > screenWidth ) {
382 newleft = screenWidth - menuWidth - 30;
386 self.listbox.append( self.list )
387 .removeClass( "ui-selectmenu-hidden" )
396 // duplicate with value set in page show for dialog sized selects
401 _buildList: function() {
404 placeholder = this.placeholder,
407 dataIcon = self.isMultiple ? "checkbox-off" : "false";
409 self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
411 // Populate menu with options from select element
412 self.select.find( "option" ).each( function( i ) {
413 var $this = $( this ),
414 $parent = $this.parent(),
415 text = $this.getEncodedText(),
416 anchor = "<a href='#'>"+ text +"</a>",
420 // Are we inside an optgroup?
421 if ( $parent.is( "optgroup" ) ) {
422 var optLabel = $parent.attr( "label" );
424 // has this optgroup already been built yet?
425 if ( $.inArray( optLabel, optgroups ) === -1 ) {
426 lis.push( "<li data-" + $.mobile.ns + "role='list-divider'>"+ optLabel +"</li>" );
427 optgroups.push( optLabel );
431 // Find placeholder text
432 // TODO: Are you sure you want to use getAttribute? ^RW
433 if ( !this.getAttribute( "value" ) || text.length == 0 || $this.jqmData( "placeholder" ) ) {
434 if ( o.hidePlaceholderMenuItems ) {
435 classes.push( "ui-selectmenu-placeholder" );
437 placeholder = self.placeholder = text;
440 // support disabled option tags
441 if ( this.disabled ) {
442 classes.push( "ui-disabled" );
443 extraAttrs.push( "aria-disabled='true'" );
446 lis.push( "<li data-" + $.mobile.ns + "option-index='" + i + "' data-" + $.mobile.ns + "icon='"+ dataIcon +"' class='"+ classes.join(" ") + "' " + extraAttrs.join(" ") +">"+ anchor +"</li>" );
449 self.list.html( lis.join(" ") );
451 self.list.find( "li" )
452 .attr({ "role": "option", "tabindex": "-1" })
453 .first().attr( "tabindex", "0" );
455 // Hide header close link for single selects
456 if ( !this.isMultiple ) {
457 this.headerClose.hide();
460 // Hide header if it's not a multiselect and there's no placeholder
461 if ( !this.isMultiple && !placeholder.length ) {
464 this.headerTitle.text( this.placeholder );
467 // Now populated, create listview
468 self.list.listview();
475 // TODO value is undefined at creation
477 "aria-haspopup": "true",
479 // TODO value is undefined at creation
480 "aria-owns": this.menuId
486 $( "select" ).live( "selectmenubeforecreate", function(){
487 var selectmenuWidget = $( this ).data( "selectmenu" );
489 if( !selectmenuWidget.options.nativeMenu ){
490 extendSelect( selectmenuWidget );