2 * custom "selectmenu" plugin
5 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
6 //>>description: Extension to select menus to support menu styling, placeholder options, and multi-select features.
7 //>>label: Selects: Custom menus
9 //>>css: ../css/themes/default/jquery.mobile.theme.css, ../css/structure/jquery.mobile.forms.select.css
13 "./jquery.mobile.buttonMarkup",
14 "./jquery.mobile.core",
15 "./jquery.mobile.dialog",
16 "./jquery.mobile.forms.select",
17 "./jquery.mobile.listview",
18 "./jquery.mobile.page",
19 // NOTE expects ui content in the defined page, see selector for menuPageContent definition
20 "./jquery.mobile.page.sections" ], function( $ ) {
21 //>>excludeEnd("jqmBuildExclude");
22 (function( $, undefined ) {
23 var extendSelect = function( widget ){
25 var select = widget.select,
26 selectID = widget.selectID,
28 thisPage = widget.select.closest( ".ui-page" ),
29 screen = $( "<div>", {"class": "ui-selectmenu-screen ui-screen-hidden"} ).appendTo( thisPage ),
30 selectOptions = widget._selectOptions(),
31 isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
32 buttonId = selectID + "-button",
33 menuId = selectID + "-menu",
34 menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
35 "<div data-" + $.mobile.ns + "role='header'>" +
36 "<div class='ui-title'>" + label.getEncodedText() + "</div>"+
38 "<div data-" + $.mobile.ns + "role='content'></div>"+
41 listbox = $("<div>", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-" + widget.options.overlayTheme + " " + $.mobile.defaultDialogTransition } ).insertAfter(screen),
44 "class": "ui-selectmenu-list",
47 "aria-labelledby": buttonId
48 }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ),
50 header = $( "<div>", {
51 "class": "ui-header ui-bar-" + widget.options.theme
52 }).prependTo( listbox ),
54 headerTitle = $( "<h1>", {
56 }).appendTo( header ),
62 if( widget.isMultiple ) {
63 headerClose = $( "<a>", {
64 "text": widget.options.closeText,
66 "class": "ui-btn-left"
67 }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
71 select: widget.select,
79 selectOptions: selectOptions,
80 isMultiple: isMultiple,
81 theme: widget.options.theme,
85 headerTitle: headerTitle,
86 headerClose: headerClose,
87 menuPageContent: menuPageContent,
88 menuPageClose: menuPageClose,
94 // Create list from select, update state
97 self.select.attr( "tabindex", "-1" ).focus(function() {
103 self.button.bind( "vclick keydown" , function( event ) {
104 if ( event.type == "vclick" ||
105 event.keyCode && ( event.keyCode === $.mobile.keyCode.ENTER ||
106 event.keyCode === $.mobile.keyCode.SPACE ) ) {
109 event.preventDefault();
113 // Events for list items
114 self.list.attr( "role", "listbox" )
115 .bind( "focusin", function( e ){
117 .attr( "tabindex", "0" )
118 .trigger( "vmouseover" );
121 .bind( "focusout", function( e ){
123 .attr( "tabindex", "-1" )
124 .trigger( "vmouseout" );
126 .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
128 // index of option tag to be selected
129 var oldIndex = self.select[ 0 ].selectedIndex,
130 newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
131 option = self._selectOptions().eq( newIndex )[ 0 ];
133 // toggle selected status on the tag for multi selects
134 option.selected = self.isMultiple ? !option.selected : true;
136 // toggle checkbox class for multiple selects
137 if ( self.isMultiple ) {
138 $( this ).find( ".ui-icon" )
139 .toggleClass( "ui-icon-checkbox-on", option.selected )
140 .toggleClass( "ui-icon-checkbox-off", !option.selected );
143 // trigger change if value changed
144 if ( self.isMultiple || oldIndex !== newIndex ) {
145 self.select.trigger( "change" );
148 //hide custom select for single selects only
149 if ( !self.isMultiple ) {
153 event.preventDefault();
155 .keydown(function( event ) { //keyboard events for menu items
156 var target = $( event.target ),
157 li = target.closest( "li" ),
160 // switch logic based on which key was pressed
161 switch ( event.keyCode ) {
162 // up or left arrow keys
164 prev = li.prev().not( ".ui-selectmenu-placeholder" );
166 if( prev.is( ".ui-li-divider" ) ) {
170 // if there's a previous option, focus it
174 .attr( "tabindex", "-1" );
176 prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
182 // down or right arrow keys
186 if( next.is( ".ui-li-divider" ) ) {
190 // if there's a next option, focus it
194 .attr( "tabindex", "-1" );
196 next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
202 // If enter or space is pressed, trigger click
205 target.trigger( "click" );
212 // button refocus ensures proper height calculation
213 // by removing the inline style and ensuring page inclusion
214 self.menuPage.bind( "pagehide", function() {
215 self.list.appendTo( self.listbox );
218 // TODO centralize page removal binding / handling in the page plugin.
219 // Suggestion from @jblas to do refcounting
221 // TODO extremely confusing dependency on the open method where the pagehide.remove
222 // bindings are stripped to prevent the parent page from disappearing. The way
223 // we're keeping pages in the DOM right now sucks
225 // rebind the page remove that was unbound in the open function
226 // to allow for the parent page removal from actions other than the use
227 // of a dialog sized custom select
229 // doing this here provides for the back button on the custom select dialog
230 $.mobile._bindPageRemove.call( self.thisPage );
233 // Events on "screen" overlay
234 self.screen.bind( "vclick", function( event ) {
238 // Close button on small overlays
239 if( self.isMultiple ){
240 self.headerClose.click( function() {
241 if ( self.menuType == "overlay" ) {
248 // track this dependency so that when the parent page
249 // is removed on pagehide it will also remove the menupage
250 self.thisPage.addDependents( this.menuPage );
253 _isRebuildRequired: function() {
254 var list = this.list.find( "li" ),
255 options = this._selectOptions();
257 // TODO exceedingly naive method to determine difference
258 // ignores value changes etc in favor of a forcedRebuild
259 // from the user in the refresh method
260 return options.text() !== list.text();
263 refresh: function( forceRebuild , foo ){
265 select = this.element,
266 isMultiple = this.isMultiple,
267 options = this._selectOptions(),
268 selected = this.selected(),
269 // return an array of all selected index's
270 indicies = this.selectedIndices();
272 if ( forceRebuild || this._isRebuildRequired() ) {
276 self.setButtonText();
277 self.setButtonCount();
279 self.list.find( "li:not(.ui-li-divider)" )
280 .removeClass( $.mobile.activeBtnClass )
281 .attr( "aria-selected", false )
282 .each(function( i ) {
284 if ( $.inArray( i, indicies ) > -1 ) {
285 var item = $( this );
287 // Aria selected attr
288 item.attr( "aria-selected", true );
290 // Multiple selects: add the "on" checkbox state to the icon
291 if ( self.isMultiple ) {
292 item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
294 if( item.is( ".ui-selectmenu-placeholder" ) ) {
295 item.next().addClass( $.mobile.activeBtnClass );
297 item.addClass( $.mobile.activeBtnClass );
305 if ( this.options.disabled || !this.isOpen ) {
311 if ( self.menuType == "page" ) {
312 // doesn't solve the possible issue with calling change page
313 // where the objects don't define data urls which prevents dialog key
314 // stripping - changePage has incoming refactor
315 window.history.back();
317 self.screen.addClass( "ui-screen-hidden" );
318 self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass( "in" );
319 self.list.appendTo( self.listbox );
323 // allow the dialog to be closed again
328 if ( this.options.disabled ) {
333 $window = $( window ),
334 selfListParent = self.list.parent(),
335 menuHeight = selfListParent.outerHeight(),
336 menuWidth = selfListParent.outerWidth(),
337 activePage = $( ".ui-page-active" ),
338 tScrollElem = activePage,
339 scrollTop = $window.scrollTop(),
340 btnOffset = self.button.offset().top,
341 screenHeight = $window.height(),
342 screenWidth = $window.width();
344 //add active class to button
345 self.button.addClass( $.mobile.activeBtnClass );
348 setTimeout( function() {
349 self.button.removeClass( $.mobile.activeBtnClass );
352 function focusMenuItem() {
353 self.list.find( "." + $.mobile.activeBtnClass + " a" ).focus();
356 if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
358 self.menuPage.appendTo( $.mobile.pageContainer ).page();
359 self.menuPageContent = menuPage.find( ".ui-content" );
360 self.menuPageClose = menuPage.find( ".ui-header a" );
362 // prevent the parent page from being removed from the DOM,
363 // otherwise the results of selecting a list item in the dialog
364 // fall into a black hole
365 self.thisPage.unbind( "pagehide.remove" );
367 //for WebOS/Opera Mini (set lastscroll using button offset)
368 if ( scrollTop == 0 && btnOffset > screenHeight ) {
369 self.thisPage.one( "pagehide", function() {
370 $( this ).jqmData( "lastScroll", btnOffset );
374 self.menuPage.one( "pageshow", function() {
379 self.menuType = "page";
380 self.menuPageContent.append( self.list );
381 self.menuPage.find("div .ui-title").text(self.label.text());
382 $.mobile.changePage( self.menuPage, {
383 transition: $.mobile.defaultDialogTransition
386 self.menuType = "overlay";
388 self.screen.height( $(document).height() )
389 .removeClass( "ui-screen-hidden" );
391 // Try and center the overlay over the button
392 var roomtop = btnOffset - scrollTop,
393 roombot = scrollTop + screenHeight - btnOffset,
394 halfheight = menuHeight / 2,
395 maxwidth = parseFloat( self.list.parent().css( "max-width" ) ),
398 if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) {
399 newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight;
401 // 30px tolerance off the edges
402 newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30;
405 // If the menuwidth is smaller than the screen center is
406 if ( menuWidth < maxwidth ) {
407 newleft = ( screenWidth - menuWidth ) / 2;
410 //otherwise insure a >= 30px offset from the left
411 newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2;
413 // 30px tolerance off the edges
414 if ( newleft < 30 ) {
416 } else if ( (newleft + menuWidth) > screenWidth ) {
417 newleft = screenWidth - menuWidth - 30;
421 self.listbox.append( self.list )
422 .removeClass( "ui-selectmenu-hidden" )
431 // duplicate with value set in page show for dialog sized selects
436 _buildList: function() {
439 placeholder = this.placeholder,
440 needPlaceholder = true,
443 dataIcon = self.isMultiple ? "checkbox-off" : "false";
445 self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
447 var $options = self.select.find("option"),
448 numOptions = $options.length,
449 select = this.select[ 0 ],
450 dataPrefix = 'data-' + $.mobile.ns,
451 dataIndexAttr = dataPrefix + 'option-index',
452 dataIconAttr = dataPrefix + 'icon',
453 dataRoleAttr = dataPrefix + 'role',
454 fragment = document.createDocumentFragment(),
457 for (var i = 0; i < numOptions;i++){
458 var option = $options[i],
460 parent = option.parentNode,
461 text = $option.text(),
462 anchor = document.createElement('a'),
465 anchor.setAttribute('href','#');
466 anchor.appendChild(document.createTextNode(text));
468 // Are we inside an optgroup?
469 if (parent !== select && parent.nodeName.toLowerCase() === "optgroup"){
470 var optLabel = parent.getAttribute('label');
471 if ( optLabel != optGroup) {
472 var divider = document.createElement('li');
473 divider.setAttribute(dataRoleAttr,'list-divider');
474 divider.setAttribute('role','option');
475 divider.setAttribute('tabindex','-1');
476 divider.appendChild(document.createTextNode(optLabel));
477 fragment.appendChild(divider);
482 if (needPlaceholder && (!option.getAttribute( "value" ) || text.length == 0 || $option.jqmData( "placeholder" ))) {
483 needPlaceholder = false;
484 if ( o.hidePlaceholderMenuItems ) {
485 classes.push( "ui-selectmenu-placeholder" );
488 placeholder = self.placeholder = text;
492 var item = document.createElement('li');
493 if ( option.disabled ) {
494 classes.push( "ui-disabled" );
495 item.setAttribute('aria-disabled',true);
497 item.setAttribute(dataIndexAttr,i);
498 item.setAttribute(dataIconAttr,dataIcon);
499 item.className = classes.join(" ");
500 item.setAttribute('role','option');
501 anchor.setAttribute('tabindex','-1');
502 item.appendChild(anchor);
503 fragment.appendChild(item);
506 self.list[0].appendChild(fragment);
508 // Hide header if it's not a multiselect and there's no placeholder
509 if ( !this.isMultiple && !placeholder.length ) {
512 this.headerTitle.text( this.placeholder );
515 // Now populated, create listview
516 self.list.listview();
523 // TODO value is undefined at creation
525 "aria-haspopup": "true",
527 // TODO value is undefined at creation
528 "aria-owns": this.menuId
534 // issue #3894 - core doesn't triggered events on disabled delegates
535 $( document ).bind( "selectmenubeforecreate", function( event ){
536 var selectmenuWidget = $( event.target ).data( "selectmenu" );
538 if( !selectmenuWidget.options.nativeMenu ){
539 extendSelect( selectmenuWidget );
543 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
545 //>>excludeEnd("jqmBuildExclude");