diff --git a/Gemfile.lock b/Gemfile.lock index 7ae11c4..041f144 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - jquery_mobile_rails (1.3.0) + jquery_mobile_rails (1.3.1) railties (>= 3.1.0) GEM diff --git a/README.rdoc b/README.rdoc index 3ffdaba..a7afd29 100644 --- a/README.rdoc +++ b/README.rdoc @@ -6,7 +6,7 @@ This gem adds the JQuery Mobile files to the rails assets pipeline. ==== Gem's JQuery Mobile Version -1.3.0 (gem 1.3.0) +1.3.1 (gem 1.3.1) === Instalation diff --git a/lib/jquery_mobile_rails/version.rb b/lib/jquery_mobile_rails/version.rb index 57f519f..568ceb9 100644 --- a/lib/jquery_mobile_rails/version.rb +++ b/lib/jquery_mobile_rails/version.rb @@ -1,3 +1,3 @@ module JqueryMobileRails - VERSION = "1.3.0" + VERSION = "1.3.1" end diff --git a/vendor/assets/javascripts/jquery.mobile.js b/vendor/assets/javascripts/jquery.mobile.js index bbe96bc..6708d34 100644 --- a/vendor/assets/javascripts/jquery.mobile.js +++ b/vendor/assets/javascripts/jquery.mobile.js @@ -1,5 +1,6 @@ /* -* jQuery Mobile Git Build: SHA1: caa77b258660731d663844fe7867aa2c3a107ab1 <> Date: Wed Feb 20 15:03:27 2013 -0500 +* jQuery Mobile 1.3.1 +* Git HEAD hash: 74b4bec049fd93e4fe40205e6157de16eb64eb46 <> Date: Wed Apr 10 2013 21:57:23 UTC * http://jquerymobile.com * * Copyright 2010, 2013 jQuery Foundation, Inc. and other contributors @@ -25,14 +26,13 @@ $.mobile = {}; }( jQuery )); (function( $, window, undefined ) { - var nsNormalizeDict = {}; // jQuery.mobile configurable options $.mobile = $.extend($.mobile, { // Version of the jQuery Mobile Framework - version: "1.3.0", + version: "1.3.1", // Namespace used framework-wide for data-attrs. Default is no namespace ns: "", @@ -323,7 +323,7 @@ }; // note that this helper doesn't attempt to handle the callback - // or setting of an html elements text, its only purpose is + // or setting of an html element's text, its only purpose is // to return the html encoded version of the text in all cases. (thus the name) $.fn.getEncodedText = function() { return $( "
" ).text( $( this ).text() ).html(); @@ -1746,6 +1746,8 @@ $.extend( $.support, { // https://developers.google.com/chrome/mobile/docs/user-agent#chrome_for_ios_user-agent pushState: "pushState" in history && "replaceState" in history && + // When running inside a FF iframe, calling replaceState causes an error + !( window.navigator.userAgent.indexOf( "Firefox" ) >= 0 && window.top !== window ) && ( window.navigator.userAgent.search(/CriOS/) === -1 ), mediaquery: $.mobile.media( "only all" ), @@ -2381,18 +2383,13 @@ if ( !$.support.boxShadow ) { (function( $, undefined ) { - var path = $.mobile.path; + var path = $.mobile.path, + initialHref = location.href; $.mobile.Navigator = function( history ) { this.history = history; this.ignoreInitialHashChange = true; - // This ensures that browsers which don't fire the initial popstate - // like opera don't have further hash assignment popstates blocked - setTimeout($.proxy(function() { - this.ignoreInitialHashChange = false; - }, this), 200); - $.mobile.window.bind({ "popstate.history": $.proxy( this.popstate, this ), "hashchange.history": $.proxy( this.hashchange, this ) @@ -2555,13 +2552,18 @@ if ( !$.support.boxShadow ) { // If there is no state, and the history stack length is one were // probably getting the page load popstate fired by browsers like chrome - // avoid it and set the one time flag to false + // avoid it and set the one time flag to false. + // TODO: Do we really need all these conditions? Comparing location hrefs + // should be sufficient. if( !event.originalEvent.state && this.history.stack.length === 1 && this.ignoreInitialHashChange ) { this.ignoreInitialHashChange = false; - return; + if ( location.href === initialHref ) { + event.preventDefault(); + return; + } } // account for direct manipulation of the hash. That is, we will receive a popstate @@ -4209,6 +4211,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau .jqmData( "url", dataUrl ); } + // If we failed to find a page in the DOM, check the URL to see if it // refers to the first page in the application. If it isn't a reference // to the first page and refers to non-existent embedded page, error out. @@ -4230,7 +4233,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau return deferred.promise(); } } - + // If the page we are interested in is already in the DOM, // and the caller did not indicate that we should force a // reload of the file, we are done. Otherwise, track the @@ -4239,11 +4242,14 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau if ( !settings.reloadPage ) { enhancePage( page, settings.role ); deferred.resolve( absUrl, options, page ); + //if we are reloading the page make sure we update the base if its not a prefetch + if( base && !options.prefetch ){ + base.set(url); + } return deferred.promise(); } dupCachedPage = page; } - var mpc = settings.pageContainer, pblEvent = new $.Event( "pagebeforeload" ), triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings }; @@ -4273,9 +4279,9 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau $.mobile.hidePageLoadingMsg(); }; } - // Reset base to the default document base. - if ( base ) { + // only reset if we are not prefetching + if ( base && typeof options.prefetch === "undefined" ) { base.reset(); } @@ -4287,6 +4293,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau url: fileUrl, type: settings.type, data: settings.data, + contentType: settings.contentType, dataType: "html", success: function( html, textStatus, xhr ) { //pre-parse html to check for a data-url, @@ -4309,8 +4316,8 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau RegExp.$1 ) { url = fileUrl = path.getFilePath( $( "
" + RegExp.$1 + "
" ).text() ); } - - if ( base ) { + //dont update the base tag if we are prefetching + if ( base && typeof options.prefetch === "undefined") { base.set( fileUrl ); } @@ -4521,7 +4528,6 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau $.mobile.changePage( newPage, options ); }) .fail(function( url, options ) { - isPageTransitioning = false; //clear out the active button state removeActiveLinkClass( true ); @@ -4647,7 +4653,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau // if title element wasn't found, try the page div data attr too // If this is a deep-link or a reload ( active === undefined ) then just use pageTitle - var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText(); + var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).text(); if ( !!newPageTitle && pageTitle === document.title ) { pageTitle = newPageTitle; } @@ -4773,18 +4779,20 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau $.mobile.navreadyDeferred = $.Deferred(); $.mobile._registerInternalEvents = function() { var getAjaxFormData = function( $form, calculateOnly ) { - var type, target, url, ret = true, formData, vclickedName; + var url, ret = true, formData, vclickedName, method; + if ( !$.mobile.ajaxEnabled || // test that the form is, itself, ajax false $form.is( ":jqmData(ajax='false')" ) || // test that $.mobile.ignoreContentEnabled is set and // the form or one of it's parents is ajax=false - !$form.jqmHijackable().length ) { + !$form.jqmHijackable().length || + $form.attr( "target" ) ) { return false; } - target = $form.attr( "target" ); url = $form.attr( "action" ); + method = ( $form.attr( "method" ) || "get" ).toLowerCase(); // If no action is specified, browsers default to using the // URL of the document containing the form. Since we dynamically @@ -4794,6 +4802,13 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau if ( !url ) { // Get the @data-url for the page containing the form. url = getClosestBaseUrl( $form ); + + // NOTE: If the method is "get", we need to strip off the query string + // because it will get replaced with the new form data. See issue #5710. + if ( method === "get" ) { + url = path.parseUrl( url ).hrefNoSearch; + } + if ( url === documentBase.hrefNoHash ) { // The url we got back matches the document base, // which means the page must be an internal/embedded page, @@ -4805,12 +4820,11 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau url = path.makeUrlAbsolute( url, getClosestBaseUrl( $form ) ); - if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) { + if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) ) { return false; } if ( !calculateOnly ) { - type = $form.attr( "method" ); formData = $form.serializeArray(); if ( $lastVClicked && $lastVClicked[ 0 ].form === $form[ 0 ] ) { @@ -4833,7 +4847,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau ret = { url: url, options: { - type: type && type.length && type.toLowerCase() || "get", + type: method, data: $.param( formData ), transition: $form.jqmData( "transition" ), reverse: $form.jqmData( "direction" ) === "reverse", @@ -5030,7 +5044,7 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau if ( url && $.inArray( url, urls ) === -1 ) { urls.push( url ); - $.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ) } ); + $.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ),prefetch: true } ); } }); }); @@ -5113,7 +5127,13 @@ $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defau // TODO roll the logic here into the handleHashChange method $window.bind( "navigate", function( e, data ) { - var url = $.event.special.navigate.originalEventName.indexOf( "hashchange" ) > -1 ? data.state.hash : data.state.url; + var url; + + if ( e.originalEvent && e.originalEvent.isDefaultPrevented() ) { + return; + } + + url = $.event.special.navigate.originalEventName.indexOf( "hashchange" ) > -1 ? data.state.hash : data.state.url; if( !url ) { url = $.mobile.path.parseLocation().hash; @@ -5365,9 +5385,8 @@ $.widget( "mobile.dialog", $.mobile.widget, { _setOption: function( key, value ) { if ( key === "closeBtn" ) { this._setCloseBtn( value ); - this._super( key, value ); - this.element.attr( "data-" + ( $.mobile.ns || "" ) + "close-btn", value ); } + this._super( key, value ); }, // Close method goes back in history @@ -5496,124 +5515,6 @@ $.mobile.document.bind( "pagecreate", function( e ) { (function( $, undefined ) { -$.mobile.behaviors.addFirstLastClasses = { - _getVisibles: function( $els, create ) { - var visibles; - - if ( create ) { - visibles = $els.not( ".ui-screen-hidden" ); - } else { - visibles = $els.filter( ":visible" ); - if ( visibles.length === 0 ) { - visibles = $els.not( ".ui-screen-hidden" ); - } - } - - return visibles; - }, - - _addFirstLastClasses: function( $els, $visibles, create ) { - $els.removeClass( "ui-first-child ui-last-child" ); - $visibles.eq( 0 ).addClass( "ui-first-child" ).end().last().addClass( "ui-last-child" ); - if ( !create ) { - this.element.trigger( "updatelayout" ); - } - } -}; - -})( jQuery ); - -(function( $, undefined ) { - -// filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text) -$.fn.fieldcontain = function( options ) { - return this - .addClass( "ui-field-contain ui-body ui-br" ) - .contents().filter( function() { - return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) ); - }).remove(); -}; - -//auto self-init widgets -$( document ).bind( "pagecreate create", function( e ) { - $( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain(); -}); - -})( jQuery ); - -(function( $, undefined ) { - -$.fn.grid = function( options ) { - return this.each(function() { - - var $this = $( this ), - o = $.extend({ - grid: null - }, options ), - $kids = $this.children(), - gridCols = { solo:1, a:2, b:3, c:4, d:5 }, - grid = o.grid, - iterator; - - if ( !grid ) { - if ( $kids.length <= 5 ) { - for ( var letter in gridCols ) { - if ( gridCols[ letter ] === $kids.length ) { - grid = letter; - } - } - } else { - grid = "a"; - $this.addClass( "ui-grid-duo" ); - } - } - iterator = gridCols[grid]; - - $this.addClass( "ui-grid-" + grid ); - - $kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" ); - - if ( iterator > 1 ) { - $kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" ); - } - if ( iterator > 2 ) { - $kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" ); - } - if ( iterator > 3 ) { - $kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" ); - } - if ( iterator > 4 ) { - $kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" ); - } - }); -}; -})( jQuery ); - -(function( $, undefined ) { - -$( document ).bind( "pagecreate create", function( e ) { - $( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" ); - -}); - -})( jQuery ); - -(function( $, undefined ) { - -$.mobile.behaviors.formReset = { - _handleFormReset: function() { - this._on( this.element.closest( "form" ), { - reset: function() { - this._delay( "_reset" ); - } - }); - } -}; - -})( jQuery ); - -(function( $, undefined ) { - // This function calls getAttribute, which should be safe for data-* attributes var getAttrFixed = function( e, key ) { var value = e.getAttribute( key ); @@ -5657,7 +5558,11 @@ $.fn.buttonMarkup = function( options ) { buttonElements; for ( key in o ) { - e.setAttribute( nsKey + key, o[ key ] ); + if ( o[ key ] === undefined || o[ key ] === null ) { + el.removeAttr( nsKey + key ); + } else { + e.setAttribute( nsKey + key, o[ key ] ); + } } if ( getAttrFixed( e, nsKey + "rel" ) === "popup" && el.attr( "href" ) ) { @@ -5729,10 +5634,6 @@ $.fn.buttonMarkup = function( options ) { } } - if ( o.iconpos && o.iconpos === "notext" && !el.attr( "title" ) ) { - el.attr( "title", el.getEncodedText() ); - } - if ( buttonElements ) { el.removeClass( buttonElements.bcls || "" ); } @@ -6050,7 +5951,36 @@ $.mobile.document.bind( "pagecreate create", function( e ) { (function( $, undefined ) { -$.widget( "mobile.collapsibleset", $.mobile.widget, { +$.mobile.behaviors.addFirstLastClasses = { + _getVisibles: function( $els, create ) { + var visibles; + + if ( create ) { + visibles = $els.not( ".ui-screen-hidden" ); + } else { + visibles = $els.filter( ":visible" ); + if ( visibles.length === 0 ) { + visibles = $els.not( ".ui-screen-hidden" ); + } + } + + return visibles; + }, + + _addFirstLastClasses: function( $els, $visibles, create ) { + $els.removeClass( "ui-first-child ui-last-child" ); + $visibles.eq( 0 ).addClass( "ui-first-child" ).end().last().addClass( "ui-last-child" ); + if ( !create ) { + this.element.trigger( "updatelayout" ); + } + } +}; + +})( jQuery ); + +(function( $, undefined ) { + +$.widget( "mobile.collapsibleset", $.mobile.widget, $.extend( { options: { initSelector: ":jqmData(role='collapsible-set')" }, @@ -6119,9 +6049,7 @@ $.widget( "mobile.collapsibleset", $.mobile.widget, { refresh: function() { this._refresh( false ); } -}); - -$.widget( "mobile.collapsibleset", $.mobile.collapsibleset, $.mobile.behaviors.addFirstLastClasses ); +}, $.mobile.behaviors.addFirstLastClasses ) ); //auto self-init widgets $.mobile.document.bind( "pagecreate create", function( e ) { @@ -6132,6 +6060,72 @@ $.mobile.document.bind( "pagecreate create", function( e ) { (function( $, undefined ) { +// filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text) +$.fn.fieldcontain = function( options ) { + return this + .addClass( "ui-field-contain ui-body ui-br" ) + .contents().filter( function() { + return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) ); + }).remove(); +}; + +//auto self-init widgets +$( document ).bind( "pagecreate create", function( e ) { + $( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain(); +}); + +})( jQuery ); + +(function( $, undefined ) { + +$.fn.grid = function( options ) { + return this.each(function() { + + var $this = $( this ), + o = $.extend({ + grid: null + }, options ), + $kids = $this.children(), + gridCols = { solo:1, a:2, b:3, c:4, d:5 }, + grid = o.grid, + iterator; + + if ( !grid ) { + if ( $kids.length <= 5 ) { + for ( var letter in gridCols ) { + if ( gridCols[ letter ] === $kids.length ) { + grid = letter; + } + } + } else { + grid = "a"; + $this.addClass( "ui-grid-duo" ); + } + } + iterator = gridCols[grid]; + + $this.addClass( "ui-grid-" + grid ); + + $kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" ); + + if ( iterator > 1 ) { + $kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" ); + } + if ( iterator > 2 ) { + $kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" ); + } + if ( iterator > 3 ) { + $kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" ); + } + if ( iterator > 4 ) { + $kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" ); + } + }); +}; +})( jQuery ); + +(function( $, undefined ) { + $.widget( "mobile.navbar", $.mobile.widget, { options: { iconpos: "top", @@ -6160,13 +6154,18 @@ $.widget( "mobile.navbar", $.mobile.widget, { }); $navbar.delegate( "a", "vclick", function( event ) { - if ( !$(event.target).hasClass( "ui-disabled" ) ) { + // ui-btn-inner is returned as target + var target = $( event.target ).is( "a" ) ? $( this ) : $( this ).parent( "a" ); + + if ( !target.is( ".ui-disabled, .ui-btn-active" ) ) { $navbtns.removeClass( $.mobile.activeBtnClass ); $( this ).addClass( $.mobile.activeBtnClass ); - // The code below is a workaround to fix #1181. We have to see why removeActiveLinkClass() doesn't take care of it. - var activeNavbtn = $( this ); - $( document ).one( "pagechange", function( event ) { - activeNavbtn.removeClass( $.mobile.activeBtnClass ); + + // The code below is a workaround to fix #1181 + var activeBtn = $( this ); + + $( document ).one( "pagehide", function() { + activeBtn.removeClass( $.mobile.activeBtnClass ); }); } }); @@ -6192,7 +6191,7 @@ $.mobile.document.bind( "pagecreate create", function( e ) { //https://github.com/jquery/jquery-mobile/issues/1617 var listCountPerPage = {}; -$.widget( "mobile.listview", $.mobile.widget, { +$.widget( "mobile.listview", $.mobile.widget, $.extend( { options: { theme: null, @@ -6556,9 +6555,7 @@ $.widget( "mobile.listview", $.mobile.widget, { return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" ); } -}); - -$.widget( "mobile.listview", $.mobile.listview, $.mobile.behaviors.addFirstLastClasses ); +}, $.mobile.behaviors.addFirstLastClasses ) ); //auto self-init widgets $.mobile.document.bind( "pagecreate create", function( e ) { @@ -6567,4331 +6564,4433 @@ $.mobile.document.bind( "pagecreate create", function( e ) { })( jQuery ); -(function( $, undefined ) { +(function( $ ) { + var meta = $( "meta[name=viewport]" ), + initialContent = meta.attr( "content" ), + disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no", + enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes", + disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent ); -$.mobile.listview.prototype.options.autodividers = false; -$.mobile.listview.prototype.options.autodividersSelector = function( elt ) { - // look for the text in the given element - var text = $.trim( elt.text() ) || null; - - if ( !text ) { - return null; - } - - // create the text for the divider (first uppercased letter) - text = text.slice( 0, 1 ).toUpperCase(); - - return text; -}; - -$.mobile.document.delegate( "ul,ol", "listviewcreate", function() { - - var list = $( this ), - listview = list.data( "mobile-listview" ); - - if ( !listview || !listview.options.autodividers ) { - return; - } - - var replaceDividers = function () { - list.find( "li:jqmData(role='list-divider')" ).remove(); - - var lis = list.find( 'li' ), - lastDividerText = null, li, dividerText; - - for ( var i = 0; i < lis.length ; i++ ) { - li = lis[i]; - dividerText = listview.options.autodividersSelector( $( li ) ); - - if ( dividerText && lastDividerText !== dividerText ) { - var divider = document.createElement( 'li' ); - divider.appendChild( document.createTextNode( dividerText ) ); - divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' ); - li.parentNode.insertBefore( divider, li ); + $.mobile.zoom = $.extend( {}, { + enabled: !disabledInitially, + locked: false, + disable: function( lock ) { + if ( !disabledInitially && !$.mobile.zoom.locked ) { + meta.attr( "content", disabledZoom ); + $.mobile.zoom.enabled = false; + $.mobile.zoom.locked = lock || false; + } + }, + enable: function( unlock ) { + if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) { + meta.attr( "content", enabledZoom ); + $.mobile.zoom.enabled = true; + $.mobile.zoom.locked = false; + } + }, + restore: function() { + if ( !disabledInitially ) { + meta.attr( "content", initialContent ); + $.mobile.zoom.enabled = true; } - - lastDividerText = dividerText; } - }; - - var afterListviewRefresh = function () { - list.unbind( 'listviewafterrefresh', afterListviewRefresh ); - replaceDividers(); - listview.refresh(); - list.bind( 'listviewafterrefresh', afterListviewRefresh ); - }; - - afterListviewRefresh(); -}); - -})( jQuery ); + }); -/* -* "checkboxradio" plugin -*/ +}( jQuery )); (function( $, undefined ) { -$.widget( "mobile.checkboxradio", $.mobile.widget, { +$.widget( "mobile.textinput", $.mobile.widget, { options: { theme: null, mini: false, - initSelector: "input[type='checkbox'],input[type='radio']" + // This option defaults to true on iOS devices. + preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, + initSelector: "input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type]), input[type='file']", + clearBtn: false, + clearSearchButtonText: null, //deprecating for 1.3... + clearBtnText: "clear text", + disabled: false }, + _create: function() { + var self = this, input = this.element, o = this.options, - inheritAttr = function( input, dataAttr ) { - return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr ); - }, - // NOTE: Windows Phone could not find the label through a selector - // filter works though. - parentLabel = $( input ).closest( "label" ), - label = parentLabel.length ? parentLabel : $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ).first(), - inputtype = input[0].type, - mini = inheritAttr( input, "mini" ) || o.mini, - checkedState = inputtype + "-on", - uncheckedState = inputtype + "-off", - iconpos = inheritAttr( input, "iconpos" ), - checkedClass = "ui-" + checkedState, - uncheckedClass = "ui-" + uncheckedState; + theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ), + themeclass = " ui-body-" + theme, + miniclass = o.mini ? " ui-mini" : "", + isSearch = input.is( "[type='search'], :jqmData(type='search')" ), + focusedEl, + clearbtn, + clearBtnText = o.clearSearchButtonText || o.clearBtnText, + clearBtnBlacklist = input.is( "textarea, :jqmData(type='range')" ), + inputNeedsClearBtn = !!o.clearBtn && !clearBtnBlacklist, + inputNeedsWrap = input.is( "input" ) && !input.is( ":jqmData(type='range')" ); - if ( inputtype !== "checkbox" && inputtype !== "radio" ) { - return; + function toggleClear() { + setTimeout( function() { + clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); + }, 0 ); } - // Expose for other methods - $.extend( this, { - label: label, - inputtype: inputtype, - checkedClass: checkedClass, - uncheckedClass: uncheckedClass, - checkedicon: checkedState, - uncheckedicon: uncheckedState - }); + $( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" ); - // If there's no selected theme check the data attr - if ( !o.theme ) { - o.theme = $.mobile.getInheritedTheme( this.element, "c" ); + focusedEl = input.addClass( "ui-input-text ui-body-"+ theme ); + + // XXX: Temporary workaround for issue 785 (Apple bug 8910589). + // Turn off autocorrect and autocomplete on non-iOS 5 devices + // since the popup they use can't be dismissed by the user. Note + // that we test for the presence of the feature by looking for + // the autocorrect property on the input element. We currently + // have no test for iOS 5 or newer so we're temporarily using + // the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas + if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) { + // Set the attribute instead of the property just in case there + // is code that attempts to make modifications via HTML. + input[0].setAttribute( "autocorrect", "off" ); + input[0].setAttribute( "autocomplete", "off" ); } - label.buttonMarkup({ - theme: o.theme, - icon: uncheckedState, - shadow: false, - mini: mini, - iconpos: iconpos - }); + //"search" and "text" input widgets + if ( isSearch ) { + focusedEl = input.wrap( "" ).parent(); + } else if ( inputNeedsWrap ) { + focusedEl = input.wrap( "
" ).parent(); + } - // Wrap the input + label in a div - var wrapper = document.createElement('div'); - wrapper.className = 'ui-' + inputtype; + if( inputNeedsClearBtn || isSearch ) { + clearbtn = $( "" + clearBtnText + "" ) + .bind( "click", function( event ) { + input + .val( "" ) + .focus() + .trigger( "change" ); + clearbtn.addClass( "ui-input-clear-hidden" ); + event.preventDefault(); + }) + .appendTo( focusedEl ) + .buttonMarkup({ + icon: "delete", + iconpos: "notext", + corners: true, + shadow: true, + mini: o.mini + }); + + if ( !isSearch ) { + focusedEl.addClass( "ui-input-has-clear" ); + } - input.add( label ).wrapAll( wrapper ); + toggleClear(); - label.bind({ - vmouseover: function( event ) { - if ( $( this ).parent().is( ".ui-disabled" ) ) { - event.stopPropagation(); - } - }, + input.bind( "paste cut keyup input focus change blur", toggleClear ); + } + else if ( !inputNeedsWrap && !isSearch ) { + input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass ); + } - vclick: function( event ) { - if ( input.is( ":disabled" ) ) { - event.preventDefault(); - return; - } + input.focus(function() { + // In many situations, iOS will zoom into the input upon tap, this prevents that from happening + if ( o.preventFocusZoom ) { + $.mobile.zoom.disable( true ); + } + focusedEl.addClass( $.mobile.focusClass ); + }) + .blur(function() { + focusedEl.removeClass( $.mobile.focusClass ); + if ( o.preventFocusZoom ) { + $.mobile.zoom.enable( true ); + } + }); - self._cacheVals(); + // Autogrow + if ( input.is( "textarea" ) ) { + var extraLineHeight = 15, + keyupTimeoutBuffer = 100, + keyupTimeout; - input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) ); + this._keyup = function() { + var scrollHeight = input[ 0 ].scrollHeight, + clientHeight = input[ 0 ].clientHeight; - // trigger click handler's bound directly to the input as a substitute for - // how label clicks behave normally in the browsers - // TODO: it would be nice to let the browser's handle the clicks and pass them - // through to the associate input. we can swallow that click at the parent - // wrapper element level - input.triggerHandler( 'click' ); + if ( clientHeight < scrollHeight ) { + var paddingTop = parseFloat( input.css( "padding-top" ) ), + paddingBottom = parseFloat( input.css( "padding-bottom" ) ), + paddingHeight = paddingTop + paddingBottom; + + input.height( scrollHeight - paddingHeight + extraLineHeight ); + } + }; - // Input set for common radio buttons will contain all the radio - // buttons, but will not for checkboxes. clearing the checked status - // of other radios ensures the active button state is applied properly - self._getInputSet().not( input ).prop( "checked", false ); + input.on( "keyup change input paste", function() { + clearTimeout( keyupTimeout ); + keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer ); + }); - self._updateAll(); - return false; - } - }); + // binding to pagechange here ensures that for pages loaded via + // ajax the height is recalculated without user input + this._on( true, $.mobile.document, { "pagechange": "_keyup" }); - input - .bind({ - vmousedown: function() { - self._cacheVals(); - }, - - vclick: function() { - var $this = $( this ); - - // Adds checked attribute to checked input when keyboard is used - if ( $this.is( ":checked" ) ) { - - $this.prop( "checked", true); - self._getInputSet().not( $this ).prop( "checked", false ); - } else { - - $this.prop( "checked", false ); - } - - self._updateAll(); - }, - - focus: function() { - label.addClass( $.mobile.focusClass ); - }, - - blur: function() { - label.removeClass( $.mobile.focusClass ); - } - }); - - if ( this._handleFormReset ) { - this._handleFormReset(); - } - this.refresh(); - }, - - _cacheVals: function() { - this._getInputSet().each(function() { - $( this ).jqmData( "cacheVal", this.checked ); - }); - }, - - //returns either a set of radios with the same name attribute, or a single checkbox - _getInputSet: function() { - if ( this.inputtype === "checkbox" ) { - return this.element; - } - - return this.element.closest( "form, :jqmData(role='page'), :jqmData(role='dialog')" ) - .find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" ); - }, - - _updateAll: function() { - var self = this; - - this._getInputSet().each(function() { - var $this = $( this ); - - if ( this.checked || self.inputtype === "checkbox" ) { - $this.trigger( "change" ); + // Issue 509: the browser is not providing scrollHeight properly until the styles load + if ( $.trim( input.val() ) ) { + // bind to the window load to make sure the height is calculated based on BOTH + // the DOM and CSS + this._on( true, $.mobile.window, {"load": "_keyup"}); } - }) - .checkboxradio( "refresh" ); - }, - - _reset: function() { - this.refresh(); - }, - - refresh: function() { - var input = this.element[ 0 ], - active = " " + $.mobile.activeBtnClass, - checkedClass = this.checkedClass + ( this.element.parents( ".ui-controlgroup-horizontal" ).length ? active : "" ), - label = this.label; - - if ( input.checked ) { - label.removeClass( this.uncheckedClass + active ).addClass( checkedClass ).buttonMarkup( { icon: this.checkedicon } ); - } else { - label.removeClass( checkedClass ).addClass( this.uncheckedClass ).buttonMarkup( { icon: this.uncheckedicon } ); } - - if ( input.disabled ) { + if ( input.attr( "disabled" ) ) { this.disable(); - } else { - this.enable(); } }, disable: function() { - this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" ); + var $el, + isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ), + inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ), + parentNeedsDisabled = this.element.attr( "disabled", true ) && ( inputNeedsWrap || isSearch ); + + if ( parentNeedsDisabled ) { + $el = this.element.parent(); + } else { + $el = this.element; + } + $el.addClass( "ui-disabled" ); + return this._setOption( "disabled", true ); }, enable: function() { - this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" ); + var $el, + isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ), + inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ), + parentNeedsEnabled = this.element.attr( "disabled", false ) && ( inputNeedsWrap || isSearch ); + + if ( parentNeedsEnabled ) { + $el = this.element.parent(); + } else { + $el = this.element; + } + $el.removeClass( "ui-disabled" ); + return this._setOption( "disabled", false ); } }); -$.widget( "mobile.checkboxradio", $.mobile.checkboxradio, $.mobile.behaviors.formReset ); - //auto self-init widgets $.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.checkboxradio.prototype.enhanceWithin( e.target, true ); + $.mobile.textinput.prototype.enhanceWithin( e.target, true ); }); })( jQuery ); (function( $, undefined ) { -$.widget( "mobile.button", $.mobile.widget, { - options: { - theme: null, - icon: null, - iconpos: null, - corners: true, - shadow: true, - iconshadow: true, - inline: null, - mini: null, - initSelector: "button, [type='button'], [type='submit'], [type='reset']" - }, - _create: function() { - var $el = this.element, - $button, - // create a copy of this.options we can pass to buttonMarkup - o = ( function( tdo ) { - var key, ret = {}; +$.mobile.listview.prototype.options.filter = false; +$.mobile.listview.prototype.options.filterPlaceholder = "Filter items..."; +$.mobile.listview.prototype.options.filterTheme = "c"; +$.mobile.listview.prototype.options.filterReveal = false; +// TODO rename callback/deprecate and default to the item itself as the first argument +var defaultFilterCallback = function( text, searchValue, item ) { + return text.toString().toLowerCase().indexOf( searchValue ) === -1; + }; - for ( key in tdo ) { - if ( tdo[ key ] !== null && key !== "initSelector" ) { - ret[ key ] = tdo[ key ]; - } - } +$.mobile.listview.prototype.options.filterCallback = defaultFilterCallback; - return ret; - } )( this.options ), - classes = "", - $buttonPlaceholder; +$.mobile.document.delegate( "ul, ol", "listviewcreate", function() { + var list = $( this ), + listview = list.data( "mobile-listview" ); - // if this is a link, check if it's been enhanced and, if not, use the right function - if ( $el[ 0 ].tagName === "A" ) { - if ( !$el.hasClass( "ui-btn" ) ) { - $el.buttonMarkup(); - } - return; - } + if ( !listview || !listview.options.filter ) { + return; + } - // get the inherited theme - // TODO centralize for all widgets - if ( !this.options.theme ) { - this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); - } + if ( listview.options.filterReveal ) { + list.children().addClass( "ui-screen-hidden" ); + } - // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 - /* if ( $el[0].className.length ) { - classes = $el[0].className; - } */ - if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) { - classes = "ui-btn-left"; - } + var wrapper = $( "
", { + "class": "ui-listview-filter ui-bar-" + listview.options.filterTheme, + "role": "search" + }).submit( function( e ) { + e.preventDefault(); + search.blur(); + }), + onKeyUp = function( e ) { + var $this = $( this ), + val = this.value.toLowerCase(), + listItems = null, + li = list.children(), + lastval = $this.jqmData( "lastval" ) + "", + childItems = false, + itemtext = "", + item, + // Check if a custom filter callback applies + isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback; - if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) { - classes = "ui-btn-right"; - } + if ( lastval && lastval === val ) { + // Execute the handler only once per value change + return; + } - if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) { - classes ? classes += " ui-submit" : classes = "ui-submit"; - } - $( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" ); + listview._trigger( "beforefilter", "beforefilter", { input: this } ); - // Add ARIA role - this.button = $( "
" ) - [ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ) - .insertBefore( $el ) - .buttonMarkup( o ) - .addClass( classes ) - .append( $el.addClass( "ui-btn-hidden" ) ); + // Change val as lastval for next execution + $this.jqmData( "lastval" , val ); + if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) { - $button = this.button; + // Custom filter callback applies or removed chars or pasted something totally different, check all items + listItems = list.children(); + } else { - $el.bind({ - focus: function() { - $button.addClass( $.mobile.focusClass ); - }, + // Only chars added, not removed, only use visible subset + listItems = list.children( ":not(.ui-screen-hidden)" ); - blur: function() { - $button.removeClass( $.mobile.focusClass ); + if ( !listItems.length && listview.options.filterReveal ) { + listItems = list.children( ".ui-screen-hidden" ); + } } - }); - this.refresh(); - }, + if ( val ) { - _setOption: function( key, value ) { - var op = {}; + // This handles hiding regular rows without the text we search for + // and any list dividers without regular rows shown under it - op[ key ] = value; - if ( key !== "initSelector" ) { - this.button.buttonMarkup( op ); - // Record the option change in the options and in the DOM data-* attributes - this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); - } - this._super( "_setOption", key, value ); - }, + for ( var i = listItems.length - 1; i >= 0; i-- ) { + item = $( listItems[ i ] ); + itemtext = item.jqmData( "filtertext" ) || item.text(); - enable: function() { - this.element.attr( "disabled", false ); - this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); - return this._setOption( "disabled", false ); - }, + if ( item.is( "li:jqmData(role=list-divider)" ) ) { - disable: function() { - this.element.attr( "disabled", true ); - this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true ); - return this._setOption( "disabled", true ); - }, + item.toggleClass( "ui-filter-hidequeue" , !childItems ); - refresh: function() { - var $el = this.element; + // New bucket! + childItems = false; - if ( $el.prop("disabled") ) { - this.disable(); - } else { - this.enable(); - } + } else if ( listview.options.filterCallback( itemtext, val, item ) ) { - // Grab the button's text element from its implementation-independent data item - $( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ); - } -}); + //mark to be hidden + item.toggleClass( "ui-filter-hidequeue" , true ); + } else { -//auto self-init widgets -$.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.button.prototype.enhanceWithin( e.target, true ); -}); + // There's a shown item in the bucket + childItems = true; + } + } -})( jQuery ); + // Show items, not marked to be hidden + listItems + .filter( ":not(.ui-filter-hidequeue)" ) + .toggleClass( "ui-screen-hidden", false ); -(function( $, undefined ) { + // Hide items, marked to be hidden + listItems + .filter( ".ui-filter-hidequeue" ) + .toggleClass( "ui-screen-hidden", true ) + .toggleClass( "ui-filter-hidequeue", false ); - $.widget( "mobile.controlgroup", $.mobile.widget, { - options: { - shadow: false, - corners: true, - excludeInvisible: true, - type: "vertical", - mini: false, - initSelector: ":jqmData(role='controlgroup')" + } else { + + //filtervalue is empty => show all + listItems.toggleClass( "ui-screen-hidden", !!listview.options.filterReveal ); + } + listview._addFirstLastClasses( li, listview._getVisibles( li, false ), false ); }, + search = $( "", { + placeholder: listview.options.filterPlaceholder + }) + .attr( "data-" + $.mobile.ns + "type", "search" ) + .jqmData( "lastval", "" ) + .bind( "keyup change input", onKeyUp ) + .appendTo( wrapper ) + .textinput(); - _create: function() { - var $el = this.element, - ui = { - inner: $( "
" ), - legend: $( "
" ) - }, - grouplegend = $el.children( "legend" ), - self = this; + if ( listview.options.inset ) { + wrapper.addClass( "ui-listview-filter-inset" ); + } - // Apply the proto - $el.wrapInner( ui.inner ); - if ( grouplegend.length ) { - ui.legend.append( grouplegend ).insertBefore( $el.children( 0 ) ); - } - $el.addClass( "ui-corner-all ui-controlgroup" ); + wrapper.bind( "submit", function() { + return false; + }) + .insertBefore( list ); +}); - $.extend( this, { - _initialRefresh: true - }); +})( jQuery ); - $.each( this.options, function( key, value ) { - // Cause initial options to be applied by their handler by temporarily setting the option to undefined - // - the handler then sets it to the initial value - self.options[ key ] = undefined; - self._setOption( key, value, true ); - }); - }, +(function( $, undefined ) { - _init: function() { - this.refresh(); - }, +$.mobile.listview.prototype.options.autodividers = false; +$.mobile.listview.prototype.options.autodividersSelector = function( elt ) { + // look for the text in the given element + var text = $.trim( elt.text() ) || null; - _setOption: function( key, value ) { - var setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); + if ( !text ) { + return null; + } - if ( this[ setter ] !== undefined ) { - this[ setter ]( value ); - } + // create the text for the divider (first uppercased letter) + text = text.slice( 0, 1 ).toUpperCase(); - this._super( key, value ); - this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); - }, + return text; +}; - _setType: function( value ) { - this.element - .removeClass( "ui-controlgroup-horizontal ui-controlgroup-vertical" ) - .addClass( "ui-controlgroup-" + value ); - this.refresh(); - }, +$.mobile.document.delegate( "ul,ol", "listviewcreate", function() { - _setCorners: function( value ) { - this.element.toggleClass( "ui-corner-all", value ); - }, + var list = $( this ), + listview = list.data( "mobile-listview" ); - _setShadow: function( value ) { - this.element.toggleClass( "ui-shadow", value ); - }, + if ( !listview || !listview.options.autodividers ) { + return; + } - _setMini: function( value ) { - this.element.toggleClass( "ui-mini", value ); - }, + var replaceDividers = function () { + list.find( "li:jqmData(role='list-divider')" ).remove(); - container: function() { - return this.element.children( ".ui-controlgroup-controls" ); - }, + var lis = list.find( 'li' ), + lastDividerText = null, li, dividerText; - refresh: function() { - var els = this.element.find( ".ui-btn" ).not( ".ui-slider-handle" ), - create = this._initialRefresh; - if ( $.mobile.checkboxradio ) { - this.element.find( ":mobile-checkboxradio" ).checkboxradio( "refresh" ); + for ( var i = 0; i < lis.length ; i++ ) { + li = lis[i]; + dividerText = listview.options.autodividersSelector( $( li ) ); + + if ( dividerText && lastDividerText !== dividerText ) { + var divider = document.createElement( 'li' ); + divider.appendChild( document.createTextNode( dividerText ) ); + divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' ); + li.parentNode.insertBefore( divider, li ); } - this._addFirstLastClasses( els, this.options.excludeInvisible ? this._getVisibles( els, create ) : els, create ); - this._initialRefresh = false; + + lastDividerText = dividerText; } - }); + }; - $.widget( "mobile.controlgroup", $.mobile.controlgroup, $.mobile.behaviors.addFirstLastClasses ); + var afterListviewRefresh = function () { + list.unbind( 'listviewafterrefresh', afterListviewRefresh ); + replaceDividers(); + listview.refresh(); + list.bind( 'listviewafterrefresh', afterListviewRefresh ); + }; - // TODO: Implement a mechanism to allow widgets to become enhanced in the - // correct order when their correct enhancement depends on other widgets in - // the page being correctly enhanced already. - // - // For now, we wait until dom-ready to attach the controlgroup's enhancement - // hook, because by that time, all the other widgets' enhancement hooks should - // already be in place, ensuring that all widgets that need to be grouped will - // already have been enhanced by the time the controlgroup is created. - $( function() { - $.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.controlgroup.prototype.enhanceWithin( e.target, true ); - }); - }); -})(jQuery); + afterListviewRefresh(); +}); + +})( jQuery ); (function( $, undefined ) { $( document ).bind( "pagecreate create", function( e ) { + $( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" ); + +}); - //links within content areas, tests included with page - $( e.target ) - .find( "a" ) - .jqmEnhanceable() - .not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" ) - .addClass( "ui-link" ); +})( jQuery ); -}); +(function( $, undefined ) { + +$.mobile.behaviors.formReset = { + _handleFormReset: function() { + this._on( this.element.closest( "form" ), { + reset: function() { + this._delay( "_reset" ); + } + }); + } +}; })( jQuery ); +/* +* "checkboxradio" plugin +*/ (function( $, undefined ) { - function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { - var ret = desired; +$.widget( "mobile.checkboxradio", $.mobile.widget, $.extend( { + options: { + theme: null, + mini: false, + initSelector: "input[type='checkbox'],input[type='radio']" + }, + _create: function() { + var self = this, + input = this.element, + o = this.options, + inheritAttr = function( input, dataAttr ) { + return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr ); + }, + // NOTE: Windows Phone could not find the label through a selector + // filter works though. + parentLabel = $( input ).closest( "label" ), + label = parentLabel.length ? parentLabel : $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ).first(), + inputtype = input[0].type, + mini = inheritAttr( input, "mini" ) || o.mini, + checkedState = inputtype + "-on", + uncheckedState = inputtype + "-off", + iconpos = inheritAttr( input, "iconpos" ), + checkedClass = "ui-" + checkedState, + uncheckedClass = "ui-" + uncheckedState; - if ( winSize < segSize ) { - // Center segment if it's bigger than the window - ret = offset + ( winSize - segSize ) / 2; - } else { - // Otherwise center it at the desired coordinate while keeping it completely inside the window - ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize ); + if ( inputtype !== "checkbox" && inputtype !== "radio" ) { + return; } - return ret; - } + // Expose for other methods + $.extend( this, { + label: label, + inputtype: inputtype, + checkedClass: checkedClass, + uncheckedClass: uncheckedClass, + checkedicon: checkedState, + uncheckedicon: uncheckedState + }); - function windowCoords() { - var $win = $.mobile.window; + // If there's no selected theme check the data attr + if ( !o.theme ) { + o.theme = $.mobile.getInheritedTheme( this.element, "c" ); + } - return { - x: $win.scrollLeft(), - y: $win.scrollTop(), - cx: ( window.innerWidth || $win.width() ), - cy: ( window.innerHeight || $win.height() ) - }; - } + label.buttonMarkup({ + theme: o.theme, + icon: uncheckedState, + shadow: false, + mini: mini, + iconpos: iconpos + }); - $.widget( "mobile.popup", $.mobile.widget, { - options: { - theme: null, - overlayTheme: null, - shadow: true, - corners: true, - transition: "none", - positionTo: "origin", - tolerance: null, - initSelector: ":jqmData(role='popup')", - closeLinkSelector: "a:jqmData(rel='back')", - closeLinkEvents: "click.popup", - navigateEvents: "navigate.popup", - closeEvents: "navigate.popup pagebeforechange.popup", - dismissible: true, + // Wrap the input + label in a div + var wrapper = document.createElement('div'); + wrapper.className = 'ui-' + inputtype; - // NOTE Windows Phone 7 has a scroll position caching issue that - // requires us to disable popup history management by default - // https://github.com/jquery/jquery-mobile/issues/4784 - // - // NOTE this option is modified in _create! - history: !$.mobile.browser.oldIE - }, + input.add( label ).wrapAll( wrapper ); - _eatEventAndClose: function( e ) { - e.preventDefault(); - e.stopImmediatePropagation(); - if ( this.options.dismissible ) { - this.close(); - } - return false; - }, + label.bind({ + vmouseover: function( event ) { + if ( $( this ).parent().is( ".ui-disabled" ) ) { + event.stopPropagation(); + } + }, - // Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height - _resizeScreen: function() { - var popupHeight = this._ui.container.outerHeight( true ); + vclick: function( event ) { + if ( input.is( ":disabled" ) ) { + event.preventDefault(); + return; + } - this._ui.screen.removeAttr( "style" ); - if ( popupHeight > this._ui.screen.height() ) { - this._ui.screen.height( popupHeight ); - } - }, + self._cacheVals(); - _handleWindowKeyUp: function( e ) { - if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) { - return this._eatEventAndClose( e ); - } - }, + input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) ); - _expectResizeEvent: function() { - var winCoords = windowCoords(); + // trigger click handler's bound directly to the input as a substitute for + // how label clicks behave normally in the browsers + // TODO: it would be nice to let the browser's handle the clicks and pass them + // through to the associate input. we can swallow that click at the parent + // wrapper element level + input.triggerHandler( 'click' ); - if ( this._resizeData ) { - if ( winCoords.x === this._resizeData.winCoords.x && - winCoords.y === this._resizeData.winCoords.y && - winCoords.cx === this._resizeData.winCoords.cx && - winCoords.cy === this._resizeData.winCoords.cy ) { - // timeout not refreshed - return false; - } else { - // clear existing timeout - it will be refreshed below - clearTimeout( this._resizeData.timeoutId ); - } + // Input set for common radio buttons will contain all the radio + // buttons, but will not for checkboxes. clearing the checked status + // of other radios ensures the active button state is applied properly + self._getInputSet().not( input ).prop( "checked", false ); + + self._updateAll(); + return false; } + }); - this._resizeData = { - timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), - winCoords: winCoords - }; + input + .bind({ + vmousedown: function() { + self._cacheVals(); + }, - return true; - }, + vclick: function() { + var $this = $( this ); - _resizeTimeout: function() { - if ( this._isOpen ) { - if ( !this._expectResizeEvent() ) { - if ( this._ui.container.hasClass( "ui-popup-hidden" ) ) { - // effectively rapid-open the popup while leaving the screen intact - this._ui.container.removeClass( "ui-popup-hidden" ); - this.reposition( { positionTo: "window" } ); - this._ignoreResizeEvents(); - } + // Adds checked attribute to checked input when keyboard is used + if ( $this.is( ":checked" ) ) { - this._resizeScreen(); - this._resizeData = null; - this._orientationchangeInProgress = false; - } - } else { - this._resizeData = null; - this._orientationchangeInProgress = false; - } - }, + $this.prop( "checked", true); + self._getInputSet().not( $this ).prop( "checked", false ); + } else { - _ignoreResizeEvents: function() { - var self = this; + $this.prop( "checked", false ); + } - if ( this._ignoreResizeTo ) { - clearTimeout( this._ignoreResizeTo ); - } - this._ignoreResizeTo = setTimeout( function() { self._ignoreResizeTo = 0; }, 1000 ); - }, + self._updateAll(); + }, - _handleWindowResize: function( e ) { - if ( this._isOpen && this._ignoreResizeTo === 0 ) { - if ( ( this._expectResizeEvent() || this._orientationchangeInProgress ) && - !this._ui.container.hasClass( "ui-popup-hidden" ) ) { - // effectively rapid-close the popup while leaving the screen intact - this._ui.container - .addClass( "ui-popup-hidden" ) - .removeAttr( "style" ); - } - } - }, + focus: function() { + label.addClass( $.mobile.focusClass ); + }, - _handleWindowOrientationchange: function( e ) { - if ( !this._orientationchangeInProgress && this._isOpen && this._ignoreResizeTo === 0 ) { - this._expectResizeEvent(); - this._orientationchangeInProgress = true; - } - }, + blur: function() { + label.removeClass( $.mobile.focusClass ); + } + }); - // When the popup is open, attempting to focus on an element that is not a - // child of the popup will redirect focus to the popup - _handleDocumentFocusIn: function( e ) { - var tgt = e.target, $tgt, ui = this._ui; + this._handleFormReset(); + this.refresh(); + }, - if ( !this._isOpen ) { - return; - } + _cacheVals: function() { + this._getInputSet().each(function() { + $( this ).jqmData( "cacheVal", this.checked ); + }); + }, - if ( tgt !== ui.container[ 0 ] ) { - $tgt = $( e.target ); - if ( 0 === $tgt.parents().filter( ui.container[ 0 ] ).length ) { - $( document.activeElement ).one( "focus", function( e ) { - $tgt.blur(); - }); - ui.focusElement.focus(); - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } else if ( ui.focusElement[ 0 ] === ui.container[ 0 ] ) { - ui.focusElement = $tgt; - } - } else if ( ui.focusElement && ui.focusElement[ 0 ] !== ui.container[ 0 ] ) { - ui.container.blur(); - ui.focusElement.focus(); - } + //returns either a set of radios with the same name attribute, or a single checkbox + _getInputSet: function() { + if ( this.inputtype === "checkbox" ) { + return this.element; + } - this._ignoreResizeEvents(); - }, + return this.element.closest( "form, :jqmData(role='page'), :jqmData(role='dialog')" ) + .find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" ); + }, - _create: function() { - var ui = { - screen: $( "
" ), - placeholder: $( "
" ), - container: $( "
" ) - }, - thisPage = this.element.closest( ".ui-page" ), - myId = this.element.attr( "id" ), - self = this; + _updateAll: function() { + var self = this; - // We need to adjust the history option to be false if there's no AJAX nav. - // We can't do it in the option declarations because those are run before - // it is determined whether there shall be AJAX nav. - this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled; + this._getInputSet().each(function() { + var $this = $( this ); - if ( thisPage.length === 0 ) { - thisPage = $( "body" ); + if ( this.checked || self.inputtype === "checkbox" ) { + $this.trigger( "change" ); } + }) + .checkboxradio( "refresh" ); + }, - // define the container for navigation event bindings - // TODO this would be nice at the the mobile widget level - this.options.container = this.options.container || $.mobile.pageContainer; + _reset: function() { + this.refresh(); + }, - // Apply the proto - thisPage.append( ui.screen ); - ui.container.insertAfter( ui.screen ); - // Leave a placeholder where the element used to be - ui.placeholder.insertAfter( this.element ); - if ( myId ) { - ui.screen.attr( "id", myId + "-screen" ); - ui.container.attr( "id", myId + "-popup" ); - ui.placeholder.html( "" ); - } - ui.container.append( this.element ); - ui.focusElement = ui.container; + refresh: function() { + var input = this.element[ 0 ], + active = " " + $.mobile.activeBtnClass, + checkedClass = this.checkedClass + ( this.element.parents( ".ui-controlgroup-horizontal" ).length ? active : "" ), + label = this.label; - // Add class to popup element - this.element.addClass( "ui-popup" ); + if ( input.checked ) { + label.removeClass( this.uncheckedClass + active ).addClass( checkedClass ).buttonMarkup( { icon: this.checkedicon } ); + } else { + label.removeClass( checkedClass ).addClass( this.uncheckedClass ).buttonMarkup( { icon: this.uncheckedicon } ); + } - // Define instance variables - $.extend( this, { - _scrollTop: 0, - _page: thisPage, - _ui: ui, - _fallbackTransition: "", - _currentTransition: false, - _prereqs: null, - _isOpen: false, - _tolerance: null, - _resizeData: null, - _ignoreResizeTo: 0, - _orientationchangeInProgress: false - }); + if ( input.disabled ) { + this.disable(); + } else { + this.enable(); + } + }, - $.each( this.options, function( key, value ) { - // Cause initial options to be applied by their handler by temporarily setting the option to undefined - // - the handler then sets it to the initial value - self.options[ key ] = undefined; - self._setOption( key, value, true ); - }); + disable: function() { + this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" ); + }, - ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); + enable: function() { + this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" ); + } +}, $.mobile.behaviors.formReset ) ); - this._on( $.mobile.window, { - orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), - resize: $.proxy( this, "_handleWindowResize" ), - keyup: $.proxy( this, "_handleWindowKeyUp" ) - }); - this._on( $.mobile.document, { - focusin: $.proxy( this, "_handleDocumentFocusIn" ) - }); - }, +//auto self-init widgets +$.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.checkboxradio.prototype.enhanceWithin( e.target, true ); +}); - _applyTheme: function( dst, theme, prefix ) { - var classes = ( dst.attr( "class" ) || "").split( " " ), - alreadyAdded = true, - currentTheme = null, - matches, - themeStr = String( theme ); +})( jQuery ); - while ( classes.length > 0 ) { - currentTheme = classes.pop(); - matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme ); - if ( matches && matches.length > 1 ) { - currentTheme = matches[ 1 ]; - break; - } else { - currentTheme = null; - } - } +(function( $, undefined ) { - if ( theme !== currentTheme ) { - dst.removeClass( "ui-" + prefix + "-" + currentTheme ); - if ( ! ( theme === null || theme === "none" ) ) { - dst.addClass( "ui-" + prefix + "-" + themeStr ); - } - } - }, +$.widget( "mobile.button", $.mobile.widget, { + options: { + theme: null, + icon: null, + iconpos: null, + corners: true, + shadow: true, + iconshadow: true, + inline: null, + mini: null, + initSelector: "button, [type='button'], [type='submit'], [type='reset']" + }, + _create: function() { + var $el = this.element, + $button, + // create a copy of this.options we can pass to buttonMarkup + o = ( function( tdo ) { + var key, ret = {}; - _setTheme: function( value ) { - this._applyTheme( this.element, value, "body" ); - }, + for ( key in tdo ) { + if ( tdo[ key ] !== null && key !== "initSelector" ) { + ret[ key ] = tdo[ key ]; + } + } - _setOverlayTheme: function( value ) { - this._applyTheme( this._ui.screen, value, "overlay" ); + return ret; + } )( this.options ), + classes = "", + $buttonPlaceholder; - if ( this._isOpen ) { - this._ui.screen.addClass( "in" ); + // if this is a link, check if it's been enhanced and, if not, use the right function + if ( $el[ 0 ].tagName === "A" ) { + if ( !$el.hasClass( "ui-btn" ) ) { + $el.buttonMarkup(); } - }, + return; + } - _setShadow: function( value ) { - this.element.toggleClass( "ui-overlay-shadow", value ); - }, + // get the inherited theme + // TODO centralize for all widgets + if ( !this.options.theme ) { + this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); + } - _setCorners: function( value ) { - this.element.toggleClass( "ui-corner-all", value ); - }, + // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 + /* if ( $el[0].className.length ) { + classes = $el[0].className; + } */ + if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) { + classes = "ui-btn-left"; + } - _applyTransition: function( value ) { - this._ui.container.removeClass( this._fallbackTransition ); - if ( value && value !== "none" ) { - this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); - if ( this._fallbackTransition === "none" ) { - this._fallbackTransition = ""; - } - this._ui.container.addClass( this._fallbackTransition ); - } - }, + if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) { + classes = "ui-btn-right"; + } - _setTransition: function( value ) { - if ( !this._currentTransition ) { - this._applyTransition( value ); + if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) { + if ( classes ) { + classes += " ui-submit"; + } else { + classes = "ui-submit"; } - }, + } + $( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" ); - _setTolerance: function( value ) { - var tol = { t: 30, r: 15, b: 30, l: 15 }; + // Add ARIA role + this.button = $( "
" ) + [ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ) + .insertBefore( $el ) + .buttonMarkup( o ) + .addClass( classes ) + .append( $el.addClass( "ui-btn-hidden" ) ); - if ( value !== undefined ) { - var ar = String( value ).split( "," ); + $button = this.button; - $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); + $el.bind({ + focus: function() { + $button.addClass( $.mobile.focusClass ); + }, - switch( ar.length ) { - // All values are to be the same - case 1: - if ( !isNaN( ar[ 0 ] ) ) { - tol.t = tol.r = tol.b = tol.l = ar[ 0 ]; - } - break; + blur: function() { + $button.removeClass( $.mobile.focusClass ); + } + }); - // The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance - case 2: - if ( !isNaN( ar[ 0 ] ) ) { - tol.t = tol.b = ar[ 0 ]; - } - if ( !isNaN( ar[ 1 ] ) ) { - tol.l = tol.r = ar[ 1 ]; - } - break; + this.refresh(); + }, - // The array contains values in the order top, right, bottom, left - case 4: - if ( !isNaN( ar[ 0 ] ) ) { - tol.t = ar[ 0 ]; - } - if ( !isNaN( ar[ 1 ] ) ) { - tol.r = ar[ 1 ]; - } - if ( !isNaN( ar[ 2 ] ) ) { - tol.b = ar[ 2 ]; - } - if ( !isNaN( ar[ 3 ] ) ) { - tol.l = ar[ 3 ]; - } - break; + _setOption: function( key, value ) { + var op = {}; - default: - break; - } - } + op[ key ] = value; + if ( key !== "initSelector" ) { + this.button.buttonMarkup( op ); + // Record the option change in the options and in the DOM data-* attributes + this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); + } + this._super( "_setOption", key, value ); + }, - this._tolerance = tol; - }, + enable: function() { + this.element.attr( "disabled", false ); + this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); + return this._setOption( "disabled", false ); + }, - _setOption: function( key, value ) { - var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); + disable: function() { + this.element.attr( "disabled", true ); + this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true ); + return this._setOption( "disabled", true ); + }, - if ( this[ setter ] !== undefined ) { - this[ setter ]( value ); - } + refresh: function() { + var $el = this.element; - // TODO REMOVE FOR 1.2.1 by moving them out to a default options object - exclusions = [ - "initSelector", - "closeLinkSelector", - "closeLinkEvents", - "navigateEvents", - "closeEvents", - "history", - "container" - ]; - - $.mobile.widget.prototype._setOption.apply( this, arguments ); - if ( $.inArray( key, exclusions ) === -1 ) { - // Record the option change in the options and in the DOM data-* attributes - this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); - } - }, + if ( $el.prop("disabled") ) { + this.disable(); + } else { + this.enable(); + } - // Try and center the overlay over the given coordinates - _placementCoords: function( desired ) { - // rectangle within which the popup must fit - var - winCoords = windowCoords(), - rc = { - x: this._tolerance.l, - y: winCoords.y + this._tolerance.t, - cx: winCoords.cx - this._tolerance.l - this._tolerance.r, - cy: winCoords.cy - this._tolerance.t - this._tolerance.b - }, - menuSize, ret; + // Grab the button's text element from its implementation-independent data item + $( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ); + } +}); - // Clamp the width of the menu before grabbing its size - this._ui.container.css( "max-width", rc.cx ); - menuSize = { - cx: this._ui.container.outerWidth( true ), - cy: this._ui.container.outerHeight( true ) - }; +//auto self-init widgets +$.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.button.prototype.enhanceWithin( e.target, true ); +}); - // Center the menu over the desired coordinates, while not going outside - // the window tolerances. This will center wrt. the window if the popup is too large. - ret = { - x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ), - y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y ) - }; +})( jQuery ); - // Make sure the top of the menu is visible - ret.y = Math.max( 0, ret.y ); +(function( $, undefined ) { - // If the height of the menu is smaller than the height of the document - // align the bottom with the bottom of the document +$.widget( "mobile.slider", $.mobile.widget, $.extend( { + widgetEventPrefix: "slide", - // fix for $.mobile.document.height() bug in core 1.7.2. - var docEl = document.documentElement, docBody = document.body, - docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight ); + options: { + theme: null, + trackTheme: null, + disabled: false, + initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')", + mini: false, + highlight: false + }, - ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); + _create: function() { - return { left: ret.x, top: ret.y }; - }, + // TODO: Each of these should have comments explain what they're for + var self = this, + control = this.element, + parentTheme = $.mobile.getInheritedTheme( control, "c" ), + theme = this.options.theme || parentTheme, + trackTheme = this.options.trackTheme || parentTheme, + cType = control[ 0 ].nodeName.toLowerCase(), + isSelect = this.isToggleSwitch = cType === "select", + isRangeslider = control.parent().is( ":jqmData(role='rangeslider')" ), + selectClass = ( this.isToggleSwitch ) ? "ui-slider-switch" : "", + controlID = control.attr( "id" ), + $label = $( "[for='" + controlID + "']" ), + labelID = $label.attr( "id" ) || controlID + "-label", + label = $label.attr( "id", labelID ), + min = !this.isToggleSwitch ? parseFloat( control.attr( "min" ) ) : 0, + max = !this.isToggleSwitch ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1, + step = window.parseFloat( control.attr( "step" ) || 1 ), + miniClass = ( this.options.mini || control.jqmData( "mini" ) ) ? " ui-mini" : "", + domHandle = document.createElement( "a" ), + handle = $( domHandle ), + domSlider = document.createElement( "div" ), + slider = $( domSlider ), + valuebg = this.options.highlight && !this.isToggleSwitch ? (function() { + var bg = document.createElement( "div" ); + bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all"; + return $( bg ).prependTo( slider ); + })() : false, + options, + wrapper; + + domHandle.setAttribute( "href", "#" ); + domSlider.setAttribute( "role", "application" ); + domSlider.className = [this.isToggleSwitch ? "ui-slider " : "ui-slider-track ",selectClass," ui-btn-down-",trackTheme," ui-btn-corner-all", miniClass].join( "" ); + domHandle.className = "ui-slider-handle"; + domSlider.appendChild( domHandle ); - _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { - var self = this, prereqs; + handle.buttonMarkup({ corners: true, theme: theme, shadow: true }) + .attr({ + "role": "slider", + "aria-valuemin": min, + "aria-valuemax": max, + "aria-valuenow": this._value(), + "aria-valuetext": this._value(), + "title": this._value(), + "aria-labelledby": labelID + }); - // It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in - // the closure of the functions which call the callbacks passed in. The comparison between the local variable and - // self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called - // next time an animation completes, even if that's not the animation whose end the function was supposed to catch - // (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for - // that animation anymore, but the handler remains attached, so it is called the next time the popup is opened - // - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that - // callbacks triggered by a stale .animationComplete will be ignored. + $.extend( this, { + slider: slider, + handle: handle, + type: cType, + step: step, + max: max, + min: min, + valuebg: valuebg, + isRangeslider: isRangeslider, + dragging: false, + beforeStart: null, + userModified: false, + mouseMoved: false + }); - prereqs = { - screen: $.Deferred(), - container: $.Deferred() - }; + if ( this.isToggleSwitch ) { + wrapper = document.createElement( "div" ); + wrapper.className = "ui-slider-inneroffset"; - prereqs.screen.then( function() { - if ( prereqs === self._prereqs ) { - screenPrereq(); - } - }); + for ( var j = 0, length = domSlider.childNodes.length; j < length; j++ ) { + wrapper.appendChild( domSlider.childNodes[j] ); + } - prereqs.container.then( function() { - if ( prereqs === self._prereqs ) { - containerPrereq(); - } - }); + domSlider.appendChild( wrapper ); - $.when( prereqs.screen, prereqs.container ).done( function() { - if ( prereqs === self._prereqs ) { - self._prereqs = null; - whenDone(); - } - }); + // slider.wrapInner( "
" ); - self._prereqs = prereqs; - }, + // make the handle move with a smooth transition + handle.addClass( "ui-slider-handle-snapping" ); - _animate: function( args ) { - // NOTE before removing the default animation of the screen - // this had an animate callback that would resolve the deferred - // now the deferred is resolved immediately - // TODO remove the dependency on the screen deferred - this._ui.screen - .removeClass( args.classToRemove ) - .addClass( args.screenClassToAdd ); + options = control.find( "option" ); - args.prereqs.screen.resolve(); + for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) { + var side = !i ? "b" : "a", + sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ), + sliderLabel = document.createElement( "div" ), + sliderImg = document.createElement( "span" ); - if ( args.transition && args.transition !== "none" ) { - if ( args.applyTransition ) { - this._applyTransition( args.transition ); - } - if ( this._fallbackTransition ) { - this._ui.container - .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) - .addClass( args.containerClassToAdd ) - .removeClass( args.classToRemove ); - return; - } + sliderImg.className = ["ui-slider-label ui-slider-label-", side, sliderTheme, " ui-btn-corner-all"].join( "" ); + sliderImg.setAttribute( "role", "img" ); + sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) ); + $( sliderImg ).prependTo( slider ); } - this._ui.container.removeClass( args.classToRemove ); - args.prereqs.container.resolve(); - }, - - // The desired coordinates passed in will be returned untouched if no reference element can be identified via - // desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid - // x and y coordinates by specifying the center middle of the window if the coordinates are absent. - // options: { x: coordinate, y: coordinate, positionTo: string: "origin", "window", or jQuery selector - _desiredCoords: function( o ) { - var dst = null, offset, winCoords = windowCoords(), x = o.x, y = o.y, pTo = o.positionTo; - // Establish which element will serve as the reference - if ( pTo && pTo !== "origin" ) { - if ( pTo === "window" ) { - x = winCoords.cx / 2 + winCoords.x; - y = winCoords.cy / 2 + winCoords.y; - } else { - try { - dst = $( pTo ); - } catch( e ) { - dst = null; - } - if ( dst ) { - dst.filter( ":visible" ); - if ( dst.length === 0 ) { - dst = null; - } - } - } - } + self._labels = $( ".ui-slider-label", slider ); - // If an element was found, center over it - if ( dst ) { - offset = dst.offset(); - x = offset.left + dst.outerWidth() / 2; - y = offset.top + dst.outerHeight() / 2; - } + } - // Make sure x and y are valid numbers - center over the window - if ( $.type( x ) !== "number" || isNaN( x ) ) { - x = winCoords.cx / 2 + winCoords.x; - } - if ( $.type( y ) !== "number" || isNaN( y ) ) { - y = winCoords.cy / 2 + winCoords.y; - } + label.addClass( "ui-slider" ); + + // monitor the input for updated values + control.addClass( this.isToggleSwitch ? "ui-slider-switch" : "ui-slider-input" ); - return { x: x, y: y }; - }, + this._on( control, { + "change": "_controlChange", + "keyup": "_controlKeyup", + "blur": "_controlBlur", + "vmouseup": "_controlVMouseUp" + }); - _reposition: function( o ) { - // We only care about position-related parameters for repositioning - o = { x: o.x, y: o.y, positionTo: o.positionTo }; - this._trigger( "beforeposition", o ); - this._ui.container.offset( this._placementCoords( this._desiredCoords( o ) ) ); - }, - - reposition: function( o ) { - if ( this._isOpen ) { - this._reposition( o ); - } - }, - - _openPrereqsComplete: function() { - this._ui.container.addClass( "ui-popup-active" ); - this._isOpen = true; - this._resizeScreen(); - this._ui.container.attr( "tabindex", "0" ).focus(); - this._ignoreResizeEvents(); - this._trigger( "afteropen" ); - }, + slider.bind( "vmousedown", $.proxy( this._sliderVMouseDown, this ) ) + .bind( "vclick", false ); - _open: function( options ) { - var o = $.extend( {}, this.options, options ), - // TODO move blacklist to private method - androidBlacklist = ( function() { - var w = window, - ua = navigator.userAgent, - // Rendering engine is Webkit, and capture major version - wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ), - wkversion = !!wkmatch && wkmatch[ 1 ], - androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ), - andversion = !!androidmatch && androidmatch[ 1 ], - chromematch = ua.indexOf( "Chrome" ) > -1; + // We have to instantiate a new function object for the unbind to work properly + // since the method itself is defined in the prototype (causing it to unbind everything) + this._on( document, { "vmousemove": "_preventDocumentDrag" }); + this._on( slider.add( document ), { "vmouseup": "_sliderVMouseUp" }); - // Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome. - if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) { - return true; - } - return false; - }()); + slider.insertAfter( control ); - // Count down to triggering "popupafteropen" - we have two prerequisites: - // 1. The popup window animation completes (container()) - // 2. The screen opacity animation completes (screen()) - this._createPrereqs( - $.noop, - $.noop, - $.proxy( this, "_openPrereqsComplete" ) ); + // wrap in a div for styling purposes + if ( !this.isToggleSwitch && !isRangeslider ) { + wrapper = this.options.mini ? "
" : "
"; + + control.add( slider ).wrapAll( wrapper ); + } - this._currentTransition = o.transition; - this._applyTransition( o.transition ); + // Only add focus class to toggle switch, sliders get it automatically from ui-btn + if ( this.isToggleSwitch ) { + this.handle.bind({ + focus: function() { + slider.addClass( $.mobile.focusClass ); + }, - if ( !this.options.theme ) { - this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); - } + blur: function() { + slider.removeClass( $.mobile.focusClass ); + } + }); + } - this._ui.screen.removeClass( "ui-screen-hidden" ); - this._ui.container.removeClass( "ui-popup-hidden" ); + // bind the handle event callbacks and set the context to the widget instance + this._on( this.handle, { + "vmousedown": "_handleVMouseDown", + "keydown": "_handleKeydown", + "keyup": "_handleKeyup" + }); - // Give applications a chance to modify the contents of the container before it appears - this._reposition( o ); + this.handle.bind( "vclick", false ); - if ( this.options.overlayTheme && androidBlacklist ) { - /* TODO: - The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed - above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain - types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser: - https://github.com/scottjehl/Device-Bugs/issues/3 + this._handleFormReset(); - This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): + this.refresh( undefined, undefined, true ); + }, - https://github.com/jquery/jquery-mobile/issues/4816 - https://github.com/jquery/jquery-mobile/issues/4844 - https://github.com/jquery/jquery-mobile/issues/4874 - */ + _controlChange: function( event ) { + // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again + if ( this._trigger( "controlchange", event ) === false ) { + return false; + } + if ( !this.mouseMoved ) { + this.refresh( this._value(), true ); + } + }, - // TODO sort out why this._page isn't working - this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); - } - this._animate({ - additionalCondition: true, - transition: o.transition, - classToRemove: "", - screenClassToAdd: "in", - containerClassToAdd: "in", - applyTransition: false, - prereqs: this._prereqs - }); - }, + _controlKeyup: function( event ) { // necessary? + this.refresh( this._value(), true, true ); + }, - _closePrereqScreen: function() { - this._ui.screen - .removeClass( "out" ) - .addClass( "ui-screen-hidden" ); - }, + _controlBlur: function( event ) { + this.refresh( this._value(), true ); + }, - _closePrereqContainer: function() { - this._ui.container - .removeClass( "reverse out" ) - .addClass( "ui-popup-hidden" ) - .removeAttr( "style" ); - }, + // it appears the clicking the up and down buttons in chrome on + // range/number inputs doesn't trigger a change until the field is + // blurred. Here we check thif the value has changed and refresh + _controlVMouseUp: function( event ) { + this._checkedRefresh(); + }, - _closePrereqsDone: function() { - var opts = this.options; + // NOTE force focus on handle + _handleVMouseDown: function( event ) { + this.handle.focus(); + }, - this._ui.container.removeAttr( "tabindex" ); + _handleKeydown: function( event ) { + var index = this._value(); + if ( this.options.disabled ) { + return; + } - // remove the global mutex for popups - $.mobile.popup.active = undefined; + // In all cases prevent the default and mark the handle as active + switch ( event.keyCode ) { + case $.mobile.keyCode.HOME: + case $.mobile.keyCode.END: + case $.mobile.keyCode.PAGE_UP: + case $.mobile.keyCode.PAGE_DOWN: + case $.mobile.keyCode.UP: + case $.mobile.keyCode.RIGHT: + case $.mobile.keyCode.DOWN: + case $.mobile.keyCode.LEFT: + event.preventDefault(); - // alert users that the popup is closed - this._trigger( "afterclose" ); - }, + if ( !this._keySliding ) { + this._keySliding = true; + this.handle.addClass( "ui-state-active" ); + } - _close: function( immediate ) { - this._ui.container.removeClass( "ui-popup-active" ); - this._page.removeClass( "ui-popup-open" ); + break; + } - this._isOpen = false; + // move the slider according to the keypress + switch ( event.keyCode ) { + case $.mobile.keyCode.HOME: + this.refresh( this.min ); + break; + case $.mobile.keyCode.END: + this.refresh( this.max ); + break; + case $.mobile.keyCode.PAGE_UP: + case $.mobile.keyCode.UP: + case $.mobile.keyCode.RIGHT: + this.refresh( index + this.step ); + break; + case $.mobile.keyCode.PAGE_DOWN: + case $.mobile.keyCode.DOWN: + case $.mobile.keyCode.LEFT: + this.refresh( index - this.step ); + break; + } + }, // remove active mark - // Count down to triggering "popupafterclose" - we have two prerequisites: - // 1. The popup window reverse animation completes (container()) - // 2. The screen opacity animation completes (screen()) - this._createPrereqs( - $.proxy( this, "_closePrereqScreen" ), - $.proxy( this, "_closePrereqContainer" ), - $.proxy( this, "_closePrereqsDone" ) ); + _handleKeyup: function( event ) { + if ( this._keySliding ) { + this._keySliding = false; + this.handle.removeClass( "ui-state-active" ); + } + }, - this._animate( { - additionalCondition: this._ui.screen.hasClass( "in" ), - transition: ( immediate ? "none" : ( this._currentTransition ) ), - classToRemove: "in", - screenClassToAdd: "out", - containerClassToAdd: "reverse out", - applyTransition: true, - prereqs: this._prereqs - }); - }, + _sliderVMouseDown: function( event ) { + // NOTE: we don't do this in refresh because we still want to + // support programmatic alteration of disabled inputs + if ( this.options.disabled || !( event.which === 1 || event.which === 0 ) ) { + return false; + } + if ( this._trigger( "beforestart", event ) === false ) { + return false; + } + this.dragging = true; + this.userModified = false; + this.mouseMoved = false; - _unenhance: function() { - // Put the element back to where the placeholder was and remove the "ui-popup" class - this._setTheme( "none" ); - this.element - // Cannot directly insertAfter() - we need to detach() first, because - // insertAfter() will do nothing if the payload div was not attached - // to the DOM at the time the widget was created, and so the payload - // will remain inside the container even after we call insertAfter(). - // If that happens and we remove the container a few lines below, we - // will cause an infinite recursion - #5244 - .detach() - .insertAfter( this._ui.placeholder ) - .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); - this._ui.screen.remove(); - this._ui.container.remove(); - this._ui.placeholder.remove(); - }, + if ( this.isToggleSwitch ) { + this.beforeStart = this.element[0].selectedIndex; + } - _destroy: function() { - if ( $.mobile.popup.active === this ) { - this.element.one( "popupafterclose", $.proxy( this, "_unenhance" ) ); - this.close(); - } else { - this._unenhance(); - } - }, + + this.refresh( event ); + this._trigger( "start" ); + return false; + }, - _closePopup: function( e, data ) { - var parsedDst, toUrl, o = this.options, immediate = false; + _sliderVMouseUp: function() { + if ( this.dragging ) { + this.dragging = false; - // restore location on screen - window.scrollTo( 0, this._scrollTop ); + if ( this.isToggleSwitch ) { + // make the handle move with a smooth transition + this.handle.addClass( "ui-slider-handle-snapping" ); - if ( e && e.type === "pagebeforechange" && data ) { - // Determine whether we need to rapid-close the popup, or whether we can - // take the time to run the closing transition - if ( typeof data.toPage === "string" ) { - parsedDst = data.toPage; + if ( this.mouseMoved ) { + // this is a drag, change the value only if user dragged enough + if ( this.userModified ) { + this.refresh( this.beforeStart === 0 ? 1 : 0 ); + } else { + this.refresh( this.beforeStart ); + } } else { - parsedDst = data.toPage.jqmData( "url" ); + // this is just a click, change the value + this.refresh( this.beforeStart === 0 ? 1 : 0 ); } - parsedDst = $.mobile.path.parseUrl( parsedDst ); - toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash; + } - if ( this._myUrl !== $.mobile.path.makeUrlAbsolute( toUrl ) ) { - // Going to a different page - close immediately - immediate = true; - } else { - e.preventDefault(); - } + this.mouseMoved = false; + this._trigger( "stop" ); + return false; + } + }, + + _preventDocumentDrag: function( event ) { + // NOTE: we don't do this in refresh because we still want to + // support programmatic alteration of disabled inputs + if ( this._trigger( "drag", event ) === false) { + return false; } + if ( this.dragging && !this.options.disabled ) { + + // this.mouseMoved must be updated before refresh() because it will be used in the control "change" event + this.mouseMoved = true; - // remove nav bindings - o.container.unbind( o.closeEvents ); - // unbind click handlers added when history is disabled - this.element.undelegate( o.closeLinkSelector, o.closeLinkEvents ); + if ( this.isToggleSwitch ) { + // make the handle move in sync with the mouse + this.handle.removeClass( "ui-slider-handle-snapping" ); + } + + this.refresh( event ); - this._close( immediate ); + // only after refresh() you can calculate this.userModified + this.userModified = this.beforeStart !== this.element[0].selectedIndex; + return false; + } }, - // any navigation event after a popup is opened should close the popup - // NOTE the pagebeforechange is bound to catch navigation events that don't - // alter the url (eg, dialogs from popups) - _bindContainerClose: function() { - this.options.container - .one( this.options.closeEvents, $.proxy( this, "_closePopup" ) ); - }, + _checkedRefresh: function() { + if ( this.value !== this._value() ) { + this.refresh( this._value() ); + } + }, - // TODO no clear deliniation of what should be here and - // what should be in _open. Seems to be "visual" vs "history" for now - open: function( options ) { - var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory; + _value: function() { + return this.isToggleSwitch ? this.element[0].selectedIndex : parseFloat( this.element.val() ) ; + }, - // make sure open is idempotent - if( $.mobile.popup.active ) { - return; - } - // set the global popup mutex - $.mobile.popup.active = this; - this._scrollTop = $.mobile.window.scrollTop(); + _reset: function() { + this.refresh( undefined, false, true ); + }, - // if history alteration is disabled close on navigate events - // and leave the url as is - if( !( opts.history ) ) { - self._open( options ); - self._bindContainerClose(); + refresh: function( val, isfromControl, preventInputUpdate ) { + // NOTE: we don't return here because we want to support programmatic + // alteration of the input value, which should still update the slider + + var self = this, + parentTheme = $.mobile.getInheritedTheme( this.element, "c" ), + theme = this.options.theme || parentTheme, + trackTheme = this.options.trackTheme || parentTheme, + left, width, data, tol; - // When histoy is disabled we have to grab the data-rel - // back link clicks so we can close the popup instead of - // relying on history to do it for us - self.element - .delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) { - self.close(); - e.preventDefault(); - }); + self.slider[0].className = [ this.isToggleSwitch ? "ui-slider ui-slider-switch" : "ui-slider-track"," ui-btn-down-" + trackTheme,' ui-btn-corner-all', ( this.options.mini ) ? " ui-mini":""].join( "" ); + if ( this.options.disabled || this.element.attr( "disabled" ) ) { + this.disable(); + } - return; - } + // set the stored value for comparison later + this.value = this._value(); + if ( this.options.highlight && !this.isToggleSwitch && this.slider.find( ".ui-slider-bg" ).length === 0 ) { + this.valuebg = (function() { + var bg = document.createElement( "div" ); + bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all"; + return $( bg ).prependTo( self.slider ); + })(); + } + this.handle.buttonMarkup({ corners: true, theme: theme, shadow: true }); - // cache some values for min/readability - urlHistory = $.mobile.urlHistory; - hashkey = $.mobile.dialogHashKey; - activePage = $.mobile.activePage; - currentIsDialog = activePage.is( ".ui-dialog" ); - this._myUrl = url = urlHistory.getActive().url; - hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog && ( urlHistory.activeIndex > 0 ); + var pxStep, percent, + control = this.element, + isInput = !this.isToggleSwitch, + optionElements = isInput ? [] : control.find( "option" ), + min = isInput ? parseFloat( control.attr( "min" ) ) : 0, + max = isInput ? parseFloat( control.attr( "max" ) ) : optionElements.length - 1, + step = ( isInput && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1; + + if ( typeof val === "object" ) { + data = val; + // a slight tolerance helped get to the ends of the slider + tol = 8; - if ( hasHash ) { - self._open( options ); - self._bindContainerClose(); + left = this.slider.offset().left; + width = this.slider.width(); + pxStep = width/((max-min)/step); + if ( !this.dragging || + data.pageX < left - tol || + data.pageX > left + width + tol ) { return; } - - // if the current url has no dialog hash key proceed as normal - // otherwise, if the page is a dialog simply tack on the hash key - if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){ - url = url + (url.indexOf( "#" ) > -1 ? hashkey : "#" + hashkey); + if ( pxStep > 1 ) { + percent = ( ( data.pageX - left ) / width ) * 100; } else { - url = $.mobile.path.parseLocation().hash + hashkey; + percent = Math.round( ( ( data.pageX - left ) / width ) * 100 ); } - - // Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash - if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { - url += hashkey; + } else { + if ( val == null ) { + val = isInput ? parseFloat( control.val() || 0 ) : control[0].selectedIndex; } + percent = ( parseFloat( val ) - min ) / ( max - min ) * 100; + } - // swallow the the initial navigation event, and bind for the next - $(window).one( "beforenavigate", function( e ) { - e.preventDefault(); - self._open( options ); - self._bindContainerClose(); - }); - - this.urlAltered = true; - $.mobile.navigate( url, {role: "dialog"} ); - }, + if ( isNaN( percent ) ) { + return; + } - close: function() { - // make sure close is idempotent - if( $.mobile.popup.active !== this ) { - return; - } + var newval = ( percent / 100 ) * ( max - min ) + min; - this._scrollTop = $.mobile.window.scrollTop(); + //from jQuery UI slider, the following source will round to the nearest step + var valModStep = ( newval - min ) % step; + var alignValue = newval - valModStep; - if( this.options.history && this.urlAltered ) { - $.mobile.back(); - this.urlAltered = false; - } else { - // simulate the nav bindings having fired - this._closePopup(); - } + if ( Math.abs( valModStep ) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); } - }); + var percentPerStep = 100/((max-min)/step); + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see jQueryUI: #4124) + newval = parseFloat( alignValue.toFixed(5) ); - // TODO this can be moved inside the widget - $.mobile.popup.handleLink = function( $link ) { - var closestPage = $link.closest( ":jqmData(role='page')" ), - scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ), - // NOTE make sure to get only the hash, ie7 (wp7) return the absolute href - // in this case ruining the element selection - popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ), - offset; + if ( typeof pxStep === "undefined" ) { + pxStep = width / ( (max-min) / step ); + } + if ( pxStep > 1 && isInput ) { + percent = ( newval - min ) * percentPerStep * ( 1 / step ); + } + if ( percent < 0 ) { + percent = 0; + } - if ( popup.data( "mobile-popup" ) ) { - offset = $link.offset(); - popup.popup( "open", { - x: offset.left + $link.outerWidth() / 2, - y: offset.top + $link.outerHeight() / 2, - transition: $link.jqmData( "transition" ), - positionTo: $link.jqmData( "position-to" ) - }); + if ( percent > 100 ) { + percent = 100; } - //remove after delay - setTimeout( function() { - // Check if we are in a listview - var $parent = $link.parent().parent(); - if ($parent.hasClass("ui-li")) { - $link = $parent.parent(); - } - $link.removeClass( $.mobile.activeBtnClass ); - }, 300 ); - }; + if ( newval < min ) { + newval = min; + } - // TODO move inside _create - $.mobile.document.bind( "pagebeforechange", function( e, data ) { - if ( data.options.role === "popup" ) { - $.mobile.popup.handleLink( data.options.link ); - e.preventDefault(); + if ( newval > max ) { + newval = max; } - }); - $.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.popup.prototype.enhanceWithin( e.target, true ); - }); + this.handle.css( "left", percent + "%" ); -})( jQuery ); + this.handle[0].setAttribute( "aria-valuenow", isInput ? newval : optionElements.eq( newval ).attr( "value" ) ); -(function( $, undefined ) { + this.handle[0].setAttribute( "aria-valuetext", isInput ? newval : optionElements.eq( newval ).getEncodedText() ); -$.widget( "mobile.panel", $.mobile.widget, { - options: { - classes: { - panel: "ui-panel", - panelOpen: "ui-panel-open", - panelClosed: "ui-panel-closed", - panelFixed: "ui-panel-fixed", - panelInner: "ui-panel-inner", - modal: "ui-panel-dismiss", - modalOpen: "ui-panel-dismiss-open", - pagePanel: "ui-page-panel", - pagePanelOpen: "ui-page-panel-open", - contentWrap: "ui-panel-content-wrap", - contentWrapOpen: "ui-panel-content-wrap-open", - contentWrapClosed: "ui-panel-content-wrap-closed", - contentFixedToolbar: "ui-panel-content-fixed-toolbar", - contentFixedToolbarOpen: "ui-panel-content-fixed-toolbar-open", - contentFixedToolbarClosed: "ui-panel-content-fixed-toolbar-closed", - animate: "ui-panel-animate" - }, - animate: true, - theme: "c", - position: "left", - dismissible: true, - display: "reveal", //accepts reveal, push, overlay - initSelector: ":jqmData(role='panel')", - swipeClose: true, - positionFixed: false - }, - - _panelID: null, - _closeLink: null, - _page: null, - _modal: null, - _pannelInner: null, - _wrapper: null, - _fixedToolbar: null, - - _create: function() { - var self = this, - $el = self.element, - page = $el.closest( ":jqmData(role='page')" ), - _getPageTheme = function() { - var $theme = $.data( page[0], "mobilePage" ).options.theme, - $pageThemeClass = "ui-body-" + $theme; - return $pageThemeClass; - }, - _getPanelInner = function() { - var $pannelInner = $el.find( "." + self.options.classes.panelInner ); - if ( $pannelInner.length === 0 ) { - $pannelInner = $el.children().wrapAll( '
' ).parent(); - } - return $pannelInner; - }, - _getWrapper = function() { - var $wrapper = page.find( "." + self.options.classes.contentWrap ); - if ( $wrapper.length === 0 ) { - $wrapper = page.children( ".ui-header:not(:jqmData(position='fixed')), .ui-content:not(:jqmData(role='popup')), .ui-footer:not(:jqmData(position='fixed'))" ).wrapAll( '
' ).parent(); - if ( $.support.cssTransform3d && !!self.options.animate ) { - $wrapper.addClass( self.options.classes.animate ); - } - } - return $wrapper; - }, - _getFixedToolbar = function() { - var $fixedToolbar = page.find( "." + self.options.classes.contentFixedToolbar ); - if ( $fixedToolbar.length === 0 ) { - $fixedToolbar = page.find( ".ui-header:jqmData(position='fixed'), .ui-footer:jqmData(position='fixed')" ).addClass( self.options.classes.contentFixedToolbar ); - if ( $.support.cssTransform3d && !!self.options.animate ) { - $fixedToolbar.addClass( self.options.classes.animate ); - } - } - return $fixedToolbar; - }; - - // expose some private props to other methods - $.extend( this, { - _panelID: $el.attr( "id" ), - _closeLink: $el.find( ":jqmData(rel='close')" ), - _page: $el.closest( ":jqmData(role='page')" ), - _pageTheme: _getPageTheme(), - _pannelInner: _getPanelInner(), - _wrapper: _getWrapper(), - _fixedToolbar: _getFixedToolbar() - }); - - self._addPanelClasses(); - self._wrapper.addClass( this.options.classes.contentWrapClosed ); - self._fixedToolbar.addClass( this.options.classes.contentFixedToolbarClosed ); - // add class to page so we can set "overflow-x: hidden;" for it to fix Android zoom issue - self._page.addClass( self.options.classes.pagePanel ); - - // if animating, add the class to do so - if ( $.support.cssTransform3d && !!self.options.animate ) { - this.element.addClass( self.options.classes.animate ); - } - - self._bindUpdateLayout(); - self._bindCloseEvents(); - self._bindLinkListeners(); - self._bindPageEvents(); + this.handle[0].setAttribute( "title", isInput ? newval : optionElements.eq( newval ).getEncodedText() ); - if ( !!self.options.dismissible ) { - self._createModal(); + if ( this.valuebg ) { + this.valuebg.css( "width", percent + "%" ); } - self._bindSwipeEvents(); - }, - - _createModal: function( options ) { - var self = this; - - self._modal = $( "
" ) - .on( "mousedown", function() { - self.close(); - }) - .appendTo( this._page ); - }, + // drag the label widths + if ( this._labels ) { + var handlePercent = this.handle.width() / this.slider.width() * 100, + aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100, + bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 ); - _getPosDisplayClasses: function( prefix ) { - return prefix + "-position-" + this.options.position + " " + prefix + "-display-" + this.options.display; - }, + this._labels.each(function() { + var ab = $( this ).is( ".ui-slider-label-a" ); + $( this ).width( ( ab ? aPercent : bPercent ) + "%" ); + }); + } - _getPanelClasses: function() { - var panelClasses = this.options.classes.panel + - " " + this._getPosDisplayClasses( this.options.classes.panel ) + - " " + this.options.classes.panelClosed; + if ( !preventInputUpdate ) { + var valueChanged = false; - if ( this.options.theme ) { - panelClasses += " ui-body-" + this.options.theme; - } - if ( !!this.options.positionFixed ) { - panelClasses += " " + this.options.classes.panelFixed; + // update control"s value + if ( isInput ) { + valueChanged = control.val() !== newval; + control.val( newval ); + } else { + valueChanged = control[ 0 ].selectedIndex !== newval; + control[ 0 ].selectedIndex = newval; + } + if ( this._trigger( "beforechange", val ) === false) { + return false; + } + if ( !isfromControl && valueChanged ) { + control.trigger( "change" ); + } } - return panelClasses; - }, - - _addPanelClasses: function() { - this.element.addClass( this._getPanelClasses() ); }, - _bindCloseEvents: function() { - var self = this; - - self._closeLink.on( "click.panel" , function( e ) { - e.preventDefault(); - self.close(); - return false; - }); - self.element.on( "click.panel" , "a:jqmData(ajax='false')", function( e ) { - self.close(); - }); + enable: function() { + this.element.attr( "disabled", false ); + this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); + return this._setOption( "disabled", false ); }, - _positionPanel: function() { - var self = this, - pannelInnerHeight = self._pannelInner.outerHeight(), - expand = pannelInnerHeight > $.mobile.getScreenHeight(); + disable: function() { + this.element.attr( "disabled", true ); + this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true ); + return this._setOption( "disabled", true ); + } - if ( expand || !self.options.positionFixed ) { - if ( expand ) { - self._unfixPanel(); - $.mobile.resetActivePageHeight( pannelInnerHeight ); - } - self._scrollIntoView( pannelInnerHeight ); - } else { - self._fixPanel(); - } - }, +}, $.mobile.behaviors.formReset ) ); - _scrollIntoView: function( pannelInnerHeight ) { - if ( pannelInnerHeight < $( window ).scrollTop() ) { - window.scrollTo( 0, 0 ); - } - }, +//auto self-init widgets +$.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.slider.prototype.enhanceWithin( e.target, true ); +}); - _bindFixListener: function() { - this._on( $( window ), { "throttledresize": "_positionPanel" }); - }, +})( jQuery ); - _unbindFixListener: function() { - this._off( $( window ), "throttledresize" ); - }, +(function( $, undefined ) { + $.widget( "mobile.rangeslider", $.mobile.widget, { - _unfixPanel: function() { - if ( !!this.options.positionFixed && $.support.fixedPosition ) { - this.element.removeClass( this.options.classes.panelFixed ); - } - }, + options: { + theme: null, + trackTheme: null, + disabled: false, + initSelector: ":jqmData(role='rangeslider')", + mini: false, + highlight: true + }, - _fixPanel: function() { - if ( !!this.options.positionFixed && $.support.fixedPosition ) { - this.element.addClass( this.options.classes.panelFixed ); - } - }, - - _bindUpdateLayout: function() { - var self = this; - - self.element.on( "updatelayout", function( e ) { - if ( self._open ) { - self._positionPanel(); + _create: function() { + var secondLabel, + $el = this.element, + elClass = this.options.mini ? "ui-rangeslider ui-mini" : "ui-rangeslider", + _inputFirst = $el.find( "input" ).first(), + _inputLast = $el.find( "input" ).last(), + label = $el.find( "label" ).first(), + _sliderFirst = $.data( _inputFirst.get(0), "mobileSlider" ).slider, + _sliderLast = $.data( _inputLast.get(0), "mobileSlider" ).slider, + firstHandle = $.data( _inputFirst.get(0), "mobileSlider" ).handle, + _sliders = $( "
" ).appendTo( $el ); + + if ( $el.find( "label" ).length > 1 ) { + secondLabel = $el.find( "label" ).last().hide(); } - }); - }, - _bindLinkListeners: function() { - var self = this; + _inputFirst.addClass( "ui-rangeslider-first" ); + _inputLast.addClass( "ui-rangeslider-last" ); + $el.addClass( elClass ); + + _sliderFirst.appendTo( _sliders ); + _sliderLast.appendTo( _sliders ); + label.prependTo( $el ); + firstHandle.prependTo( _sliderLast ); - self._page.on( "click.panel" , "a", function( e ) { - if ( this.href.split( "#" )[ 1 ] === self._panelID && self._panelID !== undefined ) { - e.preventDefault(); - var $link = $( this ); - if ( ! $link.hasClass( "ui-link" ) ) { - $link.addClass( $.mobile.activeBtnClass ); - self.element.one( "panelopen panelclose", function() { - $link.removeClass( $.mobile.activeBtnClass ); - }); - } - self.toggle(); + $.extend( this, { + _inputFirst: _inputFirst, + _inputLast: _inputLast, + _sliderFirst: _sliderFirst, + _sliderLast: _sliderLast, + _targetVal: null, + _sliderTarget: false, + _sliders: _sliders, + _proxy: false + }); + + this.refresh(); + this._on( this.element.find( "input.ui-slider-input" ), { + "slidebeforestart": "_slidebeforestart", + "slidestop": "_slidestop", + "slidedrag": "_slidedrag", + "slidebeforechange": "_change", + "blur": "_change", + "keyup": "_change" + }); + this._on({ + "mousedown":"_change" + }); + this._on( this.element.closest( "form" ), { + "reset":"_handleReset" + }); + this._on( firstHandle, { + "vmousedown": "_dragFirstHandle" + }); + }, + _handleReset: function(){ + var self = this; + //we must wait for the stack to unwind before updateing other wise sliders will not have updated yet + setTimeout( function(){ + self._updateHighlight(); + },0); + }, + + _dragFirstHandle: function( event ) { + //if the first handle is dragged send the event to the first slider + $.data( this._inputFirst.get(0), "mobileSlider" ).dragging = true; + $.data( this._inputFirst.get(0), "mobileSlider" ).refresh( event ); + return false; + }, + + _slidedrag: function( event ) { + var first = $( event.target ).is( this._inputFirst ), + otherSlider = ( first ) ? this._inputLast : this._inputFirst; + + this._sliderTarget = false; + //if the drag was initiated on an extreme and the other handle is focused send the events to + //the closest handle + if ( ( this._proxy === "first" && first ) || ( this._proxy === "last" && !first ) ) { + $.data( otherSlider.get(0), "mobileSlider" ).dragging = true; + $.data( otherSlider.get(0), "mobileSlider" ).refresh( event ); return false; } - }); - }, - - _bindSwipeEvents: function() { - var self = this, - area = self._modal ? self.element.add( self._modal ) : self.element; - - // on swipe, close the panel - if( !!self.options.swipeClose ) { - if ( self.options.position === "left" ) { - area.on( "swipeleft.panel", function( e ) { - self.close(); - }); - } else { - area.on( "swiperight.panel", function( e ) { - self.close(); - }); - } - } - }, + }, - _bindPageEvents: function() { - var self = this; + _slidestop: function( event ) { + var first = $( event.target ).is( this._inputFirst ); - self._page - // Close the panel if another panel on the page opens - .on( "panelbeforeopen", function( e ) { - if ( self._open && e.target !== self.element[ 0 ] ) { - self.close(); - } - }) - // clean up open panels after page hide - .on( "pagehide", function( e ) { - if ( self._open ) { - self.close( true ); - } - }) - // on escape, close? might need to have a target check too... - .on( "keyup.panel", function( e ) { - if ( e.keyCode === 27 && self._open ) { - self.close(); - } - }); - }, + this._proxy = false; + //this stops dragging of the handle and brings the active track to the front + //this makes clicks on the track go the the last handle used + this.element.find( "input" ).trigger( "vmouseup" ); + this._sliderFirst.css( "z-index", first ? 1 : "" ); + }, - // state storage of open or closed - _open: false, + _slidebeforestart: function( event ) { + this._sliderTarget = false; + //if the track is the target remember this and the original value + if ( $( event.originalEvent.target ).hasClass( "ui-slider-track" ) ) { + this._sliderTarget = true; + this._targetVal = $( event.target ).val(); + } + }, - _contentWrapOpenClasses: null, - _fixedToolbarOpenClasses: null, - _modalOpenClasses: null, + _setOption: function( options ) { + this._superApply( options ); + this.refresh(); + }, - open: function( immediate ) { - if ( !this._open ) { - var self = this, - o = self.options, - _openPanel = function() { - self._page.off( "panelclose" ); - self._page.jqmData( "panel", "open" ); - - if ( !immediate && $.support.cssTransform3d && !!o.animate ) { - self.element.add( self._wrapper ).on( self._transitionEndEvents, complete ); - } else { - setTimeout( complete, 0 ); - } - - if ( self.options.theme && self.options.display !== "overlay" ) { - self._page - .removeClass( self._pageTheme ) - .addClass( "ui-body-" + self.options.theme ); - } - - self.element.removeClass( o.classes.panelClosed ).addClass( o.classes.panelOpen ); - - self._contentWrapOpenClasses = self._getPosDisplayClasses( o.classes.contentWrap ); - self._wrapper - .removeClass( o.classes.contentWrapClosed ) - .addClass( self._contentWrapOpenClasses + " " + o.classes.contentWrapOpen ); - - self._fixedToolbarOpenClasses = self._getPosDisplayClasses( o.classes.contentFixedToolbar ); - self._fixedToolbar - .removeClass( o.classes.contentFixedToolbarClosed ) - .addClass( self._fixedToolbarOpenClasses + " " + o.classes.contentFixedToolbarOpen ); - - self._modalOpenClasses = self._getPosDisplayClasses( o.classes.modal ) + " " + o.classes.modalOpen; - if ( self._modal ) { - self._modal.addClass( self._modalOpenClasses ); - } - }, - complete = function() { - self.element.add( self._wrapper ).off( self._transitionEndEvents, complete ); + refresh: function() { + var $el = this.element, + o = this.options; - self._page.addClass( o.classes.pagePanelOpen ); - - self._positionPanel(); - self._bindFixListener(); - - self._trigger( "open" ); - }; + $el.find( "input" ).slider({ + theme: o.theme, + trackTheme: o.trackTheme, + disabled: o.disabled, + mini: o.mini, + highlight: o.highlight + }).slider( "refresh" ); + this._updateHighlight(); + }, - if ( this.element.closest( ".ui-page-active" ).length < 0 ) { - immediate = true; + _change: function( event ) { + if ( event.type === "keyup" ) { + this._updateHighlight(); + return false; } + + var self = this, + min = parseFloat( this._inputFirst.val(), 10 ), + max = parseFloat( this._inputLast.val(), 10 ), + first = $( event.target ).hasClass( "ui-rangeslider-first" ), + thisSlider = first ? this._inputFirst : this._inputLast, + otherSlider = first ? this._inputLast : this._inputFirst; - self._trigger( "beforeopen" ); - if ( self._page.jqmData('panel') === "open" ) { - self._page.on( "panelclose", function() { - _openPanel(); - }); + if( ( this._inputFirst.val() > this._inputLast.val() && event.type === "mousedown" && !$(event.target).hasClass("ui-slider-handle")) ){ + thisSlider.blur(); + } else if( event.type === "mousedown" ){ + return; + } + if ( min > max && !this._sliderTarget ) { + //this prevents min from being greater then max + thisSlider.val( first ? max: min ).slider( "refresh" ); + this._trigger( "normalize" ); + } else if ( min > max ) { + //this makes it so clicks on the target on either extreme go to the closest handle + thisSlider.val( this._targetVal ).slider( "refresh" ); + + //You must wait for the stack to unwind so first slider is updated before updating second + setTimeout( function() { + otherSlider.val( first ? min: max ).slider( "refresh" ); + $.data( otherSlider.get(0), "mobileSlider" ).handle.focus(); + self._sliderFirst.css( "z-index", first ? "" : 1 ); + self._trigger( "normalize" ); + }, 0 ); + this._proxy = ( first ) ? "first" : "last"; + } + //fixes issue where when both _sliders are at min they cannot be adjusted + if ( min === max ) { + $.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", 1 ); + $.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", 0 ); } else { - _openPanel(); + $.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" ); + $.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" ); } - self._open = true; + this._updateHighlight(); + + if ( min >= max ) { + return false; + } + }, + + _updateHighlight: function() { + var min = parseInt( $.data( this._inputFirst.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ), + max = parseInt( $.data( this._inputLast.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ), + width = (max - min); + + this.element.find( ".ui-slider-bg" ).css({ + "margin-left": min + "%", + "width": width + "%" + }); + }, + + _destroy: function() { + this.element.removeClass( "ui-rangeslider ui-mini" ).find( "label" ).show(); + this._inputFirst.after( this._sliderFirst ); + this._inputLast.after( this._sliderLast ); + this._sliders.remove(); + this.element.find( "input" ).removeClass( "ui-rangeslider-first ui-rangeslider-last" ).slider( "destroy" ); } - }, - close: function( immediate ) { - if ( this._open ) { - var o = this.options, - self = this, - _closePanel = function() { - if ( !immediate && $.support.cssTransform3d && !!o.animate ) { - self.element.add( self._wrapper ).on( self._transitionEndEvents, complete ); - } else { - setTimeout( complete, 0 ); - } - - self._page.removeClass( o.classes.pagePanelOpen ); - self.element.removeClass( o.classes.panelOpen ); - self._wrapper.removeClass( o.classes.contentWrapOpen ); - self._fixedToolbar.removeClass( o.classes.contentFixedToolbarOpen ); - - if ( self._modal ) { - self._modal.removeClass( self._modalOpenClasses ); - } - }, - complete = function() { - if ( self.options.theme && self.options.display !== "overlay" ) { - self._page.removeClass( "ui-body-" + self.options.theme ).addClass( self._pageTheme ); - } - self.element.add( self._wrapper ).off( self._transitionEndEvents, complete ); - self.element.addClass( o.classes.panelClosed ); - - self._wrapper - .removeClass( self._contentWrapOpenClasses ) - .addClass( o.classes.contentWrapClosed ); - - self._fixedToolbar - .removeClass( self._fixedToolbarOpenClasses ) - .addClass( o.classes.contentFixedToolbarClosed ); - - self._fixPanel(); - self._unbindFixListener(); - $.mobile.resetActivePageHeight(); - - self._page.jqmRemoveData( "panel" ); - self._trigger( "close" ); - }; - - if ( this.element.closest( ".ui-page-active" ).length < 0 ) { - immediate = true; - } - self._trigger( "beforeclose" ); - - _closePanel(); - - self._open = false; - } - }, - - toggle: function( options ) { - this[ this._open ? "close" : "open" ](); - }, - - _transitionEndEvents: "webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd", - - _destroy: function() { - var classes = this.options.classes, - theme = this.options.theme, - hasOtherSiblingPanels = this.element.siblings( "." + classes.panel ).length; - - // create - if ( !hasOtherSiblingPanels ) { - this._wrapper.children().unwrap(); - this._page.find( "a" ).unbind( "panelopen panelclose" ); - this._page.removeClass( classes.pagePanel ); - if ( this._open ) { - this._page.jqmRemoveData( "panel" ); - this._page.removeClass( classes.pagePanelOpen ); - if ( theme ) { - this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme ); - } - $.mobile.resetActivePageHeight(); - } - } else if ( this._open ) { - this._wrapper.removeClass( classes.contentWrapOpen ); - this._fixedToolbar.removeClass( classes.contentFixedToolbarOpen ); - this._page.jqmRemoveData( "panel" ); - this._page.removeClass( classes.pagePanelOpen ); - if ( theme ) { - this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme ); - } - } - - this._pannelInner.children().unwrap(); - - this.element.removeClass( [ this._getPanelClasses(), classes.panelAnimate ].join( " " ) ) - .off( "swipeleft.panel swiperight.panel" ) - .off( "panelbeforeopen" ) - .off( "panelhide" ) - .off( "keyup.panel" ) - .off( "updatelayout" ); - - this._closeLink.off( "click.panel" ); - - if ( this._modal ) { - this._modal.remove(); - } + }); - // open and close - this.element.off( this._transitionEndEvents ) - .removeClass( [ classes.panelUnfixed, classes.panelClosed, classes.panelOpen ].join( " " ) ); - } -}); +$.widget( "mobile.rangeslider", $.mobile.rangeslider, $.mobile.behaviors.formReset ); //auto self-init widgets $( document ).bind( "pagecreate create", function( e ) { - $.mobile.panel.prototype.enhanceWithin( e.target ); + $.mobile.rangeslider.prototype.enhanceWithin( e.target, true ); }); })( jQuery ); (function( $, undefined ) { -$.widget( "mobile.table", $.mobile.widget, { - - options: { - classes: { - table: "ui-table" - }, - initSelector: ":jqmData(role='table')" - }, +$.widget( "mobile.selectmenu", $.mobile.widget, $.extend( { + options: { + theme: null, + disabled: false, + icon: "arrow-d", + iconpos: "right", + inline: false, + corners: true, + shadow: true, + iconshadow: true, + overlayTheme: "a", + dividerTheme: "b", + hidePlaceholderMenuItems: true, + closeText: "Close", + nativeMenu: true, + // This option defaults to true on iOS devices. + preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, + initSelector: "select:not( :jqmData(role='slider') )", + mini: false + }, - _create: function() { + _button: function() { + return $( "
" ); + }, - var self = this, - trs = this.element.find( "thead tr" ); + _setDisabled: function( value ) { + this.element.attr( "disabled", value ); + this.button.attr( "aria-disabled", value ); + return this._setOption( "disabled", value ); + }, - this.element.addClass( this.options.classes.table ); + _focusButton : function() { + var self = this; - // Expose headers and allHeaders properties on the widget - // headers references the THs within the first TR in the table - self.headers = this.element.find( "tr:eq(0)" ).children(); + setTimeout( function() { + self.button.focus(); + }, 40); + }, - // allHeaders references headers, plus all THs in the thead, which may include several rows, or not - self.allHeaders = self.headers.add( trs.children() ); + _selectOptions: function() { + return this.select.find( "option" ); + }, - trs.each(function(){ + // setup items that are generally necessary for select menu extension + _preExtension: function() { + var classes = ""; + // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 + /* if ( $el[0].className.length ) { + classes = $el[0].className; + } */ + if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) { + classes = " ui-btn-left"; + } - var coltally = 0; + if ( !!~this.element[0].className.indexOf( "ui-btn-right" ) ) { + classes = " ui-btn-right"; + } - $( this ).children().each(function( i ){ + this.select = this.element.removeClass( "ui-btn-left ui-btn-right" ).wrap( "
" ); + this.selectID = this.select.attr( "id" ); + this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" ); + this.isMultiple = this.select[ 0 ].multiple; + if ( !this.options.theme ) { + this.options.theme = $.mobile.getInheritedTheme( this.select, "c" ); + } + }, - var span = parseInt( $( this ).attr( "colspan" ), 10 ), - sel = ":nth-child(" + ( coltally + 1 ) + ")"; - - $( this ) - .jqmData( "colstart", coltally + 1 ); + _destroy: function() { + var wrapper = this.element.parents( ".ui-select" ); + if ( wrapper.length > 0 ) { + if ( wrapper.is( ".ui-btn-left, .ui-btn-right" ) ) { + this.element.addClass( wrapper.is( ".ui-btn-left" ) ? "ui-btn-left" : "ui-btn-right" ); + } + this.element.insertAfter( wrapper ); + wrapper.remove(); + } + }, - if( span ){ - for( var j = 0; j < span - 1; j++ ){ - coltally++; - sel += ", :nth-child(" + ( coltally + 1 ) + ")"; - } - } + _create: function() { + this._preExtension(); - // Store "cells" data on header as a reference to all cells in the same column as this TH - $( this ) - .jqmData( "cells", self.element.find( "tr" ).not( trs.eq(0) ).not( this ).children( sel ) ); + // Allows for extension of the native select for custom selects and other plugins + // see select.custom for example extension + // TODO explore plugin registration + this._trigger( "beforeCreate" ); - coltally++; + this.button = this._button(); - }); + var self = this, - }); + options = this.options, - } + inline = options.inline || this.select.jqmData( "inline" ), + mini = options.mini || this.select.jqmData( "mini" ), + iconpos = options.icon ? ( options.iconpos || this.select.jqmData( "iconpos" ) ) : false, -}); + // IE throws an exception at options.item() function when + // there is no selected item + // select first in this case + selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex, -//auto self-init widgets -$.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.table.prototype.enhanceWithin( e.target ); -}); + // TODO values buttonId and menuId are undefined here + button = this.button + .insertBefore( this.select ) + .buttonMarkup( { + theme: options.theme, + icon: options.icon, + iconpos: iconpos, + inline: inline, + corners: options.corners, + shadow: options.shadow, + iconshadow: options.iconshadow, + mini: mini + }); -})( jQuery ); + this.setButtonText(); + // 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 ( options.nativeMenu && window.opera && window.opera.version ) { + button.addClass( "ui-select-nativeonly" ); + } -(function( $, undefined ) { + // Add counter for multi selects + if ( this.isMultiple ) { + this.buttonCount = $( "" ) + .addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" ) + .hide() + .appendTo( button.addClass('ui-li-has-count') ); + } -$.mobile.table.prototype.options.mode = "columntoggle"; + // Disable if specified + if ( options.disabled || this.element.attr('disabled')) { + this.disable(); + } -$.mobile.table.prototype.options.columnBtnTheme = null; + // Events on native select + this.select.change(function() { + self.refresh(); + + if ( !!options.nativeMenu ) { + this.blur(); + } + }); -$.mobile.table.prototype.options.columnPopupTheme = null; + this._handleFormReset(); -$.mobile.table.prototype.options.columnBtnText = "Columns..."; + this.build(); + }, -$.mobile.table.prototype.options.classes = $.extend( - $.mobile.table.prototype.options.classes, - { - popup: "ui-table-columntoggle-popup", - columnBtn: "ui-table-columntoggle-btn", - priorityPrefix: "ui-table-priority-", - columnToggleTable: "ui-table-columntoggle" - } -); + build: function() { + var self = this; -$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate", function() { + this.select + .appendTo( self.button ) + .bind( "vmousedown", function() { + // Add active class to button + self.button.addClass( $.mobile.activeBtnClass ); + }) + .bind( "focus", function() { + self.button.addClass( $.mobile.focusClass ); + }) + .bind( "blur", function() { + self.button.removeClass( $.mobile.focusClass ); + }) + .bind( "focus vmouseover", function() { + self.button.trigger( "vmouseover" ); + }) + .bind( "vmousemove", function() { + // Remove active class on scroll/touchmove + self.button.removeClass( $.mobile.activeBtnClass ); + }) + .bind( "change blur vmouseout", function() { + self.button.trigger( "vmouseout" ) + .removeClass( $.mobile.activeBtnClass ); + }) + .bind( "change blur", function() { + self.button.removeClass( "ui-btn-down-" + self.options.theme ); + }); - var $table = $( this ), - self = $table.data( "mobile-table" ), - o = self.options, - ns = $.mobile.ns; + // In many situations, iOS will zoom into the select upon tap, this prevents that from happening + self.button.bind( "vmousedown", function() { + if ( self.options.preventFocusZoom ) { + $.mobile.zoom.disable( true ); + } + }); + self.label.bind( "click focus", function() { + if ( self.options.preventFocusZoom ) { + $.mobile.zoom.disable( true ); + } + }); + self.select.bind( "focus", function() { + if ( self.options.preventFocusZoom ) { + $.mobile.zoom.disable( true ); + } + }); + self.button.bind( "mouseup", function() { + if ( self.options.preventFocusZoom ) { + setTimeout(function() { + $.mobile.zoom.enable( true ); + }, 0 ); + } + }); + self.select.bind( "blur", function() { + if ( self.options.preventFocusZoom ) { + $.mobile.zoom.enable( true ); + } + }); - if( o.mode !== "columntoggle" ){ - return; - } + }, - self.element.addClass( o.classes.columnToggleTable ); + selected: function() { + return this._selectOptions().filter( ":selected" ); + }, - var id = ( $table.attr( "id" ) || o.classes.popup ) + "-popup", //TODO BETTER FALLBACK ID HERE - $menuButton = $( "" + o.columnBtnText + "" ), - $popup = $( "
"), - $menu = $("
"); + selectedIndices: function() { + var self = this; - // create the hide/show toggles - self.headers.not( "td" ).each(function(){ + return this.selected().map(function() { + return self._selectOptions().index( this ); + }).get(); + }, - var priority = $( this ).jqmData( "priority" ), - $cells = $( this ).add( $( this ).jqmData( "cells" ) ); + setButtonText: function() { + var self = this, + selected = this.selected(), + text = this.placeholder, + span = $( document.createElement( "span" ) ); - if( priority ){ + this.button.find( ".ui-btn-text" ).html(function() { + if ( selected.length ) { + text = selected.map(function() { + return $( this ).text(); + }).get().join( ", " ); + } else { + text = self.placeholder; + } - $cells.addClass( o.classes.priorityPrefix + priority ); + // TODO possibly aggregate multiple select option classes + return span.text( text ) + .addClass( self.select.attr( "class" ) ) + .addClass( selected.attr( "class" ) ); + }); + }, - $("" ) - .appendTo( $menu ) - .children( 0 ) - .jqmData( "cells", $cells ) - .checkboxradio({ - theme: o.columnPopupTheme - }); - } - }); - $menu.appendTo( $popup ); + setButtonCount: function() { + var selected = this.selected(); - // bind change event listeners to inputs - TODO: move to a private method? - $menu.on( "change", "input", function( e ){ - if( this.checked ){ - $( this ).jqmData( "cells" ).removeClass( "ui-table-cell-hidden" ).addClass( "ui-table-cell-visible" ); - } - else { - $( this ).jqmData( "cells" ).removeClass( "ui-table-cell-visible" ).addClass( "ui-table-cell-hidden" ); + // multiple count inside button + if ( this.isMultiple ) { + this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length ); } - }); + }, - $menuButton - .insertBefore( $table ) - .buttonMarkup({ - theme: o.columnBtnTheme - }); + _reset: function() { + this.refresh(); + }, - $popup - .insertBefore( $table ) - .popup(); + refresh: function() { + this.setButtonText(); + this.setButtonCount(); + }, - // refresh method - self.refresh = function(){ - $menu.find( "input" ).each( function(){ - this.checked = $( this ).jqmData( "cells" ).eq(0).css( "display" ) === "table-cell"; - $( this ).checkboxradio( "refresh" ); - }); - }; + // open and close preserved in native selects + // to simplify users code when looping over selects + open: $.noop, + close: $.noop, - $.mobile.window.on( "throttledresize", self.refresh ); + disable: function() { + this._setDisabled( true ); + this.button.addClass( "ui-disabled" ); + }, - self.refresh(); + enable: function() { + this._setDisabled( false ); + this.button.removeClass( "ui-disabled" ); + } +}, $.mobile.behaviors.formReset ) ); +//auto self-init widgets +$.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.selectmenu.prototype.enhanceWithin( e.target, true ); }); - })( jQuery ); (function( $, undefined ) { -$.mobile.table.prototype.options.mode = "reflow"; + function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { + var ret = desired; -$.mobile.table.prototype.options.classes = $.extend( - $.mobile.table.prototype.options.classes, - { - reflowTable: "ui-table-reflow", - cellLabels: "ui-table-cell-label" - } -); + if ( winSize < segSize ) { + // Center segment if it's bigger than the window + ret = offset + ( winSize - segSize ) / 2; + } else { + // Otherwise center it at the desired coordinate while keeping it completely inside the window + ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize ); + } -$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate", function() { + return ret; + } - var $table = $( this ), - self = $table.data( "mobile-table" ), - o = self.options; + function windowCoords() { + var $win = $.mobile.window; - // If it's not reflow mode, return here. - if( o.mode !== "reflow" ){ - return; + return { + x: $win.scrollLeft(), + y: $win.scrollTop(), + cx: ( window.innerWidth || $win.width() ), + cy: ( window.innerHeight || $win.height() ) + }; } - self.element.addClass( o.classes.reflowTable ); - - // get headers in reverse order so that top-level headers are appended last - var reverseHeaders = $( self.allHeaders.get().reverse() ); + $.widget( "mobile.popup", $.mobile.widget, { + options: { + theme: null, + overlayTheme: null, + shadow: true, + corners: true, + transition: "none", + positionTo: "origin", + tolerance: null, + initSelector: ":jqmData(role='popup')", + closeLinkSelector: "a:jqmData(rel='back')", + closeLinkEvents: "click.popup", + navigateEvents: "navigate.popup", + closeEvents: "navigate.popup pagebeforechange.popup", + dismissible: true, - // create the hide/show toggles - reverseHeaders.each(function(i){ - var $cells = $( this ).jqmData( "cells" ), - colstart = $( this ).jqmData( "colstart" ), - hierarchyClass = $cells.not( this ).filter( "thead th" ).length && " ui-table-cell-label-top", - text = $(this).text(); + // NOTE Windows Phone 7 has a scroll position caching issue that + // requires us to disable popup history management by default + // https://github.com/jquery/jquery-mobile/issues/4784 + // + // NOTE this option is modified in _create! + history: !$.mobile.browser.oldIE + }, - if( text !== "" ){ + _eatEventAndClose: function( e ) { + e.preventDefault(); + e.stopImmediatePropagation(); + if ( this.options.dismissible ) { + this.close(); + } + return false; + }, - if( hierarchyClass ){ - var iteration = parseInt( $( this ).attr( "colspan" ), 10 ), - filter = ""; + // Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height + _resizeScreen: function() { + var popupHeight = this._ui.container.outerHeight( true ); - if( iteration ){ - filter = "td:nth-child("+ iteration +"n + " + ( colstart ) +")"; - } - $cells.filter( filter ).prepend( "" + text + "" ); - } - else { - $cells.prepend( "" + text + "" ); - } + this._ui.screen.removeAttr( "style" ); + if ( popupHeight > this._ui.screen.height() ) { + this._ui.screen.height( popupHeight ); + } + }, + _handleWindowKeyUp: function( e ) { + if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) { + return this._eatEventAndClose( e ); } - }); + }, -}); + _expectResizeEvent: function() { + var winCoords = windowCoords(); -})( jQuery ); + if ( this._resizeData ) { + if ( winCoords.x === this._resizeData.winCoords.x && + winCoords.y === this._resizeData.winCoords.y && + winCoords.cx === this._resizeData.winCoords.cx && + winCoords.cy === this._resizeData.winCoords.cy ) { + // timeout not refreshed + return false; + } else { + // clear existing timeout - it will be refreshed below + clearTimeout( this._resizeData.timeoutId ); + } + } -(function( $ ) { - var meta = $( "meta[name=viewport]" ), - initialContent = meta.attr( "content" ), - disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no", - enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes", - disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent ); + this._resizeData = { + timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), + winCoords: winCoords + }; - $.mobile.zoom = $.extend( {}, { - enabled: !disabledInitially, - locked: false, - disable: function( lock ) { - if ( !disabledInitially && !$.mobile.zoom.locked ) { - meta.attr( "content", disabledZoom ); - $.mobile.zoom.enabled = false; - $.mobile.zoom.locked = lock || false; - } + return true; }, - enable: function( unlock ) { - if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) { - meta.attr( "content", enabledZoom ); - $.mobile.zoom.enabled = true; - $.mobile.zoom.locked = false; + + _resizeTimeout: function() { + if ( this._isOpen ) { + if ( !this._expectResizeEvent() ) { + if ( this._ui.container.hasClass( "ui-popup-hidden" ) ) { + // effectively rapid-open the popup while leaving the screen intact + this._ui.container.removeClass( "ui-popup-hidden" ); + this.reposition( { positionTo: "window" } ); + this._ignoreResizeEvents(); + } + + this._resizeScreen(); + this._resizeData = null; + this._orientationchangeInProgress = false; + } + } else { + this._resizeData = null; + this._orientationchangeInProgress = false; } }, - restore: function() { - if ( !disabledInitially ) { - meta.attr( "content", initialContent ); - $.mobile.zoom.enabled = true; - } - } - }); -}( jQuery )); + _ignoreResizeEvents: function() { + var self = this; -(function( $, undefined ) { + if ( this._ignoreResizeTo ) { + clearTimeout( this._ignoreResizeTo ); + } + this._ignoreResizeTo = setTimeout( function() { self._ignoreResizeTo = 0; }, 1000 ); + }, -$.widget( "mobile.textinput", $.mobile.widget, { - options: { - theme: null, - mini: false, - // This option defaults to true on iOS devices. - preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, - initSelector: "input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type]), input[type='file']", - clearBtn: false, - clearSearchButtonText: null, //deprecating for 1.3... - clearBtnText: "clear text", - disabled: false - }, + _handleWindowResize: function( e ) { + if ( this._isOpen && this._ignoreResizeTo === 0 ) { + if ( ( this._expectResizeEvent() || this._orientationchangeInProgress ) && + !this._ui.container.hasClass( "ui-popup-hidden" ) ) { + // effectively rapid-close the popup while leaving the screen intact + this._ui.container + .addClass( "ui-popup-hidden" ) + .removeAttr( "style" ); + } + } + }, - _create: function() { + _handleWindowOrientationchange: function( e ) { + if ( !this._orientationchangeInProgress && this._isOpen && this._ignoreResizeTo === 0 ) { + this._expectResizeEvent(); + this._orientationchangeInProgress = true; + } + }, - var self = this, - input = this.element, - o = this.options, - theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ), - themeclass = " ui-body-" + theme, - miniclass = o.mini ? " ui-mini" : "", - isSearch = input.is( "[type='search'], :jqmData(type='search')" ), - focusedEl, - clearbtn, - clearBtnText = o.clearSearchButtonText || o.clearBtnText, - clearBtnBlacklist = input.is( "textarea, :jqmData(type='range')" ), - inputNeedsClearBtn = !!o.clearBtn && !clearBtnBlacklist, - inputNeedsWrap = input.is( "input" ) && !input.is( ":jqmData(type='range')" ); + // When the popup is open, attempting to focus on an element that is not a + // child of the popup will redirect focus to the popup + _handleDocumentFocusIn: function( e ) { + var tgt = e.target, $tgt, ui = this._ui; - function toggleClear() { - setTimeout( function() { - clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); - }, 0 ); - } + if ( !this._isOpen ) { + return; + } - $( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" ); + if ( tgt !== ui.container[ 0 ] ) { + $tgt = $( e.target ); + if ( 0 === $tgt.parents().filter( ui.container[ 0 ] ).length ) { + $( document.activeElement ).one( "focus", function( e ) { + $tgt.blur(); + }); + ui.focusElement.focus(); + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } else if ( ui.focusElement[ 0 ] === ui.container[ 0 ] ) { + ui.focusElement = $tgt; + } + } - focusedEl = input.addClass( "ui-input-text ui-body-"+ theme ); + this._ignoreResizeEvents(); + }, - // XXX: Temporary workaround for issue 785 (Apple bug 8910589). - // Turn off autocorrect and autocomplete on non-iOS 5 devices - // since the popup they use can't be dismissed by the user. Note - // that we test for the presence of the feature by looking for - // the autocorrect property on the input element. We currently - // have no test for iOS 5 or newer so we're temporarily using - // the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas - if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) { - // Set the attribute instead of the property just in case there - // is code that attempts to make modifications via HTML. - input[0].setAttribute( "autocorrect", "off" ); - input[0].setAttribute( "autocomplete", "off" ); - } + _create: function() { + var ui = { + screen: $( "
" ), + placeholder: $( "
" ), + container: $( "
" ) + }, + thisPage = this.element.closest( ".ui-page" ), + myId = this.element.attr( "id" ), + self = this; - //"search" and "text" input widgets - if ( isSearch ) { - focusedEl = input.wrap( "" ).parent(); - } else if ( inputNeedsWrap ) { - focusedEl = input.wrap( "
" ).parent(); - } + // We need to adjust the history option to be false if there's no AJAX nav. + // We can't do it in the option declarations because those are run before + // it is determined whether there shall be AJAX nav. + this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled; - if( inputNeedsClearBtn || isSearch ) { - clearbtn = $( "" + clearBtnText + "" ) - .bind( "click", function( event ) { - input - .val( "" ) - .focus() - .trigger( "change" ); - clearbtn.addClass( "ui-input-clear-hidden" ); - event.preventDefault(); - }) - .appendTo( focusedEl ) - .buttonMarkup({ - icon: "delete", - iconpos: "notext", - corners: true, - shadow: true, - mini: o.mini - }); - - if ( !isSearch ) { - focusedEl.addClass( "ui-input-has-clear" ); + if ( thisPage.length === 0 ) { + thisPage = $( "body" ); } - toggleClear(); - - input.bind( "paste cut keyup input focus change blur", toggleClear ); - } - else if ( !inputNeedsWrap && !isSearch ) { - input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass ); - } - - input.focus(function() { - // In many situations, iOS will zoom into the input upon tap, this prevents that from happening - if ( o.preventFocusZoom ) { - $.mobile.zoom.disable( true ); - } - focusedEl.addClass( $.mobile.focusClass ); - }) - .blur(function() { - focusedEl.removeClass( $.mobile.focusClass ); - if ( o.preventFocusZoom ) { - $.mobile.zoom.enable( true ); - } - }) - - // Autogrow - if ( input.is( "textarea" ) ) { - var extraLineHeight = 15, - keyupTimeoutBuffer = 100, - keyupTimeout; + // define the container for navigation event bindings + // TODO this would be nice at the the mobile widget level + this.options.container = this.options.container || $.mobile.pageContainer; - this._keyup = function() { - var scrollHeight = input[ 0 ].scrollHeight, - clientHeight = input[ 0 ].clientHeight; + // Apply the proto + thisPage.append( ui.screen ); + ui.container.insertAfter( ui.screen ); + // Leave a placeholder where the element used to be + ui.placeholder.insertAfter( this.element ); + if ( myId ) { + ui.screen.attr( "id", myId + "-screen" ); + ui.container.attr( "id", myId + "-popup" ); + ui.placeholder.html( "" ); + } + ui.container.append( this.element ); + ui.focusElement = ui.container; - if ( clientHeight < scrollHeight ) { - input.height( scrollHeight + extraLineHeight ); - } - }; + // Add class to popup element + this.element.addClass( "ui-popup" ); - input.on( "keyup change input paste", function() { - clearTimeout( keyupTimeout ); - keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer ); - }); - - // binding to pagechange here ensures that for pages loaded via - // ajax the height is recalculated without user input - this._on( $.mobile.document, { "pagechange": "_keyup" }); + // Define instance variables + $.extend( this, { + _scrollTop: 0, + _page: thisPage, + _ui: ui, + _fallbackTransition: "", + _currentTransition: false, + _prereqs: null, + _isOpen: false, + _tolerance: null, + _resizeData: null, + _ignoreResizeTo: 0, + _orientationchangeInProgress: false + }); - // Issue 509: the browser is not providing scrollHeight properly until the styles load - if ( $.trim( input.val() ) ) { - // bind to the window load to make sure the height is calculated based on BOTH - // the DOM and CSS - this._on( $.mobile.window, {"load": "_keyup"}); - } - } - if ( input.attr( "disabled" ) ) { - this.disable(); - } - }, + $.each( this.options, function( key, value ) { + // Cause initial options to be applied by their handler by temporarily setting the option to undefined + // - the handler then sets it to the initial value + self.options[ key ] = undefined; + self._setOption( key, value, true ); + }); - disable: function() { - var $el, - isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ), - inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ), - parentNeedsDisabled = this.element.attr( "disabled", true ) && ( inputNeedsWrap || isSearch ); - - if ( parentNeedsDisabled ) { - $el = this.element.parent(); - } else { - $el = this.element; - } - $el.addClass( "ui-disabled" ); - return this._setOption( "disabled", true ); - }, + ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); - enable: function() { - var $el, - isSearch = this.element.is( "[type='search'], :jqmData(type='search')" ), - inputNeedsWrap = this.element.is( "input" ) && !this.element.is( ":jqmData(type='range')" ), - parentNeedsEnabled = this.element.attr( "disabled", false ) && ( inputNeedsWrap || isSearch ); + this._on( $.mobile.window, { + orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), + resize: $.proxy( this, "_handleWindowResize" ), + keyup: $.proxy( this, "_handleWindowKeyUp" ) + }); + this._on( $.mobile.document, { + focusin: $.proxy( this, "_handleDocumentFocusIn" ) + }); + }, - if ( parentNeedsEnabled ) { - $el = this.element.parent(); - } else { - $el = this.element; - } - $el.removeClass( "ui-disabled" ); - return this._setOption( "disabled", false ); - } -}); + _applyTheme: function( dst, theme, prefix ) { + var classes = ( dst.attr( "class" ) || "").split( " " ), + alreadyAdded = true, + currentTheme = null, + matches, + themeStr = String( theme ); -//auto self-init widgets -$.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.textinput.prototype.enhanceWithin( e.target, true ); -}); + while ( classes.length > 0 ) { + currentTheme = classes.pop(); + matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme ); + if ( matches && matches.length > 1 ) { + currentTheme = matches[ 1 ]; + break; + } else { + currentTheme = null; + } + } -})( jQuery ); + if ( theme !== currentTheme ) { + dst.removeClass( "ui-" + prefix + "-" + currentTheme ); + if ( ! ( theme === null || theme === "none" ) ) { + dst.addClass( "ui-" + prefix + "-" + themeStr ); + } + } + }, -(function( $, undefined ) { + _setTheme: function( value ) { + this._applyTheme( this.element, value, "body" ); + }, -$.mobile.listview.prototype.options.filter = false; -$.mobile.listview.prototype.options.filterPlaceholder = "Filter items..."; -$.mobile.listview.prototype.options.filterTheme = "c"; -$.mobile.listview.prototype.options.filterReveal = false; -// TODO rename callback/deprecate and default to the item itself as the first argument -var defaultFilterCallback = function( text, searchValue, item ) { - return text.toString().toLowerCase().indexOf( searchValue ) === -1; - }; + _setOverlayTheme: function( value ) { + this._applyTheme( this._ui.screen, value, "overlay" ); -$.mobile.listview.prototype.options.filterCallback = defaultFilterCallback; + if ( this._isOpen ) { + this._ui.screen.addClass( "in" ); + } + }, -$.mobile.document.delegate( "ul, ol", "listviewcreate", function() { + _setShadow: function( value ) { + this.element.toggleClass( "ui-overlay-shadow", value ); + }, - var list = $( this ), - listview = list.data( "mobile-listview" ); + _setCorners: function( value ) { + this.element.toggleClass( "ui-corner-all", value ); + }, - if ( !listview.options.filter ) { - return; - } + _applyTransition: function( value ) { + this._ui.container.removeClass( this._fallbackTransition ); + if ( value && value !== "none" ) { + this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); + if ( this._fallbackTransition === "none" ) { + this._fallbackTransition = ""; + } + this._ui.container.addClass( this._fallbackTransition ); + } + }, - if ( listview.options.filterReveal ) { - list.children().addClass( "ui-screen-hidden" ); - } + _setTransition: function( value ) { + if ( !this._currentTransition ) { + this._applyTransition( value ); + } + }, - var wrapper = $( "", { - "class": "ui-listview-filter ui-bar-" + listview.options.filterTheme, - "role": "search" - }).submit( function( e ) { - e.preventDefault(); - search.blur(); - }), - onKeyUp = function( e ) { - var $this = $( this ), - val = this.value.toLowerCase(), - listItems = null, - li = list.children(), - lastval = $this.jqmData( "lastval" ) + "", - childItems = false, - itemtext = "", - item, - // Check if a custom filter callback applies - isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback; + _setTolerance: function( value ) { + var tol = { t: 30, r: 15, b: 30, l: 15 }; - if ( lastval && lastval === val ) { - // Execute the handler only once per value change - return; - } + if ( value !== undefined ) { + var ar = String( value ).split( "," ); - listview._trigger( "beforefilter", "beforefilter", { input: this } ); + $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); - // Change val as lastval for next execution - $this.jqmData( "lastval" , val ); - if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) { + switch( ar.length ) { + // All values are to be the same + case 1: + if ( !isNaN( ar[ 0 ] ) ) { + tol.t = tol.r = tol.b = tol.l = ar[ 0 ]; + } + break; - // Custom filter callback applies or removed chars or pasted something totally different, check all items - listItems = list.children(); - } else { + // The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance + case 2: + if ( !isNaN( ar[ 0 ] ) ) { + tol.t = tol.b = ar[ 0 ]; + } + if ( !isNaN( ar[ 1 ] ) ) { + tol.l = tol.r = ar[ 1 ]; + } + break; - // Only chars added, not removed, only use visible subset - listItems = list.children( ":not(.ui-screen-hidden)" ); + // The array contains values in the order top, right, bottom, left + case 4: + if ( !isNaN( ar[ 0 ] ) ) { + tol.t = ar[ 0 ]; + } + if ( !isNaN( ar[ 1 ] ) ) { + tol.r = ar[ 1 ]; + } + if ( !isNaN( ar[ 2 ] ) ) { + tol.b = ar[ 2 ]; + } + if ( !isNaN( ar[ 3 ] ) ) { + tol.l = ar[ 3 ]; + } + break; - if ( !listItems.length && listview.options.filterReveal ) { - listItems = list.children( ".ui-screen-hidden" ); + default: + break; } } - if ( val ) { - - // This handles hiding regular rows without the text we search for - // and any list dividers without regular rows shown under it - - for ( var i = listItems.length - 1; i >= 0; i-- ) { - item = $( listItems[ i ] ); - itemtext = item.jqmData( "filtertext" ) || item.text(); + this._tolerance = tol; + }, - if ( item.is( "li:jqmData(role=list-divider)" ) ) { + _setOption: function( key, value ) { + var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); - item.toggleClass( "ui-filter-hidequeue" , !childItems ); + if ( this[ setter ] !== undefined ) { + this[ setter ]( value ); + } - // New bucket! - childItems = false; + // TODO REMOVE FOR 1.2.1 by moving them out to a default options object + exclusions = [ + "initSelector", + "closeLinkSelector", + "closeLinkEvents", + "navigateEvents", + "closeEvents", + "history", + "container" + ]; - } else if ( listview.options.filterCallback( itemtext, val, item ) ) { + $.mobile.widget.prototype._setOption.apply( this, arguments ); + if ( $.inArray( key, exclusions ) === -1 ) { + // Record the option change in the options and in the DOM data-* attributes + this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); + } + }, - //mark to be hidden - item.toggleClass( "ui-filter-hidequeue" , true ); - } else { + // Try and center the overlay over the given coordinates + _placementCoords: function( desired ) { + // rectangle within which the popup must fit + var + winCoords = windowCoords(), + rc = { + x: this._tolerance.l, + y: winCoords.y + this._tolerance.t, + cx: winCoords.cx - this._tolerance.l - this._tolerance.r, + cy: winCoords.cy - this._tolerance.t - this._tolerance.b + }, + menuSize, ret; - // There's a shown item in the bucket - childItems = true; - } - } + // Clamp the width of the menu before grabbing its size + this._ui.container.css( "max-width", rc.cx ); + menuSize = { + cx: this._ui.container.outerWidth( true ), + cy: this._ui.container.outerHeight( true ) + }; - // Show items, not marked to be hidden - listItems - .filter( ":not(.ui-filter-hidequeue)" ) - .toggleClass( "ui-screen-hidden", false ); + // Center the menu over the desired coordinates, while not going outside + // the window tolerances. This will center wrt. the window if the popup is too large. + ret = { + x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ), + y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y ) + }; - // Hide items, marked to be hidden - listItems - .filter( ".ui-filter-hidequeue" ) - .toggleClass( "ui-screen-hidden", true ) - .toggleClass( "ui-filter-hidequeue", false ); + // Make sure the top of the menu is visible + ret.y = Math.max( 0, ret.y ); - } else { + // If the height of the menu is smaller than the height of the document + // align the bottom with the bottom of the document - //filtervalue is empty => show all - listItems.toggleClass( "ui-screen-hidden", !!listview.options.filterReveal ); - } - listview._addFirstLastClasses( li, listview._getVisibles( li, false ), false ); - }, - search = $( "", { - placeholder: listview.options.filterPlaceholder - }) - .attr( "data-" + $.mobile.ns + "type", "search" ) - .jqmData( "lastval", "" ) - .bind( "keyup change input", onKeyUp ) - .appendTo( wrapper ) - .textinput(); + // fix for $.mobile.document.height() bug in core 1.7.2. + var docEl = document.documentElement, docBody = document.body, + docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight ); - if ( listview.options.inset ) { - wrapper.addClass( "ui-listview-filter-inset" ); - } + ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); - wrapper.bind( "submit", function() { - return false; - }) - .insertBefore( list ); -}); + return { left: ret.x, top: ret.y }; + }, -})( jQuery ); + _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { + var self = this, prereqs; -(function( $, undefined ) { + // It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in + // the closure of the functions which call the callbacks passed in. The comparison between the local variable and + // self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called + // next time an animation completes, even if that's not the animation whose end the function was supposed to catch + // (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for + // that animation anymore, but the handler remains attached, so it is called the next time the popup is opened + // - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that + // callbacks triggered by a stale .animationComplete will be ignored. -$.widget( "mobile.slider", $.mobile.widget, { - widgetEventPrefix: "slide", + prereqs = { + screen: $.Deferred(), + container: $.Deferred() + }; - options: { - theme: null, - trackTheme: null, - disabled: false, - initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')", - mini: false, - highlight: false - }, + prereqs.screen.then( function() { + if ( prereqs === self._prereqs ) { + screenPrereq(); + } + }); - _create: function() { + prereqs.container.then( function() { + if ( prereqs === self._prereqs ) { + containerPrereq(); + } + }); - // TODO: Each of these should have comments explain what they're for - var self = this, - control = this.element, - parentTheme = $.mobile.getInheritedTheme( control, "c" ), - theme = this.options.theme || parentTheme, - trackTheme = this.options.trackTheme || parentTheme, - cType = control[ 0 ].nodeName.toLowerCase(), - isSelect = this.isToggleSwitch = cType === "select", - isRangeslider = control.parent().is( ":jqmData(role='rangeslider')" ), - selectClass = ( this.isToggleSwitch ) ? "ui-slider-switch" : "", - controlID = control.attr( "id" ), - $label = $( "[for='" + controlID + "']" ), - labelID = $label.attr( "id" ) || controlID + "-label", - label = $label.attr( "id", labelID ), - min = !this.isToggleSwitch ? parseFloat( control.attr( "min" ) ) : 0, - max = !this.isToggleSwitch ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1, - step = window.parseFloat( control.attr( "step" ) || 1 ), - miniClass = ( this.options.mini || control.jqmData( "mini" ) ) ? " ui-mini" : "", - domHandle = document.createElement( "a" ), - handle = $( domHandle ), - domSlider = document.createElement( "div" ), - slider = $( domSlider ), - valuebg = this.options.highlight && !this.isToggleSwitch ? (function() { - var bg = document.createElement( "div" ); - bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all"; - return $( bg ).prependTo( slider ); - })() : false, - options; - - domHandle.setAttribute( "href", "#" ); - domSlider.setAttribute( "role", "application" ); - domSlider.className = [this.isToggleSwitch ? "ui-slider " : "ui-slider-track ",selectClass," ui-btn-down-",trackTheme," ui-btn-corner-all", miniClass].join( "" ); - domHandle.className = "ui-slider-handle"; - domSlider.appendChild( domHandle ); + $.when( prereqs.screen, prereqs.container ).done( function() { + if ( prereqs === self._prereqs ) { + self._prereqs = null; + whenDone(); + } + }); - handle.buttonMarkup({ corners: true, theme: theme, shadow: true }) - .attr({ - "role": "slider", - "aria-valuemin": min, - "aria-valuemax": max, - "aria-valuenow": this._value(), - "aria-valuetext": this._value(), - "title": this._value(), - "aria-labelledby": labelID - }); + self._prereqs = prereqs; + }, - $.extend( this, { - slider: slider, - handle: handle, - type: cType, - step: step, - max: max, - min: min, - valuebg: valuebg, - isRangeslider: isRangeslider, - dragging: false, - beforeStart: null, - userModified: false, - mouseMoved: false - }); + _animate: function( args ) { + // NOTE before removing the default animation of the screen + // this had an animate callback that would resolve the deferred + // now the deferred is resolved immediately + // TODO remove the dependency on the screen deferred + this._ui.screen + .removeClass( args.classToRemove ) + .addClass( args.screenClassToAdd ); - if ( this.isToggleSwitch ) { - var wrapper = document.createElement( "div" ); - wrapper.className = "ui-slider-inneroffset"; + args.prereqs.screen.resolve(); - for ( var j = 0, length = domSlider.childNodes.length; j < length; j++ ) { - wrapper.appendChild( domSlider.childNodes[j] ); + if ( args.transition && args.transition !== "none" ) { + if ( args.applyTransition ) { + this._applyTransition( args.transition ); + } + if ( this._fallbackTransition ) { + this._ui.container + .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) + .addClass( args.containerClassToAdd ) + .removeClass( args.classToRemove ); + return; + } } + this._ui.container.removeClass( args.classToRemove ); + args.prereqs.container.resolve(); + }, - domSlider.appendChild( wrapper ); - - // slider.wrapInner( "
" ); - - // make the handle move with a smooth transition - handle.addClass( "ui-slider-handle-snapping" ); + // The desired coordinates passed in will be returned untouched if no reference element can be identified via + // desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid + // x and y coordinates by specifying the center middle of the window if the coordinates are absent. + // options: { x: coordinate, y: coordinate, positionTo: string: "origin", "window", or jQuery selector + _desiredCoords: function( o ) { + var dst = null, offset, winCoords = windowCoords(), x = o.x, y = o.y, pTo = o.positionTo; - options = control.find( "option" ); + // Establish which element will serve as the reference + if ( pTo && pTo !== "origin" ) { + if ( pTo === "window" ) { + x = winCoords.cx / 2 + winCoords.x; + y = winCoords.cy / 2 + winCoords.y; + } else { + try { + dst = $( pTo ); + } catch( e ) { + dst = null; + } + if ( dst ) { + dst.filter( ":visible" ); + if ( dst.length === 0 ) { + dst = null; + } + } + } + } - for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) { - var side = !i ? "b" : "a", - sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ), - sliderLabel = document.createElement( "div" ), - sliderImg = document.createElement( "span" ); + // If an element was found, center over it + if ( dst ) { + offset = dst.offset(); + x = offset.left + dst.outerWidth() / 2; + y = offset.top + dst.outerHeight() / 2; + } - sliderImg.className = ["ui-slider-label ui-slider-label-", side, sliderTheme, " ui-btn-corner-all"].join( "" ); - sliderImg.setAttribute( "role", "img" ); - sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) ); - $( sliderImg ).prependTo( slider ); + // Make sure x and y are valid numbers - center over the window + if ( $.type( x ) !== "number" || isNaN( x ) ) { + x = winCoords.cx / 2 + winCoords.x; + } + if ( $.type( y ) !== "number" || isNaN( y ) ) { + y = winCoords.cy / 2 + winCoords.y; } - self._labels = $( ".ui-slider-label", slider ); + return { x: x, y: y }; + }, - } + _reposition: function( o ) { + // We only care about position-related parameters for repositioning + o = { x: o.x, y: o.y, positionTo: o.positionTo }; + this._trigger( "beforeposition", o ); + this._ui.container.offset( this._placementCoords( this._desiredCoords( o ) ) ); + }, - label.addClass( "ui-slider" ); - - // monitor the input for updated values - control.addClass( this.isToggleSwitch ? "ui-slider-switch" : "ui-slider-input" ); + reposition: function( o ) { + if ( this._isOpen ) { + this._reposition( o ); + } + }, - this._on( control, { - "change": "_controlChange", - "keyup": "_controlKeyup", - "blur": "_controlBlur", - "vmouseup": "_controlVMouseUp" - }); + _openPrereqsComplete: function() { + this._ui.container.addClass( "ui-popup-active" ); + this._isOpen = true; + this._resizeScreen(); + this._ui.container.attr( "tabindex", "0" ).focus(); + this._ignoreResizeEvents(); + this._trigger( "afteropen" ); + }, - slider.bind( "vmousedown", $.proxy( this._sliderVMouseDown, this ) ) - .bind( "vclick", false ); + _open: function( options ) { + var o = $.extend( {}, this.options, options ), + // TODO move blacklist to private method + androidBlacklist = ( function() { + var w = window, + ua = navigator.userAgent, + // Rendering engine is Webkit, and capture major version + wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ), + wkversion = !!wkmatch && wkmatch[ 1 ], + androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ), + andversion = !!androidmatch && androidmatch[ 1 ], + chromematch = ua.indexOf( "Chrome" ) > -1; - // We have to instantiate a new function object for the unbind to work properly - // since the method itself is defined in the prototype (causing it to unbind everything) - this._on( document, { "vmousemove": "_preventDocumentDrag" }); - this._on( slider.add( document ), { "vmouseup": "_sliderVMouseUp" }); + // Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome. + if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) { + return true; + } + return false; + }()); - slider.insertAfter( control ); + // Count down to triggering "popupafteropen" - we have two prerequisites: + // 1. The popup window animation completes (container()) + // 2. The screen opacity animation completes (screen()) + this._createPrereqs( + $.noop, + $.noop, + $.proxy( this, "_openPrereqsComplete" ) ); - // wrap in a div for styling purposes - if ( !this.isToggleSwitch && !isRangeslider ) { - var wrapper = this.options.mini ? "
" : "
"; - - control.add( slider ).wrapAll( wrapper ); - } + this._currentTransition = o.transition; + this._applyTransition( o.transition ); - // Only add focus class to toggle switch, sliders get it automatically from ui-btn - if ( this.isToggleSwitch ) { - this.handle.bind({ - focus: function() { - slider.addClass( $.mobile.focusClass ); - }, + if ( !this.options.theme ) { + this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); + } - blur: function() { - slider.removeClass( $.mobile.focusClass ); - } - }); - } + this._ui.screen.removeClass( "ui-screen-hidden" ); + this._ui.container.removeClass( "ui-popup-hidden" ); - // bind the handle event callbacks and set the context to the widget instance - this._on( this.handle, { - "vmousedown": "_handleVMouseDown", - "keydown": "_handleKeydown", - "keyup": "_handleKeyup" - }); + // Give applications a chance to modify the contents of the container before it appears + this._reposition( o ); - this.handle.bind( "vclick", false ); + if ( this.options.overlayTheme && androidBlacklist ) { + /* TODO: + The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed + above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain + types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser: + https://github.com/scottjehl/Device-Bugs/issues/3 - if ( this._handleFormReset ) { - this._handleFormReset(); - } + This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): - this.refresh( undefined, undefined, true ); - }, + https://github.com/jquery/jquery-mobile/issues/4816 + https://github.com/jquery/jquery-mobile/issues/4844 + https://github.com/jquery/jquery-mobile/issues/4874 + */ - _controlChange: function( event ) { - // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again - if ( this._trigger( "controlchange", event ) === false ) { - return false; - } - if ( !this.mouseMoved ) { - this.refresh( this._value(), true ); - } - }, + // TODO sort out why this._page isn't working + this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); + } + this._animate({ + additionalCondition: true, + transition: o.transition, + classToRemove: "", + screenClassToAdd: "in", + containerClassToAdd: "in", + applyTransition: false, + prereqs: this._prereqs + }); + }, - _controlKeyup: function( event ) { // necessary? - this.refresh( this._value(), true, true ); - }, + _closePrereqScreen: function() { + this._ui.screen + .removeClass( "out" ) + .addClass( "ui-screen-hidden" ); + }, - _controlBlur: function( event ) { - this.refresh( this._value(), true ); - }, + _closePrereqContainer: function() { + this._ui.container + .removeClass( "reverse out" ) + .addClass( "ui-popup-hidden" ) + .removeAttr( "style" ); + }, - // it appears the clicking the up and down buttons in chrome on - // range/number inputs doesn't trigger a change until the field is - // blurred. Here we check thif the value has changed and refresh - _controlVMouseUp: function( event ) { - this._checkedRefresh(); - }, + _closePrereqsDone: function() { + var opts = this.options; - // NOTE force focus on handle - _handleVMouseDown: function( event ) { - this.handle.focus(); - }, + this._ui.container.removeAttr( "tabindex" ); - _handleKeydown: function( event ) { - var index = this._value(); - if ( this.options.disabled ) { - return; - } + // remove the global mutex for popups + $.mobile.popup.active = undefined; - // In all cases prevent the default and mark the handle as active - switch ( event.keyCode ) { - case $.mobile.keyCode.HOME: - case $.mobile.keyCode.END: - case $.mobile.keyCode.PAGE_UP: - case $.mobile.keyCode.PAGE_DOWN: - case $.mobile.keyCode.UP: - case $.mobile.keyCode.RIGHT: - case $.mobile.keyCode.DOWN: - case $.mobile.keyCode.LEFT: - event.preventDefault(); + // alert users that the popup is closed + this._trigger( "afterclose" ); + }, - if ( !this._keySliding ) { - this._keySliding = true; - this.handle.addClass( "ui-state-active" ); - } + _close: function( immediate ) { + this._ui.container.removeClass( "ui-popup-active" ); + this._page.removeClass( "ui-popup-open" ); - break; - } + this._isOpen = false; - // move the slider according to the keypress - switch ( event.keyCode ) { - case $.mobile.keyCode.HOME: - this.refresh( this.min ); - break; - case $.mobile.keyCode.END: - this.refresh( this.max ); - break; - case $.mobile.keyCode.PAGE_UP: - case $.mobile.keyCode.UP: - case $.mobile.keyCode.RIGHT: - this.refresh( index + this.step ); - break; - case $.mobile.keyCode.PAGE_DOWN: - case $.mobile.keyCode.DOWN: - case $.mobile.keyCode.LEFT: - this.refresh( index - this.step ); - break; - } - }, // remove active mark + // Count down to triggering "popupafterclose" - we have two prerequisites: + // 1. The popup window reverse animation completes (container()) + // 2. The screen opacity animation completes (screen()) + this._createPrereqs( + $.proxy( this, "_closePrereqScreen" ), + $.proxy( this, "_closePrereqContainer" ), + $.proxy( this, "_closePrereqsDone" ) ); - _handleKeyup: function( event ) { - if ( this._keySliding ) { - this._keySliding = false; - this.handle.removeClass( "ui-state-active" ); - } - }, + this._animate( { + additionalCondition: this._ui.screen.hasClass( "in" ), + transition: ( immediate ? "none" : ( this._currentTransition ) ), + classToRemove: "in", + screenClassToAdd: "out", + containerClassToAdd: "reverse out", + applyTransition: true, + prereqs: this._prereqs + }); + }, - _sliderVMouseDown: function( event ) { - // NOTE: we don't do this in refresh because we still want to - // support programmatic alteration of disabled inputs - if ( this.options.disabled ) { - return false; - } - if ( this._trigger( "beforestart", event ) === false ) { - return false; - } - this.dragging = true; - this.userModified = false; - this.mouseMoved = false; + _unenhance: function() { + // Put the element back to where the placeholder was and remove the "ui-popup" class + this._setTheme( "none" ); + this.element + // Cannot directly insertAfter() - we need to detach() first, because + // insertAfter() will do nothing if the payload div was not attached + // to the DOM at the time the widget was created, and so the payload + // will remain inside the container even after we call insertAfter(). + // If that happens and we remove the container a few lines below, we + // will cause an infinite recursion - #5244 + .detach() + .insertAfter( this._ui.placeholder ) + .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); + this._ui.screen.remove(); + this._ui.container.remove(); + this._ui.placeholder.remove(); + }, - if ( this.isToggleSwitch ) { - this.beforeStart = this.element[0].selectedIndex; - } + _destroy: function() { + if ( $.mobile.popup.active === this ) { + this.element.one( "popupafterclose", $.proxy( this, "_unenhance" ) ); + this.close(); + } else { + this._unenhance(); + } + }, - - this.refresh( event ); - this._trigger( "start" ); - return false; - }, + _closePopup: function( e, data ) { + var parsedDst, toUrl, o = this.options, immediate = false; - _sliderVMouseUp: function() { - if ( this.dragging ) { - this.dragging = false; + // restore location on screen + window.scrollTo( 0, this._scrollTop ); - if ( this.isToggleSwitch ) { - // make the handle move with a smooth transition - this.handle.addClass( "ui-slider-handle-snapping" ); - - if ( this.mouseMoved ) { - // this is a drag, change the value only if user dragged enough - if ( this.userModified ) { - this.refresh( this.beforeStart === 0 ? 1 : 0 ); - } else { - this.refresh( this.beforeStart ); - } + if ( e && e.type === "pagebeforechange" && data ) { + // Determine whether we need to rapid-close the popup, or whether we can + // take the time to run the closing transition + if ( typeof data.toPage === "string" ) { + parsedDst = data.toPage; } else { - // this is just a click, change the value - this.refresh( this.beforeStart === 0 ? 1 : 0 ); + parsedDst = data.toPage.jqmData( "url" ); } - } - - this.mouseMoved = false; - this._trigger( "stop" ); - return false; - } - }, + parsedDst = $.mobile.path.parseUrl( parsedDst ); + toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash; - _preventDocumentDrag: function( event ) { - // NOTE: we don't do this in refresh because we still want to - // support programmatic alteration of disabled inputs - if ( this._trigger( "drag", event ) === false) { - return false; + if ( this._myUrl !== $.mobile.path.makeUrlAbsolute( toUrl ) ) { + // Going to a different page - close immediately + immediate = true; + } else { + e.preventDefault(); + } } - if ( this.dragging && !this.options.disabled ) { - - // this.mouseMoved must be updated before refresh() because it will be used in the control "change" event - this.mouseMoved = true; - if ( this.isToggleSwitch ) { - // make the handle move in sync with the mouse - this.handle.removeClass( "ui-slider-handle-snapping" ); - } - - this.refresh( event ); + // remove nav bindings + o.container.unbind( o.closeEvents ); + // unbind click handlers added when history is disabled + this.element.undelegate( o.closeLinkSelector, o.closeLinkEvents ); - // only after refresh() you can calculate this.userModified - this.userModified = this.beforeStart !== this.element[0].selectedIndex; - return false; - } + this._close( immediate ); }, - _checkedRefresh: function() { - if ( this.value != this._value() ) { - this.refresh( this._value() ); - } - }, + // any navigation event after a popup is opened should close the popup + // NOTE the pagebeforechange is bound to catch navigation events that don't + // alter the url (eg, dialogs from popups) + _bindContainerClose: function() { + this.options.container + .one( this.options.closeEvents, $.proxy( this, "_closePopup" ) ); + }, - _value: function() { - return this.isToggleSwitch ? this.element[0].selectedIndex : parseFloat( this.element.val() ) ; - }, + // TODO no clear deliniation of what should be here and + // what should be in _open. Seems to be "visual" vs "history" for now + open: function( options ) { + var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory; + // make sure open is idempotent + if( $.mobile.popup.active ) { + return; + } - _reset: function() { - this.refresh( undefined, false, true ); - }, + // set the global popup mutex + $.mobile.popup.active = this; + this._scrollTop = $.mobile.window.scrollTop(); - refresh: function( val, isfromControl, preventInputUpdate ) { - // NOTE: we don't return here because we want to support programmatic - // alteration of the input value, which should still update the slider - - var self = this, - parentTheme = $.mobile.getInheritedTheme( this.element, "c" ), - theme = this.options.theme || parentTheme, - trackTheme = this.options.trackTheme || parentTheme; + // if history alteration is disabled close on navigate events + // and leave the url as is + if( !( opts.history ) ) { + self._open( options ); + self._bindContainerClose(); - self.slider[0].className = [ this.isToggleSwitch ? "ui-slider ui-slider-switch" : "ui-slider-track"," ui-btn-down-" + trackTheme,' ui-btn-corner-all', ( this.options.mini ) ? " ui-mini":""].join( "" ); - if ( this.options.disabled || this.element.attr( "disabled" ) ) { - this.disable(); - } + // When histoy is disabled we have to grab the data-rel + // back link clicks so we can close the popup instead of + // relying on history to do it for us + self.element + .delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) { + self.close(); + e.preventDefault(); + }); - // set the stored value for comparison later - this.value = this._value(); - if ( this.options.highlight && !this.isToggleSwitch && this.slider.find( ".ui-slider-bg" ).length === 0 ) { - this.valuebg = (function() { - var bg = document.createElement( "div" ); - bg.className = "ui-slider-bg " + $.mobile.activeBtnClass + " ui-btn-corner-all"; - return $( bg ).prependTo( self.slider ); - })(); - } - this.handle.buttonMarkup({ corners: true, theme: theme, shadow: true }); + return; + } - var pxStep, percent, - control = this.element, - isInput = !this.isToggleSwitch, - optionElements = isInput ? [] : control.find( "option" ), - min = isInput ? parseFloat( control.attr( "min" ) ) : 0, - max = isInput ? parseFloat( control.attr( "max" ) ) : optionElements.length - 1, - step = ( isInput && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1; - - if ( typeof val === "object" ) { - var left, width, data = val, - // a slight tolerance helped get to the ends of the slider - tol = 8; + // cache some values for min/readability + urlHistory = $.mobile.urlHistory; + hashkey = $.mobile.dialogHashKey; + activePage = $.mobile.activePage; + currentIsDialog = activePage.is( ".ui-dialog" ); + this._myUrl = url = urlHistory.getActive().url; + hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog && ( urlHistory.activeIndex > 0 ); - left = this.slider.offset().left; - width = this.slider.width(); - pxStep = width/((max-min)/step); - if ( !this.dragging || - data.pageX < left - tol || - data.pageX > left + width + tol ) { + if ( hasHash ) { + self._open( options ); + self._bindContainerClose(); return; } - if ( pxStep > 1 ) { - percent = ( ( data.pageX - left ) / width ) * 100; + + // if the current url has no dialog hash key proceed as normal + // otherwise, if the page is a dialog simply tack on the hash key + if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){ + url = url + (url.indexOf( "#" ) > -1 ? hashkey : "#" + hashkey); } else { - percent = Math.round( ( ( data.pageX - left ) / width ) * 100 ); + url = $.mobile.path.parseLocation().hash + hashkey; } - } else { - if ( val == null ) { - val = isInput ? parseFloat( control.val() || 0 ) : control[0].selectedIndex; + + // Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash + if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { + url += hashkey; } - percent = ( parseFloat( val ) - min ) / ( max - min ) * 100; - } - if ( isNaN( percent ) ) { - return; - } + // swallow the the initial navigation event, and bind for the next + $(window).one( "beforenavigate", function( e ) { + e.preventDefault(); + self._open( options ); + self._bindContainerClose(); + }); - var newval = ( percent / 100 ) * ( max - min ) + min; + this.urlAltered = true; + $.mobile.navigate( url, {role: "dialog"} ); + }, - //from jQuery UI slider, the following source will round to the nearest step - var valModStep = ( newval - min ) % step; - var alignValue = newval - valModStep; + close: function() { + // make sure close is idempotent + if( $.mobile.popup.active !== this ) { + return; + } - if ( Math.abs( valModStep ) * 2 >= step ) { - alignValue += ( valModStep > 0 ) ? step : ( -step ); + this._scrollTop = $.mobile.window.scrollTop(); + + if( this.options.history && this.urlAltered ) { + $.mobile.back(); + this.urlAltered = false; + } else { + // simulate the nav bindings having fired + this._closePopup(); + } } + }); - var percentPerStep = 100/((max-min)/step); - // Since JavaScript has problems with large floats, round - // the final value to 5 digits after the decimal point (see jQueryUI: #4124) - newval = parseFloat( alignValue.toFixed(5) ); - if ( typeof pxStep === "undefined" ) { - pxStep = width / ( (max-min) / step ); - } - if ( pxStep > 1 && isInput ) { - percent = ( newval - min ) * percentPerStep * ( 1 / step ); - } - if ( percent < 0 ) { - percent = 0; - } + // TODO this can be moved inside the widget + $.mobile.popup.handleLink = function( $link ) { + var closestPage = $link.closest( ":jqmData(role='page')" ), + scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ), + // NOTE make sure to get only the hash, ie7 (wp7) return the absolute href + // in this case ruining the element selection + popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ), + offset; - if ( percent > 100 ) { - percent = 100; + if ( popup.data( "mobile-popup" ) ) { + offset = $link.offset(); + popup.popup( "open", { + x: offset.left + $link.outerWidth() / 2, + y: offset.top + $link.outerHeight() / 2, + transition: $link.jqmData( "transition" ), + positionTo: $link.jqmData( "position-to" ) + }); } - if ( newval < min ) { - newval = min; - } + //remove after delay + setTimeout( function() { + // Check if we are in a listview + var $parent = $link.parent().parent(); + if ($parent.hasClass("ui-li")) { + $link = $parent.parent(); + } + $link.removeClass( $.mobile.activeBtnClass ); + }, 300 ); + }; - if ( newval > max ) { - newval = max; + // TODO move inside _create + $.mobile.document.bind( "pagebeforechange", function( e, data ) { + if ( data.options.role === "popup" ) { + $.mobile.popup.handleLink( data.options.link ); + e.preventDefault(); } + }); - this.handle.css( "left", percent + "%" ); + $.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.popup.prototype.enhanceWithin( e.target, true ); + }); - this.handle[0].setAttribute( "aria-valuenow", isInput ? newval : optionElements.eq( newval ).attr( "value" ) ); +})( jQuery ); - this.handle[0].setAttribute( "aria-valuetext", isInput ? newval : optionElements.eq( newval ).getEncodedText() ); - - this.handle[0].setAttribute( "title", isInput ? newval : optionElements.eq( newval ).getEncodedText() ); +/* +* custom "selectmenu" plugin +*/ - if ( this.valuebg ) { - this.valuebg.css( "width", percent + "%" ); - } +(function( $, undefined ) { + var extendSelect = function( widget ) { - // drag the label widths - if ( this._labels ) { - var handlePercent = this.handle.width() / this.slider.width() * 100, - aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100, - bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 ); + var select = widget.select, + origDestroy = widget._destroy, + selectID = widget.selectID, + prefix = ( selectID ? selectID : ( ( $.mobile.ns || "" ) + "uuid-" + widget.uuid ) ), + popupID = prefix + "-listbox", + dialogID = prefix + "-dialog", + label = widget.label, + thisPage = widget.select.closest( ".ui-page" ), + selectOptions = widget._selectOptions(), + isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, + buttonId = selectID + "-button", + menuId = selectID + "-menu", + menuPage = $( "
" + + "
" + + "
" + label.getEncodedText() + "
"+ + "
"+ + "
"+ + "
" ), - this._labels.each(function() { - var ab = $( this ).is( ".ui-slider-label-a" ); - $( this ).width( ( ab ? aPercent : bPercent ) + "%" ); - }); - } + listbox = $( "
" ).insertAfter( widget.select ).popup( { theme: widget.options.overlayTheme } ), - if ( !preventInputUpdate ) { - var valueChanged = false; + list = $( "
    ", { + "class": "ui-selectmenu-list", + "id": menuId, + "role": "listbox", + "aria-labelledby": buttonId + }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ) + .attr( "data-" + $.mobile.ns + "divider-theme", widget.options.dividerTheme ) + .appendTo( listbox ), - // update control"s value - if ( isInput ) { - valueChanged = control.val() !== newval; - control.val( newval ); - } else { - valueChanged = control[ 0 ].selectedIndex !== newval; - control[ 0 ].selectedIndex = newval; - } - if ( this._trigger( "beforechange", val ) === false) { - return false; - } - if ( !isfromControl && valueChanged ) { - control.trigger( "change" ); - } - } - }, - enable: function() { - this.element.attr( "disabled", false ); - this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); - return this._setOption( "disabled", false ); - }, + header = $( "
    ", { + "class": "ui-header ui-bar-" + widget.options.theme + }).prependTo( listbox ), - disable: function() { - this.element.attr( "disabled", true ); - this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true ); - return this._setOption( "disabled", true ); - } + headerTitle = $( "

    ", { + "class": "ui-title" + }).appendTo( header ), -}); + menuPageContent, + menuPageClose, + headerClose; -$.widget( "mobile.slider", $.mobile.slider, $.mobile.behaviors.formReset ); + if ( widget.isMultiple ) { + headerClose = $( "", { + "text": widget.options.closeText, + "href": "#", + "class": "ui-btn-left" + }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(); + } -//auto self-init widgets -$.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.slider.prototype.enhanceWithin( e.target, true ); -}); + $.extend( widget, { + select: widget.select, + selectID: selectID, + buttonId: buttonId, + menuId: menuId, + popupID: popupID, + dialogID: dialogID, + thisPage: thisPage, + menuPage: menuPage, + label: label, + selectOptions: selectOptions, + isMultiple: isMultiple, + theme: widget.options.theme, + listbox: listbox, + list: list, + header: header, + headerTitle: headerTitle, + headerClose: headerClose, + menuPageContent: menuPageContent, + menuPageClose: menuPageClose, + placeholder: "", -})( jQuery ); + build: function() { + var self = this; -(function( $, undefined ) { - $.widget( "mobile.rangeslider", $.mobile.widget, { + // Create list from select, update state + self.refresh(); - options: { - theme: null, - trackTheme: null, - disabled: false, - initSelector: ":jqmData(role='rangeslider')", - mini: false, - highlight: true - }, + if ( self._origTabIndex === undefined ) { + // Map undefined to false, because self._origTabIndex === undefined + // indicates that we have not yet checked whether the select has + // originally had a tabindex attribute, whereas false indicates that + // we have checked the select for such an attribute, and have found + // none present. + self._origTabIndex = ( self.select[ 0 ].getAttribute( "tabindex" ) === null ) ? false : self.select.attr( "tabindex" ); + } + self.select.attr( "tabindex", "-1" ).focus(function() { + $( this ).blur(); + self.button.focus(); + }); - _create: function() { - var secondLabel, - $el = this.element, - elClass = this.options.mini ? "ui-rangeslider ui-mini" : "ui-rangeslider", - _inputFirst = $el.find( "input" ).first(), - _inputLast = $el.find( "input" ).last(), - label = $el.find( "label" ).first(), - _sliderFirst = $.data( _inputFirst.get(0), "mobileSlider" ).slider, - _sliderLast = $.data( _inputLast.get(0), "mobileSlider" ).slider, - firstHandle = $.data( _inputFirst.get(0), "mobileSlider" ).handle, - _sliders = $( "
    " ).appendTo( $el ); - - if ( $el.find( "label" ).length > 1 ) { - secondLabel = $el.find( "label" ).last().hide(); - } + // Button events + self.button.bind( "vclick keydown" , function( event ) { + if ( self.options.disabled || self.isOpen ) { + return; + } - _inputFirst.addClass( "ui-rangeslider-first" ); - _inputLast.addClass( "ui-rangeslider-last" ); - $el.addClass( elClass ); - - _sliderFirst.appendTo( _sliders ); - _sliderLast.appendTo( _sliders ); - label.prependTo( $el ); - firstHandle.prependTo( _sliderLast ); + if (event.type === "vclick" || + event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER || + event.keyCode === $.mobile.keyCode.SPACE)) { - $.extend( this, { - _inputFirst: _inputFirst, - _inputLast: _inputLast, - _sliderFirst: _sliderFirst, - _sliderLast: _sliderLast, - _targetVal: null, - _sliderTarget: false, - _sliders: _sliders, - _proxy: false - }); - - this.refresh(); - this._on( this.element.find( "input.ui-slider-input" ), { - "slidebeforestart": "_slidebeforestart", - "slidestop": "_slidestop", - "slidedrag": "_slidedrag", - "slidebeforechange": "_change", - "blur": "_change", - "keyup": "_change" - }); - this._on( firstHandle, { - "vmousedown": "_dragFirstHandle" - }); - }, + self._decideFormat(); + if ( self.menuType === "overlay" ) { + self.button.attr( "href", "#" + self.popupID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "popup" ); + } else { + self.button.attr( "href", "#" + self.dialogID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "dialog" ); + } + self.isOpen = true; + // Do not prevent default, so the navigation may have a chance to actually open the chosen format + } + }); - _dragFirstHandle: function( event ) { - //if the first handle is dragged send the event to the first slider - $.data( this._inputFirst.get(0), "mobileSlider" ).dragging = true; - $.data( this._inputFirst.get(0), "mobileSlider" ).refresh( event ); - return false; - }, + // Events for list items + self.list.attr( "role", "listbox" ) + .bind( "focusin", function( e ) { + $( e.target ) + .attr( "tabindex", "0" ) + .trigger( "vmouseover" ); - _slidedrag: function( event ) { - var first = $( event.target ).is( this._inputFirst ), - otherSlider = ( first ) ? this._inputLast : this._inputFirst; + }) + .bind( "focusout", function( e ) { + $( e.target ) + .attr( "tabindex", "-1" ) + .trigger( "vmouseout" ); + }) + .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) { - this._sliderTarget = false; - //if the drag was initaed on an extream and the other handle is focused send the events to - //the closest handle - if ( ( this._proxy === "first" && first ) || ( this._proxy === "last" && !first ) ) { - $.data( otherSlider.get(0), "mobileSlider" ).dragging = true; - $.data( otherSlider.get(0), "mobileSlider" ).refresh( event ); - return false; - } - }, + // index of option tag to be selected + var oldIndex = self.select[ 0 ].selectedIndex, + newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ), + option = self._selectOptions().eq( newIndex )[ 0 ]; - _slidestop: function( event ) { - var first = $( event.target ).is( this._inputFirst ); - - this._proxy = false; - //this stops dragging of the handle and brings the active track to the front - //this makes clicks on the track go the the last handle used - this.element.find( "input" ).trigger( "vmouseup" ); - this._sliderFirst.css( "z-index", first ? 1 : "" ); - }, + // toggle selected status on the tag for multi selects + option.selected = self.isMultiple ? !option.selected : true; - _slidebeforestart: function( event ) { - this._sliderTarget = false; - //if the track is the target remember this and the original value - if ( $( event.originalEvent.target ).hasClass( "ui-slider-track" ) ) { - this._sliderTarget = true; - this._targetVal = $( event.target ).val(); - } - }, - - _setOption: function( options ) { - this._superApply( options ); - this.refresh(); - }, + // toggle checkbox class for multiple selects + if ( self.isMultiple ) { + $( this ).find( ".ui-icon" ) + .toggleClass( "ui-icon-checkbox-on", option.selected ) + .toggleClass( "ui-icon-checkbox-off", !option.selected ); + } - refresh: function() { - var $el = this.element, - o = this.options; + // trigger change if value changed + if ( self.isMultiple || oldIndex !== newIndex ) { + self.select.trigger( "change" ); + } - $el.find( "input" ).slider({ - theme: o.theme, - trackTheme: o.trackTheme, - disabled: o.disabled, - mini: o.mini, - highlight: o.highlight - }).slider( "refresh" ); - this._updateHighlight(); - }, + // hide custom select for single selects only - otherwise focus clicked item + // We need to grab the clicked item the hard way, because the list may have been rebuilt + if ( self.isMultiple ) { + self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex ) + .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); + } + else { + self.close(); + } - _change: function( event ) { - if ( event.type == "keyup" ) { - this._updateHighlight(); - return false; - } + event.preventDefault(); + }) + .keydown(function( event ) { //keyboard events for menu items + var target = $( event.target ), + li = target.closest( "li" ), + prev, next; - var min = parseFloat( this._inputFirst.val(), 10 ), - max = parseFloat( this._inputLast.val(), 10 ), - first = $( event.target ).hasClass( "ui-rangeslider-first" ), - thisSlider = first ? this._inputFirst : this._inputLast, - otherSlider = first ? this._inputLast : this._inputFirst; - - if ( min > max && !this._sliderTarget ) { - //this prevents min from being greater then max - thisSlider.val( first ? max: min ).slider( "refresh" ); - this._trigger( "normalize" ); - } else if ( min > max ) { - //this makes it so clicks on the target on either extream go to the closest handle - thisSlider.val( this._targetVal ).slider( "refresh" ); - - var self = this; - //You must wait for the stack to unwind so first slider is updated before updating second - setTimeout( function() { - otherSlider.val( first ? min: max ).slider( "refresh" ); - $.data( otherSlider.get(0), "mobileSlider" ).handle.focus(); - self._sliderFirst.css( "z-index", first ? "" : 1 ); - self._trigger( "normalize" ); - }, 0 ); - this._proxy = ( first ) ? "first" : "last"; - } - //fixes issue where when both _sliders are at min they cannot be adjusted - if ( min === max ) { - $.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", 1 ); - $.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", 0 ); - } else { - $.data( otherSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" ); - $.data( thisSlider.get(0), "mobileSlider" ).handle.css( "z-index", "" ); - } - - this._updateHighlight(); - - if ( min >= max ) { - return false; - } - }, + // switch logic based on which key was pressed + switch ( event.keyCode ) { + // up or left arrow keys + case 38: + prev = li.prev().not( ".ui-selectmenu-placeholder" ); - _updateHighlight: function() { - var min = parseInt( $.data( this._inputFirst.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ), - max = parseInt( $.data( this._inputLast.get(0), "mobileSlider" ).handle.get(0).style.left, 10 ), - width = (max - min); + if ( prev.is( ".ui-li-divider" ) ) { + prev = prev.prev(); + } - this.element.find( ".ui-slider-bg" ).css({ - "margin-left": min + "%", - "width": width + "%" - }); - }, + // if there's a previous option, focus it + if ( prev.length ) { + target + .blur() + .attr( "tabindex", "-1" ); - _destroy: function() { - this.element.removeClass( "ui-rangeslider ui-mini" ).find( "label" ).show(); - this._inputFirst.after( this._sliderFirst ); - this._inputLast.after( this._sliderLast ); - this._sliders.remove(); - this.element.find( "input" ).removeClass( "ui-rangeslider-first ui-rangeslider-last" ).slider( "destroy" ); - } + prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); + } - }); + return false; + // down or right arrow keys + case 40: + next = li.next(); -$.widget( "mobile.rangeslider", $.mobile.rangeslider, $.mobile.behaviors.formReset ); + if ( next.is( ".ui-li-divider" ) ) { + next = next.next(); + } -//auto self-init widgets -$( document ).bind( "pagecreate create", function( e ) { - $.mobile.rangeslider.prototype.enhanceWithin( e.target, true ); -}); + // if there's a next option, focus it + if ( next.length ) { + target + .blur() + .attr( "tabindex", "-1" ); -})( jQuery ); -(function( $, undefined ) { + next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); + } -$.widget( "mobile.selectmenu", $.mobile.widget, { - options: { - theme: null, - disabled: false, - icon: "arrow-d", - iconpos: "right", - inline: false, - corners: true, - shadow: true, - iconshadow: true, - overlayTheme: "a", - dividerTheme: "b", - hidePlaceholderMenuItems: true, - closeText: "Close", - nativeMenu: true, - // This option defaults to true on iOS devices. - preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, - initSelector: "select:not( :jqmData(role='slider') )", - mini: false - }, + return false; + // If enter or space is pressed, trigger click + case 13: + case 32: + target.trigger( "click" ); - _button: function() { - return $( "
    " ); - }, + return false; + } + }); - _setDisabled: function( value ) { - this.element.attr( "disabled", value ); - this.button.attr( "aria-disabled", value ); - return this._setOption( "disabled", value ); - }, + // button refocus ensures proper height calculation + // by removing the inline style and ensuring page inclusion + self.menuPage.bind( "pagehide", function() { + // TODO centralize page removal binding / handling in the page plugin. + // Suggestion from @jblas to do refcounting + // + // TODO extremely confusing dependency on the open method where the pagehide.remove + // bindings are stripped to prevent the parent page from disappearing. The way + // we're keeping pages in the DOM right now sucks + // + // rebind the page remove that was unbound in the open function + // to allow for the parent page removal from actions other than the use + // of a dialog sized custom select + // + // doing this here provides for the back button on the custom select dialog + $.mobile._bindPageRemove.call( self.thisPage ); + }); - _focusButton : function() { - var self = this; + // Events on the popup + self.listbox.bind( "popupafterclose", function( event ) { + self.close(); + }); - setTimeout( function() { - self.button.focus(); - }, 40); - }, + // Close button on small overlays + if ( self.isMultiple ) { + self.headerClose.click(function() { + if ( self.menuType === "overlay" ) { + self.close(); + return false; + } + }); + } - _selectOptions: function() { - return this.select.find( "option" ); - }, + // track this dependency so that when the parent page + // is removed on pagehide it will also remove the menupage + self.thisPage.addDependents( this.menuPage ); + }, - // setup items that are generally necessary for select menu extension - _preExtension: function() { - var classes = ""; - // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 - /* if ( $el[0].className.length ) { - classes = $el[0].className; - } */ - if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) { - classes = " ui-btn-left"; - } + _isRebuildRequired: function() { + var list = this.list.find( "li" ), + options = this._selectOptions(); - if ( !!~this.element[0].className.indexOf( "ui-btn-right" ) ) { - classes = " ui-btn-right"; - } + // TODO exceedingly naive method to determine difference + // ignores value changes etc in favor of a forcedRebuild + // from the user in the refresh method + return options.text() !== list.text(); + }, - this.select = this.element.removeClass( "ui-btn-left ui-btn-right" ).wrap( "
    " ); - this.selectID = this.select.attr( "id" ); - this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" ); - this.isMultiple = this.select[ 0 ].multiple; - if ( !this.options.theme ) { - this.options.theme = $.mobile.getInheritedTheme( this.select, "c" ); - } - }, + selected: function() { + return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" ); + }, - _destroy: function() { - var wrapper = this.element.parents( ".ui-select" ); - if ( wrapper.length > 0 ) { - if ( wrapper.is( ".ui-btn-left, .ui-btn-right" ) ) { - this.element.addClass( wrapper.is( ".ui-btn-left" ) ? "ui-btn-left" : "ui-btn-right" ); - } - this.element.insertAfter( wrapper ); - wrapper.remove(); - } - }, - - _create: function() { - this._preExtension(); + refresh: function( forceRebuild , foo ) { + var self = this, + select = this.element, + isMultiple = this.isMultiple, + indicies; - // Allows for extension of the native select for custom selects and other plugins - // see select.custom for example extension - // TODO explore plugin registration - this._trigger( "beforeCreate" ); + if ( forceRebuild || this._isRebuildRequired() ) { + self._buildList(); + } - this.button = this._button(); + indicies = this.selectedIndices(); - var self = this, + self.setButtonText(); + self.setButtonCount(); - options = this.options, + self.list.find( "li:not(.ui-li-divider)" ) + .removeClass( $.mobile.activeBtnClass ) + .attr( "aria-selected", false ) + .each(function( i ) { - inline = options.inline || this.select.jqmData( "inline" ), - mini = options.mini || this.select.jqmData( "mini" ), - iconpos = options.icon ? ( options.iconpos || this.select.jqmData( "iconpos" ) ) : false, + if ( $.inArray( i, indicies ) > -1 ) { + var item = $( this ); - // IE throws an exception at options.item() function when - // there is no selected item - // select first in this case - selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex, + // Aria selected attr + item.attr( "aria-selected", true ); - // TODO values buttonId and menuId are undefined here - button = this.button - .insertBefore( this.select ) - .buttonMarkup( { - theme: options.theme, - icon: options.icon, - iconpos: iconpos, - inline: inline, - corners: options.corners, - shadow: options.shadow, - iconshadow: options.iconshadow, - mini: mini - }); + // Multiple selects: add the "on" checkbox state to the icon + if ( self.isMultiple ) { + item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" ); + } else { + if ( item.is( ".ui-selectmenu-placeholder" ) ) { + item.next().addClass( $.mobile.activeBtnClass ); + } else { + item.addClass( $.mobile.activeBtnClass ); + } + } + } + }); + }, - this.setButtonText(); + close: function() { + if ( this.options.disabled || !this.isOpen ) { + return; + } - // 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 ( options.nativeMenu && window.opera && window.opera.version ) { - button.addClass( "ui-select-nativeonly" ); - } + var self = this; - // Add counter for multi selects - if ( this.isMultiple ) { - this.buttonCount = $( "" ) - .addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" ) - .hide() - .appendTo( button.addClass('ui-li-has-count') ); - } + if ( self.menuType === "page" ) { + self.menuPage.dialog( "close" ); + self.list.appendTo( self.listbox ); + } else { + self.listbox.popup( "close" ); + } - // Disable if specified - if ( options.disabled || this.element.attr('disabled')) { - this.disable(); - } + self._focusButton(); + // allow the dialog to be closed again + self.isOpen = false; + }, - // Events on native select - this.select.change(function() { - self.refresh(); - }); + open: function() { + this.button.click(); + }, - if ( this._handleFormReset ) { - this._handleFormReset(); - } - this.build(); - }, + _decideFormat: function() { + var self = this, + $window = $.mobile.window, + selfListParent = self.list.parent(), + menuHeight = selfListParent.outerHeight(), + menuWidth = selfListParent.outerWidth(), + activePage = $( "." + $.mobile.activePageClass ), + scrollTop = $window.scrollTop(), + btnOffset = self.button.offset().top, + screenHeight = $window.height(), + screenWidth = $window.width(); - build: function() { - var self = this; + function focusMenuItem() { + var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" ); + if ( selector.length === 0 ) { + selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" ); + } + selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme ); + } - this.select - .appendTo( self.button ) - .bind( "vmousedown", function() { - // Add active class to button - self.button.addClass( $.mobile.activeBtnClass ); - }) - .bind( "focus", function() { - self.button.addClass( $.mobile.focusClass ); - }) - .bind( "blur", function() { - self.button.removeClass( $.mobile.focusClass ); - }) - .bind( "focus vmouseover", function() { - self.button.trigger( "vmouseover" ); - }) - .bind( "vmousemove", function() { - // Remove active class on scroll/touchmove - self.button.removeClass( $.mobile.activeBtnClass ); - }) - .bind( "change blur vmouseout", function() { - self.button.trigger( "vmouseout" ) - .removeClass( $.mobile.activeBtnClass ); - }) - .bind( "change blur", function() { - self.button.removeClass( "ui-btn-down-" + self.options.theme ); - }); + if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { - // In many situations, iOS will zoom into the select upon tap, this prevents that from happening - self.button.bind( "vmousedown", function() { - if ( self.options.preventFocusZoom ) { - $.mobile.zoom.disable( true ); - } - }); - self.label.bind( "click focus", function() { - if ( self.options.preventFocusZoom ) { - $.mobile.zoom.disable( true ); - } - }); - self.select.bind( "focus", function() { - if ( self.options.preventFocusZoom ) { - $.mobile.zoom.disable( true ); - } - }); - self.button.bind( "mouseup", function() { - if ( self.options.preventFocusZoom ) { - setTimeout(function() { - $.mobile.zoom.enable( true ); - }, 0 ); - } - }); - self.select.bind( "blur", function() { - if ( self.options.preventFocusZoom ) { - $.mobile.zoom.enable( true ); - } - }); + self.menuPage.appendTo( $.mobile.pageContainer ).page(); + self.menuPageContent = menuPage.find( ".ui-content" ); + self.menuPageClose = menuPage.find( ".ui-header a" ); - }, + // prevent the parent page from being removed from the DOM, + // otherwise the results of selecting a list item in the dialog + // fall into a black hole + self.thisPage.unbind( "pagehide.remove" ); - selected: function() { - return this._selectOptions().filter( ":selected" ); - }, + //for WebOS/Opera Mini (set lastscroll using button offset) + if ( scrollTop === 0 && btnOffset > screenHeight ) { + self.thisPage.one( "pagehide", function() { + $( this ).jqmData( "lastScroll", btnOffset ); + }); + } - selectedIndices: function() { - var self = this; + self.menuPage + .one( "pageshow", function() { + focusMenuItem(); + }) + .one( "pagehide", function() { + self.close(); + }); - return this.selected().map(function() { - return self._selectOptions().index( this ); - }).get(); - }, + self.menuType = "page"; + self.menuPageContent.append( self.list ); + self.menuPage.find("div .ui-title").text(self.label.text()); + } else { + self.menuType = "overlay"; - setButtonText: function() { - var self = this, - selected = this.selected(), - text = this.placeholder, - span = $( document.createElement( "span" ) ); + self.listbox.one( "popupafteropen", focusMenuItem ); + } + }, - this.button.find( ".ui-btn-text" ).html(function() { - if ( selected.length ) { - text = selected.map(function() { - return $( this ).text(); - }).get().join( ", " ); - } else { - text = self.placeholder; - } + _buildList: function() { + var self = this, + o = this.options, + placeholder = this.placeholder, + needPlaceholder = true, + optgroups = [], + lis = [], + dataIcon = self.isMultiple ? "checkbox-off" : "false"; - // TODO possibly aggregate multiple select option classes - return span.text( text ) - .addClass( self.select.attr( "class" ) ) - .addClass( selected.attr( "class" ) ); - }); - }, + self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); - setButtonCount: function() { - var selected = this.selected(); + var $options = self.select.find( "option" ), + numOptions = $options.length, + select = this.select[ 0 ], + dataPrefix = 'data-' + $.mobile.ns, + dataIndexAttr = dataPrefix + 'option-index', + dataIconAttr = dataPrefix + 'icon', + dataRoleAttr = dataPrefix + 'role', + dataPlaceholderAttr = dataPrefix + 'placeholder', + fragment = document.createDocumentFragment(), + isPlaceholderItem = false, + optGroup; - // multiple count inside button - if ( this.isMultiple ) { - this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length ); - } - }, + for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) { + var option = $options[i], + $option = $( option ), + parent = option.parentNode, + text = $option.text(), + anchor = document.createElement( 'a' ), + classes = []; - _reset: function() { - this.refresh(); - }, + anchor.setAttribute( 'href', '#' ); + anchor.appendChild( document.createTextNode( text ) ); - refresh: function() { - this.setButtonText(); - this.setButtonCount(); - }, + // Are we inside an optgroup? + if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) { + var optLabel = parent.getAttribute( 'label' ); + if ( optLabel !== optGroup ) { + var divider = document.createElement( 'li' ); + divider.setAttribute( dataRoleAttr, 'list-divider' ); + divider.setAttribute( 'role', 'option' ); + divider.setAttribute( 'tabindex', '-1' ); + divider.appendChild( document.createTextNode( optLabel ) ); + fragment.appendChild( divider ); + optGroup = optLabel; + } + } - // open and close preserved in native selects - // to simplify users code when looping over selects - open: $.noop, - close: $.noop, + if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) { + needPlaceholder = false; + isPlaceholderItem = true; - disable: function() { - this._setDisabled( true ); - this.button.addClass( "ui-disabled" ); - }, + // If we have identified a placeholder, record the fact that it was + // us who have added the placeholder to the option and mark it + // retroactively in the select as well + if ( null === option.getAttribute( dataPlaceholderAttr ) ) { + this._removePlaceholderAttr = true; + } + option.setAttribute( dataPlaceholderAttr, true ); + if ( o.hidePlaceholderMenuItems ) { + classes.push( "ui-selectmenu-placeholder" ); + } + if ( placeholder !== text ) { + placeholder = self.placeholder = text; + } + } - enable: function() { - this._setDisabled( false ); - this.button.removeClass( "ui-disabled" ); - } -}); + var item = document.createElement('li'); + if ( option.disabled ) { + classes.push( "ui-disabled" ); + item.setAttribute('aria-disabled',true); + } + item.setAttribute( dataIndexAttr,i ); + item.setAttribute( dataIconAttr, dataIcon ); + if ( isPlaceholderItem ) { + item.setAttribute( dataPlaceholderAttr, true ); + } + item.className = classes.join( " " ); + item.setAttribute( 'role', 'option' ); + anchor.setAttribute( 'tabindex', '-1' ); + item.appendChild( anchor ); + fragment.appendChild( item ); + } -$.widget( "mobile.selectmenu", $.mobile.selectmenu, $.mobile.behaviors.formReset ); + self.list[0].appendChild( fragment ); -//auto self-init widgets -$.mobile.document.bind( "pagecreate create", function( e ) { - $.mobile.selectmenu.prototype.enhanceWithin( e.target, true ); -}); -})( jQuery ); + // 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 ); + } -/* -* custom "selectmenu" plugin -*/ + // Now populated, create listview + self.list.listview(); + }, -(function( $, undefined ) { - var extendSelect = function( widget ) { + _button: function() { + return $( "", { + "href": "#", + "role": "button", + // TODO value is undefined at creation + "id": this.buttonId, + "aria-haspopup": "true", - var select = widget.select, - origDestroy = widget._destroy, - selectID = widget.selectID, - prefix = ( selectID ? selectID : ( ( $.mobile.ns || "" ) + "uuid-" + widget.uuid ) ), - popupID = prefix + "-listbox", - dialogID = prefix + "-dialog", - label = widget.label, - thisPage = widget.select.closest( ".ui-page" ), - selectOptions = widget._selectOptions(), - isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, - buttonId = selectID + "-button", - menuId = selectID + "-menu", - menuPage = $( "
    " + - "
    " + - "
    " + label.getEncodedText() + "
    "+ - "
    "+ - "
    "+ - "
    " ), + // TODO value is undefined at creation + "aria-owns": this.menuId + }); + }, - listbox = $( "
    " ).insertAfter( widget.select ).popup( { theme: widget.options.overlayTheme } ), + _destroy: function() { + this.close(); - list = $( "
      ", { - "class": "ui-selectmenu-list", - "id": menuId, - "role": "listbox", - "aria-labelledby": buttonId - }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ) - .attr( "data-" + $.mobile.ns + "divider-theme", widget.options.dividerTheme ) - .appendTo( listbox ), + // Restore the tabindex attribute to its original value + if ( this._origTabIndex !== undefined ) { + if ( this._origTabIndex !== false ) { + this.select.attr( "tabindex", this._origTabIndex ); + } else { + this.select.removeAttr( "tabindex" ); + } + } + // Remove the placeholder attribute if we were the ones to add it + if ( this._removePlaceholderAttr ) { + this._selectOptions().removeAttr( "data-" + $.mobile.ns + "placeholder" ); + } - header = $( "
      ", { - "class": "ui-header ui-bar-" + widget.options.theme - }).prependTo( listbox ), + // Remove the popup + this.listbox.remove(); - headerTitle = $( "

      ", { - "class": "ui-title" - }).appendTo( header ), + // Chain up + origDestroy.apply( this, arguments ); + } + }); + }; - menuPageContent, - menuPageClose, - headerClose; + // issue #3894 - core doesn't trigger events on disabled delegates + $.mobile.document.bind( "selectmenubeforecreate", function( event ) { + var selectmenuWidget = $( event.target ).data( "mobile-selectmenu" ); - if ( widget.isMultiple ) { - headerClose = $( "", { - "text": widget.options.closeText, - "href": "#", - "class": "ui-btn-left" - }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(); + if ( !selectmenuWidget.options.nativeMenu && + selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) { + extendSelect( selectmenuWidget ); } + }); +})( jQuery ); - $.extend( widget, { - select: widget.select, - selectID: selectID, - buttonId: buttonId, - menuId: menuId, - popupID: popupID, - dialogID: dialogID, - thisPage: thisPage, - menuPage: menuPage, - label: label, - selectOptions: selectOptions, - isMultiple: isMultiple, - theme: widget.options.theme, - listbox: listbox, - list: list, - header: header, - headerTitle: headerTitle, - headerClose: headerClose, - menuPageContent: menuPageContent, - menuPageClose: menuPageClose, - placeholder: "", +(function( $, undefined ) { - build: function() { - var self = this; + $.widget( "mobile.controlgroup", $.mobile.widget, $.extend( { + options: { + shadow: false, + corners: true, + excludeInvisible: true, + type: "vertical", + mini: false, + initSelector: ":jqmData(role='controlgroup')" + }, - // Create list from select, update state - self.refresh(); + _create: function() { + var $el = this.element, + ui = { + inner: $( "
      " ), + legend: $( "
      " ) + }, + grouplegend = $el.children( "legend" ), + self = this; - if ( self._origTabIndex === undefined ) { - // Map undefined to false, because self._origTabIndex === undefined - // indicates that we have not yet checked whether the select has - // originally had a tabindex attribute, whereas false indicates that - // we have checked the select for such an attribute, and have found - // none present. - self._origTabIndex = ( self.select[ 0 ].getAttribute( "tabindex" ) === null ) ? false : self.select.attr( "tabindex" ); - } - self.select.attr( "tabindex", "-1" ).focus(function() { - $( this ).blur(); - self.button.focus(); - }); + // Apply the proto + $el.wrapInner( ui.inner ); + if ( grouplegend.length ) { + ui.legend.append( grouplegend ).insertBefore( $el.children( 0 ) ); + } + $el.addClass( "ui-corner-all ui-controlgroup" ); - // Button events - self.button.bind( "vclick keydown" , function( event ) { - if ( self.options.disabled || self.isOpen ) { - return; - } - - if (event.type === "vclick" || - event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER || - event.keyCode === $.mobile.keyCode.SPACE)) { - - self._decideFormat(); - if ( self.menuType === "overlay" ) { - self.button.attr( "href", "#" + self.popupID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "popup" ); - } else { - self.button.attr( "href", "#" + self.dialogID ).attr( "data-" + ( $.mobile.ns || "" ) + "rel", "dialog" ); - } - self.isOpen = true; - // Do not prevent default, so the navigation may have a chance to actually open the chosen format - } - }); + $.extend( this, { + _initialRefresh: true + }); - // Events for list items - self.list.attr( "role", "listbox" ) - .bind( "focusin", function( e ) { - $( e.target ) - .attr( "tabindex", "0" ) - .trigger( "vmouseover" ); + $.each( this.options, function( key, value ) { + // Cause initial options to be applied by their handler by temporarily setting the option to undefined + // - the handler then sets it to the initial value + self.options[ key ] = undefined; + self._setOption( key, value, true ); + }); + }, - }) - .bind( "focusout", function( e ) { - $( e.target ) - .attr( "tabindex", "-1" ) - .trigger( "vmouseout" ); - }) - .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) { + _init: function() { + this.refresh(); + }, - // index of option tag to be selected - var oldIndex = self.select[ 0 ].selectedIndex, - newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ), - option = self._selectOptions().eq( newIndex )[ 0 ]; + _setOption: function( key, value ) { + var setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); - // toggle selected status on the tag for multi selects - option.selected = self.isMultiple ? !option.selected : true; + if ( this[ setter ] !== undefined ) { + this[ setter ]( value ); + } - // toggle checkbox class for multiple selects - if ( self.isMultiple ) { - $( this ).find( ".ui-icon" ) - .toggleClass( "ui-icon-checkbox-on", option.selected ) - .toggleClass( "ui-icon-checkbox-off", !option.selected ); - } + this._super( key, value ); + this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); + }, - // trigger change if value changed - if ( self.isMultiple || oldIndex !== newIndex ) { - self.select.trigger( "change" ); - } + _setType: function( value ) { + this.element + .removeClass( "ui-controlgroup-horizontal ui-controlgroup-vertical" ) + .addClass( "ui-controlgroup-" + value ); + this.refresh(); + }, - // hide custom select for single selects only - otherwise focus clicked item - // We need to grab the clicked item the hard way, because the list may have been rebuilt - if ( self.isMultiple ) { - self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex ) - .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); - } - else { - self.close(); - } + _setCorners: function( value ) { + this.element.toggleClass( "ui-corner-all", value ); + }, - event.preventDefault(); - }) - .keydown(function( event ) { //keyboard events for menu items - var target = $( event.target ), - li = target.closest( "li" ), - prev, next; + _setShadow: function( value ) { + this.element.toggleClass( "ui-shadow", value ); + }, - // switch logic based on which key was pressed - switch ( event.keyCode ) { - // up or left arrow keys - case 38: - prev = li.prev().not( ".ui-selectmenu-placeholder" ); + _setMini: function( value ) { + this.element.toggleClass( "ui-mini", value ); + }, - if ( prev.is( ".ui-li-divider" ) ) { - prev = prev.prev(); - } + container: function() { + return this.element.children( ".ui-controlgroup-controls" ); + }, - // if there's a previous option, focus it - if ( prev.length ) { - target - .blur() - .attr( "tabindex", "-1" ); + refresh: function() { + var els = this.element.find( ".ui-btn" ).not( ".ui-slider-handle" ), + create = this._initialRefresh; + if ( $.mobile.checkboxradio ) { + this.element.find( ":mobile-checkboxradio" ).checkboxradio( "refresh" ); + } + this._addFirstLastClasses( els, this.options.excludeInvisible ? this._getVisibles( els, create ) : els, create ); + this._initialRefresh = false; + } + }, $.mobile.behaviors.addFirstLastClasses ) ); - prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); - } + // TODO: Implement a mechanism to allow widgets to become enhanced in the + // correct order when their correct enhancement depends on other widgets in + // the page being correctly enhanced already. + // + // For now, we wait until dom-ready to attach the controlgroup's enhancement + // hook, because by that time, all the other widgets' enhancement hooks should + // already be in place, ensuring that all widgets that need to be grouped will + // already have been enhanced by the time the controlgroup is created. + $( function() { + $.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.controlgroup.prototype.enhanceWithin( e.target, true ); + }); + }); +})(jQuery); - return false; - // down or right arrow keys - case 40: - next = li.next(); +(function( $, undefined ) { - if ( next.is( ".ui-li-divider" ) ) { - next = next.next(); - } +$( document ).bind( "pagecreate create", function( e ) { - // if there's a next option, focus it - if ( next.length ) { - target - .blur() - .attr( "tabindex", "-1" ); + //links within content areas, tests included with page + $( e.target ) + .find( "a" ) + .jqmEnhanceable() + .not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" ) + .addClass( "ui-link" ); - next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); - } +}); - return false; - // If enter or space is pressed, trigger click - case 13: - case 32: - target.trigger( "click" ); +})( jQuery ); - return false; - } - }); - // button refocus ensures proper height calculation - // by removing the inline style and ensuring page inclusion - self.menuPage.bind( "pagehide", function() { - // TODO centralize page removal binding / handling in the page plugin. - // Suggestion from @jblas to do refcounting - // - // TODO extremely confusing dependency on the open method where the pagehide.remove - // bindings are stripped to prevent the parent page from disappearing. The way - // we're keeping pages in the DOM right now sucks - // - // rebind the page remove that was unbound in the open function - // to allow for the parent page removal from actions other than the use - // of a dialog sized custom select - // - // doing this here provides for the back button on the custom select dialog - $.mobile._bindPageRemove.call( self.thisPage ); - }); +(function( $, undefined ) { - // Events on the popup - self.listbox.bind( "popupafterclose", function( event ) { - self.close(); - }); - // Close button on small overlays - if ( self.isMultiple ) { - self.headerClose.click(function() { - if ( self.menuType === "overlay" ) { - self.close(); - return false; - } - }); - } + $.widget( "mobile.fixedtoolbar", $.mobile.widget, { + options: { + visibleOnPageShow: true, + disablePageZoom: true, + transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown) + fullscreen: false, + tapToggle: true, + tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup, .ui-panel, .ui-panel-dismiss-open", + hideDuringFocus: "input, textarea, select", + updatePagePadding: true, + trackPersistentToolbars: true, - // track this dependency so that when the parent page - // is removed on pagehide it will also remove the menupage - self.thisPage.addDependents( this.menuPage ); + // Browser detection! Weeee, here we go... + // Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately. + // Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience. + // Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window + // The following function serves to rule out some popular browsers with known fixed-positioning issues + // This is a plugin option like any other, so feel free to improve or overwrite it + supportBlacklist: function() { + return !$.support.fixedPosition; }, + initSelector: ":jqmData(position='fixed')" + }, - _isRebuildRequired: function() { - var list = this.list.find( "li" ), - options = this._selectOptions(); + _create: function() { - // TODO exceedingly naive method to determine difference - // ignores value changes etc in favor of a forcedRebuild - // from the user in the refresh method - return options.text() !== list.text(); - }, + var self = this, + o = self.options, + $el = self.element, + tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer", + $page = $el.closest( ".ui-page" ); - selected: function() { - return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" ); - }, + // Feature detecting support for + if ( o.supportBlacklist() ) { + self.destroy(); + return; + } - refresh: function( forceRebuild , foo ) { - var self = this, - select = this.element, - isMultiple = this.isMultiple, - indicies; + $el.addClass( "ui-"+ tbtype +"-fixed" ); - if ( forceRebuild || this._isRebuildRequired() ) { - self._buildList(); - } + // "fullscreen" overlay positioning + if ( o.fullscreen ) { + $el.addClass( "ui-"+ tbtype +"-fullscreen" ); + $page.addClass( "ui-page-" + tbtype + "-fullscreen" ); + } + // If not fullscreen, add class to page to set top or bottom padding + else{ + $page.addClass( "ui-page-" + tbtype + "-fixed" ); + } - indicies = this.selectedIndices(); + $.extend( this, { + _thisPage: null + }); + + self._addTransitionClass(); + self._bindPageEvents(); + self._bindToggleHandlers(); + }, - self.setButtonText(); - self.setButtonCount(); + _addTransitionClass: function() { + var tclass = this.options.transition; - self.list.find( "li:not(.ui-li-divider)" ) - .removeClass( $.mobile.activeBtnClass ) - .attr( "aria-selected", false ) - .each(function( i ) { + if ( tclass && tclass !== "none" ) { + // use appropriate slide for header or footer + if ( tclass === "slide" ) { + tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup"; + } - if ( $.inArray( i, indicies ) > -1 ) { - var item = $( this ); + this.element.addClass( tclass ); + } + }, - // Aria selected attr - item.attr( "aria-selected", true ); + _bindPageEvents: function() { + this._thisPage = this.element.closest( ".ui-page" ); + //page event bindings + // Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up + // This method is meant to disable zoom while a fixed-positioned toolbar page is visible + this._on( this._thisPage, { + "pagebeforeshow": "_handlePageBeforeShow", + "webkitAnimationStart":"_handleAnimationStart", + "animationstart":"_handleAnimationStart", + "updatelayout": "_handleAnimationStart", + "pageshow": "_handlePageShow", + "pagebeforehide": "_handlePageBeforeHide" + }); + }, - // Multiple selects: add the "on" checkbox state to the icon - if ( self.isMultiple ) { - item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" ); - } else { - if ( item.is( ".ui-selectmenu-placeholder" ) ) { - item.next().addClass( $.mobile.activeBtnClass ); - } else { - item.addClass( $.mobile.activeBtnClass ); - } - } - } - }); - }, + _handlePageBeforeShow: function() { + var o = this.options; + if ( o.disablePageZoom ) { + $.mobile.zoom.disable( true ); + } + if ( !o.visibleOnPageShow ) { + this.hide( true ); + } + }, - close: function() { - if ( this.options.disabled || !this.isOpen ) { - return; - } + _handleAnimationStart: function() { + if ( this.options.updatePagePadding ) { + this.updatePagePadding( this._thisPage ); + } + }, - var self = this; + _handlePageShow: function() { + this.updatePagePadding( this._thisPage ); + if ( this.options.updatePagePadding ) { + this._on( $.mobile.window, { "throttledresize": "updatePagePadding" } ); + } + }, - if ( self.menuType === "page" ) { - self.menuPage.dialog( "close" ); - self.list.appendTo( self.listbox ); - } else { - self.listbox.popup( "close" ); - } + _handlePageBeforeHide: function( e, ui ) { + var o = this.options; - self._focusButton(); - // allow the dialog to be closed again - self.isOpen = false; - }, + if ( o.disablePageZoom ) { + $.mobile.zoom.enable( true ); + } + if ( o.updatePagePadding ) { + this._off( $.mobile.window, "throttledresize" ); + } - open: function() { - this.button.click(); - }, + if ( o.trackPersistentToolbars ) { + var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this._thisPage ), + thisHeader = $( ".ui-header-fixed:jqmData(id)", this._thisPage ), + nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(), + nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $(); - _decideFormat: function() { - var self = this, - $window = $.mobile.window, - selfListParent = self.list.parent(), - menuHeight = selfListParent.outerHeight(), - menuWidth = selfListParent.outerWidth(), - activePage = $( "." + $.mobile.activePageClass ), - scrollTop = $window.scrollTop(), - btnOffset = self.button.offset().top, - screenHeight = $window.height(), - screenWidth = $window.width(); + if ( nextFooter.length || nextHeader.length ) { - function focusMenuItem() { - var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" ); - if ( selector.length === 0 ) { - selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" ); - } - selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme ); + nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer ); + + ui.nextPage.one( "pageshow", function() { + nextHeader.prependTo( this ); + nextFooter.appendTo( this ); + }); } + } + }, - if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { + _visible: true, - self.menuPage.appendTo( $.mobile.pageContainer ).page(); - self.menuPageContent = menuPage.find( ".ui-content" ); - self.menuPageClose = menuPage.find( ".ui-header a" ); + // This will set the content element's top or bottom padding equal to the toolbar's height + updatePagePadding: function( tbPage ) { + var $el = this.element, + header = $el.is( ".ui-header" ), + pos = parseFloat( $el.css( header ? "top" : "bottom" ) ); - // prevent the parent page from being removed from the DOM, - // otherwise the results of selecting a list item in the dialog - // fall into a black hole - self.thisPage.unbind( "pagehide.remove" ); + // This behavior only applies to "fixed", not "fullscreen" + if ( this.options.fullscreen ) { return; } - //for WebOS/Opera Mini (set lastscroll using button offset) - if ( scrollTop === 0 && btnOffset > screenHeight ) { - self.thisPage.one( "pagehide", function() { - $( this ).jqmData( "lastScroll", btnOffset ); - }); - } + // tbPage argument can be a Page object or an event, if coming from throttled resize. + tbPage = ( tbPage && tbPage.type === undefined && tbPage ) || this._thisPage || $el.closest( ".ui-page" ); + $( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() + pos ); + }, - self.menuPage - .one( "pageshow", function() { - focusMenuItem(); - }) - .one( "pagehide", function() { - self.close(); - }); + _useTransition: function( notransition ) { + var $win = $.mobile.window, + $el = this.element, + scroll = $win.scrollTop(), + elHeight = $el.height(), + pHeight = $el.closest( ".ui-page" ).height(), + viewportHeight = $.mobile.getScreenHeight(), + tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer"; - self.menuType = "page"; - self.menuPageContent.append( self.list ); - self.menuPage.find("div .ui-title").text(self.label.text()); - } else { - self.menuType = "overlay"; + return !notransition && + ( this.options.transition && this.options.transition !== "none" && + ( + ( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) || + ( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight ) + ) || this.options.fullscreen + ); + }, - self.listbox.one( "popupafteropen", focusMenuItem ); - } - }, + show: function( notransition ) { + var hideClass = "ui-fixed-hidden", + $el = this.element; - _buildList: function() { - var self = this, - o = this.options, - placeholder = this.placeholder, - needPlaceholder = true, - optgroups = [], - lis = [], - dataIcon = self.isMultiple ? "checkbox-off" : "false"; + if ( this._useTransition( notransition ) ) { + $el + .removeClass( "out " + hideClass ) + .addClass( "in" ) + .animationComplete(function () { + $el.removeClass('in'); + }); + } + else { + $el.removeClass( hideClass ); + } + this._visible = true; + }, - self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); + hide: function( notransition ) { + var hideClass = "ui-fixed-hidden", + $el = this.element, + // if it's a slide transition, our new transitions need the reverse class as well to slide outward + outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" ); - var $options = self.select.find( "option" ), - numOptions = $options.length, - select = this.select[ 0 ], - dataPrefix = 'data-' + $.mobile.ns, - dataIndexAttr = dataPrefix + 'option-index', - dataIconAttr = dataPrefix + 'icon', - dataRoleAttr = dataPrefix + 'role', - dataPlaceholderAttr = dataPrefix + 'placeholder', - fragment = document.createDocumentFragment(), - isPlaceholderItem = false, - optGroup; + if( this._useTransition( notransition ) ) { + $el + .addClass( outclass ) + .removeClass( "in" ) + .animationComplete(function() { + $el.addClass( hideClass ).removeClass( outclass ); + }); + } + else { + $el.addClass( hideClass ).removeClass( outclass ); + } + this._visible = false; + }, - for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) { - var option = $options[i], - $option = $( option ), - parent = option.parentNode, - text = $option.text(), - anchor = document.createElement( 'a' ), - classes = []; + toggle: function() { + this[ this._visible ? "hide" : "show" ](); + }, - anchor.setAttribute( 'href', '#' ); - anchor.appendChild( document.createTextNode( text ) ); + _bindToggleHandlers: function() { + var self = this, + o = self.options, + $el = self.element, + delayShow, delayHide, + isVisible = true; - // Are we inside an optgroup? - if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) { - var optLabel = parent.getAttribute( 'label' ); - if ( optLabel !== optGroup ) { - var divider = document.createElement( 'li' ); - divider.setAttribute( dataRoleAttr, 'list-divider' ); - divider.setAttribute( 'role', 'option' ); - divider.setAttribute( 'tabindex', '-1' ); - divider.appendChild( document.createTextNode( optLabel ) ); - fragment.appendChild( divider ); - optGroup = optLabel; + // tap toggle + $el.closest( ".ui-page" ) + .bind( "vclick", function( e ) { + if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) { + self.toggle(); + } + }) + .bind( "focusin focusout", function( e ) { + //this hides the toolbars on a keyboard pop to give more screen room and prevent ios bug which + //positions fixed toolbars in the middle of the screen on pop if the input is near the top or + //bottom of the screen addresses issues #4410 Footer navbar moves up when clicking on a textbox in an Android environment + //and issue #4113 Header and footer change their position after keyboard popup - iOS + //and issue #4410 Footer navbar moves up when clicking on a textbox in an Android environment + if ( screen.width < 1025 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) { + //Fix for issue #4724 Moving through form in Mobile Safari with "Next" and "Previous" system + //controls causes fixed position, tap-toggle false Header to reveal itself + // isVisible instead of self._visible because the focusin and focusout events fire twice at the same time + // Also use a delay for hiding the toolbars because on Android native browser focusin is direclty followed + // by a focusout when a native selects opens and the other way around when it closes. + if ( e.type === "focusout" && !isVisible ) { + isVisible = true; + //wait for the stack to unwind and see if we have jumped to another input + clearTimeout( delayHide ); + delayShow = setTimeout( function() { + self.show(); + }, 0 ); + } else if ( e.type === "focusin" && !!isVisible ) { + //if we have jumped to another input clear the time out to cancel the show. + clearTimeout( delayShow ); + isVisible = false; + delayHide = setTimeout( function() { + self.hide(); + }, 0 ); } } + }); + }, - if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) { - needPlaceholder = false; - isPlaceholderItem = true; + _destroy: function() { + var $el = this.element, + header = $el.is( ".ui-header" ); - // If we have identified a placeholder, record the fact that it was - // us who have added the placeholder to the option and mark it - // retroactively in the select as well - if ( null === option.getAttribute( dataPlaceholderAttr ) ) { - this._removePlaceholderAttr = true; - } - option.setAttribute( dataPlaceholderAttr, true ); - if ( o.hidePlaceholderMenuItems ) { - classes.push( "ui-selectmenu-placeholder" ); - } - if ( placeholder !== text ) { - placeholder = self.placeholder = text; - } - } + $el.closest( ".ui-page" ).css( "padding-" + ( header ? "top" : "bottom" ), "" ); + $el.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" ); + $el.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" ); + } - var item = document.createElement('li'); - if ( option.disabled ) { - classes.push( "ui-disabled" ); - item.setAttribute('aria-disabled',true); - } - item.setAttribute( dataIndexAttr,i ); - item.setAttribute( dataIconAttr, dataIcon ); - if ( isPlaceholderItem ) { - item.setAttribute( dataPlaceholderAttr, true ); - } - item.className = classes.join( " " ); - item.setAttribute( 'role', 'option' ); - anchor.setAttribute( 'tabindex', '-1' ); - item.appendChild( anchor ); - fragment.appendChild( item ); - } + }); - self.list[0].appendChild( fragment ); + //auto self-init widgets + $.mobile.document + .bind( "pagecreate create", function( e ) { - // Hide header if it's not a multiselect and there's no placeholder - if ( !this.isMultiple && !placeholder.length ) { - this.header.hide(); + // DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element. + // This line ensures it still works, but we recommend moving the attribute to the toolbars themselves. + if ( $( e.target ).jqmData( "fullscreen" ) ) { + $( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true ); + } + + $.mobile.fixedtoolbar.prototype.enhanceWithin( e.target ); + }); + +})( jQuery ); + +(function( $, undefined ) { + $.widget( "mobile.fixedtoolbar", $.mobile.fixedtoolbar, { + + _create: function() { + this._super(); + this._workarounds(); + }, + + //check the browser and version and run needed workarounds + _workarounds: function() { + var ua = navigator.userAgent, + platform = navigator.platform, + // Rendering engine is Webkit, and capture major version + wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ), + wkversion = !!wkmatch && wkmatch[ 1 ], + os = null, + self = this; + //set the os we are working in if it dosent match one with workarounds return + if( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ){ + os = "ios"; + } else if( ua.indexOf( "Android" ) > -1 ){ + os = "android"; } else { - this.headerTitle.text( this.placeholder ); + return; + } + //check os version if it dosent match one with workarounds return + if( os === "ios" ) { + //iOS workarounds + self._bindScrollWorkaround(); + } else if( os === "android" && wkversion && wkversion < 534 ) { + //Android 2.3 run all Android 2.3 workaround + self._bindScrollWorkaround(); + self._bindListThumbWorkaround(); + } else { + return; } + }, - // Now populated, create listview - self.list.listview(); + //Utility class for checking header and footer positions relative to viewport + _viewportOffset: function() { + var $el = this.element, + header = $el.is( ".ui-header" ), + offset = Math.abs($el.offset().top - $.mobile.window.scrollTop()); + if( !header ) { + offset = Math.round(offset - $.mobile.window.height() + $el.outerHeight())-60; + } + return offset; }, - _button: function() { - return $( "
      ", { - "href": "#", - "role": "button", - // TODO value is undefined at creation - "id": this.buttonId, - "aria-haspopup": "true", + //bind events for _triggerRedraw() function + _bindScrollWorkaround: function() { + var self = this; + //bind to scrollstop and check if the toolbars are correctly positioned + this._on( $.mobile.window, { scrollstop: function() { + var viewportOffset = self._viewportOffset(); + //check if the header is visible and if its in the right place + if( viewportOffset > 2 && self._visible) { + self._triggerRedraw(); + } + }}); + }, + + //this addresses issue #4250 Persistent footer instability in v1.1 with long select lists in Android 2.3.3 + //and issue #3748 Android 2.x: Page transitions broken when fixed toolbars used + //the absolutely positioned thumbnail in a list view causes problems with fixed position buttons above in a nav bar + //setting the li's to -webkit-transform:translate3d(0,0,0); solves this problem to avoide potential issues in other + //platforms we scope this with the class ui-android-2x-fix + _bindListThumbWorkaround: function() { + this.element.closest(".ui-page").addClass( "ui-android-2x-fixed" ); + }, + //this addresses issues #4337 Fixed header problem after scrolling content on iOS and Android + //and device bugs project issue #1 Form elements can lose click hit area in position: fixed containers. + //this also addresses not on fixed toolbars page in docs + //adding 1px of padding to the bottom then removing it causes a "redraw" + //which positions the toolbars correctly (they will always be visually correct) + _triggerRedraw: function() { + var paddingBottom = parseFloat( $( ".ui-page-active" ).css( "padding-bottom" ) ); + //trigger page redraw to fix incorrectly positioned fixed elements + $( ".ui-page-active" ).css( "padding-bottom", ( paddingBottom + 1 ) +"px" ); + //if the padding is reset with out a timeout the reposition will not occure. + //this is independant of JQM the browser seems to need the time to react. + setTimeout( function() { + $( ".ui-page-active" ).css( "padding-bottom", paddingBottom + "px" ); + }, 0 ); + }, + + destroy: function() { + this._super(); + //Remove the class we added to the page previously in android 2.x + this.element.closest(".ui-page-active").removeClass( "ui-android-2x-fix" ); + } + }); + + })( jQuery ); + +(function( $, undefined ) { + +$.widget( "mobile.panel", $.mobile.widget, { + options: { + classes: { + panel: "ui-panel", + panelOpen: "ui-panel-open", + panelClosed: "ui-panel-closed", + panelFixed: "ui-panel-fixed", + panelInner: "ui-panel-inner", + modal: "ui-panel-dismiss", + modalOpen: "ui-panel-dismiss-open", + pagePanel: "ui-page-panel", + pagePanelOpen: "ui-page-panel-open", + contentWrap: "ui-panel-content-wrap", + contentWrapOpen: "ui-panel-content-wrap-open", + contentWrapClosed: "ui-panel-content-wrap-closed", + contentFixedToolbar: "ui-panel-content-fixed-toolbar", + contentFixedToolbarOpen: "ui-panel-content-fixed-toolbar-open", + contentFixedToolbarClosed: "ui-panel-content-fixed-toolbar-closed", + animate: "ui-panel-animate" + }, + animate: true, + theme: "c", + position: "left", + dismissible: true, + display: "reveal", //accepts reveal, push, overlay + initSelector: ":jqmData(role='panel')", + swipeClose: true, + positionFixed: false + }, + + _panelID: null, + _closeLink: null, + _page: null, + _modal: null, + _panelInner: null, + _wrapper: null, + _fixedToolbar: null, + + _create: function() { + var self = this, + $el = self.element, + page = $el.closest( ":jqmData(role='page')" ), + _getPageTheme = function() { + var $theme = $.data( page[0], "mobilePage" ).options.theme, + $pageThemeClass = "ui-body-" + $theme; + return $pageThemeClass; + }, + _getPanelInner = function() { + var $panelInner = $el.find( "." + self.options.classes.panelInner ); + if ( $panelInner.length === 0 ) { + $panelInner = $el.children().wrapAll( '
      ' ).parent(); + } + return $panelInner; + }, + _getWrapper = function() { + var $wrapper = page.find( "." + self.options.classes.contentWrap ); + if ( $wrapper.length === 0 ) { + $wrapper = page.children( ".ui-header:not(:jqmData(position='fixed')), .ui-content:not(:jqmData(role='popup')), .ui-footer:not(:jqmData(position='fixed'))" ).wrapAll( '
      ' ).parent(); + if ( $.support.cssTransform3d && !!self.options.animate ) { + $wrapper.addClass( self.options.classes.animate ); + } + } + return $wrapper; + }, + _getFixedToolbar = function() { + var $fixedToolbar = page.find( "." + self.options.classes.contentFixedToolbar ); + if ( $fixedToolbar.length === 0 ) { + $fixedToolbar = page.find( ".ui-header:jqmData(position='fixed'), .ui-footer:jqmData(position='fixed')" ).addClass( self.options.classes.contentFixedToolbar ); + if ( $.support.cssTransform3d && !!self.options.animate ) { + $fixedToolbar.addClass( self.options.classes.animate ); + } + } + return $fixedToolbar; + }; + + // expose some private props to other methods + $.extend( this, { + _panelID: $el.attr( "id" ), + _closeLink: $el.find( ":jqmData(rel='close')" ), + _page: $el.closest( ":jqmData(role='page')" ), + _pageTheme: _getPageTheme(), + _panelInner: _getPanelInner(), + _wrapper: _getWrapper(), + _fixedToolbar: _getFixedToolbar() + }); + + self._addPanelClasses(); + self._wrapper.addClass( this.options.classes.contentWrapClosed ); + self._fixedToolbar.addClass( this.options.classes.contentFixedToolbarClosed ); + // add class to page so we can set "overflow-x: hidden;" for it to fix Android zoom issue + self._page.addClass( self.options.classes.pagePanel ); + + // if animating, add the class to do so + if ( $.support.cssTransform3d && !!self.options.animate ) { + this.element.addClass( self.options.classes.animate ); + } + + self._bindUpdateLayout(); + self._bindCloseEvents(); + self._bindLinkListeners(); + self._bindPageEvents(); + + if ( !!self.options.dismissible ) { + self._createModal(); + } + + self._bindSwipeEvents(); + }, + + _createModal: function( options ) { + var self = this; + + self._modal = $( "
      " ) + .on( "mousedown", function() { + self.close(); + }) + .appendTo( this._page ); + }, + + _getPosDisplayClasses: function( prefix ) { + return prefix + "-position-" + this.options.position + " " + prefix + "-display-" + this.options.display; + }, + + _getPanelClasses: function() { + var panelClasses = this.options.classes.panel + + " " + this._getPosDisplayClasses( this.options.classes.panel ) + + " " + this.options.classes.panelClosed; + + if ( this.options.theme ) { + panelClasses += " ui-body-" + this.options.theme; + } + if ( !!this.options.positionFixed ) { + panelClasses += " " + this.options.classes.panelFixed; + } + return panelClasses; + }, + + _addPanelClasses: function() { + this.element.addClass( this._getPanelClasses() ); + }, + + _bindCloseEvents: function() { + var self = this; + + self._closeLink.on( "click.panel" , function( e ) { + e.preventDefault(); + self.close(); + return false; + }); + self.element.on( "click.panel" , "a:jqmData(ajax='false')", function( e ) { + self.close(); + }); + }, + + _positionPanel: function() { + var self = this, + panelInnerHeight = self._panelInner.outerHeight(), + expand = panelInnerHeight > $.mobile.getScreenHeight(); + + if ( expand || !self.options.positionFixed ) { + if ( expand ) { + self._unfixPanel(); + $.mobile.resetActivePageHeight( panelInnerHeight ); + } + self._scrollIntoView( panelInnerHeight ); + } else { + self._fixPanel(); + } + }, + + _scrollIntoView: function( panelInnerHeight ) { + if ( panelInnerHeight < $( window ).scrollTop() ) { + window.scrollTo( 0, 0 ); + } + }, + + _bindFixListener: function() { + this._on( $( window ), { "throttledresize": "_positionPanel" }); + }, + + _unbindFixListener: function() { + this._off( $( window ), "throttledresize" ); + }, + + _unfixPanel: function() { + if ( !!this.options.positionFixed && $.support.fixedPosition ) { + this.element.removeClass( this.options.classes.panelFixed ); + } + }, + + _fixPanel: function() { + if ( !!this.options.positionFixed && $.support.fixedPosition ) { + this.element.addClass( this.options.classes.panelFixed ); + } + }, + + _bindUpdateLayout: function() { + var self = this; + + self.element.on( "updatelayout", function( e ) { + if ( self._open ) { + self._positionPanel(); + } + }); + }, + + _bindLinkListeners: function() { + var self = this; + + self._page.on( "click.panel" , "a", function( e ) { + if ( this.href.split( "#" )[ 1 ] === self._panelID && self._panelID !== undefined ) { + e.preventDefault(); + var $link = $( this ); + if ( ! $link.hasClass( "ui-link" ) ) { + $link.addClass( $.mobile.activeBtnClass ); + self.element.one( "panelopen panelclose", function() { + $link.removeClass( $.mobile.activeBtnClass ); + }); + } + self.toggle(); + return false; + } + }); + }, + + _bindSwipeEvents: function() { + var self = this, + area = self._modal ? self.element.add( self._modal ) : self.element; + + // on swipe, close the panel + if( !!self.options.swipeClose ) { + if ( self.options.position === "left" ) { + area.on( "swipeleft.panel", function( e ) { + self.close(); + }); + } else { + area.on( "swiperight.panel", function( e ) { + self.close(); + }); + } + } + }, + + _bindPageEvents: function() { + var self = this; + + self._page + // Close the panel if another panel on the page opens + .on( "panelbeforeopen", function( e ) { + if ( self._open && e.target !== self.element[ 0 ] ) { + self.close(); + } + }) + // clean up open panels after page hide + .on( "pagehide", function( e ) { + if ( self._open ) { + self.close( true ); + } + }) + // on escape, close? might need to have a target check too... + .on( "keyup.panel", function( e ) { + if ( e.keyCode === 27 && self._open ) { + self.close(); + } + }); + }, + + // state storage of open or closed + _open: false, + + _contentWrapOpenClasses: null, + _fixedToolbarOpenClasses: null, + _modalOpenClasses: null, + + open: function( immediate ) { + if ( !this._open ) { + var self = this, + o = self.options, + _openPanel = function() { + self._page.off( "panelclose" ); + self._page.jqmData( "panel", "open" ); + + if ( !immediate && $.support.cssTransform3d && !!o.animate ) { + self.element.add( self._wrapper ).on( self._transitionEndEvents, complete ); + } else { + setTimeout( complete, 0 ); + } + + if ( self.options.theme && self.options.display !== "overlay" ) { + self._page + .removeClass( self._pageTheme ) + .addClass( "ui-body-" + self.options.theme ); + } + + self.element.removeClass( o.classes.panelClosed ).addClass( o.classes.panelOpen ); + + self._positionPanel(); + + // Fix for IE7 min-height bug + if ( self.options.theme && self.options.display !== "overlay" ) { + self._wrapper.css( "min-height", self._page.css( "min-height" ) ); + } + + self._contentWrapOpenClasses = self._getPosDisplayClasses( o.classes.contentWrap ); + self._wrapper + .removeClass( o.classes.contentWrapClosed ) + .addClass( self._contentWrapOpenClasses + " " + o.classes.contentWrapOpen ); + + self._fixedToolbarOpenClasses = self._getPosDisplayClasses( o.classes.contentFixedToolbar ); + self._fixedToolbar + .removeClass( o.classes.contentFixedToolbarClosed ) + .addClass( self._fixedToolbarOpenClasses + " " + o.classes.contentFixedToolbarOpen ); + + self._modalOpenClasses = self._getPosDisplayClasses( o.classes.modal ) + " " + o.classes.modalOpen; + if ( self._modal ) { + self._modal.addClass( self._modalOpenClasses ); + } + }, + complete = function() { + self.element.add( self._wrapper ).off( self._transitionEndEvents, complete ); + + self._page.addClass( o.classes.pagePanelOpen ); + + self._bindFixListener(); + + self._trigger( "open" ); + }; + + if ( this.element.closest( ".ui-page-active" ).length < 0 ) { + immediate = true; + } + + self._trigger( "beforeopen" ); + + if ( self._page.jqmData('panel') === "open" ) { + self._page.on( "panelclose", function() { + _openPanel(); + }); + } else { + _openPanel(); + } + + self._open = true; + } + }, + + close: function( immediate ) { + if ( this._open ) { + var o = this.options, + self = this, + _closePanel = function() { + if ( !immediate && $.support.cssTransform3d && !!o.animate ) { + self.element.add( self._wrapper ).on( self._transitionEndEvents, complete ); + } else { + setTimeout( complete, 0 ); + } + + self._page.removeClass( o.classes.pagePanelOpen ); + self.element.removeClass( o.classes.panelOpen ); + self._wrapper.removeClass( o.classes.contentWrapOpen ); + self._fixedToolbar.removeClass( o.classes.contentFixedToolbarOpen ); + + if ( self._modal ) { + self._modal.removeClass( self._modalOpenClasses ); + } + }, + complete = function() { + if ( self.options.theme && self.options.display !== "overlay" ) { + self._page.removeClass( "ui-body-" + self.options.theme ).addClass( self._pageTheme ); + // reset fix for IE7 min-height bug + self._wrapper.css( "min-height", "" ); + } + self.element.add( self._wrapper ).off( self._transitionEndEvents, complete ); + self.element.addClass( o.classes.panelClosed ); + + self._wrapper + .removeClass( self._contentWrapOpenClasses ) + .addClass( o.classes.contentWrapClosed ); + + self._fixedToolbar + .removeClass( self._fixedToolbarOpenClasses ) + .addClass( o.classes.contentFixedToolbarClosed ); + + self._fixPanel(); + self._unbindFixListener(); + $.mobile.resetActivePageHeight(); + + self._page.jqmRemoveData( "panel" ); + self._trigger( "close" ); + }; + + if ( this.element.closest( ".ui-page-active" ).length < 0 ) { + immediate = true; + } + self._trigger( "beforeclose" ); + + _closePanel(); + + self._open = false; + } + }, + + toggle: function( options ) { + this[ this._open ? "close" : "open" ](); + }, - // TODO value is undefined at creation - "aria-owns": this.menuId - }); - }, + _transitionEndEvents: "webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd", - _destroy: function() { - this.close(); + _destroy: function() { + var classes = this.options.classes, + theme = this.options.theme, + hasOtherSiblingPanels = this.element.siblings( "." + classes.panel ).length; - // Restore the tabindex attribute to its original value - if ( this._origTabIndex !== undefined ) { - if ( this._origTabIndex !== false ) { - this.select.attr( "tabindex", this._origTabIndex ); - } else { - this.select.removeAttr( "tabindex" ); - } + // create + if ( !hasOtherSiblingPanels ) { + this._wrapper.children().unwrap(); + this._page.find( "a" ).unbind( "panelopen panelclose" ); + this._page.removeClass( classes.pagePanel ); + if ( this._open ) { + this._page.jqmRemoveData( "panel" ); + this._page.removeClass( classes.pagePanelOpen ); + if ( theme ) { + this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme ); } + $.mobile.resetActivePageHeight(); + } + } else if ( this._open ) { + this._wrapper.removeClass( classes.contentWrapOpen ); + this._fixedToolbar.removeClass( classes.contentFixedToolbarOpen ); + this._page.jqmRemoveData( "panel" ); + this._page.removeClass( classes.pagePanelOpen ); + if ( theme ) { + this._page.removeClass( "ui-body-" + theme ).addClass( this._pageTheme ); + } + } + + this._panelInner.children().unwrap(); - // Remove the placeholder attribute if we were the ones to add it - if ( this._removePlaceholderAttr ) { - this._selectOptions().removeAttr( "data-" + $.mobile.ns + "placeholder" ); - } + this.element.removeClass( [ this._getPanelClasses(), classes.panelAnimate ].join( " " ) ) + .off( "swipeleft.panel swiperight.panel" ) + .off( "panelbeforeopen" ) + .off( "panelhide" ) + .off( "keyup.panel" ) + .off( "updatelayout" ); - // Remove the popup - this.listbox.remove(); + this._closeLink.off( "click.panel" ); - // Chain up - origDestroy.apply( this, arguments ); - } - }); - }; + if ( this._modal ) { + this._modal.remove(); + } - // issue #3894 - core doesn't trigger events on disabled delegates - $.mobile.document.bind( "selectmenubeforecreate", function( event ) { - var selectmenuWidget = $( event.target ).data( "mobile-selectmenu" ); + // open and close + this.element.off( this._transitionEndEvents ) + .removeClass( [ classes.panelUnfixed, classes.panelClosed, classes.panelOpen ].join( " " ) ); + } +}); + +//auto self-init widgets +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.panel.prototype.enhanceWithin( e.target ); +}); - if ( !selectmenuWidget.options.nativeMenu && - selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) { - extendSelect( selectmenuWidget ); - } - }); })( jQuery ); (function( $, undefined ) { +$.widget( "mobile.table", $.mobile.widget, { - $.widget( "mobile.fixedtoolbar", $.mobile.widget, { options: { - visibleOnPageShow: true, - disablePageZoom: true, - transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown) - fullscreen: false, - tapToggle: true, - tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup, .ui-panel, .ui-panel-dismiss-open", - hideDuringFocus: "input, textarea, select", - updatePagePadding: true, - trackPersistentToolbars: true, - - // Browser detection! Weeee, here we go... - // Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately. - // Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience. - // Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window - // The following function serves to rule out some popular browsers with known fixed-positioning issues - // This is a plugin option like any other, so feel free to improve or overwrite it - supportBlacklist: function() { - return !$.support.fixedPosition; + classes: { + table: "ui-table" }, - initSelector: ":jqmData(position='fixed')" + initSelector: ":jqmData(role='table')" }, _create: function() { + var self = this; + self.refresh( true ); + }, + refresh: function (create) { var self = this, - o = self.options, - $el = self.element, - tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer", - $page = $el.closest( ".ui-page" ); + trs = this.element.find( "thead tr" ); - // Feature detecting support for - if ( o.supportBlacklist() ) { - self.destroy(); - return; + if ( create ) { + this.element.addClass( this.options.classes.table ); } - $el.addClass( "ui-"+ tbtype +"-fixed" ); + // Expose headers and allHeaders properties on the widget + // headers references the THs within the first TR in the table + self.headers = this.element.find( "tr:eq(0)" ).children(); - // "fullscreen" overlay positioning - if ( o.fullscreen ) { - $el.addClass( "ui-"+ tbtype +"-fullscreen" ); - $page.addClass( "ui-page-" + tbtype + "-fullscreen" ); - } - // If not fullscreen, add class to page to set top or bottom padding - else{ - $page.addClass( "ui-page-" + tbtype + "-fixed" ); - } + // allHeaders references headers, plus all THs in the thead, which may include several rows, or not + self.allHeaders = self.headers.add( trs.children() ); - $.extend( this, { - _thisPage: null - }); + trs.each(function(){ - self._addTransitionClass(); - self._bindPageEvents(); - self._bindToggleHandlers(); - }, + var coltally = 0; - _addTransitionClass: function() { - var tclass = this.options.transition; + $( this ).children().each(function( i ){ - if ( tclass && tclass !== "none" ) { - // use appropriate slide for header or footer - if ( tclass === "slide" ) { - tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup"; - } + var span = parseInt( $( this ).attr( "colspan" ), 10 ), + sel = ":nth-child(" + ( coltally + 1 ) + ")"; + $( this ) + .jqmData( "colstart", coltally + 1 ); - this.element.addClass( tclass ); - } - }, + if( span ){ + for( var j = 0; j < span - 1; j++ ){ + coltally++; + sel += ", :nth-child(" + ( coltally + 1 ) + ")"; + } + } + + if ( create === undefined ) { + $(this).jqmData("cells", ""); + } + // Store "cells" data on header as a reference to all cells in the same column as this TH + $( this ) + .jqmData( "cells", self.element.find( "tr" ).not( trs.eq(0) ).not( this ).children( sel ) ); + + coltally++; + + }); - _bindPageEvents: function() { - this._thisPage = this.element.closest( ".ui-page" ); - //page event bindings - // Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up - // This method is meant to disable zoom while a fixed-positioned toolbar page is visible - this._on( this._thisPage, { - "pagebeforeshow": "_handlePageBeforeShow", - "webkitAnimationStart":"_handleAnimationStart", - "animationstart":"_handleAnimationStart", - "updatelayout": "_handleAnimationStart", - "pageshow": "_handlePageShow", - "pagebeforehide": "_handlePageBeforeHide" }); - }, - _handlePageBeforeShow: function() { - var o = this.options; - if ( o.disablePageZoom ) { - $.mobile.zoom.disable( true ); - } - if ( !o.visibleOnPageShow ) { - this.hide( true ); + // update table modes + if ( create === undefined ) { + this.element.trigger( 'refresh' ); } - }, + } - _handleAnimationStart: function() { - if ( this.options.updatePagePadding ) { - this.updatePagePadding( this._thisPage ); - } - }, +}); - _handlePageShow: function() { - this.updatePagePadding( this._thisPage ); - if ( this.options.updatePagePadding ) { - this._on( $.mobile.window, { "throttledresize": "updatePagePadding" } ); - } - }, +//auto self-init widgets +$.mobile.document.bind( "pagecreate create", function( e ) { + $.mobile.table.prototype.enhanceWithin( e.target ); +}); - _handlePageBeforeHide: function( e, ui ) { - var o = this.options; +})( jQuery ); - if ( o.disablePageZoom ) { - $.mobile.zoom.enable( true ); - } - if ( o.updatePagePadding ) { - this._off( $.mobile.window, "throttledresize" ); - } - if ( o.trackPersistentToolbars ) { - var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this._thisPage ), - thisHeader = $( ".ui-header-fixed:jqmData(id)", this._thisPage ), - nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(), - nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $(); +(function( $, undefined ) { - if ( nextFooter.length || nextHeader.length ) { +$.mobile.table.prototype.options.mode = "columntoggle"; - nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer ); +$.mobile.table.prototype.options.columnBtnTheme = null; - ui.nextPage.one( "pageshow", function() { - nextHeader.prependTo( this ); - nextFooter.appendTo( this ); - }); - } - } - }, +$.mobile.table.prototype.options.columnPopupTheme = null; - _visible: true, +$.mobile.table.prototype.options.columnBtnText = "Columns..."; - // This will set the content element's top or bottom padding equal to the toolbar's height - updatePagePadding: function( tbPage ) { - var $el = this.element, - header = $el.is( ".ui-header" ), - pos = parseFloat( $el.css( header ? "top" : "bottom" ) ); +$.mobile.table.prototype.options.classes = $.extend( + $.mobile.table.prototype.options.classes, + { + popup: "ui-table-columntoggle-popup", + columnBtn: "ui-table-columntoggle-btn", + priorityPrefix: "ui-table-priority-", + columnToggleTable: "ui-table-columntoggle" + } +); - // This behavior only applies to "fixed", not "fullscreen" - if ( this.options.fullscreen ) { return; } +$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate refresh", function( e ) { + + var $table = $( this ), + self = $table.data( "mobile-table" ), + event = e.type, + o = self.options, + ns = $.mobile.ns, + id = ( $table.attr( "id" ) || o.classes.popup ) + "-popup", /* TODO BETTER FALLBACK ID HERE */ + $menuButton, + $popup, + $menu, + $switchboard; + + if ( o.mode !== "columntoggle" ) { + return; + } - tbPage = tbPage || this._thisPage || $el.closest( ".ui-page" ); - $( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() + pos ); - }, + if ( event !== "refresh" ) { + self.element.addClass( o.classes.columnToggleTable ); + + $menuButton = $( "
      " + o.columnBtnText + "" ), + $popup = $( "
      "), + $menu = $("
      "); + } + + // create the hide/show toggles + self.headers.not( "td" ).each(function( i ) { - _useTransition: function( notransition ) { - var $win = $.mobile.window, - $el = this.element, - scroll = $win.scrollTop(), - elHeight = $el.height(), - pHeight = $el.closest( ".ui-page" ).height(), - viewportHeight = $.mobile.getScreenHeight(), - tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer"; + var priority = $( this ).jqmData( "priority" ), + $cells = $( this ).add( $( this ).jqmData( "cells" ) ); - return !notransition && - ( this.options.transition && this.options.transition !== "none" && - ( - ( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) || - ( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight ) - ) || this.options.fullscreen - ); - }, + if ( priority ) { - show: function( notransition ) { - var hideClass = "ui-fixed-hidden", - $el = this.element; + $cells.addClass( o.classes.priorityPrefix + priority ); - if ( this._useTransition( notransition ) ) { - $el - .removeClass( "out " + hideClass ) - .addClass( "in" ) - .animationComplete(function () { - $el.removeClass('in'); + if ( event !== "refresh" ) { + $("" ) + .appendTo( $menu ) + .children( 0 ) + .jqmData( "cells", $cells ) + .checkboxradio({ + theme: o.columnPopupTheme }); + } else { + $( '#' + id + ' fieldset div:eq(' + i +')').find('input').jqmData( 'cells', $cells ); } - else { - $el.removeClass( hideClass ); - } - this._visible = true; - }, + } + }); + + if ( event !== "refresh" ) { + $menu.appendTo( $popup ); + } - hide: function( notransition ) { - var hideClass = "ui-fixed-hidden", - $el = this.element, - // if it's a slide transition, our new transitions need the reverse class as well to slide outward - outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" ); + // bind change event listeners to inputs - TODO: move to a private method? + if ( $menu === undefined ) { + $switchboard = $('#' + id + ' fieldset'); + } else { + $switchboard = $menu; + } - if( this._useTransition( notransition ) ) { - $el - .addClass( outclass ) - .removeClass( "in" ) - .animationComplete(function() { - $el.addClass( hideClass ).removeClass( outclass ); - }); + if ( event !== "refresh" ) { + $switchboard.on( "change", "input", function( e ){ + if( this.checked ){ + $( this ).jqmData( "cells" ).removeClass( "ui-table-cell-hidden" ).addClass( "ui-table-cell-visible" ); + } else { + $( this ).jqmData( "cells" ).removeClass( "ui-table-cell-visible" ).addClass( "ui-table-cell-hidden" ); } - else { - $el.addClass( hideClass ).removeClass( outclass ); + }); + + $menuButton + .insertBefore( $table ) + .buttonMarkup({ + theme: o.columnBtnTheme + }); + + $popup + .insertBefore( $table ) + .popup(); + } + + // refresh method + self.update = function(){ + $switchboard.find( "input" ).each( function(){ + if (this.checked) { + this.checked = $( this ).jqmData( "cells" ).eq(0).css( "display" ) === "table-cell"; + if (event === "refresh") { + $( this ).jqmData( "cells" ).addClass('ui-table-cell-visible'); + } + } else { + $( this ).jqmData( "cells" ).addClass('ui-table-cell-hidden'); } - this._visible = false; - }, + $( this ).checkboxradio( "refresh" ); + }); + }; - toggle: function() { - this[ this._visible ? "hide" : "show" ](); - }, + $.mobile.window.on( "throttledresize", self.update ); - _bindToggleHandlers: function() { - var self = this, delay, - o = self.options, - $el = self.element; + self.update(); - // tap toggle - $el.closest( ".ui-page" ) - .bind( "vclick", function( e ) { - if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) { - self.toggle(); - } - }) - .bind( "focusin focusout", function( e ) { - //this hides the toolbars on a keyboard pop to give more screen room and prevent ios bug which - //positions fixed toolbars in the middle of the screen on pop if the input is near the top or - //bottom of the screen addresses issues #4410 Footer navbar moves up when clicking on a textbox in an Android environment - //and issue #4113 Header and footer change their position after keyboard popup - iOS - //and issue #4410 Footer navbar moves up when clicking on a textbox in an Android environment - if ( screen.width < 1025 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) { - //Fix for issue #4724 Moving through form in Mobile Safari with "Next" and "Previous" system - //controls causes fixed position, tap-toggle false Header to reveal itself - if ( e.type === "focusout" && !self._visible ) { - //wait for the stack to unwind and see if we have jumped to another input - delay = setTimeout( function() { - self.show(); - }, 0 ); - } else if ( e.type === "focusin" && self._visible ) { - //if we have jumped to another input clear the time out to cancel the show. - clearTimeout( delay ); - self.hide(); - } - } - }); - }, +}); - _destroy: function() { - var $el = this.element, - header = $el.is( ".ui-header" ); +})( jQuery ); - $el.closest( ".ui-page" ).css( "padding-" + ( header ? "top" : "bottom" ), "" ); - $el.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" ); - $el.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" ); - } +(function( $, undefined ) { - }); +$.mobile.table.prototype.options.mode = "reflow"; - //auto self-init widgets - $.mobile.document - .bind( "pagecreate create", function( e ) { +$.mobile.table.prototype.options.classes = $.extend( + $.mobile.table.prototype.options.classes, + { + reflowTable: "ui-table-reflow", + cellLabels: "ui-table-cell-label" + } +); - // DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element. - // This line ensures it still works, but we recommend moving the attribute to the toolbars themselves. - if ( $( e.target ).jqmData( "fullscreen" ) ) { - $( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true ); - } +$.mobile.document.delegate( ":jqmData(role='table')", "tablecreate refresh", function( e ) { - $.mobile.fixedtoolbar.prototype.enhanceWithin( e.target ); - }); + var $table = $( this ), + event = e.type, + self = $table.data( "mobile-table" ), + o = self.options; -})( jQuery ); + // If it's not reflow mode, return here. + if( o.mode !== "reflow" ){ + return; + } -(function( $, undefined ) { - $.widget( "mobile.fixedtoolbar", $.mobile.fixedtoolbar, { + if ( event !== "refresh" ) { + self.element.addClass( o.classes.reflowTable ); + } - _create: function() { - this._super(); - this._workarounds(); - }, + // get headers in reverse order so that top-level headers are appended last + var reverseHeaders = $( self.allHeaders.get().reverse() ); - //check the browser and version and run needed workarounds - _workarounds: function() { - var ua = navigator.userAgent, - platform = navigator.platform, - // Rendering engine is Webkit, and capture major version - wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ), - wkversion = !!wkmatch && wkmatch[ 1 ], - os = null, - self = this; - //set the os we are working in if it dosent match one with workarounds return - if( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ){ - os = "ios"; - } else if( ua.indexOf( "Android" ) > -1 ){ - os = "android"; - } else { - return; - } - //check os version if it dosent match one with workarounds return - if( os === "ios" ) { - //iOS workarounds - self._bindScrollWorkaround(); - } else if( os === "android" && wkversion && wkversion < 534 ) { - //Android 2.3 run all Android 2.3 workaround - self._bindScrollWorkaround(); - self._bindListThumbWorkaround(); - } else { - return; - } - }, + // create the hide/show toggles + reverseHeaders.each(function( i ){ + var $cells = $( this ).jqmData( "cells" ), + colstart = $( this ).jqmData( "colstart" ), + hierarchyClass = $cells.not( this ).filter( "thead th" ).length && " ui-table-cell-label-top", + text = $(this).text(); - //Utility class for checking header and footer positions relative to viewport - _viewportOffset: function() { - var $el = this.element, - header = $el.is( ".ui-header" ), - offset = Math.abs($el.offset().top - $.mobile.window.scrollTop()); - if( !header ) { - offset = Math.round(offset - $.mobile.window.height() + $el.outerHeight())-60; - } - return offset; - }, + if( text !== "" ){ - //bind events for _triggerRedraw() function - _bindScrollWorkaround: function() { - var self = this; - //bind to scrollstop and check if the toolbars are correctly positioned - this._on( $.mobile.window, { scrollstop: function() { - var viewportOffset = self._viewportOffset(); - //check if the header is visible and if its in the right place - if( viewportOffset > 2 && self._visible) { - self._triggerRedraw(); - } - }}); - }, + if( hierarchyClass ){ + var iteration = parseInt( $( this ).attr( "colspan" ), 10 ), + filter = ""; - //this addresses issue #4250 Persistent footer instability in v1.1 with long select lists in Android 2.3.3 - //and issue #3748 Android 2.x: Page transitions broken when fixed toolbars used - //the absolutely positioned thumbnail in a list view causes problems with fixed position buttons above in a nav bar - //setting the li's to -webkit-transform:translate3d(0,0,0); solves this problem to avoide potential issues in other - //platforms we scope this with the class ui-android-2x-fix - _bindListThumbWorkaround: function() { - this.element.closest(".ui-page").addClass( "ui-android-2x-fixed" ); - }, - //this addresses issues #4337 Fixed header problem after scrolling content on iOS and Android - //and device bugs project issue #1 Form elements can lose click hit area in position: fixed containers. - //this also addresses not on fixed toolbars page in docs - //adding 1px of padding to the bottom then removing it causes a "redraw" - //which positions the toolbars correctly (they will always be visually correct) - _triggerRedraw: function() { - var paddingBottom = parseFloat( $( ".ui-page-active" ).css( "padding-bottom" ) ); - //trigger page redraw to fix incorrectly positioned fixed elements - $( ".ui-page-active" ).css( "padding-bottom", ( paddingBottom + 1 ) +"px" ); - //if the padding is reset with out a timeout the reposition will not occure. - //this is independant of JQM the browser seems to need the time to react. - setTimeout( function() { - $( ".ui-page-active" ).css( "padding-bottom", paddingBottom + "px" ); - }, 0 ); - }, + if( iteration ){ + filter = "td:nth-child("+ iteration +"n + " + ( colstart ) +")"; + } + $cells.filter( filter ).prepend( "" + text + "" ); + } + else { + $cells.prepend( "" + text + "" ); + } - destroy: function() { - this._super(); - //Remove the class we added to the page previously in android 2.x - this.element.closest(".ui-page-active").removeClass( "ui-android-2x-fix" ); } }); - })( jQuery ); +}); + +})( jQuery ); (function( $, window ) { diff --git a/vendor/assets/javascripts/jquery.mobile.min.js b/vendor/assets/javascripts/jquery.mobile.min.js index 9363e28..a20cefe 100644 --- a/vendor/assets/javascripts/jquery.mobile.min.js +++ b/vendor/assets/javascripts/jquery.mobile.min.js @@ -1,2 +1,7 @@ -/*! jQuery Mobile vGit Build: SHA1: caa77b258660731d663844fe7867aa2c3a107ab1 <> Date: Wed Feb 20 15:03:27 2013 -0500 jquerymobile.com | jquery.org/license !*/ -(function(a,b,c){typeof define=="function"&&define.amd?define(["jquery"],function(d){return c(d,a,b),d.mobile}):c(a.jQuery,a,b)})(this,document,function(a,b,c,d){(function(a){a.mobile={}})(a),function(a,b,d){var e={};a.mobile=a.extend(a.mobile,{version:"1.3.0",ns:"",subPageUrlKey:"ui-page",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",focusClass:"ui-focus",ajaxEnabled:!0,hashListeningEnabled:!0,linkBindingEnabled:!0,defaultPageTransition:"fade",maxTransitionWidth:!1,minScrollBack:250,touchOverflowEnabled:!1,defaultDialogTransition:"pop",pageLoadErrorMessage:"Error Loading Page",pageLoadErrorMessageTheme:"e",phonegapNavigationEnabled:!1,autoInitializePage:!0,pushStateEnabled:!0,ignoreContentEnabled:!1,orientationChangeEnabled:!0,buttonMarkup:{hoverDelay:200},window:a(b),document:a(c),keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91},behaviors:{},silentScroll:function(c){a.type(c)!=="number"&&(c=a.mobile.defaultHomeScroll),a.event.special.scrollstart.enabled=!1,setTimeout(function(){b.scrollTo(0,c),a.mobile.document.trigger("silentscroll",{x:0,y:c})},20),setTimeout(function(){a.event.special.scrollstart.enabled=!0},150)},nsNormalizeDict:e,nsNormalize:function(b){if(!b)return;return e[b]||(e[b]=a.camelCase(a.mobile.ns+b))},getInheritedTheme:function(a,b){var c=a[0],d="",e=/ui-(bar|body|overlay)-([a-z])\b/,f,g;while(c){f=c.className||"";if(f&&(g=e.exec(f))&&(d=g[2]))break;c=c.parentNode}return d||b||"a"},closestPageData:function(a){return a.closest(':jqmData(role="page"), :jqmData(role="dialog")').data("mobile-page")},enhanceable:function(a){return this.haveParents(a,"enhance")},hijackable:function(a){return this.haveParents(a,"ajax")},haveParents:function(b,c){if(!a.mobile.ignoreContentEnabled)return b;var d=b.length,e=a(),f,g,h;for(var i=0;i").text(a(this).text()).html()},a.fn.jqmEnhanceable=function(){return a.mobile.enhanceable(this)},a.fn.jqmHijackable=function(){return a.mobile.hijackable(this)};var f=a.find,g=/:jqmData\(([^)]*)\)/g;a.find=function(b,c,d,e){return b=b.replace(g,"[data-"+(a.mobile.ns||"")+"$1]"),f.call(this,b,c,d,e)},a.extend(a.find,f),a.find.matches=function(b,c){return a.find(b,null,null,c)},a.find.matchesSelector=function(b,c){return a.find(c,null,null,[b]).length>0}}(a,this),function(a,b){var c=0,d=Array.prototype.slice,e=a.cleanData;a.cleanData=function(b){for(var c=0,d;(d=b[c])!=null;c++)try{a(d).triggerHandler("remove")}catch(f){}e(b)},a.widget=function(b,c,d){var e,f,g,h,i=b.split(".")[0];b=b.split(".")[1],e=i+"-"+b,d||(d=c,c=a.Widget),a.expr[":"][e.toLowerCase()]=function(b){return!!a.data(b,e)},a[i]=a[i]||{},f=a[i][b],g=a[i][b]=function(a,b){if(!this._createWidget)return new g(a,b);arguments.length&&this._createWidget(a,b)},a.extend(g,f,{version:d.version,_proto:a.extend({},d),_childConstructors:[]}),h=new c,h.options=a.widget.extend({},h.options),a.each(d,function(b,e){a.isFunction(e)&&(d[b]=function(){var a=function(){return c.prototype[b].apply(this,arguments)},d=function(a){return c.prototype[b].apply(this,a)};return function(){var b=this._super,c=this._superApply,f;return this._super=a,this._superApply=d,f=e.apply(this,arguments),this._super=b,this._superApply=c,f}}())}),g.prototype=a.widget.extend(h,{widgetEventPrefix:f?h.widgetEventPrefix:b},d,{constructor:g,namespace:i,widgetName:b,widgetFullName:e}),f?(a.each(f._childConstructors,function(b,c){var d=c.prototype;a.widget(d.namespace+"."+d.widgetName,g,c._proto)}),delete f._childConstructors):c._childConstructors.push(g),a.widget.bridge(b,g)},a.widget.extend=function(c){var e=d.call(arguments,1),f=0,g=e.length,h,i;for(;f",options:{disabled:!1,create:null},_createWidget:function(b,d){d=a(d||this.defaultElement||this)[0],this.element=a(d),this.uuid=c++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=a.widget.extend({},this.options,this._getCreateOptions(),b),this.bindings=a(),this.hoverable=a(),this.focusable=a(),d!==this&&(a.data(d,this.widgetFullName,this),this._on(!0,this.element,{remove:function(a){a.target===d&&this.destroy()}}),this.document=a(d.style?d.ownerDocument:d.document||d),this.window=a(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:a.noop,_getCreateEventData:a.noop,_create:a.noop,_init:a.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(a.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:a.noop,widget:function(){return this.element},option:function(c,d){var e=c,f,g,h;if(arguments.length===0)return a.widget.extend({},this.options);if(typeof c=="string"){e={},f=c.split("."),c=f.shift();if(f.length){g=e[c]=a.widget.extend({},this.options[c]);for(h=0;h"+""+"

      "+"
      ",fakeFixLoader:function(){var b=a("."+a.mobile.activeBtnClass).first();this.element.css({top:a.support.scrollTop&&f.scrollTop()+f.height()/2||b.length&&b.offset().top||100})},checkLoaderPosition:function(){var b=this.element.offset(),c=f.scrollTop(),d=a.mobile.getScreenHeight();if(b.topd)this.element.addClass("ui-loader-fakefix"),this.fakeFixLoader(),f.unbind("scroll",this.checkLoaderPosition).bind("scroll",a.proxy(this.fakeFixLoader,this))},resetHtml:function(){this.element.html(a(this.defaultHtml).html())},show:function(b,g,h){var i,j,k,l;this.resetHtml(),a.type(b)==="object"?(l=a.extend({},this.options,b),b=l.theme||a.mobile.loadingMessageTheme):(l=this.options,b=b||a.mobile.loadingMessageTheme||l.theme),j=g||a.mobile.loadingMessage||l.text,e.addClass("ui-loading");if(a.mobile.loadingMessage!==!1||l.html)a.mobile.loadingMessageTextVisible!==d?i=a.mobile.loadingMessageTextVisible:i=l.textVisible,this.element.attr("class",c+" ui-corner-all ui-body-"+b+" ui-loader-"+(i||g||b.text?"verbose":"default")+(l.textonly||h?" ui-loader-textonly":"")),l.html?this.element.html(l.html):this.element.find("h1").text(j),this.element.appendTo(a.mobile.pageContainer),this.checkLoaderPosition(),f.bind("scroll",a.proxy(this.checkLoaderPosition,this))},hide:function(){e.removeClass("ui-loading"),a.mobile.loadingMessage&&this.element.removeClass("ui-loader-fakefix"),a.mobile.window.unbind("scroll",this.fakeFixLoader),a.mobile.window.unbind("scroll",this.checkLoaderPosition)}}),f.bind("pagecontainercreate",function(){a.mobile.loaderWidget=a.mobile.loaderWidget||a(a.mobile.loader.prototype.defaultHtml).loader()})}(a,this),function(a,b,d){function k(a){return a=a||location.href,"#"+a.replace(/^[^#]*#?(.*)$/,"$1")}var e="hashchange",f=c,g,h=a.event.special,i=f.documentMode,j="on"+e in b&&(i===d||i>7);a.fn[e]=function(a){return a?this.bind(e,a):this.trigger(e)},a.fn[e].delay=50,h[e]=a.extend(h[e],{setup:function(){if(j)return!1;a(g.start)},teardown:function(){if(j)return!1;a(g.stop)}}),g=function(){function n(){var c=k(),d=m(h);c!==h?(l(h=c,d),a(b).trigger(e)):d!==h&&(location.href=location.href.replace(/#.*/,"")+d),g=setTimeout(n,a.fn[e].delay)}var c={},g,h=k(),i=function(a){return a},l=i,m=i;return c.start=function(){g||n()},c.stop=function(){g&&clearTimeout(g),g=d},b.attachEvent&&!b.addEventListener&&!j&&function(){var b,d;c.start=function(){b||(d=a.fn[e].src,d=d&&d+k(),b=a('