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 selectOptions = widget._selectOptions(),
13 isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
14 buttonId = selectID + "-button",
15 menuId = selectID + "-menu",
16 menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
17 "<div data-" + $.mobile.ns + "role='header'>" +
18 "<div class='ui-title'>" + label.getEncodedText() + "</div>"+
20 "<div data-" + $.mobile.ns + "role='content'></div>"+
23 listbox = $( "<div>", { "class": "ui-selectmenu" } ).insertAfter( widget.select ).popup( { theme: "a" } ),
26 "class": "ui-selectmenu-list",
29 "aria-labelledby": buttonId
30 }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ),
32 header = $( "<div>", {
33 "class": "ui-header ui-bar-" + widget.options.theme
34 }).prependTo( listbox ),
36 headerTitle = $( "<h1>", {
38 }).appendTo( header ),
44 if ( widget.isMultiple ) {
45 headerClose = $( "<a>", {
46 "text": widget.options.closeText,
48 "class": "ui-btn-left"
49 }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
53 select: widget.select,
60 selectOptions: selectOptions,
61 isMultiple: isMultiple,
62 theme: widget.options.theme,
66 headerTitle: headerTitle,
67 headerClose: headerClose,
68 menuPageContent: menuPageContent,
69 menuPageClose: menuPageClose,
75 // Create list from select, update state
78 self.select.attr( "tabindex", "-1" ).focus(function() {
84 self.button.bind( "vclick keydown" , function( event ) {
85 if (event.type === "vclick" ||
86 event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER ||
87 event.keyCode === $.mobile.keyCode.SPACE)) {
90 event.preventDefault();
94 // Events for list items
95 self.list.attr( "role", "listbox" )
96 .bind( "focusin", function( e ) {
98 .attr( "tabindex", "0" )
99 .trigger( "vmouseover" );
102 .bind( "focusout", function( e ) {
104 .attr( "tabindex", "-1" )
105 .trigger( "vmouseout" );
107 .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
109 // index of option tag to be selected
110 var oldIndex = self.select[ 0 ].selectedIndex,
111 newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
112 option = self._selectOptions().eq( newIndex )[ 0 ];
114 // toggle selected status on the tag for multi selects
115 option.selected = self.isMultiple ? !option.selected : true;
117 // toggle checkbox class for multiple selects
118 if ( self.isMultiple ) {
119 $( this ).find( ".ui-icon" )
120 .toggleClass( "ui-icon-checkbox-on", option.selected )
121 .toggleClass( "ui-icon-checkbox-off", !option.selected );
124 // trigger change if value changed
125 if ( self.isMultiple || oldIndex !== newIndex ) {
126 self.select.trigger( "change" );
129 // hide custom select for single selects only - otherwise focus clicked item
130 // We need to grab the clicked item the hard way, because the list may have been rebuilt
131 if ( self.isMultiple ) {
132 self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex )
133 .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
139 event.preventDefault();
141 .keydown(function( event ) { //keyboard events for menu items
142 var target = $( event.target ),
143 li = target.closest( "li" ),
146 // switch logic based on which key was pressed
147 switch ( event.keyCode ) {
148 // up or left arrow keys
150 prev = li.prev().not( ".ui-selectmenu-placeholder" );
152 if ( prev.is( ".ui-li-divider" ) ) {
156 // if there's a previous option, focus it
160 .attr( "tabindex", "-1" );
162 prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
166 // down or right arrow keys
170 if ( next.is( ".ui-li-divider" ) ) {
174 // if there's a next option, focus it
178 .attr( "tabindex", "-1" );
180 next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
184 // If enter or space is pressed, trigger click
187 target.trigger( "click" );
193 // button refocus ensures proper height calculation
194 // by removing the inline style and ensuring page inclusion
195 self.menuPage.bind( "pagehide", function() {
196 self.list.appendTo( self.listbox );
199 // TODO centralize page removal binding / handling in the page plugin.
200 // Suggestion from @jblas to do refcounting
202 // TODO extremely confusing dependency on the open method where the pagehide.remove
203 // bindings are stripped to prevent the parent page from disappearing. The way
204 // we're keeping pages in the DOM right now sucks
206 // rebind the page remove that was unbound in the open function
207 // to allow for the parent page removal from actions other than the use
208 // of a dialog sized custom select
210 // doing this here provides for the back button on the custom select dialog
211 $.mobile._bindPageRemove.call( self.thisPage );
214 // Events on the popup
215 self.listbox.bind( "popupafterclose", function( event ) {
219 // Close button on small overlays
220 if ( self.isMultiple ) {
221 self.headerClose.click(function() {
222 if ( self.menuType === "overlay" ) {
229 // track this dependency so that when the parent page
230 // is removed on pagehide it will also remove the menupage
231 self.thisPage.addDependents( this.menuPage );
234 _isRebuildRequired: function() {
235 var list = this.list.find( "li" ),
236 options = this._selectOptions();
238 // TODO exceedingly naive method to determine difference
239 // ignores value changes etc in favor of a forcedRebuild
240 // from the user in the refresh method
241 return options.text() !== list.text();
244 selected: function() {
245 return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" );
248 refresh: function( forceRebuild , foo ) {
250 select = this.element,
251 isMultiple = this.isMultiple,
254 if ( forceRebuild || this._isRebuildRequired() ) {
258 indicies = this.selectedIndices();
260 self.setButtonText();
261 self.setButtonCount();
263 self.list.find( "li:not(.ui-li-divider)" )
264 .removeClass( $.mobile.activeBtnClass )
265 .attr( "aria-selected", false )
266 .each(function( i ) {
268 if ( $.inArray( i, indicies ) > -1 ) {
269 var item = $( this );
271 // Aria selected attr
272 item.attr( "aria-selected", true );
274 // Multiple selects: add the "on" checkbox state to the icon
275 if ( self.isMultiple ) {
276 item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
278 if ( item.is( ".ui-selectmenu-placeholder" ) ) {
279 item.next().addClass( $.mobile.activeBtnClass );
281 item.addClass( $.mobile.activeBtnClass );
289 if ( this.options.disabled || !this.isOpen ) {
295 if ( self.menuType === "page" ) {
296 // doesn't solve the possible issue with calling change page
297 // where the objects don't define data urls which prevents dialog key
298 // stripping - changePage has incoming refactor
301 self.listbox.popup( "close" );
302 self.list.appendTo( self.listbox );
306 // allow the dialog to be closed again
311 if ( this.options.disabled ) {
316 $window = $.mobile.$window,
317 selfListParent = self.list.parent(),
318 menuHeight = selfListParent.outerHeight(),
319 menuWidth = selfListParent.outerWidth(),
320 activePage = $( "." + $.mobile.activePageClass ),
321 scrollTop = $window.scrollTop(),
322 btnOffset = self.button.offset().top,
323 screenHeight = $window.height(),
324 screenWidth = $window.width();
326 //add active class to button
327 self.button.addClass( $.mobile.activeBtnClass );
330 setTimeout( function() {
331 self.button.removeClass( $.mobile.activeBtnClass );
334 function focusMenuItem() {
335 var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" );
336 if ( selector.length === 0 ) {
337 selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" );
339 selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme );
342 if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
344 self.menuPage.appendTo( $.mobile.pageContainer ).page();
345 self.menuPageContent = menuPage.find( ".ui-content" );
346 self.menuPageClose = menuPage.find( ".ui-header a" );
348 // prevent the parent page from being removed from the DOM,
349 // otherwise the results of selecting a list item in the dialog
350 // fall into a black hole
351 self.thisPage.unbind( "pagehide.remove" );
353 //for WebOS/Opera Mini (set lastscroll using button offset)
354 if ( scrollTop === 0 && btnOffset > screenHeight ) {
355 self.thisPage.one( "pagehide", function() {
356 $( this ).jqmData( "lastScroll", btnOffset );
360 self.menuPage.one( "pageshow", function() {
365 self.menuType = "page";
366 self.menuPageContent.append( self.list );
367 self.menuPage.find("div .ui-title").text(self.label.text());
368 $.mobile.changePage( self.menuPage, {
369 transition: $.mobile.defaultDialogTransition
372 self.menuType = "overlay";
375 .one( "popupafteropen", focusMenuItem )
377 x: self.button.offset().left + self.button.outerWidth() / 2,
378 y: self.button.offset().top + self.button.outerHeight() / 2
381 // duplicate with value set in page show for dialog sized selects
386 _buildList: function() {
389 placeholder = this.placeholder,
390 needPlaceholder = true,
393 dataIcon = self.isMultiple ? "checkbox-off" : "false";
395 self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
397 var $options = self.select.find( "option" ),
398 numOptions = $options.length,
399 select = this.select[ 0 ],
400 dataPrefix = 'data-' + $.mobile.ns,
401 dataIndexAttr = dataPrefix + 'option-index',
402 dataIconAttr = dataPrefix + 'icon',
403 dataRoleAttr = dataPrefix + 'role',
404 dataPlaceholderAttr = dataPrefix + 'placeholder',
405 fragment = document.createDocumentFragment(),
406 isPlaceholderItem = false,
409 for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) {
410 var option = $options[i],
411 $option = $( option ),
412 parent = option.parentNode,
413 text = $option.text(),
414 anchor = document.createElement( 'a' ),
417 anchor.setAttribute( 'href', '#' );
418 anchor.appendChild( document.createTextNode( text ) );
420 // Are we inside an optgroup?
421 if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) {
422 var optLabel = parent.getAttribute( 'label' );
423 if ( optLabel !== optGroup ) {
424 var divider = document.createElement( 'li' );
425 divider.setAttribute( dataRoleAttr, 'list-divider' );
426 divider.setAttribute( 'role', 'option' );
427 divider.setAttribute( 'tabindex', '-1' );
428 divider.appendChild( document.createTextNode( optLabel ) );
429 fragment.appendChild( divider );
434 if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) {
435 needPlaceholder = false;
436 isPlaceholderItem = true;
438 // If we have identified a placeholder, mark it retroactively in the select as well
439 option.setAttribute( dataPlaceholderAttr, true );
440 if ( o.hidePlaceholderMenuItems ) {
441 classes.push( "ui-selectmenu-placeholder" );
444 placeholder = self.placeholder = text;
448 var item = document.createElement('li');
449 if ( option.disabled ) {
450 classes.push( "ui-disabled" );
451 item.setAttribute('aria-disabled',true);
453 item.setAttribute( dataIndexAttr,i );
454 item.setAttribute( dataIconAttr, dataIcon );
455 if ( isPlaceholderItem ) {
456 item.setAttribute( dataPlaceholderAttr, true );
458 item.className = classes.join( " " );
459 item.setAttribute( 'role', 'option' );
460 anchor.setAttribute( 'tabindex', '-1' );
461 item.appendChild( anchor );
462 fragment.appendChild( item );
465 self.list[0].appendChild( fragment );
467 // Hide header if it's not a multiselect and there's no placeholder
468 if ( !this.isMultiple && !placeholder.length ) {
471 this.headerTitle.text( this.placeholder );
474 // Now populated, create listview
475 self.list.listview();
478 _button: function() {
482 // TODO value is undefined at creation
484 "aria-haspopup": "true",
486 // TODO value is undefined at creation
487 "aria-owns": this.menuId
493 // issue #3894 - core doesn't trigger events on disabled delegates
494 $.mobile.$document.bind( "selectmenubeforecreate", function( event ) {
495 var selectmenuWidget = $( event.target ).data( "selectmenu" );
497 if ( !selectmenuWidget.options.nativeMenu &&
498 selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) {
499 extendSelect( selectmenuWidget );