diff --git a/app/assets/javascripts/vendor/jquery.mobile.js b/app/assets/javascripts/vendor/jquery.mobile.js index d424dd5..8a6edd2 100644 --- a/app/assets/javascripts/vendor/jquery.mobile.js +++ b/app/assets/javascripts/vendor/jquery.mobile.js @@ -1,52 +1,383 @@ /* -* jQuery Mobile Framework 1.0 +* jQuery Mobile Framework Git Build: SHA1: c2d61e2e592c67519d9a9ed0ba796fa44787e136 <> Date: Tue Sep 25 10:38:12 2012 -0700 * http://jquerymobile.com * -* Copyright 2011 (c) jQuery Project -* Dual licensed under the MIT or GPL Version 2 licenses. +* Copyright 2012 jQuery Foundation and other contributors +* Released under the MIT license. * http://jquery.org/license * */ + + +(function ( root, doc, factory ) { + if ( typeof define === "function" && define.amd ) { + // AMD. Register as an anonymous module. + define( [ "jquery" ], function ( $ ) { + factory( $, root, doc ); + return $.mobile; + }); + } else { + // Browser globals + factory( root.jQuery, root, doc ); + } +}( this, document, function ( jQuery, window, document, undefined ) { +(function( $, window, undefined ) { + + var nsNormalizeDict = {}; + + // jQuery.mobile configurable options + $.mobile = $.extend( {}, { + + // Version of the jQuery Mobile Framework + version: "1.2.0", + + // Namespace used framework-wide for data-attrs. Default is no namespace + ns: "", + + // Define the url parameter used for referencing widget-generated sub-pages. + // Translates to to example.html&ui-page=subpageIdentifier + // hash segment before &ui-page= is used to make Ajax request + subPageUrlKey: "ui-page", + + // Class assigned to page currently in view, and during transitions + activePageClass: "ui-page-active", + + // Class used for "active" button state, from CSS framework + activeBtnClass: "ui-btn-active", + + // Class used for "focus" form element state, from CSS framework + focusClass: "ui-focus", + + // Automatically handle clicks and form submissions through Ajax, when same-domain + ajaxEnabled: true, + + // Automatically load and show pages based on location.hash + hashListeningEnabled: true, + + // disable to prevent jquery from bothering with links + linkBindingEnabled: true, + + // Set default page transition - 'none' for no transitions + defaultPageTransition: "fade", + + // Set maximum window width for transitions to apply - 'false' for no limit + maxTransitionWidth: false, + + // Minimum scroll distance that will be remembered when returning to a page + minScrollBack: 250, + + // DEPRECATED: the following property is no longer in use, but defined until 2.0 to prevent conflicts + touchOverflowEnabled: false, + + // Set default dialog transition - 'none' for no transitions + defaultDialogTransition: "pop", + + // Error response message - appears when an Ajax page request fails + pageLoadErrorMessage: "Error Loading Page", + + // For error messages, which theme does the box uses? + pageLoadErrorMessageTheme: "e", + + // replace calls to window.history.back with phonegaps navigation helper + // where it is provided on the window object + phonegapNavigationEnabled: false, + + //automatically initialize the DOM when it's ready + autoInitializePage: true, + + pushStateEnabled: true, + + // allows users to opt in to ignoring content by marking a parent element as + // data-ignored + ignoreContentEnabled: false, + + // turn of binding to the native orientationchange due to android orientation behavior + orientationChangeEnabled: true, + + buttonMarkup: { + hoverDelay: 200 + }, + + // TODO might be useful upstream in jquery itself ? + keyCode: { + ALT: 18, + BACKSPACE: 8, + CAPS_LOCK: 20, + COMMA: 188, + COMMAND: 91, + COMMAND_LEFT: 91, // COMMAND + COMMAND_RIGHT: 93, + CONTROL: 17, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + INSERT: 45, + LEFT: 37, + MENU: 93, // COMMAND_RIGHT + 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 // COMMAND + }, + + // Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value + silentScroll: function( ypos ) { + if ( $.type( ypos ) !== "number" ) { + ypos = $.mobile.defaultHomeScroll; + } + + // prevent scrollstart and scrollstop events + $.event.special.scrollstart.enabled = false; + + setTimeout( function() { + window.scrollTo( 0, ypos ); + $( document ).trigger( "silentscroll", { x: 0, y: ypos }); + }, 20 ); + + setTimeout( function() { + $.event.special.scrollstart.enabled = true; + }, 150 ); + }, + + // Expose our cache for testing purposes. + nsNormalizeDict: nsNormalizeDict, + + // Take a data attribute property, prepend the namespace + // and then camel case the attribute string. Add the result + // to our nsNormalizeDict so we don't have to do this again. + nsNormalize: function( prop ) { + if ( !prop ) { + return; + } + + return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) ); + }, + + // Find the closest parent with a theme class on it. Note that + // we are not using $.fn.closest() on purpose here because this + // method gets called quite a bit and we need it to be as fast + // as possible. + getInheritedTheme: function( el, defaultTheme ) { + var e = el[ 0 ], + ltr = "", + re = /ui-(bar|body|overlay)-([a-z])\b/, + c, m; + + while ( e ) { + c = e.className || ""; + if ( c && ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) { + // We found a parent with a theme class + // on it so bail from this loop. + break; + } + + e = e.parentNode; + } + + // Return the theme letter we found, if none, return the + // specified default. + + return ltr || defaultTheme || "a"; + }, + + // TODO the following $ and $.fn extensions can/probably should be moved into jquery.mobile.core.helpers + // + // Find the closest javascript page element to gather settings data jsperf test + // http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit + // possibly naive, but it shows that the parsing overhead for *just* the page selector vs + // the page and dialog selector is negligable. This could probably be speed up by + // doing a similar parent node traversal to the one found in the inherited theme code above + closestPageData: function( $target ) { + return $target + .closest( ':jqmData(role="page"), :jqmData(role="dialog")' ) + .data( "page" ); + }, + + enhanceable: function( $set ) { + return this.haveParents( $set, "enhance" ); + }, + + hijackable: function( $set ) { + return this.haveParents( $set, "ajax" ); + }, + + haveParents: function( $set, attr ) { + if ( !$.mobile.ignoreContentEnabled ) { + return $set; + } + + var count = $set.length, + $newSet = $(), + e, $element, excluded; + + for ( var i = 0; i < count; i++ ) { + $element = $set.eq( i ); + excluded = false; + e = $set[ i ]; + + while ( e ) { + var c = e.getAttribute ? e.getAttribute( "data-" + $.mobile.ns + attr ) : ""; + + if ( c === "false" ) { + excluded = true; + break; + } + + e = e.parentNode; + } + + if ( !excluded ) { + $newSet = $newSet.add( $element ); + } + } + + return $newSet; + }, + + getScreenHeight: function() { + // Native innerHeight returns more accurate value for this across platforms, + // jQuery version is here as a normalized fallback for platforms like Symbian + return window.innerHeight || $( window ).height(); + } + }, $.mobile ); + + // Mobile version of data and removeData and hasData methods + // ensures all data is set and retrieved using jQuery Mobile's data namespace + $.fn.jqmData = function( prop, value ) { + var result; + if ( typeof prop !== "undefined" ) { + if ( prop ) { + prop = $.mobile.nsNormalize( prop ); + } + + // undefined is permitted as an explicit input for the second param + // in this case it returns the value and does not set it to undefined + if( arguments.length < 2 || value === undefined ){ + result = this.data( prop ); + } else { + result = this.data( prop, value ); + } + } + return result; + }; + + $.jqmData = function( elem, prop, value ) { + var result; + if ( typeof prop !== "undefined" ) { + result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value ); + } + return result; + }; + + $.fn.jqmRemoveData = function( prop ) { + return this.removeData( $.mobile.nsNormalize( prop ) ); + }; + + $.jqmRemoveData = function( elem, prop ) { + return $.removeData( elem, $.mobile.nsNormalize( prop ) ); + }; + + $.fn.removeWithDependents = function() { + $.removeWithDependents( this ); + }; + + $.removeWithDependents = function( elem ) { + var $elem = $( elem ); + + ( $elem.jqmData( 'dependents' ) || $() ).remove(); + $elem.remove(); + }; + + $.fn.addDependents = function( newDependents ) { + $.addDependents( $( this ), newDependents ); + }; + + $.addDependents = function( elem, newDependents ) { + var dependents = $( elem ).jqmData( 'dependents' ) || $(); + + $( elem ).jqmData( 'dependents', $.merge( dependents, newDependents ) ); + }; + + // note that this helper doesn't attempt to handle the callback + // or setting of an html elements 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(); + }; + + // fluent helper function for the mobile namespaced equivalent + $.fn.jqmEnhanceable = function() { + return $.mobile.enhanceable( this ); + }; + + $.fn.jqmHijackable = function() { + return $.mobile.hijackable( this ); + }; + + // Monkey-patching Sizzle to filter the :jqmData selector + var oldFind = $.find, + jqmDataRE = /:jqmData\(([^)]*)\)/g; + + $.find = function( selector, context, ret, extra ) { + selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" ); + + return oldFind.call( this, selector, context, ret, extra ); + }; + + $.extend( $.find, oldFind ); + + $.find.matches = function( expr, set ) { + return $.find( expr, null, null, set ); + }; + + $.find.matchesSelector = function( node, expr ) { + return $.find( expr, null, null, [ node ] ).length > 0; + }; +})( jQuery, this ); + + /*! - * jQuery UI Widget @VERSION + * jQuery UI Widget v1.9.0-beta.1 * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Copyright 2012, https://github.com/jquery/jquery-ui/blob/1.9.0-beta.1/AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Widget */ - (function( $, undefined ) { -// jQuery 1.4+ -if ( $.cleanData ) { - var _cleanData = $.cleanData; - $.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { $( elem ).triggerHandler( "remove" ); - } - _cleanData( elems ); - }; -} else { - var _remove = $.fn.remove; - $.fn.remove = function( selector, keepData ) { - return this.each(function() { - if ( !keepData ) { - if ( !selector || $.filter( selector, [ this ] ).length ) { - $( "*", this ).add( [ this ] ).each(function() { - $( this ).triggerHandler( "remove" ); - }); - } - } - return _remove.call( $(this), selector, keepData ); - }); - }; -} + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; $.widget = function( name, base, prototype ) { - var namespace = name.split( "." )[ 0 ], - fullName; + var fullName, existingConstructor, constructor, basePrototype, + namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; fullName = namespace + "-" + name; @@ -57,76 +388,157 @@ $.widget = function( name, base, prototype ) { // create selector for plugin $.expr[ ":" ][ fullName ] = function( elem ) { - return !!$.data( elem, name ); + return !!$.data( elem, fullName ); }; $[ namespace ] = $[ namespace ] || {}; - $[ namespace ][ name ] = function( options, element ) { + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) if ( arguments.length ) { this._createWidget( options, element ); } }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); - var basePrototype = new base(); + basePrototype = new base(); // we need to make the options hash a property directly on the new instance // otherwise we'll modify the options hash on the prototype that we're // inheriting from -// $.each( basePrototype, function( key, val ) { -// if ( $.isPlainObject(val) ) { -// basePrototype[ key ] = $.extend( {}, val ); -// } -// }); - basePrototype.options = $.extend( true, {}, basePrototype.options ); - $[ namespace ][ name ].prototype = $.extend( true, basePrototype, { + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( $.isFunction( value ) ) { + prototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + } + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: name + }, prototype, { + constructor: constructor, namespace: namespace, widgetName: name, - widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name, - widgetBaseClass: fullName - }, prototype ); + // TODO remove widgetBaseClass, see #8155 + widgetBaseClass: fullName, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } - $.widget.bridge( name, $[ namespace ][ name ] ); + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value; + } + } + } + return target; }; $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName; $.fn[ name ] = function( options ) { var isMethodCall = typeof options === "string", - args = Array.prototype.slice.call( arguments, 1 ), + args = slice.call( arguments, 1 ), returnValue = this; // allow multiple hashes to be passed on init options = !isMethodCall && args.length ? - $.extend.apply( null, [ true, options ].concat(args) ) : + $.widget.extend.apply( null, [ options ].concat(args) ) : options; - // prevent calls to internal methods - if ( isMethodCall && options.charAt( 0 ) === "_" ) { - return returnValue; - } - if ( isMethodCall ) { this.each(function() { - var instance = $.data( this, name ); + var methodValue, + instance = $.data( this, fullName ); if ( !instance ) { - throw "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'"; + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); } - if ( !$.isFunction( instance[options] ) ) { - throw "no such method '" + options + "' for " + name + " widget instance"; + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); } - var methodValue = instance[ options ].apply( instance, args ); + methodValue = instance[ options ].apply( instance, args ); if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue; + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; return false; } }); } else { this.each(function() { - var instance = $.data( this, name ); + var instance = $.data( this, fullName ); if ( instance ) { instance.option( options || {} )._init(); } else { - $.data( this, name, new object( options, this ) ); + new object( options, this ); } }); } @@ -135,78 +547,120 @@ $.widget.bridge = function( name, object ) { }; }; -$.Widget = function( options, element ) { - // allow instantiation without initializing for simple inheritance - if ( arguments.length ) { - this._createWidget( options, element ); - } -}; +$.Widget = function( options, element ) {}; +$.Widget._childConstructors = []; $.Widget.prototype = { widgetName: "widget", widgetEventPrefix: "", + defaultElement: "
", options: { - disabled: false + disabled: false, + + // callbacks + create: null }, _createWidget: function( options, element ) { - // $.widget.bridge stores the plugin instance, but we do it anyway - // so that it's stored even before the _create function runs - $.data( element, this.widgetName, this ); + element = $( element || this.defaultElement || this )[ 0 ]; this.element = $( element ); - this.options = $.extend( true, {}, + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, this.options, this._getCreateOptions(), options ); - var self = this; - this.element.bind( "remove." + this.widgetName, function() { - self.destroy(); - }); + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + // 1.9 BC for #7810 + // TODO remove dual storage + $.data( element, this.widgetName, this ); + $.data( element, this.widgetFullName, this ); + this._on({ remove: "destroy" }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } this._create(); - this._trigger( "create" ); + this._trigger( "create", null, this._getCreateEventData() ); this._init(); }, - _getCreateOptions: function() { - var options = {}; - if ( $.metadata ) { - options = $.metadata.get( element )[ this.widgetName ]; - } - return options; - }, - _create: function() {}, - _init: function() {}, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() this.element - .unbind( "." + this.widgetName ) - .removeData( this.widgetName ); + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); this.widget() - .unbind( "." + this.widgetName ) + .unbind( this.eventNamespace ) .removeAttr( "aria-disabled" ) .removeClass( - this.widgetBaseClass + "-disabled " + + this.widgetFullName + "-disabled " + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); }, + _destroy: $.noop, widget: function() { return this.element; }, option: function( key, value ) { - var options = key; + var options = key, + parts, + curOption, + i; if ( arguments.length === 0 ) { // don't return a reference to the internal hash - return $.extend( {}, this.options ); + return $.widget.extend( {}, this.options ); } - if (typeof key === "string" ) { - if ( value === undefined ) { - return this.options[ key ]; - } + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } options = {}; - options[ key ] = value; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( value === undefined ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( value === undefined ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } } this._setOptions( options ); @@ -214,10 +668,11 @@ $.Widget.prototype = { return this; }, _setOptions: function( options ) { - var self = this; - $.each( options, function( key, value ) { - self._setOption( key, value ); - }); + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } return this; }, @@ -226,53 +681,176 @@ $.Widget.prototype = { if ( key === "disabled" ) { this.widget() - [ value ? "addClass" : "removeClass"]( - this.widgetBaseClass + "-disabled" + " " + - "ui-state-disabled" ) + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); } - return this; + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( element, handlers ) { + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + } else { + // accept selectors, DOM elements + element = $( element ); + this.bindings = this.bindings.add( element ); + } + + var instance = this; + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + instance.widget().delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); }, - enable: function() { - return this._setOption( "disabled", false ); + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); }, - disable: function() { - return this._setOption( "disabled", true ); + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); }, _trigger: function( type, event, data ) { - var callback = this.options[ type ]; + var prop, orig, + callback = this.options[ type ]; + data = data || {}; event = $.Event( event ); event.type = ( type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type ).toLowerCase(); - data = data || {}; + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; // copy original event properties over to the new event - // this would happen if we could call $.event.fix instead of $.Event - // but we don't have a way to force an event to be fixed multiple times - if ( event.originalEvent ) { - for ( var i = $.event.props.length, prop; i; ) { - prop = $.event.props[ --i ]; - event[ prop ] = event.originalEvent[ prop ]; + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } } } this.element.trigger( event, data ); - - return !( $.isFunction(callback) && - callback.call( this.element[0], event, data ) === false || + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || event.isDefaultPrevented() ); } }; +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +// DEPRECATED +if ( $.uiBackCompat !== false ) { + $.Widget.prototype._getCreateOptions = function() { + return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ]; + }; +} + })( jQuery ); -/* -* widget factory extentions for mobile -*/ (function( $, undefined ) { @@ -307,184 +885,214 @@ $.widget( "mobile.widget", { return options; }, - enhanceWithin: function( target ) { - // TODO remove dependency on the page widget for the keepNative. - // Currently the keepNative value is defined on the page prototype so - // the method is as well - var page = $(target).closest(":jqmData(role='page')").data( "page" ), - keepNative = (page && page.keepNativeSelector()) || ""; + enhanceWithin: function( target, useKeepNative ) { + this.enhance( $( this.options.initSelector, $( target )), useKeepNative ); + }, + + enhance: function( targets, useKeepNative ) { + var page, keepNative, $widgetElements = $( targets ), self = this; - $( this.options.initSelector, target ).not( keepNative )[ this.widgetName ](); + // if ignoreContentEnabled is set to true the framework should + // only enhance the selected elements when they do NOT have a + // parent with the data-namespace-ignore attribute + $widgetElements = $.mobile.enhanceable( $widgetElements ); + + if ( useKeepNative && $widgetElements.length ) { + // TODO remove dependency on the page widget for the keepNative. + // Currently the keepNative value is defined on the page prototype so + // the method is as well + page = $.mobile.closestPageData( $widgetElements ); + keepNative = ( page && page.keepNativeSelector()) || ""; + + $widgetElements = $widgetElements.not( keepNative ); + } + + $widgetElements[ this.widgetName ](); + }, + + raise: function( msg ) { + throw "Widget [" + this.widgetName + "]: " + msg; } }); })( jQuery ); -/* -* a workaround for window.matchMedia -*/ -(function( $, undefined ) { -var $window = $( window ), - $html = $( "html" ); +(function( $, window ) { + // DEPRECATED + // NOTE global mobile object settings + $.extend( $.mobile, { + // DEPRECATED Should the text be visble in the loading message? + loadingMessageTextVisible: undefined, -/* $.mobile.media method: pass a CSS media type or query and get a bool return - note: this feature relies on actual media query support for media queries, though types will work most anywhere - examples: - $.mobile.media('screen') //>> tests for screen media type - $.mobile.media('screen and (min-width: 480px)') //>> tests for screen media type with window width > 480px - $.mobile.media('@media screen and (-webkit-min-device-pixel-ratio: 2)') //>> tests for webkit 2x pixel ratio (iPhone 4) -*/ -$.mobile.media = (function() { - // TODO: use window.matchMedia once at least one UA implements it - var cache = {}, - testDiv = $( "
" ), - fakeBody = $( "" ).append( testDiv ); + // DEPRECATED When the text is visible, what theme does the loading box use? + loadingMessageTheme: undefined, - return function( query ) { - if ( !( query in cache ) ) { - var styleBlock = document.createElement( "style" ), - cssrule = "@media " + query + " { #jquery-mediatest { position:absolute; } }"; + // DEPRECATED default message setting + loadingMessage: undefined, - //must set type for IE! - styleBlock.type = "text/css"; + // DEPRECATED + // Turn on/off page loading message. Theme doubles as an object argument + // with the following shape: { theme: '', text: '', html: '', textVisible: '' } + // NOTE that the $.mobile.loading* settings and params past the first are deprecated + showPageLoadingMsg: function( theme, msgText, textonly ) { + $.mobile.loading( 'show', theme, msgText, textonly ); + }, - if ( styleBlock.styleSheet ){ - styleBlock.styleSheet.cssText = cssrule; - } else { - styleBlock.appendChild( document.createTextNode(cssrule) ); - } + // DEPRECATED + hidePageLoadingMsg: function() { + $.mobile.loading( 'hide' ); + }, - $html.prepend( fakeBody ).prepend( styleBlock ); - cache[ query ] = testDiv.css( "position" ) === "absolute"; - fakeBody.add( styleBlock ).remove(); + loading: function() { + this.loaderWidget.loader.apply( this.loaderWidget, arguments ); } - return cache[ query ]; - }; -})(); + }); -})(jQuery); -/* -* support tests -*/ + // TODO move loader class down into the widget settings + var loaderClass = "ui-loader", $html = $( "html" ), $window = $( window ); -(function( $, undefined ) { + $.widget( "mobile.loader", { + // NOTE if the global config settings are defined they will override these + // options + options: { + // the theme for the loading message + theme: "a", -var fakeBody = $( "" ).prependTo( "html" ), - fbCSS = fakeBody[ 0 ].style, - vendors = [ "Webkit", "Moz", "O" ], - webos = "palmGetResource" in window, //only used to rule out scrollTop - operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]", - bb = window.blackberry; //only used to rule out box shadow, as it's filled opaque on BB + // whether the text in the loading message is shown + textVisible: false, -// thx Modernizr -function propExists( prop ) { - var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), - props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " ); + // custom html for the inner content of the loading message + html: "", - for ( var v in props ){ - if ( fbCSS[ props[ v ] ] !== undefined ) { - return true; - } - } -} + // the text to be displayed when the popup is shown + text: "loading" + }, -// Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting ) -function baseTagTest() { - var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/", - base = $( "head base" ), - fauxEle = null, - href = "", - link, rebase; + defaultHtml: "
" + + "" + + "

" + + "
", - if ( !base.length ) { - base = fauxEle = $( "", { "href": fauxBase }).appendTo( "head" ); - } else { - href = base.attr( "href" ); - } + // For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top + fakeFixLoader: function() { + var activeBtn = $( "." + $.mobile.activeBtnClass ).first(); - link = $( "" ).prependTo( fakeBody ); - rebase = link[ 0 ].href; - base[ 0 ].href = href || location.pathname; + this.element + .css({ + top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 || + activeBtn.length && activeBtn.offset().top || 100 + }); + }, - if ( fauxEle ) { - fauxEle.remove(); - } - return rebase.indexOf( fauxBase ) === 0; -} + // check position of loader to see if it appears to be "fixed" to center + // if not, use abs positioning + checkLoaderPosition: function() { + var offset = this.element.offset(), + scrollTop = $window.scrollTop(), + screenHeight = $.mobile.getScreenHeight(); + + if ( offset.top < scrollTop || ( offset.top - scrollTop ) > screenHeight ) { + this.element.addClass( "ui-loader-fakefix" ); + this.fakeFixLoader(); + $window + .unbind( "scroll", this.checkLoaderPosition ) + .bind( "scroll", this.fakeFixLoader ); + } + }, + resetHtml: function() { + this.element.html( $( this.defaultHtml ).html() ); + }, -// non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683 -// allows for inclusion of IE 6+, including Windows Mobile 7 -$.mobile.browser = {}; -$.mobile.browser.ie = (function() { - var v = 3, - div = document.createElement( "div" ), - a = div.all || []; + // Turn on/off page loading message. Theme doubles as an object argument + // with the following shape: { theme: '', text: '', html: '', textVisible: '' } + // NOTE that the $.mobile.loading* settings and params past the first are deprecated + // TODO sweet jesus we need to break some of this out + show: function( theme, msgText, textonly ) { + var textVisible, message, $header, loadSettings; - while ( div.innerHTML = "", a[ 0 ] ); + this.resetHtml(); - return v > 4 ? v : !v; -})(); + // use the prototype options so that people can set them globally at + // mobile init. Consistency, it's what's for dinner + if ( $.type(theme) === "object" ) { + loadSettings = $.extend( {}, this.options, theme ); + // prefer object property from the param then the old theme setting + theme = loadSettings.theme || $.mobile.loadingMessageTheme; + } else { + loadSettings = this.options; -$.extend( $.support, { - orientation: "orientation" in window && "onorientationchange" in window, - touch: "ontouchend" in document, - cssTransitions: "WebKitTransitionEvent" in window, - pushState: "pushState" in history && "replaceState" in history, - mediaquery: $.mobile.media( "only all" ), - cssPseudoElement: !!propExists( "content" ), - touchOverflow: !!propExists( "overflowScrolling" ), - boxShadow: !!propExists( "boxShadow" ) && !bb, - scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos && !operamini, - dynamicBaseTag: baseTagTest() -}); + // here we prefer the them value passed as a string argument, then + // we prefer the global option because we can't use undefined default + // prototype options, then the prototype option + theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme; + } -fakeBody.remove(); + // set the message text, prefer the param, then the settings object + // then loading message + message = msgText || $.mobile.loadingMessage || loadSettings.text; + // prepare the dom + $html.addClass( "ui-loading" ); -// $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian) -// or that generally work better browsing in regular http for full page refreshes (Opera Mini) -// Note: This detection below is used as a last resort. -// We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible -var nokiaLTE7_3 = (function(){ + if ( $.mobile.loadingMessage !== false || loadSettings.html ) { + // boolean values require a bit more work :P, supports object properties + // and old settings + if ( $.mobile.loadingMessageTextVisible !== undefined ) { + textVisible = $.mobile.loadingMessageTextVisible; + } else { + textVisible = loadSettings.textVisible; + } - var ua = window.navigator.userAgent; + // add the proper css given the options (theme, text, etc) + // Force text visibility if the second argument was supplied, or + // if the text was explicitly set in the object args + this.element.attr("class", loaderClass + + " ui-corner-all ui-body-" + theme + + " ui-loader-" + ( textVisible || msgText || theme.text ? "verbose" : "default" ) + + ( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) ); + + // TODO verify that jquery.fn.html is ok to use in both cases here + // this might be overly defensive in preventing unknowing xss + // if the html attribute is defined on the loading settings, use that + // otherwise use the fallbacks from above + if ( loadSettings.html ) { + this.element.html( loadSettings.html ); + } else { + this.element.find( "h1" ).text( message ); + } - //The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older - return ua.indexOf( "Nokia" ) > -1 && - ( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) && - ua.indexOf( "AppleWebKit" ) > -1 && - ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ ); -})(); + // attach the loader to the DOM + this.element.appendTo( $.mobile.pageContainer ); -$.mobile.ajaxBlacklist = - // BlackBerry browsers, pre-webkit - window.blackberry && !window.WebKitPoint || - // Opera Mini - operamini || - // Symbian webkits pre 7.3 - nokiaLTE7_3; + // check that the loader is visible + this.checkLoaderPosition(); -// Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices -// to render the stylesheets when they're referenced before this script, as we'd recommend doing. -// This simply reappends the CSS in place, which for some reason makes it apply -if ( nokiaLTE7_3 ) { - $(function() { - $( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" ); + // on scroll check the loader position + $window.bind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); + } + }, + + hide: function() { + $html.removeClass( "ui-loading" ); + + if ( $.mobile.loadingMessage ) { + this.element.removeClass( "ui-loader-fakefix" ); + } + + $( window ).unbind( "scroll", $.proxy( this.fakeFixLoader, this) ); + $( window ).unbind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); + } }); -} -// For ruling out shadows via css -if ( !$.support.boxShadow ) { - $( "html" ).addClass( "ui-mobile-nosupport-boxshadow" ); -} + $window.bind( 'pagecontainercreate', function() { + $.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader(); + }); +})(jQuery, this); + -})( jQuery ); -/* -* "mouse" plugin -*/ // This plugin is an experiment for abstracting away the touch and mouse // events so that developers don't have to worry about which method of input @@ -507,6 +1115,8 @@ var dataPropertyName = "virtualMouseBindings", touchTargetPropertyName = "virtualTouchID", virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ), touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ), + mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [], + mouseEventProps = $.event.props.concat( mouseHookProps ), activeDocHandlers = {}, resetTimerID = 0, startX = 0, @@ -518,7 +1128,7 @@ var dataPropertyName = "virtualMouseBindings", eventCaptureSupported = "addEventListener" in document, $document = $( document ), nextTouchID = 1, - lastTouchID = 0; + lastTouchID = 0, threshold; $.vmouse = { moveDistanceThreshold: 10, @@ -537,14 +1147,20 @@ function getNativeEvent( event ) { function createVirtualEvent( event, eventType ) { var t = event.type, - oe, props, ne, prop, ct, touch, i, j; + oe, props, ne, prop, ct, touch, i, j, len; - event = $.Event(event); + event = $.Event( event ); event.type = eventType; oe = event.originalEvent; props = $.event.props; + // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280 + // https://github.com/jquery/jquery-mobile/issues/3280 + if ( t.search( /^(mouse|click)/ ) > -1 ) { + props = mouseEventProps; + } + // copy original event properties over to the new event // this would happen if we could call $.event.fix instead of $.Event // but we don't have a way to force an event to be fixed multiple times @@ -557,7 +1173,7 @@ function createVirtualEvent( event, eventType ) { // make sure that if the mouse and click virtual events are generated // without a .which one is defined - if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ){ + if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) { event.which = 1; } @@ -565,10 +1181,10 @@ function createVirtualEvent( event, eventType ) { ne = getNativeEvent( oe ); t = ne.touches; ct = ne.changedTouches; - touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined ); + touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined ); if ( touch ) { - for ( j = 0, len = touchEventProps.length; j < len; j++){ + for ( j = 0, len = touchEventProps.length; j < len; j++) { prop = touchEventProps[ j ]; event[ prop ] = touch[ prop ]; } @@ -637,14 +1253,14 @@ function disableMouseBindings() { function startResetTimer() { clearResetTimer(); - resetTimerID = setTimeout(function(){ + resetTimerID = setTimeout( function() { resetTimerID = 0; enableMouseBindings(); }, $.vmouse.resetTimerDuration ); } function clearResetTimer() { - if ( resetTimerID ){ + if ( resetTimerID ) { clearTimeout( resetTimerID ); resetTimerID = 0; } @@ -665,9 +1281,9 @@ function triggerVirtualEvent( eventType, event, flags ) { } function mouseEventCallback( event ) { - var touchID = $.data(event.target, touchTargetPropertyName); + var touchID = $.data( event.target, touchTargetPropertyName ); - if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){ + if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) { var ve = triggerVirtualEvent( "v" + event.type, event ); if ( ve ) { if ( ve.isDefaultPrevented() ) { @@ -733,12 +1349,14 @@ function handleTouchMove( event ) { var t = getNativeEvent( event ).touches[ 0 ], didCancel = didScroll, - moveThreshold = $.vmouse.moveDistanceThreshold; - didScroll = didScroll || - ( Math.abs(t.pageX - startX) > moveThreshold || - Math.abs(t.pageY - startY) > moveThreshold ), + moveThreshold = $.vmouse.moveDistanceThreshold, flags = getVirtualBindingFlags( event.target ); + didScroll = didScroll || + ( Math.abs( t.pageX - startX ) > moveThreshold || + Math.abs( t.pageY - startY ) > moveThreshold ); + + if ( didScroll && !didCancel ) { triggerVirtualEvent( "vmousecancel", event, flags ); } @@ -797,7 +1415,7 @@ function hasVirtualBindings( ele ) { return false; } -function dummyMouseHandler(){} +function dummyMouseHandler() {} function getSpecialEventObject( eventType ) { var realType = eventType.substr( 1 ); @@ -808,7 +1426,7 @@ function getSpecialEventObject( eventType ) { // add a bindings object to its data. if ( !hasVirtualBindings( this ) ) { - $.data( this, dataPropertyName, {}); + $.data( this, dataPropertyName, {} ); } // If setup is called, we know it is the first binding for this @@ -838,7 +1456,7 @@ function getSpecialEventObject( eventType ) { activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1; - if (activeDocHandlers[ "touchstart" ] === 1) { + if ( activeDocHandlers[ "touchstart" ] === 1 ) { $document.bind( "touchstart", handleTouchStart ) .bind( "touchend", handleTouchEnd ) @@ -910,7 +1528,7 @@ function getSpecialEventObject( eventType ) { // Expose our custom events to the jQuery bind/unbind mechanism. -for ( var i = 0; i < virtualEventNames.length; i++ ){ +for ( var i = 0; i < virtualEventNames.length; i++ ) { $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] ); } @@ -918,7 +1536,7 @@ for ( var i = 0; i < virtualEventNames.length; i++ ){ // Note that we require event capture support for this so if the device // doesn't support it, we punt for now and rely solely on mouse events. if ( eventCaptureSupported ) { - document.addEventListener( "click", function( e ){ + document.addEventListener( "click", function( e ) { var cnt = clickBlockList.length, target = e.target, x, y, ele, i, o, touchID; @@ -955,346 +1573,705 @@ if ( eventCaptureSupported ) { // innermost child of the touched element, even if that child is no where // near the point of touch. - ele = target; + ele = target; + + while ( ele ) { + for ( i = 0; i < cnt; i++ ) { + o = clickBlockList[ i ]; + touchID = 0; + + if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) || + $.data( ele, touchTargetPropertyName ) === o.touchID ) { + // XXX: We may want to consider removing matches from the block list + // instead of waiting for the reset timer to fire. + e.preventDefault(); + e.stopPropagation(); + return; + } + } + ele = ele.parentNode; + } + } + }, true); +} +})( jQuery, window, document ); + + (function( $, undefined ) { + var support = { + touch: "ontouchend" in document + }; + + $.mobile = $.mobile || {}; + $.mobile.support = $.mobile.support || {}; + $.extend( $.support, support ); + $.extend( $.mobile.support, support ); + }( jQuery )); + + +(function( $, window, undefined ) { + // add new event shortcuts + $.each( ( "touchstart touchmove touchend " + + "tap taphold " + + "swipe swipeleft swiperight " + + "scrollstart scrollstop" ).split( " " ), function( i, name ) { + + $.fn[ name ] = function( fn ) { + return fn ? this.bind( name, fn ) : this.trigger( name ); + }; + + // jQuery < 1.8 + if ( $.attrFn ) { + $.attrFn[ name ] = true; + } + }); + + var supportTouch = $.mobile.support.touch, + scrollEvent = "touchmove scroll", + touchStartEvent = supportTouch ? "touchstart" : "mousedown", + touchStopEvent = supportTouch ? "touchend" : "mouseup", + touchMoveEvent = supportTouch ? "touchmove" : "mousemove"; + + function triggerCustomEvent( obj, eventType, event ) { + var originalType = event.type; + event.type = eventType; + $.event.handle.call( obj, event ); + event.type = originalType; + } + + // also handles scrollstop + $.event.special.scrollstart = { + + enabled: true, + + setup: function() { + + var thisObject = this, + $this = $( thisObject ), + scrolling, + timer; + + function trigger( event, state ) { + scrolling = state; + triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event ); + } + + // iPhone triggers scroll after a small delay; use touchmove instead + $this.bind( scrollEvent, function( event ) { + + if ( !$.event.special.scrollstart.enabled ) { + return; + } + + if ( !scrolling ) { + trigger( event, true ); + } + + clearTimeout( timer ); + timer = setTimeout( function() { + trigger( event, false ); + }, 50 ); + }); + } + }; + + // also handles taphold + $.event.special.tap = { + tapholdThreshold: 750, + + setup: function() { + var thisObject = this, + $this = $( thisObject ); + + $this.bind( "vmousedown", function( event ) { + + if ( event.which && event.which !== 1 ) { + return false; + } + + var origTarget = event.target, + origEvent = event.originalEvent, + timer; + + function clearTapTimer() { + clearTimeout( timer ); + } + + function clearTapHandlers() { + clearTapTimer(); - while ( ele ) { - for ( i = 0; i < cnt; i++ ) { - o = clickBlockList[ i ]; - touchID = 0; + $this.unbind( "vclick", clickHandler ) + .unbind( "vmouseup", clearTapTimer ); + $( document ).unbind( "vmousecancel", clearTapHandlers ); + } - if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) || - $.data( ele, touchTargetPropertyName ) === o.touchID ) { - // XXX: We may want to consider removing matches from the block list - // instead of waiting for the reset timer to fire. - e.preventDefault(); - e.stopPropagation(); - return; + function clickHandler( event ) { + clearTapHandlers(); + + // ONLY trigger a 'tap' event if the start target is + // the same as the stop target. + if ( origTarget === event.target ) { + triggerCustomEvent( thisObject, "tap", event ); } } - ele = ele.parentNode; - } + + $this.bind( "vmouseup", clearTapTimer ) + .bind( "vclick", clickHandler ); + $( document ).bind( "vmousecancel", clearTapHandlers ); + + timer = setTimeout( function() { + triggerCustomEvent( thisObject, "taphold", $.Event( "taphold", { target: origTarget } ) ); + }, $.event.special.tap.tapholdThreshold ); + }); } - }, true); -} -})( jQuery, window, document ); -/* -* "events" plugin - Handles events -*/ + }; -(function( $, window, undefined ) { + // also handles swipeleft, swiperight + $.event.special.swipe = { + scrollSupressionThreshold: 30, // More than this horizontal displacement, and we will suppress scrolling. -// add new event shortcuts -$.each( ( "touchstart touchmove touchend orientationchange throttledresize " + - "tap taphold swipe swipeleft swiperight scrollstart scrollstop" ).split( " " ), function( i, name ) { + durationThreshold: 1000, // More time than this, and it isn't a swipe. - $.fn[ name ] = function( fn ) { - return fn ? this.bind( name, fn ) : this.trigger( name ); - }; + horizontalDistanceThreshold: 30, // Swipe horizontal displacement must be more than this. - $.attrFn[ name ] = true; -}); + verticalDistanceThreshold: 75, // Swipe vertical displacement must be less than this. -var supportTouch = $.support.touch, - scrollEvent = "touchmove scroll", - touchStartEvent = supportTouch ? "touchstart" : "mousedown", - touchStopEvent = supportTouch ? "touchend" : "mouseup", - touchMoveEvent = supportTouch ? "touchmove" : "mousemove"; + setup: function() { + var thisObject = this, + $this = $( thisObject ); -function triggerCustomEvent( obj, eventType, event ) { - var originalType = event.type; - event.type = eventType; - $.event.handle.call( obj, event ); - event.type = originalType; -} + $this.bind( touchStartEvent, function( event ) { + var data = event.originalEvent.touches ? + event.originalEvent.touches[ 0 ] : event, + start = { + time: ( new Date() ).getTime(), + coords: [ data.pageX, data.pageY ], + origin: $( event.target ) + }, + stop; -// also handles scrollstop -$.event.special.scrollstart = { + function moveHandler( event ) { - enabled: true, + if ( !start ) { + return; + } - setup: function() { + var data = event.originalEvent.touches ? + event.originalEvent.touches[ 0 ] : event; - var thisObject = this, - $this = $( thisObject ), - scrolling, - timer; + stop = { + time: ( new Date() ).getTime(), + coords: [ data.pageX, data.pageY ] + }; - function trigger( event, state ) { - scrolling = state; - triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event ); - } + // prevent scrolling + if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) { + event.preventDefault(); + } + } - // iPhone triggers scroll after a small delay; use touchmove instead - $this.bind( scrollEvent, function( event ) { + $this.bind( touchMoveEvent, moveHandler ) + .one( touchStopEvent, function( event ) { + $this.unbind( touchMoveEvent, moveHandler ); - if ( !$.event.special.scrollstart.enabled ) { - return; - } + if ( start && stop ) { + if ( stop.time - start.time < $.event.special.swipe.durationThreshold && + Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold && + Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) { - if ( !scrolling ) { - trigger( event, true ); + start.origin.trigger( "swipe" ) + .trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" ); + } + } + start = stop = undefined; + }); + }); + } + }; + $.each({ + scrollstop: "scrollstart", + taphold: "tap", + swipeleft: "swipe", + swiperight: "swipe" + }, function( event, sourceEvent ) { + + $.event.special[ event ] = { + setup: function() { + $( this ).bind( sourceEvent, $.noop ); } + }; + }); - clearTimeout( timer ); - timer = setTimeout(function() { - trigger( event, false ); - }, 50 ); +})( jQuery, this ); + + (function( $, undefined ) { + $.extend( $.support, { + orientation: "orientation" in window && "onorientationchange" in window }); - } -}; + }( jQuery )); + + + // throttled resize event + (function( $ ) { + $.event.special.throttledresize = { + setup: function() { + $( this ).bind( "resize", handler ); + }, + teardown: function() { + $( this ).unbind( "resize", handler ); + } + }; + + var throttle = 250, + handler = function() { + curr = ( new Date() ).getTime(); + diff = curr - lastCall; + + if ( diff >= throttle ) { + + lastCall = curr; + $( this ).trigger( "throttledresize" ); -// also handles taphold -$.event.special.tap = { - setup: function() { - var thisObject = this, - $this = $( thisObject ); + } else { + + if ( heldCall ) { + clearTimeout( heldCall ); + } + + // Promise a held call will still execute + heldCall = setTimeout( handler, throttle - diff ); + } + }, + lastCall = 0, + heldCall, + curr, + diff; + })( jQuery ); + +(function( $, window ) { + var win = $( window ), + event_name = "orientationchange", + special_event, + get_orientation, + last_orientation, + initial_orientation_is_landscape, + initial_orientation_is_default, + portrait_map = { "0": true, "180": true }; + + // It seems that some device/browser vendors use window.orientation values 0 and 180 to + // denote the "default" orientation. For iOS devices, and most other smart-phones tested, + // the default orientation is always "portrait", but in some Android and RIM based tablets, + // the default orientation is "landscape". The following code attempts to use the window + // dimensions to figure out what the current orientation is, and then makes adjustments + // to the to the portrait_map if necessary, so that we can properly decode the + // window.orientation value whenever get_orientation() is called. + // + // Note that we used to use a media query to figure out what the orientation the browser + // thinks it is in: + // + // initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)"); + // + // but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1, + // where the browser *ALWAYS* applied the landscape media query. This bug does not + // happen on iPad. + + if ( $.support.orientation ) { + + // Check the window width and height to figure out what the current orientation + // of the device is at this moment. Note that we've initialized the portrait map + // values to 0 and 180, *AND* we purposely check for landscape so that if we guess + // wrong, , we default to the assumption that portrait is the default orientation. + // We use a threshold check below because on some platforms like iOS, the iPhone + // form-factor can report a larger width than height if the user turns on the + // developer console. The actual threshold value is somewhat arbitrary, we just + // need to make sure it is large enough to exclude the developer console case. - $this.bind( "vmousedown", function( event ) { + var ww = window.innerWidth || $( window ).width(), + wh = window.innerHeight || $( window ).height(), + landscape_threshold = 50; - if ( event.which && event.which !== 1 ) { + initial_orientation_is_landscape = ww > wh && ( ww - wh ) > landscape_threshold; + + + // Now check to see if the current window.orientation is 0 or 180. + initial_orientation_is_default = portrait_map[ window.orientation ]; + + // If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR* + // if the initial orientation is portrait, but window.orientation reports 90 or -90, we + // need to flip our portrait_map values because landscape is the default orientation for + // this device/browser. + if ( ( initial_orientation_is_landscape && initial_orientation_is_default ) || ( !initial_orientation_is_landscape && !initial_orientation_is_default ) ) { + portrait_map = { "-90": true, "90": true }; + } + } + + $.event.special.orientationchange = $.extend( {}, $.event.special.orientationchange, { + setup: function() { + // If the event is supported natively, return false so that jQuery + // will bind to the event using DOM methods. + if ( $.support.orientation && !$.event.special.orientationchange.disabled ) { return false; } - var origTarget = event.target, - origEvent = event.originalEvent, - timer; + // Get the current orientation to avoid initial double-triggering. + last_orientation = get_orientation(); - function clearTapTimer() { - clearTimeout( timer ); + // Because the orientationchange event doesn't exist, simulate the + // event by testing window dimensions on resize. + win.bind( "throttledresize", handler ); + }, + teardown: function() { + // If the event is not supported natively, return false so that + // jQuery will unbind the event using DOM methods. + if ( $.support.orientation && !$.event.special.orientationchange.disabled ) { + return false; } - function clearTapHandlers() { - clearTapTimer(); + // Because the orientationchange event doesn't exist, unbind the + // resize event handler. + win.unbind( "throttledresize", handler ); + }, + add: function( handleObj ) { + // Save a reference to the bound event handler. + var old_handler = handleObj.handler; - $this.unbind( "vclick", clickHandler ) - .unbind( "vmouseup", clearTapTimer ) - .unbind( "vmousecancel", clearTapHandlers ); - } - function clickHandler(event) { - clearTapHandlers(); + handleObj.handler = function( event ) { + // Modify event object, adding the .orientation property. + event.orientation = get_orientation(); - // ONLY trigger a 'tap' event if the start target is - // the same as the stop target. - if ( origTarget == event.target ) { - triggerCustomEvent( thisObject, "tap", event ); - } - } + // Call the originally-bound event handler and return its result. + return old_handler.apply( this, arguments ); + }; + } + }); - $this.bind( "vmousecancel", clearTapHandlers ) - .bind( "vmouseup", clearTapTimer ) - .bind( "vclick", clickHandler ); + // If the event is not supported natively, this handler will be bound to + // the window resize event to simulate the orientationchange event. + function handler() { + // Get the current orientation. + var orientation = get_orientation(); - timer = setTimeout(function() { - triggerCustomEvent( thisObject, "taphold", $.Event( "taphold" ) ); - }, 750 ); - }); + if ( orientation !== last_orientation ) { + // The orientation has changed, so trigger the orientationchange event. + last_orientation = orientation; + win.trigger( event_name ); + } } -}; -// also handles swipeleft, swiperight -$.event.special.swipe = { - scrollSupressionThreshold: 10, // More than this horizontal displacement, and we will suppress scrolling. + // Get the current page orientation. This method is exposed publicly, should it + // be needed, as jQuery.event.special.orientationchange.orientation() + $.event.special.orientationchange.orientation = get_orientation = function() { + var isPortrait = true, elem = document.documentElement; - durationThreshold: 1000, // More time than this, and it isn't a swipe. + // prefer window orientation to the calculation based on screensize as + // the actual screen resize takes place before or after the orientation change event + // has been fired depending on implementation (eg android 2.3 is before, iphone after). + // More testing is required to determine if a more reliable method of determining the new screensize + // is possible when orientationchange is fired. (eg, use media queries + element + opacity) + if ( $.support.orientation ) { + // if the window orientation registers as 0 or 180 degrees report + // portrait, otherwise landscape + isPortrait = portrait_map[ window.orientation ]; + } else { + isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; + } - horizontalDistanceThreshold: 30, // Swipe horizontal displacement must be more than this. + return isPortrait ? "portrait" : "landscape"; + }; - verticalDistanceThreshold: 75, // Swipe vertical displacement must be less than this. + $.fn[ event_name ] = function( fn ) { + return fn ? this.bind( event_name, fn ) : this.trigger( event_name ); + }; - setup: function() { - var thisObject = this, - $this = $( thisObject ); + // jQuery < 1.8 + if ( $.attrFn ) { + $.attrFn[ event_name ] = true; + } - $this.bind( touchStartEvent, function( event ) { - var data = event.originalEvent.touches ? - event.originalEvent.touches[ 0 ] : event, - start = { - time: ( new Date() ).getTime(), - coords: [ data.pageX, data.pageY ], - origin: $( event.target ) - }, - stop; +}( jQuery, this )); - function moveHandler( event ) { - if ( !start ) { - return; - } +(function( $, undefined ) { - var data = event.originalEvent.touches ? - event.originalEvent.touches[ 0 ] : event; +var $window = $( window ), + $html = $( "html" ); - stop = { - time: ( new Date() ).getTime(), - coords: [ data.pageX, data.pageY ] - }; +/* $.mobile.media method: pass a CSS media type or query and get a bool return + note: this feature relies on actual media query support for media queries, though types will work most anywhere + examples: + $.mobile.media('screen') // tests for screen media type + $.mobile.media('screen and (min-width: 480px)') // tests for screen media type with window width > 480px + $.mobile.media('@media screen and (-webkit-min-device-pixel-ratio: 2)') // tests for webkit 2x pixel ratio (iPhone 4) +*/ +$.mobile.media = (function() { + // TODO: use window.matchMedia once at least one UA implements it + var cache = {}, + testDiv = $( "
" ), + fakeBody = $( "" ).append( testDiv ); - // prevent scrolling - if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) { - event.preventDefault(); - } + return function( query ) { + if ( !( query in cache ) ) { + var styleBlock = document.createElement( "style" ), + cssrule = "@media " + query + " { #jquery-mediatest { position:absolute; } }"; + + //must set type for IE! + styleBlock.type = "text/css"; + + if ( styleBlock.styleSheet ) { + styleBlock.styleSheet.cssText = cssrule; + } else { + styleBlock.appendChild( document.createTextNode(cssrule) ); } - $this.bind( touchMoveEvent, moveHandler ) - .one( touchStopEvent, function( event ) { - $this.unbind( touchMoveEvent, moveHandler ); + $html.prepend( fakeBody ).prepend( styleBlock ); + cache[ query ] = testDiv.css( "position" ) === "absolute"; + fakeBody.add( styleBlock ).remove(); + } + return cache[ query ]; + }; +})(); - if ( start && stop ) { - if ( stop.time - start.time < $.event.special.swipe.durationThreshold && - Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold && - Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) { +})(jQuery); - start.origin.trigger( "swipe" ) - .trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" ); - } - } - start = stop = undefined; - }); - }); +(function( $, undefined ) { + +// thx Modernizr +function propExists( prop ) { + var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), + props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " ); + + for ( var v in props ) { + if ( fbCSS[ props[ v ] ] !== undefined ) { + return true; + } } -}; +} -(function( $, window ) { - // "Cowboy" Ben Alman +var fakeBody = $( "" ).prependTo( "html" ), + fbCSS = fakeBody[ 0 ].style, + vendors = [ "Webkit", "Moz", "O" ], + webos = "palmGetResource" in window, //only used to rule out scrollTop + opera = window.opera, + operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]", + bb = window.blackberry && !propExists( "-webkit-transform" ); //only used to rule out box shadow, as it's filled opaque on BB 5 and lower - var win = $( window ), - special_event, - get_orientation, - last_orientation; - $.event.special.orientationchange = special_event = { - setup: function() { - // If the event is supported natively, return false so that jQuery - // will bind to the event using DOM methods. - if ( $.support.orientation && $.mobile.orientationChangeEnabled ) { - return false; +function validStyle( prop, value, check_vend ) { + var div = document.createElement( 'div' ), + uc = function( txt ) { + return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 ); + }, + vend_pref = function( vend ) { + return "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-"; + }, + check_style = function( vend ) { + var vend_prop = vend_pref( vend ) + prop + ": " + value + ";", + uc_vend = uc( vend ), + propStyle = uc_vend + uc( prop ); + + div.setAttribute( "style", vend_prop ); + + if ( !!div.style[ propStyle ] ) { + ret = true; } + }, + check_vends = check_vend ? [ check_vend ] : vendors, + ret; + + for( var i = 0; i < check_vends.length; i++ ) { + check_style( check_vends[i] ); + } + return !!ret; +} + +// Thanks to Modernizr src for this test idea. `perspective` check is limited to Moz to prevent a false positive for 3D transforms on Android. +function transform3dTest() { + var prop = "transform-3d"; + return validStyle( 'perspective', '10px', 'moz' ) || $.mobile.media( "(-" + vendors.join( "-" + prop + "),(-" ) + "-" + prop + "),(" + prop + ")" ); +} + +// Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting ) +function baseTagTest() { + var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/", + base = $( "head base" ), + fauxEle = null, + href = "", + link, rebase; + + if ( !base.length ) { + base = fauxEle = $( "", { "href": fauxBase }).appendTo( "head" ); + } else { + href = base.attr( "href" ); + } + + link = $( "
" ).prependTo( fakeBody ); + rebase = link[ 0 ].href; + base[ 0 ].href = href || location.pathname; + + if ( fauxEle ) { + fauxEle.remove(); + } + return rebase.indexOf( fauxBase ) === 0; +} + +// Thanks Modernizr +function cssPointerEventsTest() { + var element = document.createElement( 'x' ), + documentElement = document.documentElement, + getComputedStyle = window.getComputedStyle, + supports; + + if ( !( 'pointerEvents' in element.style ) ) { + return false; + } + + element.style.pointerEvents = 'auto'; + element.style.pointerEvents = 'x'; + documentElement.appendChild( element ); + supports = getComputedStyle && + getComputedStyle( element, '' ).pointerEvents === 'auto'; + documentElement.removeChild( element ); + return !!supports; +} - // Get the current orientation to avoid initial double-triggering. - last_orientation = get_orientation(); +function boundingRect() { + var div = document.createElement( "div" ); + return typeof div.getBoundingClientRect !== "undefined"; +} - // Because the orientationchange event doesn't exist, simulate the - // event by testing window dimensions on resize. - win.bind( "throttledresize", handler ); - }, - teardown: function(){ - // If the event is not supported natively, return false so that - // jQuery will unbind the event using DOM methods. - if ( $.support.orientation && $.mobile.orientationChangeEnabled ) { - return false; - } +// non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683 +// allows for inclusion of IE 6+, including Windows Mobile 7 +$.extend( $.mobile, { browser: {} } ); +$.mobile.browser.ie = (function() { + var v = 3, + div = document.createElement( "div" ), + a = div.all || []; - // Because the orientationchange event doesn't exist, unbind the - // resize event handler. - win.unbind( "throttledresize", handler ); - }, - add: function( handleObj ) { - // Save a reference to the bound event handler. - var old_handler = handleObj.handler; + do { + div.innerHTML = ""; + } while( a[0] ); + return v > 4 ? v : !v; +})(); - handleObj.handler = function( event ) { - // Modify event object, adding the .orientation property. - event.orientation = get_orientation(); - // Call the originally-bound event handler and return its result. - return old_handler.apply( this, arguments ); - }; - } - }; +$.extend( $.support, { + cssTransitions: "WebKitTransitionEvent" in window || validStyle( 'transition', 'height 100ms linear' ) && !opera, + pushState: "pushState" in history && "replaceState" in history, + mediaquery: $.mobile.media( "only all" ), + cssPseudoElement: !!propExists( "content" ), + touchOverflow: !!propExists( "overflowScrolling" ), + cssTransform3d: transform3dTest(), + boxShadow: !!propExists( "boxShadow" ) && !bb, + scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos && !operamini, + dynamicBaseTag: baseTagTest(), + cssPointerEvents: cssPointerEventsTest(), + boundingRect: boundingRect() +}); - // If the event is not supported natively, this handler will be bound to - // the window resize event to simulate the orientationchange event. - function handler() { - // Get the current orientation. - var orientation = get_orientation(); +fakeBody.remove(); - if ( orientation !== last_orientation ) { - // The orientation has changed, so trigger the orientationchange event. - last_orientation = orientation; - win.trigger( "orientationchange" ); - } - } - // Get the current page orientation. This method is exposed publicly, should it - // be needed, as jQuery.event.special.orientationchange.orientation() - $.event.special.orientationchange.orientation = get_orientation = function() { - var isPortrait = true, elem = document.documentElement; +// $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian) +// or that generally work better browsing in regular http for full page refreshes (Opera Mini) +// Note: This detection below is used as a last resort. +// We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible +var nokiaLTE7_3 = (function() { - // prefer window orientation to the calculation based on screensize as - // the actual screen resize takes place before or after the orientation change event - // has been fired depending on implementation (eg android 2.3 is before, iphone after). - // More testing is required to determine if a more reliable method of determining the new screensize - // is possible when orientationchange is fired. (eg, use media queries + element + opacity) - if ( $.support.orientation ) { - // if the window orientation registers as 0 or 180 degrees report - // portrait, otherwise landscape - isPortrait = window.orientation % 180 == 0; - } else { - isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; - } + var ua = window.navigator.userAgent; - return isPortrait ? "portrait" : "landscape"; - }; + //The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older + return ua.indexOf( "Nokia" ) > -1 && + ( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) && + ua.indexOf( "AppleWebKit" ) > -1 && + ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ ); +})(); -})( jQuery, window ); +// Support conditions that must be met in order to proceed +// default enhanced qualifications are media query support OR IE 7+ +$.mobile.gradeA = function() { + return ( $.support.mediaquery || $.mobile.browser.ie && $.mobile.browser.ie >= 7 ) && ( $.support.boundingRect || $.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null ); +}; -// throttled resize event -(function() { +$.mobile.ajaxBlacklist = + // BlackBerry browsers, pre-webkit + window.blackberry && !window.WebKitPoint || + // Opera Mini + operamini || + // Symbian webkits pre 7.3 + nokiaLTE7_3; - $.event.special.throttledresize = { - setup: function() { - $( this ).bind( "resize", handler ); - }, - teardown: function(){ - $( this ).unbind( "resize", handler ); - } - }; +// Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices +// to render the stylesheets when they're referenced before this script, as we'd recommend doing. +// This simply reappends the CSS in place, which for some reason makes it apply +if ( nokiaLTE7_3 ) { + $(function() { + $( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" ); + }); +} - var throttle = 250, - handler = function() { - curr = ( new Date() ).getTime(); - diff = curr - lastCall; +// For ruling out shadows via css +if ( !$.support.boxShadow ) { + $( "html" ).addClass( "ui-mobile-nosupport-boxshadow" ); +} - if ( diff >= throttle ) { +})( jQuery ); - lastCall = curr; - $( this ).trigger( "throttledresize" ); +(function( $, undefined ) { - } else { +$.widget( "mobile.page", $.mobile.widget, { + options: { + theme: "c", + domCache: false, + keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')" + }, - if ( heldCall ) { - clearTimeout( heldCall ); - } + _create: function() { + + var self = this; + + // if false is returned by the callbacks do not create the page + if ( self._trigger( "beforecreate" ) === false ) { + return false; + } - // Promise a held call will still execute - heldCall = setTimeout( handler, throttle - diff ); - } - }, - lastCall = 0, - heldCall, - curr, - diff; -})(); + self.element + .attr( "tabindex", "0" ) + .addClass( "ui-page ui-body-" + self.options.theme ) + .bind( "pagebeforehide", function() { + self.removeContainerBackground(); + } ) + .bind( "pagebeforeshow", function() { + self.setContainerBackground(); + } ); + }, + + removeContainerBackground: function() { + $.mobile.pageContainer.removeClass( "ui-overlay-" + $.mobile.getInheritedTheme( this.element.parent() ) ); + }, + + // set the page container background to the page theme + setContainerBackground: function( theme ) { + if ( this.options.theme ) { + $.mobile.pageContainer.addClass( "ui-overlay-" + ( theme || this.options.theme ) ); + } + }, -$.each({ - scrollstop: "scrollstart", - taphold: "tap", - swipeleft: "swipe", - swiperight: "swipe" -}, function( event, sourceEvent ) { + keepNativeSelector: function() { + var options = this.options, + keepNativeDefined = options.keepNative && $.trim( options.keepNative ); - $.event.special[ event ] = { - setup: function() { - $( this ).bind( sourceEvent, $.noop ); + if ( keepNativeDefined && options.keepNative !== options.keepNativeDefault ) { + return [options.keepNative, options.keepNativeDefault].join( ", " ); } - }; + + return options.keepNativeDefault; + } }); +})( jQuery ); -})( jQuery, this ); // Script: jQuery hashchange event // // *Version: 1.3, Last updated: 7/21/2010* @@ -1373,9 +2350,7 @@ $.each({ // extra awesomeness that BBQ provides. This plugin will be included as // part of jQuery BBQ, but also be available separately. -(function($,window,undefined){ - '$:nomunge'; // Used by YUI compressor. - +(function( $, window, undefined ) { // Reused string. var str_hashchange = 'hashchange', @@ -1541,14 +2516,14 @@ $.each({ // event for browsers that don't natively support it, including creating a // polling loop to watch for hash changes and in IE 6/7 creating a hidden // Iframe to enable back and forward. - fake_onhashchange = (function(){ + fake_onhashchange = (function() { var self = {}, timeout_id, // Remember the initial hash so it doesn't get triggered immediately. last_hash = get_fragment(), - fn_retval = function(val){ return val; }, + fn_retval = function( val ) { return val; }, history_set = fn_retval, history_get = fn_retval; @@ -1585,7 +2560,7 @@ $.each({ // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - $.browser.msie && !supports_onhashchange && (function(){ + $.browser.msie && !supports_onhashchange && (function() { // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 // when running in "IE7 compatibility" mode. @@ -1594,7 +2569,7 @@ $.each({ // When the event is bound and polling starts in IE 6/7, create a hidden // Iframe for history handling. - self.start = function(){ + self.start = function() { if ( !iframe ) { iframe_src = $.fn[ str_hashchange ].src; iframe_src = iframe_src && iframe_src + get_fragment(); @@ -1605,7 +2580,7 @@ $.each({ // When Iframe has completely loaded, initialize the history and // start polling. - .one( 'load', function(){ + .one( 'load', function() { iframe_src || history_set( get_fragment() ); poll(); }) @@ -1621,7 +2596,7 @@ $.each({ // prettify the back/next history menu entries. Since IE sometimes // errors with "Unspecified error" the very first time this is set // (yes, very useful) wrap this with a try/catch block. - doc.onpropertychange = function(){ + doc.onpropertychange = function() { try { if ( event.propertyName === 'title' ) { iframe.document.title = doc.title; @@ -1676,282 +2651,174 @@ $.each({ })(); })(jQuery,this); -/* -* "page" plugin -*/ - -(function( $, undefined ) { - -$.widget( "mobile.page", $.mobile.widget, { - options: { - theme: "c", - domCache: false, - keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')" - }, - - _create: function() { - - this._trigger( "beforecreate" ); - - this.element - .attr( "tabindex", "0" ) - .addClass( "ui-page ui-body-" + this.options.theme ); - }, - - keepNativeSelector: function() { - var options = this.options, - keepNativeDefined = options.keepNative && $.trim(options.keepNative); - - if( keepNativeDefined && options.keepNative !== options.keepNativeDefault ){ - return [options.keepNative, options.keepNativeDefault].join(", "); - } - return options.keepNativeDefault; - } -}); -})( jQuery ); -/* -* "core" - The base file for jQm -*/ (function( $, window, undefined ) { - var nsNormalizeDict = {}; - - // jQuery.mobile configurable options - $.extend( $.mobile, { - - // Namespace used framework-wide for data-attrs. Default is no namespace - ns: "", - - // Define the url parameter used for referencing widget-generated sub-pages. - // Translates to to example.html&ui-page=subpageIdentifier - // hash segment before &ui-page= is used to make Ajax request - subPageUrlKey: "ui-page", - - // Class assigned to page currently in view, and during transitions - activePageClass: "ui-page-active", - - // Class used for "active" button state, from CSS framework - activeBtnClass: "ui-btn-active", - - // Automatically handle clicks and form submissions through Ajax, when same-domain - ajaxEnabled: true, - - // Automatically load and show pages based on location.hash - hashListeningEnabled: true, - - // disable to prevent jquery from bothering with links - linkBindingEnabled: true, - - // Set default page transition - 'none' for no transitions - defaultPageTransition: "slide", - - // Minimum scroll distance that will be remembered when returning to a page - minScrollBack: 250, - - // Set default dialog transition - 'none' for no transitions - defaultDialogTransition: "pop", +var createHandler = function( sequential ) { - // Show loading message during Ajax requests - // if false, message will not appear, but loading classes will still be toggled on html el - loadingMessage: "loading", + // Default to sequential + if ( sequential === undefined ) { + sequential = true; + } - // Error response message - appears when an Ajax page request fails - pageLoadErrorMessage: "Error Loading Page", + return function( name, reverse, $to, $from ) { + + var deferred = new $.Deferred(), + reverseClass = reverse ? " reverse" : "", + active = $.mobile.urlHistory.getActive(), + toScroll = active.lastScroll || $.mobile.defaultHomeScroll, + screenHeight = $.mobile.getScreenHeight(), + maxTransitionOverride = $.mobile.maxTransitionWidth !== false && $( window ).width() > $.mobile.maxTransitionWidth, + none = !$.support.cssTransitions || maxTransitionOverride || !name || name === "none" || Math.max( $( window ).scrollTop(), toScroll ) > $.mobile.getMaxScrollForTransition(), + toPreClass = " ui-page-pre-in", + toggleViewportClass = function() { + $.mobile.pageContainer.toggleClass( "ui-mobile-viewport-transitioning viewport-" + name ); + }, + scrollPage = function() { + // By using scrollTo instead of silentScroll, we can keep things better in order + // Just to be precautios, disable scrollstart listening like silentScroll would + $.event.special.scrollstart.enabled = false; - //automatically initialize the DOM when it's ready - autoInitializePage: true, + window.scrollTo( 0, toScroll ); - pushStateEnabled: true, + // reenable scrollstart listening like silentScroll would + setTimeout( function() { + $.event.special.scrollstart.enabled = true; + }, 150 ); + }, + cleanFrom = function() { + $from + .removeClass( $.mobile.activePageClass + " out in reverse " + name ) + .height( "" ); + }, + startOut = function() { + // if it's not sequential, call the doneOut transition to start the TO page animating in simultaneously + if ( !sequential ) { + doneOut(); + } + else { + $from.animationComplete( doneOut ); + } - // turn of binding to the native orientationchange due to android orientation behavior - orientationChangeEnabled: true, + // Set the from page's height and start it transitioning out + // Note: setting an explicit height helps eliminate tiling in the transitions + $from + .height( screenHeight + $( window ).scrollTop() ) + .addClass( name + " out" + reverseClass ); + }, - // Support conditions that must be met in order to proceed - // default enhanced qualifications are media query support OR IE 7+ - gradeA: function(){ - return $.support.mediaquery || $.mobile.browser.ie && $.mobile.browser.ie >= 7; - }, + doneOut = function() { - // TODO might be useful upstream in jquery itself ? - keyCode: { - ALT: 18, - BACKSPACE: 8, - CAPS_LOCK: 20, - COMMA: 188, - COMMAND: 91, - COMMAND_LEFT: 91, // COMMAND - COMMAND_RIGHT: 93, - CONTROL: 17, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - INSERT: 45, - LEFT: 37, - MENU: 93, // COMMAND_RIGHT - 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 // COMMAND - }, + if ( $from && sequential ) { + cleanFrom(); + } - // Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value - silentScroll: function( ypos ) { - if ( $.type( ypos ) !== "number" ) { - ypos = $.mobile.defaultHomeScroll; - } + startIn(); + }, - // prevent scrollstart and scrollstop events - $.event.special.scrollstart.enabled = false; + startIn = function() { - setTimeout(function() { - window.scrollTo( 0, ypos ); - $( document ).trigger( "silentscroll", { x: 0, y: ypos }); - }, 20 ); + // Prevent flickering in phonegap container: see comments at #4024 regarding iOS + $to.css( "z-index", -10 ); - setTimeout(function() { - $.event.special.scrollstart.enabled = true; - }, 150 ); - }, + $to.addClass( $.mobile.activePageClass + toPreClass ); - // Expose our cache for testing purposes. - nsNormalizeDict: nsNormalizeDict, + // Send focus to page as it is now display: block + $.mobile.focusPage( $to ); - // Take a data attribute property, prepend the namespace - // and then camel case the attribute string. Add the result - // to our nsNormalizeDict so we don't have to do this again. - nsNormalize: function( prop ) { - if ( !prop ) { - return; - } + // Set to page height + $to.height( screenHeight + toScroll ); - return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) ); - }, + scrollPage(); - getInheritedTheme: function( el, defaultTheme ) { + // Restores visibility of the new page: added together with $to.css( "z-index", -10 ); + $to.css( "z-index", "" ); - // Find the closest parent with a theme class on it. Note that - // we are not using $.fn.closest() on purpose here because this - // method gets called quite a bit and we need it to be as fast - // as possible. + if ( !none ) { + $to.animationComplete( doneIn ); + } - var e = el[ 0 ], - ltr = "", - re = /ui-(bar|body)-([a-z])\b/, - c, m; + $to + .removeClass( toPreClass ) + .addClass( name + " in" + reverseClass ); - while ( e ) { - var c = e.className || ""; - if ( ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) { - // We found a parent with a theme class - // on it so bail from this loop. - break; + if ( none ) { + doneIn(); } - e = e.parentNode; - } - - // Return the theme letter we found, if none, return the - // specified default. - return ltr || defaultTheme || "a"; - } - }); + }, - // Mobile version of data and removeData and hasData methods - // ensures all data is set and retrieved using jQuery Mobile's data namespace - $.fn.jqmData = function( prop, value ) { - var result; - if ( typeof prop != "undefined" ) { - result = this.data( prop ? $.mobile.nsNormalize( prop ) : prop, value ); - } - return result; - }; + doneIn = function() { - $.jqmData = function( elem, prop, value ) { - var result; - if ( typeof prop != "undefined" ) { - result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value ); - } - return result; - }; + if ( !sequential ) { - $.fn.jqmRemoveData = function( prop ) { - return this.removeData( $.mobile.nsNormalize( prop ) ); - }; + if ( $from ) { + cleanFrom(); + } + } - $.jqmRemoveData = function( elem, prop ) { - return $.removeData( elem, $.mobile.nsNormalize( prop ) ); - }; + $to + .removeClass( "out in reverse " + name ) + .height( "" ); - $.fn.removeWithDependents = function() { - $.removeWithDependents( this ); - }; + toggleViewportClass(); - $.removeWithDependents = function( elem ) { - var $elem = $( elem ); + // In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition + // This ensures we jump to that spot after the fact, if we aren't there already. + if ( $( window ).scrollTop() !== toScroll ) { + scrollPage(); + } - ( $elem.jqmData('dependents') || $() ).remove(); - $elem.remove(); - }; + deferred.resolve( name, reverse, $to, $from, true ); + }; - $.fn.addDependents = function( newDependents ) { - $.addDependents( $(this), newDependents ); - }; + toggleViewportClass(); - $.addDependents = function( elem, newDependents ) { - var dependents = $(elem).jqmData( 'dependents' ) || $(); + if ( $from && !none ) { + startOut(); + } + else { + doneOut(); + } - $(elem).jqmData( 'dependents', $.merge(dependents, newDependents) ); + return deferred.promise(); }; +}; - // note that this helper doesn't attempt to handle the callback - // or setting of an html elements 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(); +// generate the handlers from the above +var sequentialHandler = createHandler(), + simultaneousHandler = createHandler( false ), + defaultGetMaxScrollForTransition = function() { + return $.mobile.getScreenHeight() * 3; }; - // Monkey-patching Sizzle to filter the :jqmData selector - var oldFind = $.find, - jqmDataRE = /:jqmData\(([^)]*)\)/g; +// Make our transition handler the public default. +$.mobile.defaultTransitionHandler = sequentialHandler; - $.find = function( selector, context, ret, extra ) { - selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" ); +//transition handler dictionary for 3rd party transitions +$.mobile.transitionHandlers = { + "default": $.mobile.defaultTransitionHandler, + "sequential": sequentialHandler, + "simultaneous": simultaneousHandler +}; - return oldFind.call( this, selector, context, ret, extra ); - }; +$.mobile.transitionFallbacks = {}; - $.extend( $.find, oldFind ); +// If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified +$.mobile._maybeDegradeTransition = function( transition ) { + if ( transition && !$.support.cssTransform3d && $.mobile.transitionFallbacks[ transition ] ) { + transition = $.mobile.transitionFallbacks[ transition ]; + } - $.find.matches = function( expr, set ) { - return $.find( expr, null, null, set ); - }; + return transition; +}; - $.find.matchesSelector = function( node, expr ) { - return $.find( expr, null, null, [ node ] ).length > 0; - }; +// Set the getMaxScrollForTransition to default if no implementation was set by user +$.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition; })( jQuery, this ); -/* -* core utilities for auto ajax navigation, base tag mgmt, -*/ - -( function( $, undefined ) { +(function( $, undefined ) { //define vars for interal use var $window = $( window ), @@ -1988,6 +2855,26 @@ $.widget( "mobile.page", $.mobile.widget, { // urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, + // Abstraction to address xss (Issue #4787) by removing the authority in + // browsers that auto decode it. All references to location.href should be + // replaced with a call to this method so that it can be dealt with properly here + getLocation: function( url ) { + var uri = url ? this.parseUrl( url ) : location, + hash = this.parseUrl( url || location.href ).hash; + + // mimic the browser with an empty string when the hash is empty + hash = hash === "#" ? "" : hash; + + // Make sure to parse the url or the location object for the hash because using location.hash + // is autodecoded in firefox, the rest of the url should be from the object (location unless + // we're testing) to avoid the inclusion of the authority + return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash; + }, + + parseLocation: function() { + return this.parseUrl( this.getLocation() ); + }, + //Parse a URL into a structure that allows easy access to //all of the URL components by name. parseUrl: function( url ) { @@ -2078,6 +2965,10 @@ $.widget( "mobile.page", $.mobile.widget, { return relUrl; } + if ( absUrl === undefined ) { + absUrl = documentBase; + } + var relObj = path.parseUrl( relUrl ), absObj = path.parseUrl( absUrl ), protocol = relObj.protocol || absObj.protocol, @@ -2102,19 +2993,20 @@ $.widget( "mobile.page", $.mobile.widget, { convertUrlToDataUrl: function( absUrl ) { var u = path.parseUrl( absUrl ); if ( path.isEmbeddedPage( u ) ) { - // For embedded pages, remove the dialog hash key as in getFilePath(), - // otherwise the Data Url won't match the id of the embedded Page. + // For embedded pages, remove the dialog hash key as in getFilePath(), + // otherwise the Data Url won't match the id of the embedded Page. return u.hash.split( dialogHashKey )[0].replace( /^#/, "" ); } else if ( path.isSameDomain( u, documentBase ) ) { - return u.hrefNoHash.replace( documentBase.domain, "" ); + return u.hrefNoHash.replace( documentBase.domain, "" ).split( dialogHashKey )[0]; } - return absUrl; + + return window.decodeURIComponent(absUrl); }, //get path from current hash, or from a file path get: function( newPath ) { - if( newPath === undefined ) { - newPath = location.hash; + if ( newPath === undefined ) { + newPath = path.parseLocation().hash; } return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' ); }, @@ -2151,6 +3043,10 @@ $.widget( "mobile.page", $.mobile.widget, { return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) ); }, + isHashValid: function( hash ) { + return ( /^#[^#]+$/ ).test( hash ); + }, + //check whether a url is referencing the same domain, or an external domain or different protocol //could be mailto, etc isExternal: function( url ) { @@ -2193,7 +3089,20 @@ $.widget( "mobile.page", $.mobile.widget, { if ( u.protocol !== "" ) { return ( u.hash && ( u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ) ) ); } - return (/^#/).test( u.href ); + return ( /^#/ ).test( u.href ); + }, + + + // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR + // requests if the document doing the request was loaded via the file:// protocol. + // This is usually to allow the application to "phone home" and fetch app specific + // data. We normally let the browser handle external/cross-domain urls, but if the + // allowCrossDomainPages option is true, we will allow cross-domain http/https + // requests to go through our page loading logic. + isPermittedCrossDomainRequest: function( docUrl, reqUrl ) { + return $.mobile.allowCrossDomainPages && + docUrl.protocol === "file:" && + reqUrl.search( /^https?:/ ) !== -1; } }, @@ -2226,7 +3135,7 @@ $.widget( "mobile.page", $.mobile.widget, { // addNew is used whenever a new page is added addNew: function( url, transition, title, pageUrl, role ) { //if there's forward history, wipe it - if( urlHistory.getNext() ) { + if ( urlHistory.getNext() ) { urlHistory.clearForward(); } @@ -2243,11 +3152,11 @@ $.widget( "mobile.page", $.mobile.widget, { directHashChange: function( opts ) { var back , forward, newActiveIndex, prev = this.getActive(); - // check if url isp in history and if it's ahead or behind current page + // check if url is in history and if it's ahead or behind current page $.each( urlHistory.stack, function( i, historyEntry ) { //if the url is in the stack, it's a forward or a back - if( opts.currentUrl === historyEntry.url ) { + if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) { //define back and forward by whether url is older or newer than current page back = i < urlHistory.activeIndex; forward = !back; @@ -2258,9 +3167,9 @@ $.widget( "mobile.page", $.mobile.widget, { // save new page index, null check to prevent falsey 0 result this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex; - if( back ) { + if ( back ) { ( opts.either || opts.isBack )( true ); - } else if( forward ) { + } else if ( forward ) { ( opts.either || opts.isForward )( false ); } }, @@ -2286,14 +3195,16 @@ $.widget( "mobile.page", $.mobile.widget, { $base = $head.children( "base" ), //tuck away the original document URL minus any fragment. - documentUrl = path.parseUrl( location.href ), + documentUrl = path.parseLocation(), //if the document has an embedded base tag, documentBase is set to its //initial value. If a base tag does not exist, then we default to the documentUrl. documentBase = $base.length ? path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), documentUrl.href ) ) : documentUrl, //cache the comparison once. - documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ); + documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ), + + getScreenHeight = $.mobile.getScreenHeight; //base element management, defined depending on dynamic base tag support var base = $.support.dynamicBaseTag ? { @@ -2313,26 +3224,48 @@ $.widget( "mobile.page", $.mobile.widget, { } : undefined; -/* - internal utility functions ---------------------------------------*/ - + /* internal utility functions */ + + // NOTE Issue #4950 Android phonegap doesn't navigate back properly + // when a full page refresh has taken place. It appears that hashchange + // and replacestate history alterations work fine but we need to support + // both forms of history traversal in our code that uses backward history + // movement + $.mobile.back = function() { + var nav = window.navigator; + + // if the setting is on and the navigator object is + // available use the phonegap navigation capability + if( this.phonegapNavigationEnabled && + nav && + nav.app && + nav.app.backHistory ){ + nav.app.backHistory(); + } else { + window.history.back(); + } + }; //direct focus to the page title, or otherwise first focusable element - function reFocus( page ) { - var pageTitle = page.find( ".ui-title:eq(0)" ); + $.mobile.focusPage = function ( page ) { + var autofocus = page.find( "[autofocus]" ), + pageTitle = page.find( ".ui-title:eq(0)" ); - if( pageTitle.length ) { - pageTitle.focus(); + if ( autofocus.length ) { + autofocus.focus(); + return; } - else{ + + if ( pageTitle.length ) { + pageTitle.focus(); + } else{ page.focus(); } - } + }; //remove active classes after page transition or error function removeActiveLinkClass( forceRemoval ) { - if( !!$activeClickedLink && ( !$activeClickedLink.closest( '.ui-page-active' ).length || forceRemoval ) ) { + if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) { $activeClickedLink.removeClass( $.mobile.activeBtnClass ); } $activeClickedLink = null; @@ -2340,38 +3273,26 @@ $.widget( "mobile.page", $.mobile.widget, { function releasePageTransitionLock() { isPageTransitioning = false; - if( pageTransitionQueue.length > 0 ) { + if ( pageTransitionQueue.length > 0 ) { $.mobile.changePage.apply( null, pageTransitionQueue.pop() ); } } // Save the last scroll distance per page, before it is hidden var setLastScrollEnabled = true, - firstScrollElem, getScrollElem, setLastScroll, delayedSetLastScroll; - - getScrollElem = function() { - var scrollElem = $window, activePage, - touchOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled; - - if( touchOverflow ){ - activePage = $( ".ui-page-active" ); - scrollElem = activePage.is( ".ui-native-fixed" ) ? activePage.find( ".ui-content" ) : activePage; - } - - return scrollElem; - }; + setLastScroll, delayedSetLastScroll; - setLastScroll = function( scrollElem ) { + setLastScroll = function() { // this barrier prevents setting the scroll value based on the browser // scrolling the window based on a hashchange - if( !setLastScrollEnabled ) { + if ( !setLastScrollEnabled ) { return; } var active = $.mobile.urlHistory.getActive(); - if( active ) { - var lastScroll = scrollElem && scrollElem.scrollTop(); + if ( active ) { + var lastScroll = $window.scrollTop(); // Set active page's lastScroll prop. // If the location we're scrolling to is less than minScrollBack, let it go. @@ -2384,14 +3305,14 @@ $.widget( "mobile.page", $.mobile.widget, { // to the hash targets location (sometimes the top of the page). once pagechange fires // getLastScroll is again permitted to operate delayedSetLastScroll = function() { - setTimeout( setLastScroll, 100, $(this) ); + setTimeout( setLastScroll, 100 ); }; // disable an scroll setting when a hashchange has been fired, this only works // because the recording of the scroll position is delayed for 100ms after // the browser might have changed the position because of the hashchange $window.bind( $.support.pushState ? "popstate" : "hashchange", function() { - setLastScrollEnabled = false; + setLastScrollEnabled = false; }); // handle initial hashchange from chrome :( @@ -2400,106 +3321,56 @@ $.widget( "mobile.page", $.mobile.widget, { }); // wait until the mobile page container has been determined to bind to pagechange - $window.one( "pagecontainercreate", function(){ + $window.one( "pagecontainercreate", function() { // once the page has changed, re-enable the scroll recording $.mobile.pageContainer.bind( "pagechange", function() { - var scrollElem = getScrollElem(); - setLastScrollEnabled = true; + setLastScrollEnabled = true; // remove any binding that previously existed on the get scroll // which may or may not be different than the scroll element determined for // this page previously - scrollElem.unbind( "scrollstop", delayedSetLastScroll ); + $window.unbind( "scrollstop", delayedSetLastScroll ); // determine and bind to the current scoll element which may be the window // or in the case of touch overflow the element with touch overflow - scrollElem.bind( "scrollstop", delayedSetLastScroll ); + $window.bind( "scrollstop", delayedSetLastScroll ); }); }); // bind to scrollstop for the first page as "pagechange" won't be fired in that case - getScrollElem().bind( "scrollstop", delayedSetLastScroll ); - - // Make the iOS clock quick-scroll work again if we're using native overflow scrolling - /* - if( $.support.touchOverflow ){ - if( $.mobile.touchOverflowEnabled ){ - $( window ).bind( "scrollstop", function(){ - if( $( this ).scrollTop() === 0 ){ - $.mobile.activePage.scrollTop( 0 ); - } - }); - } - } - */ + $window.bind( "scrollstop", delayedSetLastScroll ); + + // No-op implementation of transition degradation + $.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) { + return transition; + }; //function for transitioning between two existing pages function transitionPages( toPage, fromPage, transition, reverse ) { - //get current scroll distance - var active = $.mobile.urlHistory.getActive(), - touchOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled, - toScroll = active.lastScroll || ( touchOverflow ? 0 : $.mobile.defaultHomeScroll ), - screenHeight = getScreenHeight(); - - // Scroll to top, hide addr bar - window.scrollTo( 0, $.mobile.defaultHomeScroll ); - - if( fromPage ) { + if ( fromPage ) { //trigger before show/hide events fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } ); } - if( !touchOverflow){ - toPage.height( screenHeight + toScroll ); - } - toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } ); //clear page loader $.mobile.hidePageLoadingMsg(); - if( touchOverflow && toScroll ){ - - toPage.addClass( "ui-mobile-pre-transition" ); - // Send focus to page as it is now display: block - reFocus( toPage ); - - //set page's scrollTop to remembered distance - if( toPage.is( ".ui-native-fixed" ) ){ - toPage.find( ".ui-content" ).scrollTop( toScroll ); - } - else{ - toPage.scrollTop( toScroll ); - } - } + transition = $.mobile._maybeDegradeTransition( transition ); //find the transition handler for the specified transition. If there //isn't one in our transitionHandlers dictionary, use the default one. //call the handler immediately to kick-off the transition. - var th = $.mobile.transitionHandlers[transition || "none"] || $.mobile.defaultTransitionHandler, + var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler, promise = th( transition, reverse, toPage, fromPage ); promise.done(function() { - //reset toPage height back - if( !touchOverflow ){ - toPage.height( "" ); - // Send focus to the newly shown page - reFocus( toPage ); - } - - // Jump to top or prev scroll, sometimes on iOS the page has not rendered yet. - if( !touchOverflow ){ - $.mobile.silentScroll( toScroll ); - } //trigger show/hide events - if( fromPage ) { - if( !touchOverflow ){ - fromPage.height( "" ); - } - + if ( fromPage ) { fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } ); } @@ -2511,33 +3382,21 @@ $.widget( "mobile.page", $.mobile.widget, { } //simply set the active page's minimum height to screen height, depending on orientation - function getScreenHeight(){ - var orientation = $.event.special.orientationchange.orientation(), - port = orientation === "portrait", - winMin = port ? 480 : 320, - screenHeight = port ? screen.availHeight : screen.availWidth, - winHeight = Math.max( winMin, $( window ).height() ), - pageMin = Math.min( screenHeight, winHeight ); - - return pageMin; - } - - $.mobile.getScreenHeight = getScreenHeight; - - //simply set the active page's minimum height to screen height, depending on orientation - function resetActivePageHeight(){ - // Don't apply this height in touch overflow enabled mode - if( $.support.touchOverflow && $.mobile.touchOverflowEnabled ){ - return; - } - $( "." + $.mobile.activePageClass ).css( "min-height", getScreenHeight() ); + function resetActivePageHeight() { + var aPage = $( "." + $.mobile.activePageClass ), + aPagePadT = parseFloat( aPage.css( "padding-top" ) ), + aPagePadB = parseFloat( aPage.css( "padding-bottom" ) ), + aPageBorderT = parseFloat( aPage.css( "border-top-width" ) ), + aPageBorderB = parseFloat( aPage.css( "border-bottom-width" ) ); + + aPage.css( "min-height", getScreenHeight() - aPagePadT - aPagePadB - aPageBorderT - aPageBorderB ); } //shared page enhancements function enhancePage( $page, role ) { // If a role was specified, make sure the data-role attribute // on the page element is in sync. - if( role ) { + if ( role ) { $page.attr( "data-" + $.mobile.ns + "role", role ); } @@ -2545,12 +3404,12 @@ $.widget( "mobile.page", $.mobile.widget, { $page.page(); } -/* exposed $.mobile methods */ + /* exposed $.mobile methods */ //animation complete callback $.fn.animationComplete = function( callback ) { - if( $.support.cssTransitions ) { - return $( this ).one( 'webkitAnimationEnd', callback ); + if ( $.support.cssTransitions ) { + return $( this ).one( 'webkitAnimationEnd animationend', callback ); } else{ // defer execution for consistency between webkit/non webkit @@ -2570,43 +3429,27 @@ $.widget( "mobile.page", $.mobile.widget, { $.mobile.dialogHashKey = dialogHashKey; - //default non-animation transition handler - $.mobile.noneTransitionHandler = function( name, reverse, $toPage, $fromPage ) { - if ( $fromPage ) { - $fromPage.removeClass( $.mobile.activePageClass ); - } - $toPage.addClass( $.mobile.activePageClass ); - - return $.Deferred().resolve( name, reverse, $toPage, $fromPage ).promise(); - }; - - //default handler for unknown transitions - $.mobile.defaultTransitionHandler = $.mobile.noneTransitionHandler; - //transition handler dictionary for 3rd party transitions - $.mobile.transitionHandlers = { - none: $.mobile.defaultTransitionHandler - }; //enable cross-domain page support $.mobile.allowCrossDomainPages = false; //return the original document url - $.mobile.getDocumentUrl = function(asParsedObject) { + $.mobile.getDocumentUrl = function( asParsedObject ) { return asParsedObject ? $.extend( {}, documentUrl ) : documentUrl.href; }; //return the original document base url - $.mobile.getDocumentBase = function(asParsedObject) { + $.mobile.getDocumentBase = function( asParsedObject ) { return asParsedObject ? $.extend( {}, documentBase ) : documentBase.href; }; $.mobile._bindPageRemove = function() { - var page = $(this); + var page = $( this ); // when dom caching is not enabled or the page is embedded bind to remove the page on hide - if( !page.data("page").options.domCache - && page.is(":jqmData(external-page='true')") ) { + if ( !page.data( "page" ).options.domCache && + page.is( ":jqmData(external-page='true')" ) ) { page.bind( 'pagehide.remove', function() { var $this = $( this ), @@ -2614,7 +3457,7 @@ $.widget( "mobile.page", $.mobile.widget, { $this.trigger( prEvent ); - if( !prEvent.isDefaultPrevented() ){ + if ( !prEvent.isDefaultPrevented() ) { $this.removeWithDependents(); } }); @@ -2641,7 +3484,7 @@ $.widget( "mobile.page", $.mobile.widget, { dupCachedPage = null, // determine the current base url - findBaseWithDefault = function(){ + findBaseWithDefault = function() { var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) ); return closestBase || documentBase.hrefNoHash; }, @@ -2659,12 +3502,12 @@ $.widget( "mobile.page", $.mobile.widget, { } // If the caller is using a "post" request, reloadPage must be true - if( settings.data && settings.type === "post" ){ + if ( settings.data && settings.type === "post" ) { settings.reloadPage = true; } - // The absolute version of the URL minus any dialog/subpage params. - // In otherwords the real URL of the page to be loaded. + // The absolute version of the URL minus any dialog/subpage params. + // In otherwords the real URL of the page to be loaded. var fileUrl = path.getFilePath( absUrl ), // The version of the Url actually stored in the data-url attribute of @@ -2678,7 +3521,9 @@ $.widget( "mobile.page", $.mobile.widget, { settings.pageContainer = settings.pageContainer || $.mobile.pageContainer; // Check to see if the page already exists in the DOM. - page = settings.pageContainer.children( ":jqmData(url='" + dataUrl + "')" ); + // NOTE do _not_ use the :jqmData psuedo selector because parenthesis + // are a valid url char and it breaks on the first occurence + page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); // If we failed to find the page, check to see if the url is a // reference to an embedded page. If so, it may have been dynamically @@ -2686,7 +3531,8 @@ $.widget( "mobile.page", $.mobile.widget, { // attribute and in need of enhancement. if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) { page = settings.pageContainer.children( "#" + dataUrl ) - .attr( "data-" + $.mobile.ns + "url", dataUrl ); + .attr( "data-" + $.mobile.ns + "url", dataUrl ) + .jqmData( "url", dataUrl ); } // If we failed to find a page in the DOM, check the URL to see if it @@ -2711,11 +3557,6 @@ $.widget( "mobile.page", $.mobile.widget, { } } - // Reset base to the default document base. - if ( base ) { - base.reset(); - } - // 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 @@ -2737,19 +3578,19 @@ $.widget( "mobile.page", $.mobile.widget, { mpc.trigger( pblEvent, triggerData ); // If the default behavior is prevented, stop here! - if( pblEvent.isDefaultPrevented() ){ + if ( pblEvent.isDefaultPrevented() ) { return deferred.promise(); } if ( settings.showLoadMsg ) { // This configurable timeout allows cached pages a brief delay to load without showing a message - var loadMsgDelay = setTimeout(function(){ + var loadMsgDelay = setTimeout(function() { $.mobile.showPageLoadingMsg(); }, settings.loadMsgDelay ), // Shared logic for clearing timeout and removing message. - hideMsg = function(){ + hideMsg = function() { // Stop message show timer clearTimeout( loadMsgDelay ); @@ -2759,6 +3600,11 @@ $.widget( "mobile.page", $.mobile.widget, { }; } + // Reset base to the default document base. + if ( base ) { + base.reset(); + } + if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) { deferred.reject( absUrl, options ); } else { @@ -2783,11 +3629,11 @@ $.widget( "mobile.page", $.mobile.widget, { // data-url must be provided for the base tag so resource requests can be directed to the // correct url. loading into a temprorary element makes these requests immediately - if( pageElemRegex.test( html ) - && RegExp.$1 - && dataUrlRegex.test( RegExp.$1 ) - && RegExp.$1 ) { - url = fileUrl = path.getFilePath( RegExp.$1 ); + if ( pageElemRegex.test( html ) && + RegExp.$1 && + dataUrlRegex.test( RegExp.$1 ) && + RegExp.$1 ) { + url = fileUrl = path.getFilePath( $( "
" + RegExp.$1 + "
" ).text() ); } if ( base ) { @@ -2799,7 +3645,7 @@ $.widget( "mobile.page", $.mobile.widget, { page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first(); //if page elem couldn't be found, create one and insert the body element's contents - if( !page.length ){ + if ( !page.length ) { page = $( "
" + html.split( /<\/?body[^>]*>/gmi )[1] + "
" ); } @@ -2811,11 +3657,11 @@ $.widget( "mobile.page", $.mobile.widget, { } //rewrite src and href attrs to use a base url - if( !$.support.dynamicBaseTag ) { + if ( !$.support.dynamicBaseTag ) { var newPath = path.get( fileUrl ); page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() { var thisAttr = $( this ).is( '[href]' ) ? 'href' : - $(this).is('[src]') ? 'src' : 'action', + $( this ).is( '[src]' ) ? 'src' : 'action', thisUrl = $( this ).attr( thisAttr ); // XXX_jblas: We need to fix this so that it removes the document @@ -2823,7 +3669,7 @@ $.widget( "mobile.page", $.mobile.widget, { //if full path exists and is same, chop it - helps IE out thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' ); - if( !/^(\w+:|#|\/)/.test( thisUrl ) ) { + if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) { $( this ).attr( thisAttr, newPath + thisUrl ); } }); @@ -2847,7 +3693,7 @@ $.widget( "mobile.page", $.mobile.widget, { // into the DOM. If the original absUrl refers to a sub-page, that is the // real page we are interested in. if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) { - page = settings.pageContainer.children( ":jqmData(url='" + dataUrl + "')" ); + page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); } //bind pageHide to removePage after it's hidden, if the page options specify to do so @@ -2869,7 +3715,7 @@ $.widget( "mobile.page", $.mobile.widget, { }, error: function( xhr, textStatus, errorThrown ) { //set base back to current path - if( base ) { + if ( base ) { base.set( path.get() ); } @@ -2887,7 +3733,7 @@ $.widget( "mobile.page", $.mobile.widget, { // Note that it is the responsibility of the listener/handler // that called preventDefault(), to resolve/reject the // deferred object within the triggerData. - if( plfEvent.isDefaultPrevented() ){ + if ( plfEvent.isDefaultPrevented() ) { return; } @@ -2897,14 +3743,11 @@ $.widget( "mobile.page", $.mobile.widget, { // Remove loading message. hideMsg(); - //show error message - $( "

"+ $.mobile.pageLoadErrorMessage +"

" ) - .css({ "display": "block", "opacity": 0.96, "top": $window.scrollTop() + 100 }) - .appendTo( settings.pageContainer ) - .delay( 800 ) - .fadeOut( 400, function() { - $( this ).remove(); - }); + // show error message + $.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true ); + + // hide after delay + setTimeout( $.mobile.hidePageLoadingMsg, 1500 ); } deferred.reject( absUrl, options ); @@ -2930,7 +3773,7 @@ $.widget( "mobile.page", $.mobile.widget, { // If we are in the midst of a transition, queue the current request. // We'll call changePage() once we're done with the current transition to // service the request. - if( isPageTransitioning ) { + if ( isPageTransitioning ) { pageTransitionQueue.unshift( arguments ); return; } @@ -2951,7 +3794,7 @@ $.widget( "mobile.page", $.mobile.widget, { mpc.trigger( pbcEvent, triggerData ); // If the default behavior is prevented, stop here! - if( pbcEvent.isDefaultPrevented() ){ + if ( pbcEvent.isDefaultPrevented() ) { return; } @@ -2970,7 +3813,7 @@ $.widget( "mobile.page", $.mobile.widget, { // to make sure it is loaded into the DOM. We'll listen // to the promise object it returns so we know when // it is done loading or if an error ocurred. - if ( typeof toPage == "string" ) { + if ( typeof toPage === "string" ) { $.mobile.loadPage( toPage, settings ) .done(function( url, options, newPage, dupCachedPage ) { isPageTransitioning = false; @@ -3021,9 +3864,19 @@ $.widget( "mobile.page", $.mobile.widget, { // It is up to the developer that turns on the allowSamePageTransitiona option // to either turn off transition animations, or make sure that an appropriate // animation transition is used. - if( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) { + if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) { isPageTransitioning = false; mpc.trigger( "pagechange", triggerData ); + + // Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes + if ( settings.fromHashChange ) { + urlHistory.directHashChange({ + currentUrl: url, + isBack: function() {}, + isForward: function() {} + }); + } + return; } @@ -3033,7 +3886,7 @@ $.widget( "mobile.page", $.mobile.widget, { // If the changePage request was sent from a hashChange event, check to see if the // page is already within the urlHistory stack. If so, we'll assume the user hit // the forward/back button and will try to match the transition accordingly. - if( settings.fromHashChange ) { + if ( settings.fromHashChange ) { urlHistory.directHashChange({ currentUrl: url, isBack: function() { historyDir = -1; }, @@ -3043,17 +3896,20 @@ $.widget( "mobile.page", $.mobile.widget, { // Kill the keyboard. // XXX_jblas: We need to stop crawling the entire document to kill focus. Instead, - // we should be tracking focus with a live() handler so we already have + // we should be tracking focus with a delegate() handler so we already have // the element in hand at this point. // Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement // is undefined when we are in an IFrame. try { - if(document.activeElement && document.activeElement.nodeName.toLowerCase() != 'body') { - $(document.activeElement).blur(); + if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) { + $( document.activeElement ).blur(); } else { $( "input:focus, textarea:focus, select:focus" ).blur(); } - } catch(e) {} + } catch( e ) {} + + // Record whether we are at a place in history where a dialog used to be - if so, do not add a new history entry and do not change the hash either + var alreadyThere = false; // If we're displaying the page as a dialog, we don't want the url // for the dialog content to be used in the hash. Instead, we want @@ -3063,11 +3919,30 @@ $.widget( "mobile.page", $.mobile.widget, { // be an empty string. Moving the undefined -> empty string back into // urlHistory.addNew seemed imprudent given undefined better represents // the url state - url = ( active.url || "" ) + dialogHashKey; + + // If we are at a place in history that once belonged to a dialog, reuse + // this state without adding to urlHistory and without modifying the hash. + // However, if a dialog is already displayed at this point, and we're + // about to display another dialog, then we must add another hash and + // history entry on top so that one may navigate back to the original dialog + if ( active.url.indexOf( dialogHashKey ) > -1 && !$.mobile.activePage.is( ".ui-dialog" ) ) { + settings.changeHash = false; + alreadyThere = true; + } + + // Normally, we tack on a dialog hash key, but if this is the location of a stale dialog, + // we reuse the URL from the entry + url = ( active.url || "" ) + ( alreadyThere ? "" : dialogHashKey ); + + // tack on another dialogHashKey if this is the same as the initial hash + // this makes sure that a history entry is created for this dialog + if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { + url += dialogHashKey; + } } // Set the location hash. - if( settings.changeHash !== false && url ) { + if ( settings.changeHash !== false && url ) { //disable hash listening temporarily urlHistory.ignoreNextHashChange = true; //update hash and history @@ -3076,8 +3951,8 @@ $.widget( "mobile.page", $.mobile.widget, { // 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(); - if( !!newPageTitle && pageTitle == document.title ) { + var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText(); + if ( !!newPageTitle && pageTitle === document.title ) { pageTitle = newPageTitle; } if ( !toPage.jqmData( "title" ) ) { @@ -3085,12 +3960,16 @@ $.widget( "mobile.page", $.mobile.widget, { } // Make sure we have a transition defined. - settings.transition = settings.transition - || ( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) - || ( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition ); + settings.transition = settings.transition || + ( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) || + ( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition ); //add page to history stack if it's not back or forward - if( !historyDir ) { + if ( !historyDir ) { + // Overwrite the current entry if it's a leftover from a dialog + if ( alreadyThere ) { + urlHistory.activeIndex = Math.max( 0, urlHistory.activeIndex - 1 ); + } urlHistory.addNew( url, settings.transition, pageTitle, pageUrl, settings.role ); } @@ -3104,7 +3983,7 @@ $.widget( "mobile.page", $.mobile.widget, { settings.reverse = settings.reverse || historyDir < 0; transitionPages( toPage, fromPage, settings.transition, settings.reverse ) - .done(function() { + .done(function( name, reverse, $to, $from, alreadyFocused ) { removeActiveLinkClass(); //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden @@ -3112,8 +3991,13 @@ $.widget( "mobile.page", $.mobile.widget, { settings.duplicateCachedPage.remove(); } - //remove initial build class (only present on first pageshow) - $html.removeClass( "ui-mobile-rendering" ); + // Send focus to the newly shown page. Moved from promise .done binding in transitionPages + // itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility) + // despite visibility: hidden addresses issue #2965 + // https://github.com/jquery/jquery-mobile/issues/2965 + if ( !alreadyFocused ) { + $.mobile.focusPage( toPage ); + } releasePageTransitionLock(); @@ -3147,7 +4031,7 @@ $.widget( "mobile.page", $.mobile.widget, { // an embedded SVG document where some symbol instance elements // don't have nodeName defined on them, or strings are of type // SVGAnimatedString. - if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() == "a" ) { + if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) { break; } ele = ele.parentNode; @@ -3169,18 +4053,22 @@ $.widget( "mobile.page", $.mobile.widget, { return path.makeUrlAbsolute( url, base); } - //The following event bindings should be bound after mobileinit has been triggered - //the following function is called in the init file - $.mobile._registerInternalEvents = function(){ - + //the following deferred is resolved in the init file + $.mobile.navreadyDeferred = $.Deferred(); + $.mobile.navreadyDeferred.done(function() { //bind to form submit events, handle with Ajax - $( "form" ).live('submit', function( event ) { + $( document ).delegate( "form", "submit", function( event ) { var $this = $( this ); - if( !$.mobile.ajaxEnabled || - $this.is( ":jqmData(ajax='false')" ) ) { - return; - } + + if ( !$.mobile.ajaxEnabled || + // test that the form is, itself, ajax false + $this.is( ":jqmData(ajax='false')" ) || + // test that $.mobile.ignoreContentEnabled is set and + // the form or one of it's parents is ajax=false + !$this.jqmHijackable().length ) { + return; + } var type = $this.attr( "method" ), target = $this.attr( "target" ), @@ -3203,10 +4091,9 @@ $.widget( "mobile.page", $.mobile.widget, { } } - url = path.makeUrlAbsolute( url, getClosestBaseUrl($this) ); + url = path.makeUrlAbsolute( url, getClosestBaseUrl( $this ) ); - //external submits use regular HTTP - if( path.isExternal( url ) || target ) { + if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) { return; } @@ -3216,7 +4103,7 @@ $.widget( "mobile.page", $.mobile.widget, { type: type && type.length && type.toLowerCase() || "get", data: $this.serialize(), transition: $this.jqmData( "transition" ), - direction: $this.jqmData( "direction" ), + reverse: $this.jqmData( "direction" ) === "reverse", reloadPage: true } ); @@ -3226,45 +4113,53 @@ $.widget( "mobile.page", $.mobile.widget, { //add active state on vclick $( document ).bind( "vclick", function( event ) { // if this isn't a left click we don't care. Its important to note - // that when the virtual event is generated it will create - if ( event.which > 1 || !$.mobile.linkBindingEnabled ){ + // that when the virtual event is generated it will create the which attr + if ( event.which > 1 || !$.mobile.linkBindingEnabled ) { return; } var link = findClosestLink( event.target ); + + // split from the previous return logic to avoid find closest where possible + // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping + // can be avoided + if ( !$( link ).jqmHijackable().length ) { + return; + } + if ( link ) { if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) { removeActiveLinkClass( true ); $activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" ); $activeClickedLink.addClass( $.mobile.activeBtnClass ); - $( "." + $.mobile.activePageClass + " .ui-btn" ).not( link ).blur(); } } }); // click routing - direct to HTTP or Ajax, accordingly $( document ).bind( "click", function( event ) { - if( !$.mobile.linkBindingEnabled ){ + if ( !$.mobile.linkBindingEnabled ) { return; } - var link = findClosestLink( event.target ); + var link = findClosestLink( event.target ), $link = $( link ), httpCleanup; // If there is no link associated with the click or its not a left // click we want to ignore the click - if ( !link || event.which > 1) { + // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping + // can be avoided + if ( !link || event.which > 1 || !$link.jqmHijackable().length ) { return; } - var $link = $( link ), - //remove active link class if external (then it won't be there if you come back) - httpCleanup = function(){ - window.setTimeout( function() { removeActiveLinkClass( true ); }, 200 ); - }; + //remove active link class if external (then it won't be there if you come back) + httpCleanup = function() { + window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 ); + }; //if there's a data-rel=back attr, go back in history - if( $link.is( ":jqmData(rel='back')" ) ) { - window.history.back(); + if ( $link.is( ":jqmData(rel='back')" ) ) { + $.mobile.back(); return false; } @@ -3274,7 +4169,7 @@ $.widget( "mobile.page", $.mobile.widget, { href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl ); //if ajax is disabled, exit early - if( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ){ + if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) { httpCleanup(); //use default click handling return; @@ -3289,7 +4184,7 @@ $.widget( "mobile.page", $.mobile.widget, { // the current value of the base tag is at the time this code // is called. For now we are just assuming that any url with a // hash in it is an application page reference. - if ( href.search( "#" ) != -1 ) { + if ( href.search( "#" ) !== -1 ) { href = href.replace( /[^#]*#/, "" ); if ( !href ) { //link was an empty hash meant purely @@ -3314,14 +4209,13 @@ $.widget( "mobile.page", $.mobile.widget, { // data. We normally let the browser handle external/cross-domain urls, but if the // allowCrossDomainPages option is true, we will allow cross-domain http/https // requests to go through our page loading logic. - isCrossDomainPageLoad = ( $.mobile.allowCrossDomainPages && documentUrl.protocol === "file:" && href.search( /^https?:/ ) != -1 ), //check for protocol or rel and its not an embedded page //TODO overlap in logic from isExternal, rel=external check should be // moved into more comprehensive isExternalLink - isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !isCrossDomainPageLoad ); + isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) ); - if( isExternal ) { + if ( isExternal ) { httpCleanup(); //use default click handling return; @@ -3329,29 +4223,28 @@ $.widget( "mobile.page", $.mobile.widget, { //use ajax var transition = $link.jqmData( "transition" ), - direction = $link.jqmData( "direction" ), - reverse = ( direction && direction === "reverse" ) || + reverse = $link.jqmData( "direction" ) === "reverse" || // deprecated - remove by 1.0 $link.jqmData( "back" ), //this may need to be more specific as we use data-rel more role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined; - $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role } ); + $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } ); event.preventDefault(); }); //prefetch pages when anchors with data-prefetch are encountered - $( ".ui-page" ).live( "pageshow.prefetch", function() { + $( document ).delegate( ".ui-page", "pageshow.prefetch", function() { var urls = []; - $( this ).find( "a:jqmData(prefetch)" ).each(function(){ - var $link = $(this), + $( this ).find( "a:jqmData(prefetch)" ).each(function() { + var $link = $( this ), url = $link.attr( "href" ); 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" ) } ); } }); }); @@ -3362,6 +4255,9 @@ $.widget( "mobile.page", $.mobile.widget, { //transition is false if it's the first page, undefined otherwise (and may be overridden by default) transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined, + // "navigate" event fired to allow others to take advantage of the more robust hashchange handling + navEvent = new $.Event( "navigate" ), + // default options for the changPage calls made after examining the current state // of the page and the hash changePageOptions = { @@ -3370,23 +4266,34 @@ $.widget( "mobile.page", $.mobile.widget, { fromHashChange: true }; + if ( 0 === urlHistory.stack.length ) { + urlHistory.initialDst = to; + } + + // We should probably fire the "navigate" event from those places that make calls to _handleHashChange, + // and have _handleHashChange hook into the "navigate" event instead of triggering it here + $.mobile.pageContainer.trigger( navEvent ); + if ( navEvent.isDefaultPrevented() ) { + return; + } + //if listening is disabled (either globally or temporarily), or it's a dialog hash - if( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) { + if ( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) { urlHistory.ignoreNextHashChange = false; return; } // special case for dialogs - if( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 ) { + if ( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) { // If current active page is not a dialog skip the dialog and continue // in the same direction - if(!$.mobile.activePage.is( ".ui-dialog" )) { + if ( !$.mobile.activePage.is( ".ui-dialog" ) ) { //determine if we're heading forward or backward and continue accordingly past //the current dialog urlHistory.directHashChange({ currentUrl: to, - isBack: function() { window.history.back(); }, + isBack: function() { $.mobile.back(); }, isForward: function() { window.history.forward(); } }); @@ -3409,7 +4316,7 @@ $.widget( "mobile.page", $.mobile.widget, { // as most of this is lost by the domCache cleaning $.extend( changePageOptions, { role: active.role, - transition: active.transition, + transition: active.transition, reverse: isBack }); } @@ -3425,6 +4332,14 @@ $.widget( "mobile.page", $.mobile.widget, { // since the hashchange could've been the result of a forward/backward navigation // that crosses from an external page/dialog to an internal page/dialog. to = ( typeof to === "string" && !path.isPath( to ) ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to; + + // If we're about to go to an initial URL that contains a reference to a non-existent + // internal page, go to the first page instead. We know that the initial hash refers to a + // non-existent page, because the initial hash did not end up in the initial urlHistory entry + if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) && + urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) { + to = $.mobile.firstPage; + } $.mobile.changePage( to, changePageOptions ); } else { //there's no hash, go to the first page in the dom @@ -3434,28 +4349,33 @@ $.widget( "mobile.page", $.mobile.widget, { //hashchange event handler $window.bind( "hashchange", function( e, triggered ) { - $.mobile._handleHashChange( location.hash ); + // Firefox auto-escapes the location.hash as for v13 but + // leaves the href untouched + $.mobile._handleHashChange( path.parseLocation().hash ); }); //set page min-heights to be device specific $( document ).bind( "pageshow", resetActivePageHeight ); $( window ).bind( "throttledresize", resetActivePageHeight ); - };//_registerInternalEvents callback + });//navreadyDeferred done callback })( jQuery ); -/* -* history.pushState support, layered on top of hashchange -*/ -( function( $, window ) { +(function( $, window ) { // For now, let's Monkeypatch this onto the end of $.mobile._registerInternalEvents // Scope self to pushStateHandler so we can reference it sanely within the // methods handed off as event handlers var pushStateHandler = {}, self = pushStateHandler, $win = $( window ), - url = $.mobile.path.parseUrl( location.href ); + url = $.mobile.path.parseLocation(), + mobileinitDeferred = $.Deferred(), + domreadyDeferred = $.Deferred(); + + $( document ).ready( $.proxy( domreadyDeferred, "resolve" ) ); + + $( document ).one( "mobileinit", $.proxy( mobileinitDeferred, "resolve" ) ); $.extend( pushStateHandler, { // TODO move to a path helper, this is rather common functionality @@ -3463,14 +4383,16 @@ $.widget( "mobile.page", $.mobile.widget, { return url.pathname + url.search; })(), - initialHref: url.hrefNoHash, + hashChangeTimeout: 200, + + hashChangeEnableTimer: undefined, - // Flag for tracking if a Hashchange naturally occurs after each popstate + replace - hashchangeFired: false, + initialHref: url.hrefNoHash, state: function() { return { - hash: location.hash || "#" + self.initialFilePath, + // firefox auto decodes the url when using location.hash but not href + hash: $.mobile.path.parseLocation().hash || "#" + self.initialFilePath, title: document.title, // persist across refresh @@ -3483,9 +4405,9 @@ $.widget( "mobile.page", $.mobile.widget, { subkey = "&" + $.mobile.subPageUrlKey, dialogIndex = url.indexOf( dialog ); - if( dialogIndex > -1 ) { + if ( dialogIndex > -1 ) { url = url.slice( 0, dialogIndex ) + "#" + url.slice( dialogIndex ); - } else if( url.indexOf( subkey ) > -1 ) { + } else if ( url.indexOf( subkey ) > -1 ) { url = url.split( subkey ).join( "#" + subkey ); } @@ -3503,16 +4425,19 @@ $.widget( "mobile.page", $.mobile.widget, { // handling has taken place and set the state of the DOM onHashChange: function( e ) { // disable this hash change - if( self.onHashChangeDisabled ){ + if ( self.onHashChangeDisabled ) { return; } - + var href, state, - hash = location.hash, + // firefox auto decodes the url when using location.hash but not href + hash = $.mobile.path.parseLocation().hash, isPath = $.mobile.path.isPath( hash ), - resolutionUrl = isPath ? location.href : $.mobile.getDocumentUrl(); + resolutionUrl = isPath ? $.mobile.path.getLocation() : $.mobile.getDocumentUrl(); + hash = isPath ? hash.replace( "#", "" ) : hash; + // propulate the hash when its not available state = self.state(); @@ -3527,7 +4452,7 @@ $.widget( "mobile.page", $.mobile.widget, { // Note that in some cases we might be replacing an url with the // same url. We do this anyways because we need to make sure that // all of our history entries have a state object associated with - // them. This allows us to work around the case where window.history.back() + // them. This allows us to work around the case where $.mobile.back() // is called to transition from an external page to an embedded page. // In that particular case, a hashchange event is *NOT* generated by the browser. // Ensuring each history entry has a state object means that onPopState() @@ -3539,23 +4464,29 @@ $.widget( "mobile.page", $.mobile.widget, { // on popstate (ie back or forward) we need to replace the hash that was there previously // cleaned up by the additional hash handling onPopState: function( e ) { - var poppedState = e.originalEvent.state, holdnexthashchange = false; + var poppedState = e.originalEvent.state, + fromHash, toHash, hashChanged; + + // if there's no state its not a popstate we care about, eg chrome's initial popstate + if ( poppedState ) { + // if we get two pop states in under this.hashChangeTimeout + // make sure to clear any timer set for the previous change + clearTimeout( self.hashChangeEnableTimer ); + + // make sure to enable hash handling for the the _handleHashChange call + self.nextHashChangePrevented( false ); - // if there's no state its not a popstate we care about, ie chrome's initial popstate - // or forward popstate - if( poppedState ) { - // disable any hashchange triggered by the browser + // change the page based on the hash in the popped state + $.mobile._handleHashChange( poppedState.hash ); + + // prevent any hashchange in the next self.hashChangeTimeout self.nextHashChangePrevented( true ); - // defer our manual hashchange until after the browser fired - // version has come and gone - setTimeout(function() { - // make sure that the manual hash handling takes place + // re-enable hash change handling after swallowing a possible hash + // change event that comes on all popstates courtesy of browsers like Android + self.hashChangeEnableTimer = setTimeout( function() { self.nextHashChangePrevented( false ); - - // change the page based on the hash - $.mobile._handleHashChange( poppedState.hash ); - }, 100); + }, self.hashChangeTimeout ); } }, @@ -3567,66 +4498,97 @@ $.widget( "mobile.page", $.mobile.widget, { // if there's no hash, we need to replacestate for returning to home if ( location.hash === "" ) { - history.replaceState( self.state(), document.title, location.href ); + history.replaceState( self.state(), document.title, $.mobile.path.getLocation() ); } } }); - $( function() { - if( $.mobile.pushStateEnabled && $.support.pushState ){ - pushStateHandler.init(); - } - }); + // We need to init when "mobileinit", "domready", and "navready" have all happened + $.when( domreadyDeferred, mobileinitDeferred, $.mobile.navreadyDeferred ).done(function() { + if ( $.mobile.pushStateEnabled && $.support.pushState ) { + pushStateHandler.init(); + } + }); +})( jQuery, this ); + +/* +* fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ + +(function( $, window, undefined ) { + +$.mobile.transitionFallbacks.flip = "fade"; + +})( jQuery, this ); +/* +* fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ + +(function( $, window, undefined ) { + +$.mobile.transitionFallbacks.flow = "fade"; + +})( jQuery, this ); +/* +* fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ + +(function( $, window, undefined ) { + +$.mobile.transitionFallbacks.pop = "fade"; + })( jQuery, this ); /* -* "transitions" plugin - Page change tranistions +* fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general */ (function( $, window, undefined ) { -function css3TransitionHandler( name, reverse, $to, $from ) { - - var deferred = new $.Deferred(), - reverseClass = reverse ? " reverse" : "", - viewportClass = "ui-mobile-viewport-transitioning viewport-" + name, - doneFunc = function() { +// Use the simultaneous transitions handler for slide transitions +$.mobile.transitionHandlers.slide = $.mobile.transitionHandlers.simultaneous; - $to.add( $from ).removeClass( "out in reverse " + name ); +// Set the slide transitions's fallback to "fade" +$.mobile.transitionFallbacks.slide = "fade"; - if ( $from && $from[ 0 ] !== $to[ 0 ] ) { - $from.removeClass( $.mobile.activePageClass ); - } +})( jQuery, this ); +/* +* fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ - $to.parent().removeClass( viewportClass ); +(function( $, window, undefined ) { - deferred.resolve( name, reverse, $to, $from ); - }; +$.mobile.transitionFallbacks.slidedown = "fade"; - $to.animationComplete( doneFunc ); +})( jQuery, this ); +/* +* fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ - $to.parent().addClass( viewportClass ); +(function( $, window, undefined ) { - if ( $from ) { - $from.addClass( name + " out" + reverseClass ); - } - $to.addClass( $.mobile.activePageClass + " " + name + " in" + reverseClass ); +// Set the slide transitions's fallback to "fade" +$.mobile.transitionFallbacks.slidefade = "fade"; - return deferred.promise(); -} +})( jQuery, this ); +/* +* fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general +*/ -// Make our transition handler public. -$.mobile.css3TransitionHandler = css3TransitionHandler; +(function( $, window, undefined ) { -// If the default transition handler is the 'none' handler, replace it with our handler. -if ( $.mobile.defaultTransitionHandler === $.mobile.noneTransitionHandler ) { - $.mobile.defaultTransitionHandler = css3TransitionHandler; -} +$.mobile.transitionFallbacks.slideup = "fade"; })( jQuery, this ); /* -* "degradeInputs" plugin - degrades inputs to another type after custom enhancements are made. +* fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general */ +(function( $, window, undefined ) { + +$.mobile.transitionFallbacks.turn = "fade"; + +})( jQuery, this ); + (function( $, undefined ) { $.mobile.page.prototype.options.degradeInputs = { @@ -3647,11 +4609,11 @@ $.mobile.page.prototype.options.degradeInputs = { //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ +$( document ).bind( "pagecreate create", function( e ) { - var page = $(e.target).closest(':jqmData(role="page")').data("page"), options; + var page = $.mobile.closestPageData( $( e.target ) ), options; - if( !page ) { + if ( !page ) { return; } @@ -3676,48 +4638,54 @@ $( document ).bind( "pagecreate create", function( e ){ }); -})( jQuery );/* -* "dialog" plugin. -*/ +})( jQuery ); (function( $, window, undefined ) { $.widget( "mobile.dialog", $.mobile.widget, { options: { - closeBtnText : "Close", - overlayTheme : "a", - initSelector : ":jqmData(role='dialog')" + closeBtnText: "Close", + overlayTheme: "a", + initSelector: ":jqmData(role='dialog')" }, _create: function() { var self = this, $el = this.element, - headerCloseButton = $( "
"+ this.options.closeBtnText + "" ); + headerCloseButton = $( ""+ this.options.closeBtnText + "" ), + dialogWrap = $( "
", { + "role" : "dialog", + "class" : "ui-dialog-contain ui-corner-all ui-overlay-shadow" + }); - $el.addClass( "ui-overlay-" + this.options.overlayTheme ); + $el.addClass( "ui-dialog ui-overlay-" + this.options.overlayTheme ); // Class the markup for dialog styling // Set aria role - $el.attr( "role", "dialog" ) - .addClass( "ui-dialog" ) - .find( ":jqmData(role='header')" ) - .addClass( "ui-corner-top ui-overlay-shadow" ) - .prepend( headerCloseButton ) - .end() - .find( ":jqmData(role='content'),:jqmData(role='footer')" ) - .addClass( "ui-overlay-shadow" ) - .last() - .addClass( "ui-corner-bottom" ); + $el + .wrapInner( dialogWrap ) + .children() + .find( ":jqmData(role='header')" ) + .prepend( headerCloseButton ) + .end() + .children( ':first-child') + .addClass( "ui-corner-top" ) + .end() + .children( ":last-child" ) + .addClass( "ui-corner-bottom" ); // this must be an anonymous function so that select menu dialogs can replace // the close method. This is a change from previously just defining data-rel=back // on the button and letting nav handle it - headerCloseButton.bind( "vclick", function() { + // + // Use click rather than vclick in order to prevent the possibility of unintentionally + // reopening the dialog if the dialog opening item was directly under the close button. + headerCloseButton.bind( "click", function() { self.close(); }); /* bind events - clicks and submits should use the closing transition that the dialog opened with - unless a data-transition is specified on the link/form + unless a data-transition is specified on the link/form - if the click was on the close button, or the link has a data-rel="back" it'll go back in history naturally */ $el.bind( "vclick submit", function( event ) { @@ -3732,112 +4700,462 @@ $.widget( "mobile.dialog", $.mobile.widget, { .attr( "data-" + $.mobile.ns + "direction", "reverse" ); } }) - .bind( "pagehide", function() { - $( this ).find( "." + $.mobile.activeBtnClass ).removeClass( $.mobile.activeBtnClass ); + .bind( "pagehide", function( e, ui ) { + $( this ).find( "." + $.mobile.activeBtnClass ).not( ".ui-slider-bg" ).removeClass( $.mobile.activeBtnClass ); + }) + // Override the theme set by the page plugin on pageshow + .bind( "pagebeforeshow", function() { + self._isCloseable = true; + if ( self.options.overlayTheme ) { + self.element + .page( "removeContainerBackground" ) + .page( "setContainerBackground", self.options.overlayTheme ); + } }); }, // Close method goes back in history close: function() { - window.history.back(); + var dst; + + if ( this._isCloseable ) { + this._isCloseable = false; + if ( $.mobile.hashListeningEnabled ) { + $.mobile.back(); + } else { + dst = $.mobile.urlHistory.getPrev().url; + if ( !$.mobile.path.isPath( dst ) ) { + dst = $.mobile.path.makeUrlAbsolute( "#" + dst ); + } + + $.mobile.changePage( dst, { changeHash: false, fromHashChange: true } ); + } + } } }); -//auto self-init widgets -$( $.mobile.dialog.prototype.options.initSelector ).live( "pagecreate", function(){ - $( this ).dialog(); -}); +//auto self-init widgets +$( document ).delegate( $.mobile.dialog.prototype.options.initSelector, "pagecreate", function() { + $.mobile.dialog.prototype.enhance( this ); +}); + +})( jQuery, this ); + +(function( $, undefined ) { + +$.mobile.page.prototype.options.backBtnText = "Back"; +$.mobile.page.prototype.options.addBackBtn = false; +$.mobile.page.prototype.options.backBtnTheme = null; +$.mobile.page.prototype.options.headerTheme = "a"; +$.mobile.page.prototype.options.footerTheme = "a"; +$.mobile.page.prototype.options.contentTheme = null; + +// NOTE bind used to force this binding to run before the buttonMarkup binding +// which expects .ui-footer top be applied in its gigantic selector +// TODO remove the buttonMarkup giant selector and move it to the various modules +// on which it depends +$( document ).bind( "pagecreate", function( e ) { + var $page = $( e.target ), + o = $page.data( "page" ).options, + pageRole = $page.jqmData( "role" ), + pageTheme = o.theme; + + $( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", $page ) + .jqmEnhanceable() + .each(function() { + + var $this = $( this ), + role = $this.jqmData( "role" ), + theme = $this.jqmData( "theme" ), + contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ), + $headeranchors, + leftbtn, + rightbtn, + backBtn; + + $this.addClass( "ui-" + role ); + + //apply theming and markup modifications to page,header,content,footer + if ( role === "header" || role === "footer" ) { + + var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme; + + $this + //add theme class + .addClass( "ui-bar-" + thisTheme ) + // Add ARIA role + .attr( "role", role === "header" ? "banner" : "contentinfo" ); + + if ( role === "header") { + // Right,left buttons + $headeranchors = $this.children( "a, button" ); + leftbtn = $headeranchors.hasClass( "ui-btn-left" ); + rightbtn = $headeranchors.hasClass( "ui-btn-right" ); + + leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length; + + rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length; + } + + // Auto-add back btn on pages beyond first view + if ( o.addBackBtn && + role === "header" && + $( ".ui-page" ).length > 1 && + $page.jqmData( "url" ) !== $.mobile.path.stripHash( location.hash ) && + !leftbtn ) { + + backBtn = $( ""+ o.backBtnText +"" ) + // If theme is provided, override default inheritance + .attr( "data-"+ $.mobile.ns +"theme", o.backBtnTheme || thisTheme ) + .prependTo( $this ); + } + + // Page title + $this.children( "h1, h2, h3, h4, h5, h6" ) + .addClass( "ui-title" ) + // Regardless of h element number in src, it becomes h1 for the enhanced page + .attr({ + "role": "heading", + "aria-level": "1" + }); + + } else if ( role === "content" ) { + if ( contentTheme ) { + $this.addClass( "ui-body-" + ( contentTheme ) ); + } + + // Add ARIA role + $this.attr( "role", "main" ); + } + }); +}); + +})( 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 ) { + +$.fn.buttonMarkup = function( options ) { + var $workingSet = this, + mapToDataAttr = function( key, value ) { + e.setAttribute( "data-" + $.mobile.ns + key, value ); + el.jqmData( key, value ); + }; + + // Enforce options to be of type string + options = ( options && ( $.type( options ) === "object" ) )? options : {}; + for ( var i = 0; i < $workingSet.length; i++ ) { + var el = $workingSet.eq( i ), + e = el[ 0 ], + o = $.extend( {}, $.fn.buttonMarkup.defaults, { + icon: options.icon !== undefined ? options.icon : el.jqmData( "icon" ), + iconpos: options.iconpos !== undefined ? options.iconpos : el.jqmData( "iconpos" ), + theme: options.theme !== undefined ? options.theme : el.jqmData( "theme" ) || $.mobile.getInheritedTheme( el, "c" ), + inline: options.inline !== undefined ? options.inline : el.jqmData( "inline" ), + shadow: options.shadow !== undefined ? options.shadow : el.jqmData( "shadow" ), + corners: options.corners !== undefined ? options.corners : el.jqmData( "corners" ), + iconshadow: options.iconshadow !== undefined ? options.iconshadow : el.jqmData( "iconshadow" ), + mini: options.mini !== undefined ? options.mini : el.jqmData( "mini" ) + }, options ), + + // Classes Defined + innerClass = "ui-btn-inner", + textClass = "ui-btn-text", + buttonClass, iconClass, + // Button inner markup + buttonInner, + buttonText, + buttonIcon, + buttonElements; + + $.each( o, mapToDataAttr ); + + if ( el.jqmData( "rel" ) === "popup" && el.attr( "href" ) ) { + e.setAttribute( "aria-haspopup", true ); + e.setAttribute( "aria-owns", e.getAttribute( "href" ) ); + } + + // Check if this element is already enhanced + buttonElements = $.data( ( ( e.tagName === "INPUT" || e.tagName === "BUTTON" ) ? e.parentNode : e ), "buttonElements" ); + + if ( buttonElements ) { + e = buttonElements.outer; + el = $( e ); + buttonInner = buttonElements.inner; + buttonText = buttonElements.text; + // We will recreate this icon below + $( buttonElements.icon ).remove(); + buttonElements.icon = null; + } + else { + buttonInner = document.createElement( o.wrapperEls ); + buttonText = document.createElement( o.wrapperEls ); + } + buttonIcon = o.icon ? document.createElement( "span" ) : null; + + if ( attachEvents && !buttonElements ) { + attachEvents(); + } + + // if not, try to find closest theme container + if ( !o.theme ) { + o.theme = $.mobile.getInheritedTheme( el, "c" ); + } + + buttonClass = "ui-btn ui-btn-up-" + o.theme; + buttonClass += o.shadow ? " ui-shadow" : ""; + buttonClass += o.corners ? " ui-btn-corner-all" : ""; + + if ( o.mini !== undefined ) { + // Used to control styling in headers/footers, where buttons default to `mini` style. + buttonClass += o.mini === true ? " ui-mini" : " ui-fullsize"; + } + + if ( o.inline !== undefined ) { + // Used to control styling in headers/footers, where buttons default to `inline` style. + buttonClass += o.inline === true ? " ui-btn-inline" : " ui-btn-block"; + } + + if ( o.icon ) { + o.icon = "ui-icon-" + o.icon; + o.iconpos = o.iconpos || "left"; + + iconClass = "ui-icon " + o.icon; + + if ( o.iconshadow ) { + iconClass += " ui-icon-shadow"; + } + } + + if ( o.iconpos ) { + buttonClass += " ui-btn-icon-" + o.iconpos; + + if ( o.iconpos === "notext" && !el.attr( "title" ) ) { + el.attr( "title", el.getEncodedText() ); + } + } + + innerClass += o.corners ? " ui-btn-corner-all" : ""; + + if ( o.iconpos && o.iconpos === "notext" && !el.attr( "title" ) ) { + el.attr( "title", el.getEncodedText() ); + } + + if ( buttonElements ) { + el.removeClass( buttonElements.bcls || "" ); + } + el.removeClass( "ui-link" ).addClass( buttonClass ); + + buttonInner.className = innerClass; + + buttonText.className = textClass; + if ( !buttonElements ) { + buttonInner.appendChild( buttonText ); + } + if ( buttonIcon ) { + buttonIcon.className = iconClass; + if ( !( buttonElements && buttonElements.icon ) ) { + buttonIcon.innerHTML = " "; + buttonInner.appendChild( buttonIcon ); + } + } + + while ( e.firstChild && !buttonElements ) { + buttonText.appendChild( e.firstChild ); + } + + if ( !buttonElements ) { + e.appendChild( buttonInner ); + } + + // Assign a structure containing the elements of this button to the elements of this button. This + // will allow us to recognize this as an already-enhanced button in future calls to buttonMarkup(). + buttonElements = { + bcls : buttonClass, + outer : e, + inner : buttonInner, + text : buttonText, + icon : buttonIcon + }; -})( jQuery, this ); -/* -* This plugin handles theming and layout of headers, footers, and content areas -*/ + $.data( e, 'buttonElements', buttonElements ); + $.data( buttonInner, 'buttonElements', buttonElements ); + $.data( buttonText, 'buttonElements', buttonElements ); + if ( buttonIcon ) { + $.data( buttonIcon, 'buttonElements', buttonElements ); + } + } -(function( $, undefined ) { + return this; +}; -$.mobile.page.prototype.options.backBtnText = "Back"; -$.mobile.page.prototype.options.addBackBtn = false; -$.mobile.page.prototype.options.backBtnTheme = null; -$.mobile.page.prototype.options.headerTheme = "a"; -$.mobile.page.prototype.options.footerTheme = "a"; -$.mobile.page.prototype.options.contentTheme = null; +$.fn.buttonMarkup.defaults = { + corners: true, + shadow: true, + iconshadow: true, + wrapperEls: "span" +}; -$( ":jqmData(role='page'), :jqmData(role='dialog')" ).live( "pagecreate", function( e ) { - - var $page = $( this ), - o = $page.data( "page" ).options, - pageRole = $page.jqmData( "role" ), - pageTheme = o.theme; - - $( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", this ).each(function() { - var $this = $( this ), - role = $this.jqmData( "role" ), - theme = $this.jqmData( "theme" ), - contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ), - $headeranchors, - leftbtn, - rightbtn, - backBtn; - - $this.addClass( "ui-" + role ); +function closestEnabledButton( element ) { + var cname; - //apply theming and markup modifications to page,header,content,footer - if ( role === "header" || role === "footer" ) { - - var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme; + while ( element ) { + // Note that we check for typeof className below because the element we + // handed could be in an SVG DOM where className on SVG elements is defined to + // be of a different type (SVGAnimatedString). We only operate on HTML DOM + // elements, so we look for plain "string". + cname = ( typeof element.className === 'string' ) && ( element.className + ' ' ); + if ( cname && cname.indexOf( "ui-btn " ) > -1 && cname.indexOf( "ui-disabled " ) < 0 ) { + break; + } - $this - //add theme class - .addClass( "ui-bar-" + thisTheme ) - // Add ARIA role - .attr( "role", role === "header" ? "banner" : "contentinfo" ); + element = element.parentNode; + } - // Right,left buttons - $headeranchors = $this.children( "a" ); - leftbtn = $headeranchors.hasClass( "ui-btn-left" ); - rightbtn = $headeranchors.hasClass( "ui-btn-right" ); + return element; +} - leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length; - - rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length; - - // Auto-add back btn on pages beyond first view - if ( o.addBackBtn && - role === "header" && - $( ".ui-page" ).length > 1 && - $this.jqmData( "url" ) !== $.mobile.path.stripHash( location.hash ) && - !leftbtn ) { +var attachEvents = function() { + var hoverDelay = $.mobile.buttonMarkup.hoverDelay, hov, foc; - backBtn = $( ""+ o.backBtnText +"" ) - // If theme is provided, override default inheritance - .attr( "data-"+ $.mobile.ns +"theme", o.backBtnTheme || thisTheme ) - .prependTo( $this ); - } + $( document ).bind( { + "vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart": function( event ) { + var theme, + $btn = $( closestEnabledButton( event.target ) ), + isTouchEvent = event.originalEvent && /^touch/.test( event.originalEvent.type ), + evt = event.type; - // Page title - $this.children( "h1, h2, h3, h4, h5, h6" ) - .addClass( "ui-title" ) - // Regardless of h element number in src, it becomes h1 for the enhanced page - .attr({ - "tabindex": "0", - "role": "heading", - "aria-level": "1" - }); + if ( $btn.length ) { + theme = $btn.attr( "data-" + $.mobile.ns + "theme" ); - } else if ( role === "content" ) { - if ( contentTheme ) { - $this.addClass( "ui-body-" + ( contentTheme ) ); + if ( evt === "vmousedown" ) { + if ( isTouchEvent ) { + // Use a short delay to determine if the user is scrolling before highlighting + hov = setTimeout( function() { + $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); + }, hoverDelay ); + } else { + $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); + } + } else if ( evt === "vmousecancel" || evt === "vmouseup" ) { + $btn.removeClass( "ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); + } else if ( evt === "vmouseover" || evt === "focus" ) { + if ( isTouchEvent ) { + // Use a short delay to determine if the user is scrolling before highlighting + foc = setTimeout( function() { + $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); + }, hoverDelay ); + } else { + $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); + } + } else if ( evt === "vmouseout" || evt === "blur" || evt === "scrollstart" ) { + $btn.removeClass( "ui-btn-hover-" + theme + " ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); + if ( hov ) { + clearTimeout( hov ); + } + if ( foc ) { + clearTimeout( foc ); + } + } } - - // Add ARIA role - $this.attr( "role", "main" ); + }, + "focusin focus": function( event ) { + $( closestEnabledButton( event.target ) ).addClass( $.mobile.focusClass ); + }, + "focusout blur": function( event ) { + $( closestEnabledButton( event.target ) ).removeClass( $.mobile.focusClass ); } }); + + attachEvents = null; +}; + +//links in bars, or those with data-role become buttons +//auto self-init widgets +$( document ).bind( "pagecreate create", function( e ) { + + $( ":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a", e.target ) + .jqmEnhanceable() + .not( "button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')" ) + .buttonMarkup(); }); -})( jQuery );/* -* "collapsible" plugin -*/ +})( jQuery ); + (function( $, undefined ) { @@ -3849,7 +5167,8 @@ $.widget( "mobile.collapsible", $.mobile.widget, { heading: "h1,h2,h3,h4,h5,h6,legend", theme: null, contentTheme: null, - iconTheme: "d", + inset: true, + mini: false, initSelector: ":jqmData(role='collapsible')" }, _create: function() { @@ -3858,9 +5177,10 @@ $.widget( "mobile.collapsible", $.mobile.widget, { o = this.options, collapsible = $el.addClass( "ui-collapsible" ), collapsibleHeading = $el.children( o.heading ).first(), - collapsibleContent = collapsible.wrapInner( "
" ).find( ".ui-collapsible-content" ), - collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ), - collapsiblesInSet = collapsibleSet.children( ":jqmData(role='collapsible')" ); + collapsedIcon = $el.jqmData( "collapsed-icon" ) || o.collapsedIcon, + expandedIcon = $el.jqmData( "expanded-icon" ) || o.expandedIcon, + collapsibleContent = collapsible.wrapInner( "
" ).children( ".ui-collapsible-content" ), + collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ); // Replace collapsibleHeading if it's a legend if ( collapsibleHeading.is( "legend" ) ) { @@ -3872,16 +5192,51 @@ $.widget( "mobile.collapsible", $.mobile.widget, { if ( collapsibleSet.length ) { // Inherit the theme from collapsible-set if ( !o.theme ) { - o.theme = collapsibleSet.jqmData( "theme" ); + o.theme = collapsibleSet.jqmData( "theme" ) || $.mobile.getInheritedTheme( collapsibleSet, "c" ); } // Inherit the content-theme from collapsible-set if ( !o.contentTheme ) { o.contentTheme = collapsibleSet.jqmData( "content-theme" ); } - } + // Get the preference for collapsed icon in the set + if ( !o.collapsedIcon ) { + o.collapsedIcon = collapsibleSet.jqmData( "collapsed-icon" ); + } + // Get the preference for expanded icon in the set + if ( !o.expandedIcon ) { + o.expandedIcon = collapsibleSet.jqmData( "expanded-icon" ); + } + // Gets the preference icon position in the set + if ( !o.iconPos ) { + o.iconPos = collapsibleSet.jqmData( "iconpos" ); + } + // Inherit the preference for inset from collapsible-set or set the default value to ensure equalty within a set + if ( collapsibleSet.jqmData( "inset" ) !== undefined ) { + o.inset = collapsibleSet.jqmData( "inset" ); + } else { + o.inset = true; + } + // Gets the preference for mini in the set + if ( !o.mini ) { + o.mini = collapsibleSet.jqmData( "mini" ); + } + } else { + // get inherited theme if not a set and no theme has been set + if ( !o.theme ) { + o.theme = $.mobile.getInheritedTheme( $el, "c" ); + } + } + + if ( !!o.inset ) { + collapsible.addClass( "ui-collapsible-inset" ); + } + collapsibleContent.addClass( ( o.contentTheme ) ? ( "ui-body-" + o.contentTheme ) : ""); + collapsedIcon = $el.jqmData( "collapsed-icon" ) || o.collapsedIcon || "plus"; + expandedIcon = $el.jqmData( "expanded-icon" ) || o.expandedIcon || "minus"; + collapsibleHeading //drop heading in before content .insertBefore( collapsibleContent ) @@ -3894,80 +5249,44 @@ $.widget( "mobile.collapsible", $.mobile.widget, { .buttonMarkup({ shadow: false, corners: false, - iconPos: "left", - icon: "plus", + iconpos: $el.jqmData( "iconpos" ) || o.iconPos || "left", + icon: collapsedIcon, + mini: o.mini, theme: o.theme }); - if ( !collapsibleSet.length ) { + if ( !!o.inset ) { collapsibleHeading - .find( "a" ).first().add( collapsibleHeading.find( ".ui-btn-inner" ) ) + .find( "a" ).first().add( ".ui-btn-inner", $el ) .addClass( "ui-corner-top ui-corner-bottom" ); - } else { - // If we are in a collapsible set - - // Initialize the collapsible set if it's not already initialized - if ( !collapsibleSet.jqmData( "collapsiblebound" ) ) { - - collapsibleSet - .jqmData( "collapsiblebound", true ) - .bind( "expand", function( event ) { - - $( event.target ) - .closest( ".ui-collapsible" ) - .siblings( ".ui-collapsible" ) - .trigger( "collapse" ); - - }); - } - - collapsiblesInSet.first() - .find( "a" ) - .first() - .addClass( "ui-corner-top" ) - .find( ".ui-btn-inner" ) - .addClass( "ui-corner-top" ); - - collapsiblesInSet.last() - .jqmData( "collapsible-last", true ) - .find( "a" ) - .first() - .addClass( "ui-corner-bottom" ) - .find( ".ui-btn-inner" ) - .addClass( "ui-corner-bottom" ); - - - if ( collapsible.jqmData( "collapsible-last" ) ) { - collapsibleHeading - .find( "a" ).first().add ( collapsibleHeading.find( ".ui-btn-inner" ) ) - .addClass( "ui-corner-bottom" ); - } } //events collapsible .bind( "expand collapse", function( event ) { if ( !event.isDefaultPrevented() ) { - - event.preventDefault(); - var $this = $( this ), isCollapse = ( event.type === "collapse" ), - contentTheme = o.contentTheme; + contentTheme = o.contentTheme; + + event.preventDefault(); collapsibleHeading - .toggleClass( "ui-collapsible-heading-collapsed", isCollapse) + .toggleClass( "ui-collapsible-heading-collapsed", isCollapse ) .find( ".ui-collapsible-heading-status" ) .text( isCollapse ? o.expandCueText : o.collapseCueText ) .end() .find( ".ui-icon" ) - .toggleClass( "ui-icon-minus", !isCollapse ) - .toggleClass( "ui-icon-plus", isCollapse ); + .toggleClass( "ui-icon-" + expandedIcon, !isCollapse ) + // logic or cause same icon for expanded/collapsed state would remove the ui-icon-class + .toggleClass( "ui-icon-" + collapsedIcon, ( isCollapse || expandedIcon === collapsedIcon ) ) + .end() + .find( "a" ).first().removeClass( $.mobile.activeBtnClass ); $this.toggleClass( "ui-collapsible-collapsed", isCollapse ); collapsibleContent.toggleClass( "ui-collapsible-content-collapsed", isCollapse ).attr( "aria-hidden", isCollapse ); - if ( contentTheme && ( !collapsibleSet.length || collapsible.jqmData( "collapsible-last" ) ) ) { + if ( contentTheme && !!o.inset && ( !collapsibleSet.length || collapsible.jqmData( "collapsible-last" ) ) ) { collapsibleHeading .find( "a" ).first().add( collapsibleHeading.find( ".ui-btn-inner" ) ) .toggleClass( "ui-corner-bottom", isCollapse ); @@ -3979,91 +5298,134 @@ $.widget( "mobile.collapsible", $.mobile.widget, { .trigger( o.collapsed ? "collapse" : "expand" ); collapsibleHeading + .bind( "tap", function( event ) { + collapsibleHeading.find( "a" ).first().addClass( $.mobile.activeBtnClass ); + }) .bind( "click", function( event ) { - var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? - "expand" : "collapse"; + var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? "expand" : "collapse"; collapsible.trigger( type ); event.preventDefault(); + event.stopPropagation(); }); } }); //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $( $.mobile.collapsible.prototype.options.initSelector, e.target ).collapsible(); +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.collapsible.prototype.enhanceWithin( e.target ); }); })( jQuery ); -/* -* "fieldcontain" plugin - simple class additions to make form row separators -*/ (function( $, undefined ) { -$.fn.fieldcontain = function( options ) { - return this.addClass( "ui-field-contain ui-body ui-br" ); -}; - -//auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $( ":jqmData(role='fieldcontain')", e.target ).fieldcontain(); -}); - -})( jQuery );/* -* plugin for creating CSS grids -*/ +$.widget( "mobile.collapsibleset", $.mobile.widget, { + options: { + initSelector: ":jqmData(role='collapsible-set')" + }, + _create: function() { + var $el = this.element.addClass( "ui-collapsible-set" ), + o = this.options; -(function( $, undefined ) { + // Inherit the theme from collapsible-set + if ( !o.theme ) { + o.theme = $.mobile.getInheritedTheme( $el, "c" ); + } + // Inherit the content-theme from collapsible-set + if ( !o.contentTheme ) { + o.contentTheme = $el.jqmData( "content-theme" ); + } + + if ( $el.jqmData( "inset" ) !== undefined ) { + o.inset = $el.jqmData( "inset" ); + } + o.inset = o.inset !== undefined ? o.inset : true; + + // Initialize the collapsible set if it's not already initialized + if ( !$el.jqmData( "collapsiblebound" ) ) { + $el + .jqmData( "collapsiblebound", true ) + .bind( "expand collapse", function( event ) { + var isCollapse = ( event.type === "collapse" ), + collapsible = $( event.target ).closest( ".ui-collapsible" ), + widget = collapsible.data( "collapsible" ); + if ( collapsible.jqmData( "collapsible-last" ) && !!o.inset ) { + collapsible.find( ".ui-collapsible-heading" ).first() + .find( "a" ).first() + .toggleClass( "ui-corner-bottom", isCollapse ) + .find( ".ui-btn-inner" ) + .toggleClass( "ui-corner-bottom", isCollapse ); + collapsible.find( ".ui-collapsible-content" ).toggleClass( "ui-corner-bottom", !isCollapse ); + } + }) + .bind( "expand", function( event ) { + var closestCollapsible = $( event.target ) + .closest( ".ui-collapsible" ); + if ( closestCollapsible.parent().is( ":jqmData(role='collapsible-set')" ) ) { + closestCollapsible + .siblings( ".ui-collapsible" ) + .trigger( "collapse" ); + } + }); + } + }, -$.fn.grid = function( options ) { - return this.each(function() { + _init: function() { + var $el = this.element, + collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ), + expanded = collapsiblesInSet.filter( ":jqmData(collapsed='false')" ); + this.refresh(); - 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; + // Because the corners are handled by the collapsible itself and the default state is collapsed + // That was causing https://github.com/jquery/jquery-mobile/issues/4116 + expanded.trigger( "expand" ); + }, - if ( !grid ) { - if ( $kids.length <= 5 ) { - for ( var letter in gridCols ) { - if ( gridCols[ letter ] === $kids.length ) { - grid = letter; - } - } - } else { - grid = "a"; - } - } - iterator = gridCols[grid]; + refresh: function() { + var $el = this.element, + o = this.options, + collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ); + + $.mobile.collapsible.prototype.enhance( collapsiblesInSet.not( ".ui-collapsible" ) ); + + // clean up borders + if ( !!o.inset ) { + collapsiblesInSet.each(function() { + $( this ).jqmRemoveData( "collapsible-last" ) + .find( ".ui-collapsible-heading" ) + .find( "a" ).first() + .removeClass( "ui-corner-top ui-corner-bottom" ) + .find( ".ui-btn-inner" ) + .removeClass( "ui-corner-top ui-corner-bottom" ); + }); - $this.addClass( "ui-grid-" + grid ); + collapsiblesInSet.first() + .find( "a" ) + .first() + .addClass( "ui-corner-top" ) + .find( ".ui-btn-inner" ) + .addClass( "ui-corner-top" ); + + collapsiblesInSet.last() + .jqmData( "collapsible-last", true ) + .find( "a" ) + .first() + .addClass( "ui-corner-bottom" ) + .find( ".ui-btn-inner" ) + .addClass( "ui-corner-bottom" ); + } + } +}); - $kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" ); +//auto self-init widgets +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.collapsibleset.prototype.enhanceWithin( e.target ); +}); - if ( iterator > 1 ) { - $kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" ); - } - if ( iterator > 2 ) { - $kids.filter( ":nth-child(3n+3)" ).addClass( "ui-block-c" ); - } - if ( iterator > 3 ) { - $kids.filter( ":nth-child(4n+4)" ).addClass( "ui-block-d" ); - } - if ( iterator > 4 ) { - $kids.filter( ":nth-child(5n+5)" ).addClass( "ui-block-e" ); - } - }); -}; -})( jQuery );/* -* "navbar" plugin -*/ +})( jQuery ); (function( $, undefined ) { @@ -4074,44 +5436,46 @@ $.widget( "mobile.navbar", $.mobile.widget, { initSelector: ":jqmData(role='navbar')" }, - _create: function(){ + _create: function() { var $navbar = this.element, $navbtns = $navbar.find( "a" ), iconpos = $navbtns.filter( ":jqmData(icon)" ).length ? this.options.iconpos : undefined; - $navbar.addClass( "ui-navbar" ) - .attr( "role","navigation" ) + $navbar.addClass( "ui-navbar ui-mini" ) + .attr( "role", "navigation" ) .find( "ul" ) - .grid({ grid: this.options.grid }); - - if ( !iconpos ) { - $navbar.addClass( "ui-navbar-noicons" ); - } + .jqmEnhanceable() + .grid({ grid: this.options.grid }); $navbtns.buttonMarkup({ corners: false, shadow: false, + inline: true, iconpos: iconpos }); $navbar.delegate( "a", "vclick", function( event ) { - $navbtns.not( ".ui-state-persist" ).removeClass( $.mobile.activeBtnClass ); - $( this ).addClass( $.mobile.activeBtnClass ); + if ( !$(event.target).hasClass( "ui-disabled" ) ) { + $navbtns.removeClass( $.mobile.activeBtnClass ); + $( this ).addClass( $.mobile.activeBtnClass ); + } + }); + + // Buttons in the navbar with ui-state-persist class should regain their active state before page show + $navbar.closest( ".ui-page" ).bind( "pagebeforeshow", function() { + $navbtns.filter( ".ui-state-persist" ).addClass( $.mobile.activeBtnClass ); }); } }); //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $( $.mobile.navbar.prototype.options.initSelector, e.target ).navbar(); +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.navbar.prototype.enhanceWithin( e.target ); }); })( jQuery ); -/* -* "listview" plugin -*/ (function( $, undefined ) { @@ -4121,6 +5485,7 @@ $( document ).bind( "pagecreate create", function( e ){ var listCountPerPage = {}; $.widget( "mobile.listview", $.mobile.widget, { + options: { theme: null, countTheme: "c", @@ -4133,11 +5498,14 @@ $.widget( "mobile.listview", $.mobile.widget, { }, _create: function() { - var t = this; + var t = this, + listviewClasses = ""; + + listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : ""; // create listview markup t.element.addClass(function( i, orig ) { - return orig + " ui-listview " + ( t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "" ); + return orig + " ui-listview " + listviewClasses; }); t.refresh( true ); @@ -4164,11 +5532,14 @@ $.widget( "mobile.listview", $.mobile.widget, { $topli, $bottomli; - if ( this.options.inset ) { - $li = this.element.children( "li" ); - // at create time the li are not visible yet so we need to rely on .ui-screen-hidden - $visibleli = create?$li.not( ".ui-screen-hidden" ):$li.filter( ":visible" ); + $li = this.element.children( "li" ); + // At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden + $visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" ); + // ui-li-last is used for setting border-bottom on the last li + $li.filter( ".ui-li-last" ).removeClass( "ui-li-last" ); + + if ( this.options.inset ) { this._removeCorners( $li ); // Select the first visible li element @@ -4176,27 +5547,29 @@ $.widget( "mobile.listview", $.mobile.widget, { .addClass( "ui-corner-top" ); $topli.add( $topli.find( ".ui-btn-inner" ) - .not( ".ui-li-link-alt span:first-child" ) ) - .addClass( "ui-corner-top" ) - .end() + .not( ".ui-li-link-alt span:first-child" ) ) + .addClass( "ui-corner-top" ) + .end() .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" ) .addClass( "ui-corner-tr" ) .end() .find( ".ui-li-thumb" ) - .not(".ui-li-icon") + .not( ".ui-li-icon" ) .addClass( "ui-corner-tl" ); // Select the last visible li element $bottomli = $visibleli.last() - .addClass( "ui-corner-bottom" ); + .addClass( "ui-corner-bottom ui-li-last" ); $bottomli.add( $bottomli.find( ".ui-btn-inner" ) ) .find( ".ui-li-link-alt" ) .addClass( "ui-corner-br" ) .end() .find( ".ui-li-thumb" ) - .not(".ui-li-icon") + .not( ".ui-li-icon" ) .addClass( "ui-corner-bl" ); + } else { + $visibleli.last().addClass( "ui-li-last" ); } if ( !create ) { this.element.trigger( "updatelayout" ); @@ -4214,8 +5587,7 @@ $.widget( "mobile.listview", $.mobile.widget, { // the nodeName from the element every time to ensure we have // a match. Note that this function lives here for now, but may // be moved into $.mobile if other components need a similar method. - _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) - { + _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) { var dict = {}; dict[ lcName ] = dict[ ucName ] = true; while ( ele ) { @@ -4226,8 +5598,7 @@ $.widget( "mobile.listview", $.mobile.widget, { } return null; }, - _getChildrenByTagName: function( ele, lcName, ucName ) - { + _getChildrenByTagName: function( ele, lcName, ucName ) { var results = [], dict = {}; dict[ lcName ] = dict[ ucName ] = true; @@ -4241,8 +5612,7 @@ $.widget( "mobile.listview", $.mobile.widget, { return $( results ); }, - _addThumbClasses: function( containers ) - { + _addThumbClasses: function( containers ) { var i, img, len = containers.length; for ( i = 0; i < len; i++ ) { img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) ); @@ -4264,15 +5634,31 @@ $.widget( "mobile.listview", $.mobile.widget, { listsplittheme = $list.jqmData( "splittheme" ), listspliticon = $list.jqmData( "spliticon" ), li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ), - counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1, + ol = !!$.nodeName( $list[ 0 ], "ol" ), + jsCount = !$.support.cssPseudoElement, + start = $list.attr( "start" ), itemClassDict = {}, item, itemClass, itemTheme, - a, last, splittheme, countParent, icon, imgParents, img; + a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon; - if ( counter ) { + if ( ol && jsCount ) { $list.find( ".ui-li-dec" ).remove(); } - + + if ( ol ) { + // Check if a start attribute has been set while taking a value of 0 into account + if ( start || start === 0 ) { + if ( !jsCount ) { + startCount = parseFloat( start ) - 1; + $list.css( "counter-reset", "listnumbering " + startCount ); + } else { + counter = parseFloat( start ); + } + } else if ( jsCount ) { + counter = 1; + } + } + if ( !o.theme ) { o.theme = $.mobile.getInheritedTheme( this.element, "c" ); } @@ -4283,11 +5669,12 @@ $.widget( "mobile.listview", $.mobile.widget, { // If we're creating the element, we update it regardless if ( create || !item.hasClass( "ui-li" ) ) { - itemTheme = item.jqmData("theme") || o.theme; + itemTheme = item.jqmData( "theme" ) || o.theme; a = this._getChildrenByTagName( item[ 0 ], "a", "A" ); + var isDivider = ( item.jqmData( "role" ) === "list-divider" ); - if ( a.length ) { - icon = item.jqmData("icon"); + if ( a.length && !isDivider ) { + icon = item.jqmData( "icon" ); item.buttonMarkup({ wrapperEls: "div", @@ -4298,19 +5685,20 @@ $.widget( "mobile.listview", $.mobile.widget, { theme: itemTheme }); - if ( ( icon != false ) && ( a.length == 1 ) ) { + if ( ( icon !== false ) && ( a.length === 1 ) ) { item.addClass( "ui-li-has-arrow" ); } - a.first().addClass( "ui-link-inherit" ); + a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" ); if ( a.length > 1 ) { itemClass += " ui-li-has-alt"; last = a.last(); splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme; + linkIcon = last.jqmData( "icon" ); - last.appendTo(item) + last.appendTo( item ) .attr( "title", last.getEncodedText() ) .addClass( "ui-li-link-alt" ) .empty() @@ -4319,7 +5707,7 @@ $.widget( "mobile.listview", $.mobile.widget, { corners: false, theme: itemTheme, icon: false, - iconpos: false + iconpos: "notext" }) .find( ".ui-btn-inner" ) .append( @@ -4328,30 +5716,40 @@ $.widget( "mobile.listview", $.mobile.widget, { corners: true, theme: splittheme, iconpos: "notext", - icon: listspliticon || last.jqmData( "icon" ) || o.splitIcon + // link icon overrides list item icon overrides ul element overrides options + icon: linkIcon || icon || listspliticon || o.splitIcon }) ); } - } else if ( item.jqmData( "role" ) === "list-divider" ) { + } else if ( isDivider ) { - itemClass += " ui-li-divider ui-btn ui-bar-" + dividertheme; + itemClass += " ui-li-divider ui-bar-" + dividertheme; item.attr( "role", "heading" ); - //reset counter when a divider heading is encountered - if ( counter ) { - counter = 1; + if ( ol ) { + //reset counter when a divider heading is encountered + if ( start || start === 0 ) { + if ( !jsCount ) { + newStartCount = parseFloat( start ) - 1; + item.css( "counter-reset", "listnumbering " + newStartCount ); + } else { + counter = parseFloat( start ); + } + } else if ( jsCount ) { + counter = 1; + } } - + } else { - itemClass += " ui-li-static ui-body-" + itemTheme; + itemClass += " ui-li-static ui-btn-up-" + itemTheme; } } - if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) { - countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" ); + if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) { + countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" ); countParent.addClass( "ui-li-jsnumbering" ) - .prepend( "" + (counter++) + ". " ); + .prepend( "" + ( counter++ ) + ". " ); } // Instead of setting item class directly on the list item and its @@ -4383,12 +5781,12 @@ $.widget( "mobile.listview", $.mobile.widget, { .end() .find( ".ui-li-aside" ).each(function() { - var $this = $(this); + var $this = $( this ); $this.prependTo( $this.parent() ); //shift aside to front for css float }) .end() - .find( ".ui-li-count" ).each( function() { + .find( ".ui-li-count" ).each(function() { $( this ).closest( "li" ).addClass( "ui-li-has-count" ); }).addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" ); @@ -4408,6 +5806,9 @@ $.widget( "mobile.listview", $.mobile.widget, { this._addThumbClasses( $list.find( ".ui-link-inherit" ) ); this._refreshCorners( create ); + + // autodividers binds to this to redraw dividers after the listview refresh + this._trigger( "afterrefresh" ); }, //create a string for ID/subpage url creation @@ -4438,8 +5839,8 @@ $.widget( "mobile.listview", $.mobile.widget, { list = $( this ), listId = list.attr( "id" ) || parentListId + "-" + i, parent = list.parent(), - nodeEls = $( list.prevAll().toArray().reverse() ), - nodeEls = nodeEls.length ? nodeEls : $( "" + $.trim(parent.contents()[ 0 ].nodeValue) + "" ), + nodeElsFull = $( list.prevAll().toArray().reverse() ), + nodeEls = nodeElsFull.length ? nodeElsFull : $( "" + $.trim(parent.contents()[ 0 ].nodeValue) + "" ), title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId, theme = list.jqmData( "theme" ) || o.theme, @@ -4450,16 +5851,16 @@ $.widget( "mobile.listview", $.mobile.widget, { hasSubPages = true; newPage = list.detach() - .wrap( "
" ) + .wrap( "
" ) .parent() .before( "
" + title + "
" ) - .after( persistentFooterID ? $( "
") : "" ) + .after( persistentFooterID ? $( "
" ) : "" ) .parent() .appendTo( $.mobile.pageContainer ); newPage.page(); - anchor = parent.find('a:first'); + anchor = parent.find( 'a:first' ); if ( !anchor.length ) { anchor = $( "" ).html( nodeEls || title ).prependTo( parent.empty() ); @@ -4471,18 +5872,22 @@ $.widget( "mobile.listview", $.mobile.widget, { // on pagehide, remove any nested pages along with the parent page, as long as they aren't active // and aren't embedded - if( hasSubPages && + if ( hasSubPages && parentPage.is( ":jqmData(external-page='true')" ) && - parentPage.data("page").options.domCache === false ) { + parentPage.data( "page" ).options.domCache === false ) { - var newRemove = function( e, ui ){ - var nextPage = ui.nextPage, npURL; + var newRemove = function( e, ui ) { + var nextPage = ui.nextPage, npURL, + prEvent = new $.Event( "pageremove" ); - if( ui.nextPage ){ + if ( ui.nextPage ) { npURL = nextPage.jqmData( "url" ); - if( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ){ + if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) { self.childPages().remove(); - parentPage.remove(); + parentPage.trigger( prEvent ); + if ( !prEvent.isDefaultPrevented() ) { + parentPage.removeWithDependents(); + } } } }; @@ -4495,144 +5900,80 @@ $.widget( "mobile.listview", $.mobile.widget, { }, // TODO sort out a better way to track sub pages of the listview this is brittle - childPages: function(){ + childPages: function() { var parentUrl = this.parentPage.jqmData( "url" ); - return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey +"')"); + return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" ); } }); //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $( $.mobile.listview.prototype.options.initSelector, e.target ).listview(); +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.listview.prototype.enhanceWithin( e.target ); }); })( jQuery ); -/* -* "listview" filter extension -*/ (function( $, undefined ) { -$.mobile.listview.prototype.options.filter = false; -$.mobile.listview.prototype.options.filterPlaceholder = "Filter items..."; -$.mobile.listview.prototype.options.filterTheme = "c"; -$.mobile.listview.prototype.options.filterCallback = function( text, searchValue ){ - return text.toLowerCase().indexOf( searchValue ) === -1; +$.mobile.listview.prototype.options.autodividers = false; +$.mobile.listview.prototype.options.autodividersSelector = function( elt ) { + // look for the text in the given element + var text = elt.text() || null; + + if ( !text ) { + return null; + } + + // create the text for the divider (first uppercased letter) + text = text.slice( 0, 1 ).toUpperCase(); + + return text; }; -$( ":jqmData(role='listview')" ).live( "listviewcreate", function() { +$( document ).delegate( "ul,ol", "listviewcreate", function() { var list = $( this ), - listview = list.data( "listview" ); + listview = list.data( "listview" ); - if ( !listview.options.filter ) { + if ( !listview || !listview.options.autodividers ) { return; } - var wrapper = $( "
", { - "class": "ui-listview-filter ui-bar-" + listview.options.filterTheme, - "role": "search" - }), - search = $( "", { - placeholder: listview.options.filterPlaceholder - }) - .attr( "data-" + $.mobile.ns + "type", "search" ) - .jqmData( "lastval", "" ) - .bind( "keyup change", function() { - - var $this = $(this), - val = this.value.toLowerCase(), - listItems = null, - lastval = $this.jqmData( "lastval" ) + "", - childItems = false, - itemtext = "", - item, change; - - // Change val as lastval for next execution - $this.jqmData( "lastval" , val ); - change = val.substr( 0 , lastval.length - 1 ).replace( lastval , "" ); + var replaceDividers = function () { + list.find( "li:jqmData(role='list-divider')" ).remove(); - if ( val.length < lastval.length || change.length != ( val.length - lastval.length ) ) { + var lis = list.find( 'li' ), + lastDividerText = null, li, dividerText; - // Removed chars or pasted something totally different, check all items - listItems = list.children(); - } else { + for ( var i = 0; i < lis.length ; i++ ) { + li = lis[i]; + dividerText = listview.options.autodividersSelector( $( li ) ); - // Only chars added, not removed, only use visible subset - listItems = list.children( ":not(.ui-screen-hidden)" ); + 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 ); } - 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(); - - if ( item.is( "li:jqmData(role=list-divider)" ) ) { - - item.toggleClass( "ui-filter-hidequeue" , !childItems ); - - // New bucket! - childItems = false; - - } else if ( listview.options.filterCallback( itemtext, val ) ) { - - //mark to be hidden - item.toggleClass( "ui-filter-hidequeue" , true ); - } else { - - // There's a shown item in the bucket - childItems = true; - } - } - - // Show items, not marked to be hidden - listItems - .filter( ":not(.ui-filter-hidequeue)" ) - .toggleClass( "ui-screen-hidden", false ); - - // Hide items, marked to be hidden - listItems - .filter( ".ui-filter-hidequeue" ) - .toggleClass( "ui-screen-hidden", true ) - .toggleClass( "ui-filter-hidequeue", false ); - - } else { - - //filtervalue is empty => show all - listItems.toggleClass( "ui-screen-hidden", false ); - } - listview._refreshCorners(); - }) - .appendTo( wrapper ) - .textinput(); + lastDividerText = dividerText; + } + }; - if ( $( this ).jqmData( "inset" ) ) { - wrapper.addClass( "ui-listview-filter-inset" ); - } + var afterListviewRefresh = function () { + list.unbind( 'listviewafterrefresh', afterListviewRefresh ); + replaceDividers(); + listview.refresh(); + list.bind( 'listviewafterrefresh', afterListviewRefresh ); + }; - wrapper.bind( "submit", function() { - return false; - }) - .insertBefore( list ); + afterListviewRefresh(); }); -})( jQuery );/* -* "nojs" plugin - class to make elements hidden to A grade browsers -*/ - -(function( $, undefined ) { - -$( document ).bind( "pagecreate create", function( e ){ - $( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" ); - -}); +})( jQuery ); -})( jQuery );/* +/* * "checkboxradio" plugin */ @@ -4646,13 +5987,19 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { _create: function() { var self = this, input = this.element, + 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. - label = input.closest( "form,fieldset,:jqmData(role='page')" ).find( "label[for='" + input[ 0 ].id + "']"), - inputtype = input.attr( "type" ), + 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" ), checkedState = inputtype + "-on", uncheckedState = inputtype + "-off", icon = input.parents( ":jqmData(type='horizontal')" ).length ? undefined : uncheckedState, + iconpos = inheritAttr( input, "iconpos" ), activeBtn = icon ? "" : " " + $.mobile.activeBtnClass, checkedClass = "ui-" + checkedState + activeBtn, uncheckedClass = "ui-" + uncheckedState, @@ -4673,20 +6020,24 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { uncheckedicon: uncheckedicon }); - // If there's no selected theme... - if( !this.options.theme ) { - this.options.theme = this.element.jqmData( "theme" ); + // If there's no selected theme check the data attr + if ( !this.options.theme ) { + this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); } label.buttonMarkup({ theme: this.options.theme, icon: icon, - shadow: false + shadow: false, + mini: mini, + iconpos: iconpos }); // Wrap the input + label in a div - input.add( label ) - .wrapAll( "
" ); + var wrapper = document.createElement('div'); + wrapper.className = 'ui-' + inputtype; + + input.add( label ).wrapAll( wrapper ); label.bind({ vmouseover: function( event ) { @@ -4720,7 +6071,6 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { self._updateAll(); return false; } - }); input @@ -4730,13 +6080,13 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { }, vclick: function() { - var $this = $(this); + 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 ); + self._getInputSet().not( $this ).prop( "checked", false ); } else { $this.prop( "checked", false ); @@ -4746,11 +6096,11 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { }, focus: function() { - label.addClass( "ui-focus" ); + label.addClass( $.mobile.focusClass ); }, blur: function() { - label.removeClass( "ui-focus" ); + label.removeClass( $.mobile.focusClass ); } }); @@ -4759,29 +6109,27 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { _cacheVals: function() { this._getInputSet().each(function() { - var $this = $(this); - - $this.jqmData( "cacheVal", $this.is( ":checked" ) ); + $( 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") { + _getInputSet: function() { + if ( this.inputtype === "checkbox" ) { return this.element; } - return this.element.closest( "form,fieldset,:jqmData(role='page')" ) - .find( "input[name='"+ this.element.attr( "name" ) +"'][type='"+ this.inputtype +"']" ); + return this.element.closest( "form, fieldset, :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); + var $this = $( this ); - if ( $this.is( ":checked" ) || self.inputtype === "checkbox" ) { + if ( this.checked || self.inputtype === "checkbox" ) { $this.trigger( "change" ); } }) @@ -4789,24 +6137,19 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { }, refresh: function() { - var input = this.element, + var input = this.element[0], label = this.label, icon = label.find( ".ui-icon" ); - // input[0].checked expando doesn't always report the proper value - // for checked='checked' - if ( $( input[ 0 ] ).prop( "checked" ) ) { - + if ( input.checked ) { label.addClass( this.checkedClass ).removeClass( this.uncheckedClass ); icon.addClass( this.checkedicon ).removeClass( this.uncheckedicon ); - } else { - label.removeClass( this.checkedClass ).addClass( this.uncheckedClass ); icon.removeClass( this.checkedicon ).addClass( this.uncheckedicon ); } - if ( input.is( ":disabled" ) ) { + if ( input.disabled ) { this.disable(); } else { this.enable(); @@ -4823,14 +6166,11 @@ $.widget( "mobile.checkboxradio", $.mobile.widget, { }); //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $.mobile.checkboxradio.prototype.enhanceWithin( e.target ); +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.checkboxradio.prototype.enhanceWithin( e.target, true ); }); })( jQuery ); -/* -* "button" plugin - links that proxy to native input/buttons -*/ (function( $, undefined ) { @@ -4839,42 +6179,80 @@ $.widget( "mobile.button", $.mobile.widget, { theme: null, icon: null, iconpos: null, - inline: null, corners: true, shadow: true, iconshadow: true, - initSelector: "button, [type='button'], [type='submit'], [type='reset'], [type='image']" + initSelector: "button, [type='button'], [type='submit'], [type='reset']" }, _create: function() { var $el = this.element, + $button, o = this.options, type, name, + inline = o.inline || $el.jqmData( "inline" ), + mini = o.mini || $el.jqmData( "mini" ), + classes = "", $buttonPlaceholder; + // 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; + } + + // get the inherited theme + // TODO centralize for all widgets + if ( !this.options.theme ) { + this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); + } + + // 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"; + } + + if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) { + classes = "ui-btn-right"; + } + + if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) { + classes ? classes += " ui-submit" : classes = "ui-submit"; + } + $( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" ); + // Add ARIA role this.button = $( "
" ) - .text( $el.text() || $el.val() ) + [ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ) .insertBefore( $el ) .buttonMarkup({ theme: o.theme, icon: o.icon, iconpos: o.iconpos, - inline: o.inline, + inline: inline, corners: o.corners, shadow: o.shadow, - iconshadow: o.iconshadow + iconshadow: o.iconshadow, + mini: mini }) + .addClass( classes ) .append( $el.addClass( "ui-btn-hidden" ) ); + $button = this.button; type = $el.attr( "type" ); name = $el.attr( "name" ); // Add hidden input during submit if input type="submit" has a name. if ( type !== "button" && type !== "reset" && name ) { $el.bind( "vclick", function() { - // Add hidden input if it doesn’t already exist. - if( $buttonPlaceholder === undefined ) { + // Add hidden input if it doesn't already exist. + if ( $buttonPlaceholder === undefined ) { $buttonPlaceholder = $( "", { type: "hidden", name: $el.attr( "name" ), @@ -4882,7 +6260,7 @@ $.widget( "mobile.button", $.mobile.widget, { }).insertBefore( $el ); // Bind to doc to remove after submit handling - $( document ).one("submit", function(){ + $( document ).one( "submit", function() { $buttonPlaceholder.remove(); // reset the local var so that the hidden input @@ -4893,6 +6271,16 @@ $.widget( "mobile.button", $.mobile.widget, { }); } + $el.bind({ + focus: function() { + $button.addClass( $.mobile.focusClass ); + }, + + blur: function() { + $button.removeClass( $.mobile.focusClass ); + } + }); + this.refresh(); }, @@ -4917,393 +6305,965 @@ $.widget( "mobile.button", $.mobile.widget, { this.enable(); } - // the textWrapper is stored as a data element on the button object - // to prevent referencing by it's implementation details (eg 'class') - this.button.data( 'textWrapper' ).text( $el.text() || $el.val() ); - } -}); + // 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() ); + } +}); + +//auto self-init widgets +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.button.prototype.enhanceWithin( e.target, true ); +}); + +})( jQuery ); + +(function( $, undefined ) { + +$.fn.controlgroup = function( options ) { + function flipClasses( els, flCorners ) { + els.removeClass( "ui-btn-corner-all ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-controlgroup-last ui-shadow" ) + .eq( 0 ).addClass( flCorners[ 0 ] ) + .end() + .last().addClass( flCorners[ 1 ] ).addClass( "ui-controlgroup-last" ); + } + + return this.each(function() { + var $el = $( this ), + o = $.extend({ + direction: $el.jqmData( "type" ) || "vertical", + shadow: false, + excludeInvisible: true, + mini: $el.jqmData( "mini" ) + }, options ), + grouplegend = $el.children( "legend" ), + groupheading = $el.children( ".ui-controlgroup-label" ), + groupcontrols = $el.children( ".ui-controlgroup-controls" ), + flCorners = o.direction === "horizontal" ? [ "ui-corner-left", "ui-corner-right" ] : [ "ui-corner-top", "ui-corner-bottom" ], + type = $el.find( "input" ).first().attr( "type" ); + + // First unwrap the controls if the controlgroup was already enhanced + if ( groupcontrols.length ) { + groupcontrols.contents().unwrap(); + } + $el.wrapInner( "
" ); + + if ( grouplegend.length ) { + // Replace legend with more stylable replacement div + $( "
" + grouplegend.html() + "
" ).insertBefore( $el.children( 0 ) ); + grouplegend.remove(); + } else if ( groupheading.length ) { + // Just move the heading if the controlgroup was already enhanced + $el.prepend( groupheading ); + } + + $el.addClass( "ui-corner-all ui-controlgroup ui-controlgroup-" + o.direction ); + + flipClasses( $el.find( ".ui-btn" + ( o.excludeInvisible ? ":visible" : "" ) ).not( '.ui-slider-handle' ), flCorners ); + flipClasses( $el.find( ".ui-btn-inner" ), flCorners ); + + if ( o.shadow ) { + $el.addClass( "ui-shadow" ); + } + + if ( o.mini ) { + $el.addClass( "ui-mini" ); + } + + }); +}; + +// The pagecreate handler for controlgroup is in jquery.mobile.init because of the soft-dependency on the wrapped widgets + +})(jQuery); + +(function( $, undefined ) { + +$( document ).bind( "pagecreate create", function( e ) { + + //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 ) { + + function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { + var ret = desired; + + 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 ); + } + + return ret; + } + + function windowCoords() { + var $win = $( window ); + + return { + x: $win.scrollLeft(), + y: $win.scrollTop(), + cx: ( window.innerWidth || $win.width() ), + cy: ( window.innerHeight || $win.height() ) + }; + } + + $.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", + + // 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.ie + }, + + _eatEventAndClose: function( e ) { + e.preventDefault(); + e.stopImmediatePropagation(); + this.close(); + return false; + }, + + // 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 ); + + 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 ); + } + }, + + _maybeRefreshTimeout: function() { + var winCoords = windowCoords(); + + 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 ); + } + } + + this._resizeData = { + timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), + winCoords: winCoords + }; + + return true; + }, + + _resizeTimeout: function() { + if ( !this._maybeRefreshTimeout() ) { + // effectively rapid-open the popup while leaving the screen intact + this._trigger( "beforeposition" ); + this._ui.container + .removeClass( "ui-selectmenu-hidden" ) + .offset( this._placementCoords( this._desiredCoords( undefined, undefined, "window" ) ) ); + + this._resizeScreen(); + this._resizeData = null; + this._orientationchangeInProgress = false; + } + }, + + _handleWindowResize: function( e ) { + if ( this._isOpen ) { + this._maybeRefreshTimeout(); + } + }, + + _handleWindowOrientationchange: function( e ) { + + if ( !this._orientationchangeInProgress ) { + // effectively rapid-close the popup while leaving the screen intact + this._ui.container + .addClass( "ui-selectmenu-hidden" ) + .removeAttr( "style" ); + + this._orientationchangeInProgress = true; + } + }, + + _create: function() { + var ui = { + screen: $( "
" ), + placeholder: $( "
" ), + container: $( "
" ) + }, + thisPage = this.element.closest( ".ui-page" ), + myId = this.element.attr( "id" ), + 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; + + if ( thisPage.length === 0 ) { + thisPage = $( "body" ); + } + + // 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; + + // 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 ); + + // Add class to popup element + this.element.addClass( "ui-popup" ); + + // Define instance variables + $.extend( this, { + _page: thisPage, + _ui: ui, + _fallbackTransition: "", + _currentTransition: false, + _prereqs: null, + _isOpen: false, + _tolerance: null, + _resizeData: null, + _orientationchangeInProgress: false, + _globalHandlers: [ + { + src: $( window ), + handler: { + orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), + resize: $.proxy( this, "_handleWindowResize" ), + keyup: $.proxy( this, "_handleWindowKeyUp" ) + } + } + ] + }); + + $.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 ); + }); + + ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); + + $.each( this._globalHandlers, function( idx, value ) { + value.src.bind( value.handler ); + }); + }, + + _applyTheme: function( dst, theme, prefix ) { + var classes = ( dst.attr( "class" ) || "").split( " " ), + alreadyAdded = true, + currentTheme = null, + matches, + themeStr = String( theme ); + + 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; + } + } + + if ( theme !== currentTheme ) { + dst.removeClass( "ui-" + prefix + "-" + currentTheme ); + if ( ! ( theme === null || theme === "none" ) ) { + dst.addClass( "ui-" + prefix + "-" + themeStr ); + } + } + }, -//auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $.mobile.button.prototype.enhanceWithin( e.target ); -}); + _setTheme: function( value ) { + this._applyTheme( this.element, value, "body" ); + }, -})( jQuery );/* -* "slider" plugin -*/ + _setOverlayTheme: function( value ) { + this._applyTheme( this._ui.screen, value, "overlay" ); -( function( $, undefined ) { + if ( this._isOpen ) { + this._ui.screen.addClass( "in" ); + } + }, -$.widget( "mobile.slider", $.mobile.widget, { - options: { - theme: null, - trackTheme: null, - disabled: false, - initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')" - }, + _setShadow: function( value ) { + this.element.toggleClass( "ui-overlay-shadow", value ); + }, - _create: function() { + _setCorners: function( value ) { + this.element.toggleClass( "ui-corner-all", value ); + }, - // TODO: Each of these should have comments explain what they're for - var self = this, + _applyTransition: function( value ) { + this._ui.container.removeClass( this._fallbackTransition ); + if ( value && value !== "none" ) { + this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); + this._ui.container.addClass( this._fallbackTransition ); + } + }, - control = this.element, + _setTransition: function( value ) { + if ( !this._currentTransition ) { + this._applyTransition( value ); + } + }, - parentTheme = $.mobile.getInheritedTheme( control, "c" ), + _setTolerance: function( value ) { + var tol = { t: 30, r: 15, b: 30, l: 15 }; - theme = this.options.theme || parentTheme, + if ( value ) { + var ar = String( value ).split( "," ); - trackTheme = this.options.trackTheme || parentTheme, + $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); - cType = control[ 0 ].nodeName.toLowerCase(), + 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; - selectClass = ( cType == "select" ) ? "ui-slider-switch" : "", + // 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; - controlID = control.attr( "id" ), + // 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; - labelID = controlID + "-label", + default: + break; + } + } - label = $( "[for='"+ controlID +"']" ).attr( "id", labelID ), + this._tolerance = tol; + }, - val = function() { - return cType == "input" ? parseFloat( control.val() ) : control[0].selectedIndex; - }, + _setOption: function( key, value ) { + var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); - min = cType == "input" ? parseFloat( control.attr( "min" ) ) : 0, + if ( this[ setter ] !== undefined ) { + this[ setter ]( value ); + } - max = cType == "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1, + // 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 ); + } + }, - step = window.parseFloat( control.attr( "step" ) || 1 ), + // 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; - slider = $( "
" ), + // 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 ) + }; - handle = $( "
" ) - .appendTo( slider ) - .buttonMarkup({ corners: true, theme: theme, shadow: true }) - .attr({ - "role": "slider", - "aria-valuemin": min, - "aria-valuemax": max, - "aria-valuenow": val(), - "aria-valuetext": val(), - "title": val(), - "aria-labelledby": labelID - }), - options; + // 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 ) + }; - $.extend( this, { - slider: slider, - handle: handle, - dragging: false, - beforeStart: null, - userModified: false, - mouseMoved: false - }); + // Make sure the top of the menu is visible + ret.y = Math.max( 0, ret.y ); - if ( cType == "select" ) { + // If the height of the menu is smaller than the height of the document + // align the bottom with the bottom of the document - slider.wrapInner( "
" ); - - // make the handle move with a smooth transition - handle.addClass( "ui-slider-handle-snapping" ); + // fix for $( 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 = control.find( "option" ); + ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); - control.find( "option" ).each(function( i ) { + return { left: ret.x, top: ret.y }; + }, - var side = !i ? "b":"a", - corners = !i ? "right" :"left", - theme = !i ? " ui-btn-down-" + trackTheme :( " " + $.mobile.activeBtnClass ); + _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { + var self = this, prereqs; + + // 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. + + prereqs = { + screen: $.Deferred(), + container: $.Deferred() + }; - $( "
" ) - .prependTo( slider ); + prereqs.screen.then( function() { + if ( prereqs === self._prereqs ) { + screenPrereq(); + } + }); - $( "" + $( this ).getEncodedText() + "" ) - .prependTo( handle ); + prereqs.container.then( function() { + if ( prereqs === self._prereqs ) { + containerPrereq(); + } }); - } + $.when( prereqs.screen, prereqs.container ).done( function() { + if ( prereqs === self._prereqs ) { + self._prereqs = null; + whenDone(); + } + }); - label.addClass( "ui-slider" ); + self._prereqs = prereqs; + }, - // monitor the input for updated values - control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" ) - .change( function() { - // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again - if (!self.mouseMoved) { - self.refresh( val(), true ); + _animate: function( args ) { + // NOTE before removing the default animation of the screen + // this had an animate callback that would relove 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 ); + + args.prereqs.screen.resolve(); + + if ( args.transition && args.transition !== "none" ) { + if ( args.applyTransition ) { + this._applyTransition( args.transition ); } - }) - .keyup( function() { // necessary? - self.refresh( val(), true, true ); - }) - .blur( function() { - self.refresh( val(), true ); - }); + this._ui.container + .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) + .addClass( args.containerClassToAdd ) + .removeClass( args.classToRemove ); + } else { + args.prereqs.container.resolve(); + } + }, - // prevent screen drag when slider activated - $( document ).bind( "vmousemove", function( event ) { - if ( self.dragging ) { - // self.mouseMoved must be updated before refresh() because it will be used in the control "change" event - self.mouseMoved = true; - - if ( cType === "select" ) { - // make the handle move in sync with the mouse - handle.removeClass( "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. + _desiredCoords: function( x, y, positionTo ) { + var dst = null, offset, winCoords = windowCoords(); + + // Establish which element will serve as the reference + if ( positionTo && positionTo !== "origin" ) { + if ( positionTo === "window" ) { + x = winCoords.cx / 2 + winCoords.x; + y = winCoords.cy / 2 + winCoords.y; + } else { + try { + dst = $( positionTo ); + } catch( e ) { + dst = null; + } + if ( dst ) { + dst.filter( ":visible" ); + if ( dst.length === 0 ) { + dst = null; + } + } } - - self.refresh( event ); - - // only after refresh() you can calculate self.userModified - self.userModified = self.beforeStart !== control[0].selectedIndex; - return false; } - }); - slider.bind( "vmousedown", function( event ) { - self.dragging = true; - self.userModified = false; - self.mouseMoved = false; + // 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; + } - if ( cType === "select" ) { - self.beforeStart = control[0].selectedIndex; + // 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.refresh( event ); - return false; - }); - slider.add( document ) - .bind( "vmouseup", function() { - if ( self.dragging ) { + return { x: x, y: y }; + }, - self.dragging = false; + _openPrereqsComplete: function() { + var self = this; - if ( cType === "select") { - - // make the handle move with a smooth transition - handle.addClass( "ui-slider-handle-snapping" ); - - if ( self.mouseMoved ) { - - // this is a drag, change the value only if user dragged enough - if ( self.userModified ) { - self.refresh( self.beforeStart == 0 ? 1 : 0 ); - } - else { - self.refresh( self.beforeStart ); - } - - } - else { - // this is just a click, change the value - self.refresh( self.beforeStart == 0 ? 1 : 0 ); - } - + self._ui.container.addClass( "ui-popup-active" ); + self._isOpen = true; + self._resizeScreen(); + + // Android appears to trigger the animation complete before the popup + // is visible. Allowing the stack to unwind before applying focus prevents + // the "blue flash" of element focus in android 4.0 + setTimeout(function(){ + self._ui.container.attr( "tabindex", "0" ).focus(); + self._trigger( "afteropen" ); + }); + }, + + _open: function( options ) { + var coords, transition, + 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; + + // 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; } - - self.mouseMoved = false; - return false; - } - }); + }()); - slider.insertAfter( control ); + // Make sure options is defined + options = ( options || {} ); - // NOTE force focus on handle - this.handle - .bind( "vmousedown", function() { - $( this ).focus(); - }) - .bind( "vclick", false ); + // Copy out the transition, because we may be overwriting it later and we don't want to pass that change back to the caller + transition = options.transition || this.options.transition; - this.handle - .bind( "keydown", function( event ) { - var index = val(); + // Give applications a chance to modify the contents of the container before it appears + this._trigger( "beforeposition" ); - if ( self.options.disabled ) { - return; - } + coords = this._placementCoords( this._desiredCoords( options.x, options.y, options.positionTo || this.options.positionTo || "origin" ) ); - // 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(); + // 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" ) ); - if ( !self._keySliding ) { - self._keySliding = true; - $( this ).addClass( "ui-state-active" ); - } - break; - } + if ( transition ) { + this._currentTransition = transition; + this._applyTransition( transition ); + } else { + transition = this.options.transition; + } - // move the slider according to the keypress - switch ( event.keyCode ) { - case $.mobile.keyCode.HOME: - self.refresh( min ); - break; - case $.mobile.keyCode.END: - self.refresh( max ); - break; - case $.mobile.keyCode.PAGE_UP: - case $.mobile.keyCode.UP: - case $.mobile.keyCode.RIGHT: - self.refresh( index + step ); - break; - case $.mobile.keyCode.PAGE_DOWN: - case $.mobile.keyCode.DOWN: - case $.mobile.keyCode.LEFT: - self.refresh( index - step ); - break; - } - }) // remove active mark - .keyup( function( event ) { - if ( self._keySliding ) { - self._keySliding = false; - $( this ).removeClass( "ui-state-active" ); - } - }); + if ( !this.options.theme ) { + this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); + } - this.refresh(undefined, undefined, true); - }, + this._ui.screen.removeClass( "ui-screen-hidden" ); - refresh: function( val, isfromControl, preventInputUpdate ) { + this._ui.container + .removeClass( "ui-selectmenu-hidden" ) + .offset( coords ); - if ( this.options.disabled || this.element.attr('disabled')) { - this.disable(); - } + 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 - var control = this.element, percent, - cType = control[0].nodeName.toLowerCase(), - min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0, - max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1; + This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): - if ( typeof val === "object" ) { - var data = val, - // a slight tolerance helped get to the ends of the slider - tol = 8; - if ( !this.dragging || - data.pageX < this.slider.offset().left - tol || - data.pageX > this.slider.offset().left + this.slider.width() + tol ) { - return; - } - percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 ); - } else { - if ( val == null ) { - val = cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex; + https://github.com/jquery/jquery-mobile/issues/4816 + https://github.com/jquery/jquery-mobile/issues/4844 + https://github.com/jquery/jquery-mobile/issues/4874 + */ + + // TODO sort out why this._page isn't working + this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); } - percent = ( parseFloat( val ) - min ) / ( max - min ) * 100; - } + this._animate({ + additionalCondition: true, + transition: transition, + classToRemove: "", + screenClassToAdd: "in", + containerClassToAdd: "in", + applyTransition: false, + prereqs: this._prereqs + }); + }, - if ( isNaN( percent ) ) { - return; - } + _closePrereqScreen: function() { + this._ui.screen + .removeClass( "out" ) + .addClass( "ui-screen-hidden" ); + }, - if ( percent < 0 ) { - percent = 0; - } + _closePrereqContainer: function() { + this._ui.container + .removeClass( "reverse out" ) + .addClass( "ui-selectmenu-hidden" ) + .removeAttr( "style" ); + }, - if ( percent > 100 ) { - percent = 100; - } + _closePrereqsDone: function() { + var self = this, opts = self.options; - var newval = Math.round( ( percent / 100 ) * ( max - min ) ) + min; + self._ui.container.removeAttr( "tabindex" ); - if ( newval < min ) { - newval = min; - } + // remove nav bindings if they are still present + opts.container.unbind( opts.closeEvents ); - if ( newval > max ) { - newval = max; - } + // unbind click handlers added when history is disabled + self.element.undelegate( opts.closeLinkSelector, opts.closeLinkEvents ); - // Flip the stack of the bg colors - if ( percent > 60 && cType === "select" ) { - // TODO: Dead path? - } - this.handle.css( "left", percent + "%" ); - this.handle.attr( { - "aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ), - "aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(), - title: newval + // remove the global mutex for popups + $.mobile.popup.active = undefined; + + // alert users that the popup is closed + self._trigger( "afterclose" ); + }, + + _close: function() { + this._ui.container.removeClass( "ui-popup-active" ); + this._page.removeClass( "ui-popup-open" ); + + this._isOpen = false; + + // 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" ) ); + + this._animate( { + additionalCondition: this._ui.screen.hasClass( "in" ), + transition: ( this._currentTransition || this.options.transition ), + classToRemove: "in", + screenClassToAdd: "out", + containerClassToAdd: "reverse out", + applyTransition: true, + prereqs: this._prereqs }); + }, - // add/remove classes for flip toggle switch - if ( cType === "select" ) { - if ( newval === 0 ) { - this.slider.addClass( "ui-slider-switch-a" ) - .removeClass( "ui-slider-switch-b" ); - } else { - this.slider.addClass( "ui-slider-switch-b" ) - .removeClass( "ui-slider-switch-a" ); + _destroy: function() { + var self = this; + + // hide and remove bindings + self._close(); + + // Put the element back to where the placeholder was and remove the "ui-popup" class + self._setTheme( "none" ); + self.element + .insertAfter( self._ui.placeholder ) + .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); + self._ui.screen.remove(); + self._ui.container.remove(); + self._ui.placeholder.remove(); + + // Unbind handlers that were bound to elements outside self.element (the window, in self case) + $.each( self._globalHandlers, function( idx, oneSrc ) { + $.each( oneSrc.handler, function( eventType, handler ) { + oneSrc.src.unbind( eventType, handler ); + }); + }); + }, + + // 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() { + var self = this; + + self.options.container + .one( self.options.closeEvents, $.proxy( self._close, self )); + }, + + // 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; } - } - if ( !preventInputUpdate ) { - var valueChanged = false; + // set the global popup mutex + $.mobile.popup.active = this; + + // if history alteration is disabled close on navigate events + // and leave the url as is + if( !( opts.history ) ) { + self._open( options ); + self._bindContainerClose(); + + // 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(); + + // NOTE prevent the browser and navigation handlers from + // working with the link's rel=back. This may cause + // issues for developers expecting the event to bubble + return false; + }); - // update control"s value - if ( cType === "input" ) { - valueChanged = control.val() !== newval; - control.val( newval ); + return; + } + + // cache some values for min/readability + hashkey = $.mobile.dialogHashKey; + activePage = $.mobile.activePage; + currentIsDialog = activePage.is( ".ui-dialog" ); + url = $.mobile.urlHistory.getActive().url; + hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog; + urlHistory = $.mobile.urlHistory; + + if ( hasHash ) { + self._open( options ); + self._bindContainerClose(); + 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 + hashkey; } else { - valueChanged = control[ 0 ].selectedIndex !== newval; - control[ 0 ].selectedIndex = newval; + url = $.mobile.path.parseLocation().hash + hashkey; + } + + // 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; + } + + // swallow the the initial navigation event, and bind for the next + opts.container.one( opts.navigateEvents, function( e ) { + e.preventDefault(); + self._open( options ); + self._bindContainerClose(); + }); + + urlHistory.ignoreNextHashChange = currentIsDialog; + + // Gotta love methods with 1mm args :( + urlHistory.addNew( url, undefined, undefined, undefined, "dialog" ); + + // set the new url with (or without) the new dialog hash key + $.mobile.path.set( url ); + }, + + close: function() { + // make sure close is idempotent + if( !$.mobile.popup.active ){ + return; } - if ( !isfromControl && valueChanged ) { - control.trigger( "change" ); + + if( this.options.history ) { + $.mobile.back(); + } else { + this._close(); } } - }, + }); - enable: function() { - this.element.attr( "disabled", false ); - this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); - return this._setOption( "disabled", false ); - }, - disable: function() { - this.element.attr( "disabled", true ); - this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true ); - return this._setOption( "disabled", true ); - } + // 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 ( popup.data( "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" ), + link: $link + }); + } -}); + //remove after delay + setTimeout( function() { + $link.removeClass( $.mobile.activeBtnClass ); + }, 300 ); + }; -//auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $.mobile.slider.prototype.enhanceWithin( e.target ); -}); + // TODO move inside _create + $( document ).bind( "pagebeforechange", function( e, data ) { + if ( data.options.role === "popup" ) { + $.mobile.popup.handleLink( data.options.link ); + e.preventDefault(); + } + }); + + $( document ).bind( "pagecreate create", function( e ) { + $.mobile.popup.prototype.enhanceWithin( e.target, true ); + }); })( jQuery ); -/* -* "textinput" plugin for text inputs, textareas -*/ + +(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.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; + } + } + }); + +}( jQuery )); (function( $, undefined ) { $.widget( "mobile.textinput", $.mobile.widget, { options: { theme: null, - 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])" + // 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])", + clearSearchButtonText: "clear text", + disabled: false }, _create: function() { - var input = this.element, + var self = this, + input = this.element, o = this.options, theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ), themeclass = " ui-body-" + theme, + mini = input.jqmData( "mini" ) === true, + miniclass = mini ? " ui-mini" : "", focusedEl, clearbtn; + function toggleClear() { + setTimeout( function() { + clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); + }, 0 ); + } + $( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" ); focusedEl = input.addClass("ui-input-text ui-body-"+ theme ); @@ -5326,11 +7286,13 @@ $.widget( "mobile.textinput", $.mobile.widget, { //"search" input widget if ( input.is( "[type='search'],:jqmData(type='search')" ) ) { - focusedEl = input.wrap( "" ).parent(); - clearbtn = $( "clear text" ) - .tap(function( event ) { - input.val( "" ).focus(); - input.trigger( "change" ); + focusedEl = input.wrap( "" ).parent(); + clearbtn = $( "" + o.clearSearchButtonText + "" ) + .bind('click', function( event ) { + input + .val( "" ) + .focus() + .trigger( "change" ); clearbtn.addClass( "ui-input-clear-hidden" ); event.preventDefault(); }) @@ -5339,575 +7301,663 @@ $.widget( "mobile.textinput", $.mobile.widget, { icon: "delete", iconpos: "notext", corners: true, - shadow: true + shadow: true, + mini: mini }); - function toggleClear() { - setTimeout(function() { - clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); - }, 0); - } - toggleClear(); - input.bind('paste cut keyup focus change blur', toggleClear); + input.bind( 'paste cut keyup focus change blur', toggleClear ); } else { - input.addClass( "ui-corner-all ui-shadow-inset" + themeclass ); + input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass ); } input.focus(function() { - focusedEl.addClass( "ui-focus" ); + focusedEl.addClass( $.mobile.focusClass ); + }) + .blur(function() { + focusedEl.removeClass( $.mobile.focusClass ); }) - .blur(function(){ - focusedEl.removeClass( "ui-focus" ); + // In many situations, iOS will zoom into the select upon tap, this prevents that from happening + .bind( "focus", function() { + if ( o.preventFocusZoom ) { + $.mobile.zoom.disable( true ); + } + }) + .bind( "blur", function() { + if ( o.preventFocusZoom ) { + $.mobile.zoom.enable( true ); + } }); // Autogrow if ( input.is( "textarea" ) ) { var extraLineHeight = 15, keyupTimeoutBuffer = 100, - keyup = function() { - var scrollHeight = input[ 0 ].scrollHeight, - clientHeight = input[ 0 ].clientHeight; - - if ( clientHeight < scrollHeight ) { - input.height(scrollHeight + extraLineHeight); - } - }, keyupTimeout; + this._keyup = function() { + var scrollHeight = input[ 0 ].scrollHeight, + clientHeight = input[ 0 ].clientHeight; + + if ( clientHeight < scrollHeight ) { + input.height(scrollHeight + extraLineHeight); + } + }; + input.keyup(function() { clearTimeout( keyupTimeout ); - keyupTimeout = setTimeout( keyup, keyupTimeoutBuffer ); + 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( $(document), {"pagechange": "_keyup" }); + // 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 - $( window ).load( keyup ); - - // binding to pagechange here ensures that for pages loaded via - // ajax the height is recalculated without user input - $( document ).one( "pagechange", keyup ); + this._on( $(window), {"load": "_keyup"}); } } + if ( input.attr( "disabled" ) ) { + this.disable(); + } }, - disable: function(){ - ( this.element.attr( "disabled", true ).is( "[type='search'],:jqmData(type='search')" ) ? - this.element.parent() : this.element ).addClass( "ui-disabled" ); + disable: function() { + var $el; + if ( this.element.attr( "disabled", true ).is( "[type='search'], :jqmData(type='search')" ) ) { + $el = this.element.parent(); + } else { + $el = this.element; + } + $el.addClass( "ui-disabled" ); + return this._setOption( "disabled", true ); }, - enable: function(){ - ( this.element.attr( "disabled", false).is( "[type='search'],:jqmData(type='search')" ) ? - this.element.parent() : this.element ).removeClass( "ui-disabled" ); + enable: function() { + var $el; + + // TODO using more than one line of code is acceptable ;) + if ( this.element.attr( "disabled", false ).is( "[type='search'], :jqmData(type='search')" ) ) { + $el = this.element.parent(); + } else { + $el = this.element; + } + $el.removeClass( "ui-disabled" ); + return this._setOption( "disabled", false ); } }); //auto self-init widgets -$( document ).bind( "pagecreate create", function( e ){ - $.mobile.textinput.prototype.enhanceWithin( e.target ); +$( document ).bind( "pagecreate create", function( e ) { + $.mobile.textinput.prototype.enhanceWithin( e.target, true ); }); })( jQuery ); -/* -* custom "selectmenu" plugin -*/ (function( $, undefined ) { - var extendSelect = function( widget ){ - var select = widget.select, - selectID = widget.selectID, - label = widget.label, - thisPage = widget.select.closest( ".ui-page" ), - screen = $( "
", {"class": "ui-selectmenu-screen ui-screen-hidden"} ).appendTo( thisPage ), - selectOptions = widget._selectOptions(), - isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, - buttonId = selectID + "-button", - menuId = selectID + "-menu", - menuPage = $( "
" + - "
" + - "
" + label.getEncodedText() + "
"+ - "
"+ - "
"+ - "
" ).appendTo( $.mobile.pageContainer ).page(), +$.mobile.listview.prototype.options.filter = false; +$.mobile.listview.prototype.options.filterPlaceholder = "Filter items..."; +$.mobile.listview.prototype.options.filterTheme = "c"; +// 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; + }; - listbox = $("
", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-" + widget.options.overlayTheme + " " + $.mobile.defaultDialogTransition } ).insertAfter(screen), +$.mobile.listview.prototype.options.filterCallback = defaultFilterCallback; - list = $( "