2
* jQuery Mobile Framework Git Build: SHA1: 27e3c18acfebab2d47ee7ed37bd50fc4942c8838 <> Date: Fri Mar 22 08:50:04 2013 -0600
3
* http://jquerymobile.com
5
* Copyright 2010, 2013 jQuery Foundation, Inc. and other contributors
6
* Released under the MIT license.
7
* http://jquery.org/license
12
(function ( root, doc, factory ) {
13
if ( typeof define === "function" && define.amd ) {
14
// AMD. Register as an anonymous module.
15
define( [ "jquery" ], function ( $ ) {
16
factory( $, root, doc );
21
factory( root.jQuery, root, doc );
23
}( this, document, function ( jQuery, window, document, undefined ) {
24
(function( $, window, undefined ) {
26
var nsNormalizeDict = {};
28
// jQuery.mobile configurable options
29
$.mobile = $.extend( {}, {
31
// Version of the jQuery Mobile Framework
34
// Namespace used framework-wide for data-attrs. Default is no namespace
37
// Define the url parameter used for referencing widget-generated sub-pages.
38
// Translates to to example.html&ui-page=subpageIdentifier
39
// hash segment before &ui-page= is used to make Ajax request
40
subPageUrlKey: "ui-page",
42
// Class assigned to page currently in view, and during transitions
43
activePageClass: "ui-page-active",
45
// Class used for "active" button state, from CSS framework
46
activeBtnClass: "ui-btn-active",
48
// Class used for "focus" form element state, from CSS framework
49
focusClass: "ui-focus",
51
// Automatically handle clicks and form submissions through Ajax, when same-domain
54
// Automatically load and show pages based on location.hash
55
hashListeningEnabled: true,
57
// disable to prevent jquery from bothering with links
58
linkBindingEnabled: true,
60
// Set default page transition - 'none' for no transitions
61
defaultPageTransition: "fade",
63
// Set maximum window width for transitions to apply - 'false' for no limit
64
maxTransitionWidth: false,
66
// Minimum scroll distance that will be remembered when returning to a page
69
// DEPRECATED: the following property is no longer in use, but defined until 2.0 to prevent conflicts
70
touchOverflowEnabled: false,
72
// Set default dialog transition - 'none' for no transitions
73
defaultDialogTransition: "pop",
75
// Error response message - appears when an Ajax page request fails
76
pageLoadErrorMessage: "Error Loading Page",
78
// For error messages, which theme does the box uses?
79
pageLoadErrorMessageTheme: "e",
81
// replace calls to window.history.back with phonegaps navigation helper
82
// where it is provided on the window object
83
phonegapNavigationEnabled: false,
85
//automatically initialize the DOM when it's ready
86
autoInitializePage: true,
88
pushStateEnabled: true,
90
// allows users to opt in to ignoring content by marking a parent element as
92
ignoreContentEnabled: false,
94
// turn of binding to the native orientationchange due to android orientation behavior
95
orientationChangeEnabled: true,
101
// TODO might be useful upstream in jquery itself ?
108
COMMAND_LEFT: 91, // COMMAND
119
MENU: 93, // COMMAND_RIGHT
124
NUMPAD_MULTIPLY: 106,
125
NUMPAD_SUBTRACT: 109,
134
WINDOWS: 91 // COMMAND
137
// Place to store various widget extensions
140
// Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value
141
silentScroll: function( ypos ) {
142
if ( $.type( ypos ) !== "number" ) {
143
ypos = $.mobile.defaultHomeScroll;
146
// prevent scrollstart and scrollstop events
147
$.event.special.scrollstart.enabled = false;
149
setTimeout( function() {
150
window.scrollTo( 0, ypos );
151
$( document ).trigger( "silentscroll", { x: 0, y: ypos });
154
setTimeout( function() {
155
$.event.special.scrollstart.enabled = true;
159
// Expose our cache for testing purposes.
160
nsNormalizeDict: nsNormalizeDict,
162
// Take a data attribute property, prepend the namespace
163
// and then camel case the attribute string. Add the result
164
// to our nsNormalizeDict so we don't have to do this again.
165
nsNormalize: function( prop ) {
170
return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) );
173
// Find the closest parent with a theme class on it. Note that
174
// we are not using $.fn.closest() on purpose here because this
175
// method gets called quite a bit and we need it to be as fast
177
getInheritedTheme: function( el, defaultTheme ) {
180
re = /ui-(bar|body|overlay)-([a-z])\b/,
184
c = e.className || "";
185
if ( c && ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) {
186
// We found a parent with a theme class
187
// on it so bail from this loop.
194
// Return the theme letter we found, if none, return the
195
// specified default.
197
return ltr || defaultTheme || "a";
200
// TODO the following $ and $.fn extensions can/probably should be moved into jquery.mobile.core.helpers
202
// Find the closest javascript page element to gather settings data jsperf test
203
// http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit
204
// possibly naive, but it shows that the parsing overhead for *just* the page selector vs
205
// the page and dialog selector is negligable. This could probably be speed up by
206
// doing a similar parent node traversal to the one found in the inherited theme code above
207
closestPageData: function( $target ) {
209
.closest( ':jqmData(role="page"), :jqmData(role="dialog")' )
213
enhanceable: function( $set ) {
214
return this.haveParents( $set, "enhance" );
217
hijackable: function( $set ) {
218
return this.haveParents( $set, "ajax" );
221
haveParents: function( $set, attr ) {
222
if ( !$.mobile.ignoreContentEnabled ) {
226
var count = $set.length,
228
e, $element, excluded;
230
for ( var i = 0; i < count; i++ ) {
231
$element = $set.eq( i );
236
var c = e.getAttribute ? e.getAttribute( "data-" + $.mobile.ns + attr ) : "";
238
if ( c === "false" ) {
247
$newSet = $newSet.add( $element );
254
getScreenHeight: function() {
255
// Native innerHeight returns more accurate value for this across platforms,
256
// jQuery version is here as a normalized fallback for platforms like Symbian
257
return window.innerHeight || $( window ).height();
261
// Mobile version of data and removeData and hasData methods
262
// ensures all data is set and retrieved using jQuery Mobile's data namespace
263
$.fn.jqmData = function( prop, value ) {
265
if ( typeof prop !== "undefined" ) {
267
prop = $.mobile.nsNormalize( prop );
270
// undefined is permitted as an explicit input for the second param
271
// in this case it returns the value and does not set it to undefined
272
if( arguments.length < 2 || value === undefined ){
273
result = this.data( prop );
275
result = this.data( prop, value );
281
$.jqmData = function( elem, prop, value ) {
283
if ( typeof prop !== "undefined" ) {
284
result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value );
289
$.fn.jqmRemoveData = function( prop ) {
290
return this.removeData( $.mobile.nsNormalize( prop ) );
293
$.jqmRemoveData = function( elem, prop ) {
294
return $.removeData( elem, $.mobile.nsNormalize( prop ) );
297
$.fn.removeWithDependents = function() {
298
$.removeWithDependents( this );
301
$.removeWithDependents = function( elem ) {
302
var $elem = $( elem );
304
( $elem.jqmData( 'dependents' ) || $() ).remove();
308
$.fn.addDependents = function( newDependents ) {
309
$.addDependents( $( this ), newDependents );
312
$.addDependents = function( elem, newDependents ) {
313
var dependents = $( elem ).jqmData( 'dependents' ) || $();
315
$( elem ).jqmData( 'dependents', $.merge( dependents, newDependents ) );
318
// note that this helper doesn't attempt to handle the callback
319
// or setting of an html elements text, its only purpose is
320
// to return the html encoded version of the text in all cases. (thus the name)
321
$.fn.getEncodedText = function() {
322
return $( "<div/>" ).text( $( this ).text() ).html();
325
// fluent helper function for the mobile namespaced equivalent
326
$.fn.jqmEnhanceable = function() {
327
return $.mobile.enhanceable( this );
330
$.fn.jqmHijackable = function() {
331
return $.mobile.hijackable( this );
334
// Monkey-patching Sizzle to filter the :jqmData selector
335
var oldFind = $.find,
336
jqmDataRE = /:jqmData\(([^)]*)\)/g;
338
$.find = function( selector, context, ret, extra ) {
339
selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" );
341
return oldFind.call( this, selector, context, ret, extra );
344
$.extend( $.find, oldFind );
346
$.find.matches = function( expr, set ) {
347
return $.find( expr, null, null, set );
350
$.find.matchesSelector = function( node, expr ) {
351
return $.find( expr, null, null, [ node ] ).length > 0;
357
* jQuery UI Widget v1.9.0-beta.1
359
* Copyright 2012, https://github.com/jquery/jquery-ui/blob/1.9.0-beta.1/AUTHORS.txt (http://jqueryui.com/about)
360
* Dual licensed under the MIT or GPL Version 2 licenses.
361
* http://jquery.org/license
363
* http://docs.jquery.com/UI/Widget
365
(function( $, undefined ) {
368
slice = Array.prototype.slice,
369
_cleanData = $.cleanData;
370
$.cleanData = function( elems ) {
371
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
373
$( elem ).triggerHandler( "remove" );
374
// http://bugs.jquery.com/ticket/8235
380
$.widget = function( name, base, prototype ) {
381
var fullName, existingConstructor, constructor, basePrototype,
382
namespace = name.split( "." )[ 0 ];
384
name = name.split( "." )[ 1 ];
385
fullName = namespace + "-" + name;
392
// create selector for plugin
393
$.expr[ ":" ][ fullName ] = function( elem ) {
394
return !!$.data( elem, fullName );
397
$[ namespace ] = $[ namespace ] || {};
398
existingConstructor = $[ namespace ][ name ];
399
constructor = $[ namespace ][ name ] = function( options, element ) {
400
// allow instantiation without "new" keyword
401
if ( !this._createWidget ) {
402
return new constructor( options, element );
405
// allow instantiation without initializing for simple inheritance
406
// must use "new" keyword (the code above always passes args)
407
if ( arguments.length ) {
408
this._createWidget( options, element );
411
// extend with the existing constructor to carry over any static properties
412
$.extend( constructor, existingConstructor, {
413
version: prototype.version,
414
// copy the object used to create the prototype in case we need to
415
// redefine the widget later
416
_proto: $.extend( {}, prototype ),
417
// track widgets that inherit from this widget in case this widget is
418
// redefined after a widget inherits from it
419
_childConstructors: []
422
basePrototype = new base();
423
// we need to make the options hash a property directly on the new instance
424
// otherwise we'll modify the options hash on the prototype that we're
426
basePrototype.options = $.widget.extend( {}, basePrototype.options );
427
$.each( prototype, function( prop, value ) {
428
if ( $.isFunction( value ) ) {
429
prototype[ prop ] = (function() {
430
var _super = function() {
431
return base.prototype[ prop ].apply( this, arguments );
433
_superApply = function( args ) {
434
return base.prototype[ prop ].apply( this, args );
437
var __super = this._super,
438
__superApply = this._superApply,
441
this._super = _super;
442
this._superApply = _superApply;
444
returnValue = value.apply( this, arguments );
446
this._super = __super;
447
this._superApply = __superApply;
454
constructor.prototype = $.widget.extend( basePrototype, {
455
// TODO: remove support for widgetEventPrefix
456
// always use the name + a colon as the prefix, e.g., draggable:start
457
// don't prefix for widgets that aren't DOM-based
458
widgetEventPrefix: name
460
constructor: constructor,
461
namespace: namespace,
463
// TODO remove widgetBaseClass, see #8155
464
widgetBaseClass: fullName,
465
widgetFullName: fullName
468
// If this widget is being redefined then we need to find all widgets that
469
// are inheriting from it and redefine all of them so that they inherit from
470
// the new version of this widget. We're essentially trying to replace one
471
// level in the prototype chain.
472
if ( existingConstructor ) {
473
$.each( existingConstructor._childConstructors, function( i, child ) {
474
var childPrototype = child.prototype;
476
// redefine the child widget using the same prototype that was
477
// originally used, but inherit from the new version of the base
478
$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
480
// remove the list of existing child constructors from the old constructor
481
// so the old child constructors can be garbage collected
482
delete existingConstructor._childConstructors;
484
base._childConstructors.push( constructor );
487
$.widget.bridge( name, constructor );
490
$.widget.extend = function( target ) {
491
var input = slice.call( arguments, 1 ),
493
inputLength = input.length,
496
for ( ; inputIndex < inputLength; inputIndex++ ) {
497
for ( key in input[ inputIndex ] ) {
498
value = input[ inputIndex ][ key ];
499
if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
500
target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value;
507
$.widget.bridge = function( name, object ) {
508
var fullName = object.prototype.widgetFullName;
509
$.fn[ name ] = function( options ) {
510
var isMethodCall = typeof options === "string",
511
args = slice.call( arguments, 1 ),
514
// allow multiple hashes to be passed on init
515
options = !isMethodCall && args.length ?
516
$.widget.extend.apply( null, [ options ].concat(args) ) :
519
if ( isMethodCall ) {
520
this.each(function() {
522
instance = $.data( this, fullName );
524
return $.error( "cannot call methods on " + name + " prior to initialization; " +
525
"attempted to call method '" + options + "'" );
527
if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
528
return $.error( "no such method '" + options + "' for " + name + " widget instance" );
530
methodValue = instance[ options ].apply( instance, args );
531
if ( methodValue !== instance && methodValue !== undefined ) {
532
returnValue = methodValue && methodValue.jquery ?
533
returnValue.pushStack( methodValue.get() ) :
539
this.each(function() {
540
var instance = $.data( this, fullName );
542
instance.option( options || {} )._init();
544
new object( options, this );
553
$.Widget = function( options, element ) {};
554
$.Widget._childConstructors = [];
556
$.Widget.prototype = {
557
widgetName: "widget",
558
widgetEventPrefix: "",
559
defaultElement: "<div>",
566
_createWidget: function( options, element ) {
567
element = $( element || this.defaultElement || this )[ 0 ];
568
this.element = $( element );
570
this.eventNamespace = "." + this.widgetName + this.uuid;
571
this.options = $.widget.extend( {},
573
this._getCreateOptions(),
577
this.hoverable = $();
578
this.focusable = $();
580
if ( element !== this ) {
582
// TODO remove dual storage
583
$.data( element, this.widgetName, this );
584
$.data( element, this.widgetFullName, this );
585
this._on({ remove: "destroy" });
586
this.document = $( element.style ?
587
// element within the document
588
element.ownerDocument :
589
// element is window or document
590
element.document || element );
591
this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
595
this._trigger( "create", null, this._getCreateEventData() );
598
_getCreateOptions: $.noop,
599
_getCreateEventData: $.noop,
603
destroy: function() {
605
// we can probably remove the unbind calls in 2.0
606
// all event bindings should go through this._on()
608
.unbind( this.eventNamespace )
610
// TODO remove dual storage
611
.removeData( this.widgetName )
612
.removeData( this.widgetFullName )
613
// support: jquery <1.6.3
614
// http://bugs.jquery.com/ticket/9413
615
.removeData( $.camelCase( this.widgetFullName ) );
617
.unbind( this.eventNamespace )
618
.removeAttr( "aria-disabled" )
620
this.widgetFullName + "-disabled " +
621
"ui-state-disabled" );
623
// clean up events and states
624
this.bindings.unbind( this.eventNamespace );
625
this.hoverable.removeClass( "ui-state-hover" );
626
this.focusable.removeClass( "ui-state-focus" );
634
option: function( key, value ) {
640
if ( arguments.length === 0 ) {
641
// don't return a reference to the internal hash
642
return $.widget.extend( {}, this.options );
645
if ( typeof key === "string" ) {
646
// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
648
parts = key.split( "." );
650
if ( parts.length ) {
651
curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
652
for ( i = 0; i < parts.length - 1; i++ ) {
653
curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
654
curOption = curOption[ parts[ i ] ];
657
if ( value === undefined ) {
658
return curOption[ key ] === undefined ? null : curOption[ key ];
660
curOption[ key ] = value;
662
if ( value === undefined ) {
663
return this.options[ key ] === undefined ? null : this.options[ key ];
665
options[ key ] = value;
669
this._setOptions( options );
673
_setOptions: function( options ) {
676
for ( key in options ) {
677
this._setOption( key, options[ key ] );
682
_setOption: function( key, value ) {
683
this.options[ key ] = value;
685
if ( key === "disabled" ) {
687
.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
688
.attr( "aria-disabled", value );
689
this.hoverable.removeClass( "ui-state-hover" );
690
this.focusable.removeClass( "ui-state-focus" );
697
return this._setOption( "disabled", false );
699
disable: function() {
700
return this._setOption( "disabled", true );
703
_on: function( element, handlers ) {
704
// no element argument, shuffle and use this.element
707
element = this.element;
709
// accept selectors, DOM elements
710
element = $( element );
711
this.bindings = this.bindings.add( element );
715
$.each( handlers, function( event, handler ) {
716
function handlerProxy() {
717
// allow widgets to customize the disabled handling
718
// - disabled as an array instead of boolean
719
// - disabled class as method for disabling individual parts
720
if ( instance.options.disabled === true ||
721
$( this ).hasClass( "ui-state-disabled" ) ) {
724
return ( typeof handler === "string" ? instance[ handler ] : handler )
725
.apply( instance, arguments );
728
// copy the guid so direct unbinding works
729
if ( typeof handler !== "string" ) {
730
handlerProxy.guid = handler.guid =
731
handler.guid || handlerProxy.guid || $.guid++;
734
var match = event.match( /^(\w+)\s*(.*)$/ ),
735
eventName = match[1] + instance.eventNamespace,
738
instance.widget().delegate( selector, eventName, handlerProxy );
740
element.bind( eventName, handlerProxy );
745
_off: function( element, eventName ) {
746
eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
747
element.unbind( eventName ).undelegate( eventName );
750
_delay: function( handler, delay ) {
751
function handlerProxy() {
752
return ( typeof handler === "string" ? instance[ handler ] : handler )
753
.apply( instance, arguments );
756
return setTimeout( handlerProxy, delay || 0 );
759
_hoverable: function( element ) {
760
this.hoverable = this.hoverable.add( element );
762
mouseenter: function( event ) {
763
$( event.currentTarget ).addClass( "ui-state-hover" );
765
mouseleave: function( event ) {
766
$( event.currentTarget ).removeClass( "ui-state-hover" );
771
_focusable: function( element ) {
772
this.focusable = this.focusable.add( element );
774
focusin: function( event ) {
775
$( event.currentTarget ).addClass( "ui-state-focus" );
777
focusout: function( event ) {
778
$( event.currentTarget ).removeClass( "ui-state-focus" );
783
_trigger: function( type, event, data ) {
785
callback = this.options[ type ];
788
event = $.Event( event );
789
event.type = ( type === this.widgetEventPrefix ?
791
this.widgetEventPrefix + type ).toLowerCase();
792
// the original event may come from any element
793
// so we need to reset the target on the new event
794
event.target = this.element[ 0 ];
796
// copy original event properties over to the new event
797
orig = event.originalEvent;
799
for ( prop in orig ) {
800
if ( !( prop in event ) ) {
801
event[ prop ] = orig[ prop ];
806
this.element.trigger( event, data );
807
return !( $.isFunction( callback ) &&
808
callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
809
event.isDefaultPrevented() );
813
$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
814
$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
815
if ( typeof options === "string" ) {
816
options = { effect: options };
819
effectName = !options ?
821
options === true || typeof options === "number" ?
823
options.effect || defaultEffect;
824
options = options || {};
825
if ( typeof options === "number" ) {
826
options = { duration: options };
828
hasOptions = !$.isEmptyObject( options );
829
options.complete = callback;
830
if ( options.delay ) {
831
element.delay( options.delay );
833
if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) {
834
element[ method ]( options );
835
} else if ( effectName !== method && element[ effectName ] ) {
836
element[ effectName ]( options.duration, options.easing, callback );
838
element.queue(function( next ) {
839
$( this )[ method ]();
841
callback.call( element[ 0 ] );
850
if ( $.uiBackCompat !== false ) {
851
$.Widget.prototype._getCreateOptions = function() {
852
return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
858
(function( $, undefined ) {
860
$.widget( "mobile.widget", {
861
// decorate the parent _createWidget to trigger `widgetinit` for users
862
// who wish to do post post `widgetcreate` alterations/additions
864
// TODO create a pull request for jquery ui to trigger this event
865
// in the original _createWidget
866
_createWidget: function() {
867
$.Widget.prototype._createWidget.apply( this, arguments );
868
this._trigger( 'init' );
871
_getCreateOptions: function() {
873
var elem = this.element,
876
$.each( this.options, function( option ) {
878
var value = elem.jqmData( option.replace( /[A-Z]/g, function( c ) {
879
return "-" + c.toLowerCase();
883
if ( value !== undefined ) {
884
options[ option ] = value;
891
enhanceWithin: function( target, useKeepNative ) {
892
this.enhance( $( this.options.initSelector, $( target )), useKeepNative );
895
enhance: function( targets, useKeepNative ) {
896
var page, keepNative, $widgetElements = $( targets ), self = this;
898
// if ignoreContentEnabled is set to true the framework should
899
// only enhance the selected elements when they do NOT have a
900
// parent with the data-namespace-ignore attribute
901
$widgetElements = $.mobile.enhanceable( $widgetElements );
903
if ( useKeepNative && $widgetElements.length ) {
904
// TODO remove dependency on the page widget for the keepNative.
905
// Currently the keepNative value is defined on the page prototype so
906
// the method is as well
907
page = $.mobile.closestPageData( $widgetElements );
908
keepNative = ( page && page.keepNativeSelector()) || "";
910
$widgetElements = $widgetElements.not( keepNative );
913
$widgetElements[ this.widgetName ]();
916
raise: function( msg ) {
917
throw "Widget [" + this.widgetName + "]: " + msg;
924
(function( $, window ) {
926
// NOTE global mobile object settings
927
$.extend( $.mobile, {
928
// DEPRECATED Should the text be visble in the loading message?
929
loadingMessageTextVisible: undefined,
931
// DEPRECATED When the text is visible, what theme does the loading box use?
932
loadingMessageTheme: undefined,
934
// DEPRECATED default message setting
935
loadingMessage: undefined,
938
// Turn on/off page loading message. Theme doubles as an object argument
939
// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
940
// NOTE that the $.mobile.loading* settings and params past the first are deprecated
941
showPageLoadingMsg: function( theme, msgText, textonly ) {
942
$.mobile.loading( 'show', theme, msgText, textonly );
946
hidePageLoadingMsg: function() {
947
$.mobile.loading( 'hide' );
950
loading: function() {
951
this.loaderWidget.loader.apply( this.loaderWidget, arguments );
955
// TODO move loader class down into the widget settings
956
var loaderClass = "ui-loader", $html = $( "html" ), $window = $( window );
958
$.widget( "mobile.loader", {
959
// NOTE if the global config settings are defined they will override these
962
// the theme for the loading message
965
// whether the text in the loading message is shown
968
// custom html for the inner content of the loading message
971
// the text to be displayed when the popup is shown
975
defaultHtml: "<div class='" + loaderClass + "'>" +
976
"<span class='ui-icon ui-icon-loading'></span>" +
980
// For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top
981
fakeFixLoader: function() {
982
var activeBtn = $( "." + $.mobile.activeBtnClass ).first();
986
top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 ||
987
activeBtn.length && activeBtn.offset().top || 100
991
// check position of loader to see if it appears to be "fixed" to center
992
// if not, use abs positioning
993
checkLoaderPosition: function() {
994
var offset = this.element.offset(),
995
scrollTop = $window.scrollTop(),
996
screenHeight = $.mobile.getScreenHeight();
998
if ( offset.top < scrollTop || ( offset.top - scrollTop ) > screenHeight ) {
999
this.element.addClass( "ui-loader-fakefix" );
1000
this.fakeFixLoader();
1002
.unbind( "scroll", this.checkLoaderPosition )
1003
.bind( "scroll", $.proxy( this.fakeFixLoader, this ) );
1007
resetHtml: function() {
1008
this.element.html( $( this.defaultHtml ).html() );
1011
// Turn on/off page loading message. Theme doubles as an object argument
1012
// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
1013
// NOTE that the $.mobile.loading* settings and params past the first are deprecated
1014
// TODO sweet jesus we need to break some of this out
1015
show: function( theme, msgText, textonly ) {
1016
var textVisible, message, $header, loadSettings;
1020
// use the prototype options so that people can set them globally at
1021
// mobile init. Consistency, it's what's for dinner
1022
if ( $.type(theme) === "object" ) {
1023
loadSettings = $.extend( {}, this.options, theme );
1025
// prefer object property from the param then the old theme setting
1026
theme = loadSettings.theme || $.mobile.loadingMessageTheme;
1028
loadSettings = this.options;
1030
// here we prefer the them value passed as a string argument, then
1031
// we prefer the global option because we can't use undefined default
1032
// prototype options, then the prototype option
1033
theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme;
1036
// set the message text, prefer the param, then the settings object
1037
// then loading message
1038
message = msgText || $.mobile.loadingMessage || loadSettings.text;
1041
$html.addClass( "ui-loading" );
1043
if ( $.mobile.loadingMessage !== false || loadSettings.html ) {
1044
// boolean values require a bit more work :P, supports object properties
1046
if ( $.mobile.loadingMessageTextVisible !== undefined ) {
1047
textVisible = $.mobile.loadingMessageTextVisible;
1049
textVisible = loadSettings.textVisible;
1052
// add the proper css given the options (theme, text, etc)
1053
// Force text visibility if the second argument was supplied, or
1054
// if the text was explicitly set in the object args
1055
this.element.attr("class", loaderClass +
1056
" ui-corner-all ui-body-" + theme +
1057
" ui-loader-" + ( textVisible || msgText || theme.text ? "verbose" : "default" ) +
1058
( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) );
1060
// TODO verify that jquery.fn.html is ok to use in both cases here
1061
// this might be overly defensive in preventing unknowing xss
1062
// if the html attribute is defined on the loading settings, use that
1063
// otherwise use the fallbacks from above
1064
if ( loadSettings.html ) {
1065
this.element.html( loadSettings.html );
1067
this.element.find( "h1" ).text( message );
1070
// attach the loader to the DOM
1071
this.element.appendTo( $.mobile.pageContainer );
1073
// check that the loader is visible
1074
this.checkLoaderPosition();
1076
// on scroll check the loader position
1077
$window.bind( "scroll", $.proxy( this.checkLoaderPosition, this ) );
1082
$html.removeClass( "ui-loading" );
1084
if ( $.mobile.loadingMessage ) {
1085
this.element.removeClass( "ui-loader-fakefix" );
1088
$( window ).unbind( "scroll", this.fakeFixLoader );
1089
$( window ).unbind( "scroll", this.checkLoaderPosition );
1093
$window.bind( 'pagecontainercreate', function() {
1094
$.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader();
1100
// This plugin is an experiment for abstracting away the touch and mouse
1101
// events so that developers don't have to worry about which method of input
1102
// the device their document is loaded on supports.
1104
// The idea here is to allow the developer to register listeners for the
1105
// basic mouse events, such as mousedown, mousemove, mouseup, and click,
1106
// and the plugin will take care of registering the correct listeners
1107
// behind the scenes to invoke the listener at the fastest possible time
1108
// for that device, while still retaining the order of event firing in
1109
// the traditional mouse environment, should multiple handlers be registered
1110
// on the same element for different events.
1112
// The current version exposes the following virtual events to jQuery bind methods:
1113
// "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
1115
(function( $, window, document, undefined ) {
1117
var dataPropertyName = "virtualMouseBindings",
1118
touchTargetPropertyName = "virtualTouchID",
1119
virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
1120
touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
1121
mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
1122
mouseEventProps = $.event.props.concat( mouseHookProps ),
1123
activeDocHandlers = {},
1128
clickBlockList = [],
1129
blockMouseTriggers = false,
1130
blockTouchTriggers = false,
1131
eventCaptureSupported = "addEventListener" in document,
1132
$document = $( document ),
1134
lastTouchID = 0, threshold;
1137
moveDistanceThreshold: 10,
1138
clickDistanceThreshold: 10,
1139
resetTimerDuration: 1500
1142
function getNativeEvent( event ) {
1144
while ( event && typeof event.originalEvent !== "undefined" ) {
1145
event = event.originalEvent;
1150
function createVirtualEvent( event, eventType ) {
1153
oe, props, ne, prop, ct, touch, i, j, len;
1155
event = $.Event( event );
1156
event.type = eventType;
1158
oe = event.originalEvent;
1159
props = $.event.props;
1161
// addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
1162
// https://github.com/jquery/jquery-mobile/issues/3280
1163
if ( t.search( /^(mouse|click)/ ) > -1 ) {
1164
props = mouseEventProps;
1167
// copy original event properties over to the new event
1168
// this would happen if we could call $.event.fix instead of $.Event
1169
// but we don't have a way to force an event to be fixed multiple times
1171
for ( i = props.length, prop; i; ) {
1172
prop = props[ --i ];
1173
event[ prop ] = oe[ prop ];
1177
// make sure that if the mouse and click virtual events are generated
1178
// without a .which one is defined
1179
if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) {
1183
if ( t.search(/^touch/) !== -1 ) {
1184
ne = getNativeEvent( oe );
1186
ct = ne.changedTouches;
1187
touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined );
1190
for ( j = 0, len = touchEventProps.length; j < len; j++) {
1191
prop = touchEventProps[ j ];
1192
event[ prop ] = touch[ prop ];
1200
function getVirtualBindingFlags( element ) {
1207
b = $.data( element, dataPropertyName );
1211
flags[ k ] = flags.hasVirtualBinding = true;
1214
element = element.parentNode;
1219
function getClosestElementWithVirtualBinding( element, eventType ) {
1223
b = $.data( element, dataPropertyName );
1225
if ( b && ( !eventType || b[ eventType ] ) ) {
1228
element = element.parentNode;
1233
function enableTouchBindings() {
1234
blockTouchTriggers = false;
1237
function disableTouchBindings() {
1238
blockTouchTriggers = true;
1241
function enableMouseBindings() {
1243
clickBlockList.length = 0;
1244
blockMouseTriggers = false;
1246
// When mouse bindings are enabled, our
1247
// touch bindings are disabled.
1248
disableTouchBindings();
1251
function disableMouseBindings() {
1252
// When mouse bindings are disabled, our
1253
// touch bindings are enabled.
1254
enableTouchBindings();
1257
function startResetTimer() {
1259
resetTimerID = setTimeout( function() {
1261
enableMouseBindings();
1262
}, $.vmouse.resetTimerDuration );
1265
function clearResetTimer() {
1266
if ( resetTimerID ) {
1267
clearTimeout( resetTimerID );
1272
function triggerVirtualEvent( eventType, event, flags ) {
1275
if ( ( flags && flags[ eventType ] ) ||
1276
( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
1278
ve = createVirtualEvent( event, eventType );
1280
$( event.target).trigger( ve );
1286
function mouseEventCallback( event ) {
1287
var touchID = $.data( event.target, touchTargetPropertyName );
1289
if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) {
1290
var ve = triggerVirtualEvent( "v" + event.type, event );
1292
if ( ve.isDefaultPrevented() ) {
1293
event.preventDefault();
1295
if ( ve.isPropagationStopped() ) {
1296
event.stopPropagation();
1298
if ( ve.isImmediatePropagationStopped() ) {
1299
event.stopImmediatePropagation();
1305
function handleTouchStart( event ) {
1307
var touches = getNativeEvent( event ).touches,
1310
if ( touches && touches.length === 1 ) {
1312
target = event.target;
1313
flags = getVirtualBindingFlags( target );
1315
if ( flags.hasVirtualBinding ) {
1317
lastTouchID = nextTouchID++;
1318
$.data( target, touchTargetPropertyName, lastTouchID );
1322
disableMouseBindings();
1325
var t = getNativeEvent( event ).touches[ 0 ];
1329
triggerVirtualEvent( "vmouseover", event, flags );
1330
triggerVirtualEvent( "vmousedown", event, flags );
1335
function handleScroll( event ) {
1336
if ( blockTouchTriggers ) {
1341
triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
1348
function handleTouchMove( event ) {
1349
if ( blockTouchTriggers ) {
1353
var t = getNativeEvent( event ).touches[ 0 ],
1354
didCancel = didScroll,
1355
moveThreshold = $.vmouse.moveDistanceThreshold,
1356
flags = getVirtualBindingFlags( event.target );
1358
didScroll = didScroll ||
1359
( Math.abs( t.pageX - startX ) > moveThreshold ||
1360
Math.abs( t.pageY - startY ) > moveThreshold );
1363
if ( didScroll && !didCancel ) {
1364
triggerVirtualEvent( "vmousecancel", event, flags );
1367
triggerVirtualEvent( "vmousemove", event, flags );
1371
function handleTouchEnd( event ) {
1372
if ( blockTouchTriggers ) {
1376
disableTouchBindings();
1378
var flags = getVirtualBindingFlags( event.target ),
1380
triggerVirtualEvent( "vmouseup", event, flags );
1383
var ve = triggerVirtualEvent( "vclick", event, flags );
1384
if ( ve && ve.isDefaultPrevented() ) {
1385
// The target of the mouse events that follow the touchend
1386
// event don't necessarily match the target used during the
1387
// touch. This means we need to rely on coordinates for blocking
1388
// any click that is generated.
1389
t = getNativeEvent( event ).changedTouches[ 0 ];
1390
clickBlockList.push({
1391
touchID: lastTouchID,
1396
// Prevent any mouse events that follow from triggering
1397
// virtual event notifications.
1398
blockMouseTriggers = true;
1401
triggerVirtualEvent( "vmouseout", event, flags);
1407
function hasVirtualBindings( ele ) {
1408
var bindings = $.data( ele, dataPropertyName ),
1412
for ( k in bindings ) {
1413
if ( bindings[ k ] ) {
1421
function dummyMouseHandler() {}
1423
function getSpecialEventObject( eventType ) {
1424
var realType = eventType.substr( 1 );
1427
setup: function( data, namespace ) {
1428
// If this is the first virtual mouse binding for this element,
1429
// add a bindings object to its data.
1431
if ( !hasVirtualBindings( this ) ) {
1432
$.data( this, dataPropertyName, {} );
1435
// If setup is called, we know it is the first binding for this
1436
// eventType, so initialize the count for the eventType to zero.
1437
var bindings = $.data( this, dataPropertyName );
1438
bindings[ eventType ] = true;
1440
// If this is the first virtual mouse event for this type,
1441
// register a global handler on the document.
1443
activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
1445
if ( activeDocHandlers[ eventType ] === 1 ) {
1446
$document.bind( realType, mouseEventCallback );
1449
// Some browsers, like Opera Mini, won't dispatch mouse/click events
1450
// for elements unless they actually have handlers registered on them.
1451
// To get around this, we register dummy handlers on the elements.
1453
$( this ).bind( realType, dummyMouseHandler );
1455
// For now, if event capture is not supported, we rely on mouse handlers.
1456
if ( eventCaptureSupported ) {
1457
// If this is the first virtual mouse binding for the document,
1458
// register our touchstart handler on the document.
1460
activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
1462
if ( activeDocHandlers[ "touchstart" ] === 1 ) {
1463
$document.bind( "touchstart", handleTouchStart )
1464
.bind( "touchend", handleTouchEnd )
1466
// On touch platforms, touching the screen and then dragging your finger
1467
// causes the window content to scroll after some distance threshold is
1468
// exceeded. On these platforms, a scroll prevents a click event from being
1469
// dispatched, and on some platforms, even the touchend is suppressed. To
1470
// mimic the suppression of the click event, we need to watch for a scroll
1471
// event. Unfortunately, some platforms like iOS don't dispatch scroll
1472
// events until *AFTER* the user lifts their finger (touchend). This means
1473
// we need to watch both scroll and touchmove events to figure out whether
1474
// or not a scroll happenens before the touchend event is fired.
1476
.bind( "touchmove", handleTouchMove )
1477
.bind( "scroll", handleScroll );
1482
teardown: function( data, namespace ) {
1483
// If this is the last virtual binding for this eventType,
1484
// remove its global handler from the document.
1486
--activeDocHandlers[ eventType ];
1488
if ( !activeDocHandlers[ eventType ] ) {
1489
$document.unbind( realType, mouseEventCallback );
1492
if ( eventCaptureSupported ) {
1493
// If this is the last virtual mouse binding in existence,
1494
// remove our document touchstart listener.
1496
--activeDocHandlers[ "touchstart" ];
1498
if ( !activeDocHandlers[ "touchstart" ] ) {
1499
$document.unbind( "touchstart", handleTouchStart )
1500
.unbind( "touchmove", handleTouchMove )
1501
.unbind( "touchend", handleTouchEnd )
1502
.unbind( "scroll", handleScroll );
1506
var $this = $( this ),
1507
bindings = $.data( this, dataPropertyName );
1509
// teardown may be called when an element was
1510
// removed from the DOM. If this is the case,
1511
// jQuery core may have already stripped the element
1512
// of any data bindings so we need to check it before
1515
bindings[ eventType ] = false;
1518
// Unregister the dummy event handler.
1520
$this.unbind( realType, dummyMouseHandler );
1522
// If this is the last virtual mouse binding on the
1523
// element, remove the binding data from the element.
1525
if ( !hasVirtualBindings( this ) ) {
1526
$this.removeData( dataPropertyName );
1532
// Expose our custom events to the jQuery bind/unbind mechanism.
1534
for ( var i = 0; i < virtualEventNames.length; i++ ) {
1535
$.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
1538
// Add a capture click handler to block clicks.
1539
// Note that we require event capture support for this so if the device
1540
// doesn't support it, we punt for now and rely solely on mouse events.
1541
if ( eventCaptureSupported ) {
1542
document.addEventListener( "click", function( e ) {
1543
var cnt = clickBlockList.length,
1545
x, y, ele, i, o, touchID;
1550
threshold = $.vmouse.clickDistanceThreshold;
1552
// The idea here is to run through the clickBlockList to see if
1553
// the current click event is in the proximity of one of our
1554
// vclick events that had preventDefault() called on it. If we find
1555
// one, then we block the click.
1557
// Why do we have to rely on proximity?
1559
// Because the target of the touch event that triggered the vclick
1560
// can be different from the target of the click event synthesized
1561
// by the browser. The target of a mouse/click event that is syntehsized
1562
// from a touch event seems to be implementation specific. For example,
1563
// some browsers will fire mouse/click events for a link that is near
1564
// a touch event, even though the target of the touchstart/touchend event
1565
// says the user touched outside the link. Also, it seems that with most
1566
// browsers, the target of the mouse/click event is not calculated until the
1567
// time it is dispatched, so if you replace an element that you touched
1568
// with another element, the target of the mouse/click will be the new
1569
// element underneath that point.
1571
// Aside from proximity, we also check to see if the target and any
1572
// of its ancestors were the ones that blocked a click. This is necessary
1573
// because of the strange mouse/click target calculation done in the
1574
// Android 2.1 browser, where if you click on an element, and there is a
1575
// mouse/click handler on one of its ancestors, the target will be the
1576
// innermost child of the touched element, even if that child is no where
1577
// near the point of touch.
1582
for ( i = 0; i < cnt; i++ ) {
1583
o = clickBlockList[ i ];
1586
if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
1587
$.data( ele, touchTargetPropertyName ) === o.touchID ) {
1588
// XXX: We may want to consider removing matches from the block list
1589
// instead of waiting for the reset timer to fire.
1591
e.stopPropagation();
1595
ele = ele.parentNode;
1600
})( jQuery, window, document );
1602
(function( $, undefined ) {
1604
touch: "ontouchend" in document
1607
$.mobile = $.mobile || {};
1608
$.mobile.support = $.mobile.support || {};
1609
$.extend( $.support, support );
1610
$.extend( $.mobile.support, support );
1614
(function( $, window, undefined ) {
1615
// add new event shortcuts
1616
$.each( ( "touchstart touchmove touchend " +
1618
"swipe swipeleft swiperight " +
1619
"scrollstart scrollstop" ).split( " " ), function( i, name ) {
1621
$.fn[ name ] = function( fn ) {
1622
return fn ? this.bind( name, fn ) : this.trigger( name );
1627
$.attrFn[ name ] = true;
1631
var supportTouch = $.mobile.support.touch,
1632
scrollEvent = "touchmove scroll",
1633
touchStartEvent = supportTouch ? "touchstart" : "mousedown",
1634
touchStopEvent = supportTouch ? "touchend" : "mouseup",
1635
touchMoveEvent = supportTouch ? "touchmove" : "mousemove";
1637
function triggerCustomEvent( obj, eventType, event ) {
1638
var originalType = event.type;
1639
event.type = eventType;
1640
$.event.handle.call( obj, event );
1641
event.type = originalType;
1644
// also handles scrollstop
1645
$.event.special.scrollstart = {
1651
var thisObject = this,
1652
$this = $( thisObject ),
1656
function trigger( event, state ) {
1658
triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event );
1661
// iPhone triggers scroll after a small delay; use touchmove instead
1662
$this.bind( scrollEvent, function( event ) {
1664
if ( !$.event.special.scrollstart.enabled ) {
1669
trigger( event, true );
1672
clearTimeout( timer );
1673
timer = setTimeout( function() {
1674
trigger( event, false );
1680
// also handles taphold
1681
$.event.special.tap = {
1682
tapholdThreshold: 750,
1685
var thisObject = this,
1686
$this = $( thisObject );
1688
$this.bind( "vmousedown", function( event ) {
1690
if ( event.which && event.which !== 1 ) {
1694
var origTarget = event.target,
1695
origEvent = event.originalEvent,
1698
function clearTapTimer() {
1699
clearTimeout( timer );
1702
function clearTapHandlers() {
1705
$this.unbind( "vclick", clickHandler )
1706
.unbind( "vmouseup", clearTapTimer );
1707
$( document ).unbind( "vmousecancel", clearTapHandlers );
1710
function clickHandler( event ) {
1713
// ONLY trigger a 'tap' event if the start target is
1714
// the same as the stop target.
1715
if ( origTarget === event.target ) {
1716
triggerCustomEvent( thisObject, "tap", event );
1720
$this.bind( "vmouseup", clearTapTimer )
1721
.bind( "vclick", clickHandler );
1722
$( document ).bind( "vmousecancel", clearTapHandlers );
1724
timer = setTimeout( function() {
1725
triggerCustomEvent( thisObject, "taphold", $.Event( "taphold", { target: origTarget } ) );
1726
}, $.event.special.tap.tapholdThreshold );
1731
// also handles swipeleft, swiperight
1732
$.event.special.swipe = {
1733
scrollSupressionThreshold: 30, // More than this horizontal displacement, and we will suppress scrolling.
1735
durationThreshold: 1000, // More time than this, and it isn't a swipe.
1737
horizontalDistanceThreshold: 30, // Swipe horizontal displacement must be more than this.
1739
verticalDistanceThreshold: 75, // Swipe vertical displacement must be less than this.
1742
var thisObject = this,
1743
$this = $( thisObject );
1745
$this.bind( touchStartEvent, function( event ) {
1746
var data = event.originalEvent.touches ?
1747
event.originalEvent.touches[ 0 ] : event,
1749
time: ( new Date() ).getTime(),
1750
coords: [ data.pageX, data.pageY ],
1751
origin: $( event.target )
1755
function moveHandler( event ) {
1761
var data = event.originalEvent.touches ?
1762
event.originalEvent.touches[ 0 ] : event;
1765
time: ( new Date() ).getTime(),
1766
coords: [ data.pageX, data.pageY ]
1769
// prevent scrolling
1770
if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) {
1771
event.preventDefault();
1775
$this.bind( touchMoveEvent, moveHandler )
1776
.one( touchStopEvent, function( event ) {
1777
$this.unbind( touchMoveEvent, moveHandler );
1779
if ( start && stop ) {
1780
if ( stop.time - start.time < $.event.special.swipe.durationThreshold &&
1781
Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold &&
1782
Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) {
1784
start.origin.trigger( "swipe" )
1785
.trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" );
1788
start = stop = undefined;
1794
scrollstop: "scrollstart",
1798
}, function( event, sourceEvent ) {
1800
$.event.special[ event ] = {
1802
$( this ).bind( sourceEvent, $.noop );
1809
(function( $, undefined ) {
1810
$.extend( $.support, {
1811
orientation: "orientation" in window && "onorientationchange" in window
1816
// throttled resize event
1818
$.event.special.throttledresize = {
1820
$( this ).bind( "resize", handler );
1822
teardown: function() {
1823
$( this ).unbind( "resize", handler );
1828
handler = function() {
1829
curr = ( new Date() ).getTime();
1830
diff = curr - lastCall;
1832
if ( diff >= throttle ) {
1835
$( this ).trigger( "throttledresize" );
1840
clearTimeout( heldCall );
1843
// Promise a held call will still execute
1844
heldCall = setTimeout( handler, throttle - diff );
1853
(function( $, window ) {
1854
var win = $( window ),
1855
event_name = "orientationchange",
1859
initial_orientation_is_landscape,
1860
initial_orientation_is_default,
1861
portrait_map = { "0": true, "180": true };
1863
// It seems that some device/browser vendors use window.orientation values 0 and 180 to
1864
// denote the "default" orientation. For iOS devices, and most other smart-phones tested,
1865
// the default orientation is always "portrait", but in some Android and RIM based tablets,
1866
// the default orientation is "landscape". The following code attempts to use the window
1867
// dimensions to figure out what the current orientation is, and then makes adjustments
1868
// to the to the portrait_map if necessary, so that we can properly decode the
1869
// window.orientation value whenever get_orientation() is called.
1871
// Note that we used to use a media query to figure out what the orientation the browser
1874
// initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)");
1876
// but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1,
1877
// where the browser *ALWAYS* applied the landscape media query. This bug does not
1880
if ( $.support.orientation ) {
1882
// Check the window width and height to figure out what the current orientation
1883
// of the device is at this moment. Note that we've initialized the portrait map
1884
// values to 0 and 180, *AND* we purposely check for landscape so that if we guess
1885
// wrong, , we default to the assumption that portrait is the default orientation.
1886
// We use a threshold check below because on some platforms like iOS, the iPhone
1887
// form-factor can report a larger width than height if the user turns on the
1888
// developer console. The actual threshold value is somewhat arbitrary, we just
1889
// need to make sure it is large enough to exclude the developer console case.
1891
var ww = window.innerWidth || $( window ).width(),
1892
wh = window.innerHeight || $( window ).height(),
1893
landscape_threshold = 50;
1895
initial_orientation_is_landscape = ww > wh && ( ww - wh ) > landscape_threshold;
1898
// Now check to see if the current window.orientation is 0 or 180.
1899
initial_orientation_is_default = portrait_map[ window.orientation ];
1901
// If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR*
1902
// if the initial orientation is portrait, but window.orientation reports 90 or -90, we
1903
// need to flip our portrait_map values because landscape is the default orientation for
1904
// this device/browser.
1905
if ( ( initial_orientation_is_landscape && initial_orientation_is_default ) || ( !initial_orientation_is_landscape && !initial_orientation_is_default ) ) {
1906
portrait_map = { "-90": true, "90": true };
1910
$.event.special.orientationchange = $.extend( {}, $.event.special.orientationchange, {
1912
// If the event is supported natively, return false so that jQuery
1913
// will bind to the event using DOM methods.
1914
if ( $.support.orientation && !$.event.special.orientationchange.disabled ) {
1918
// Get the current orientation to avoid initial double-triggering.
1919
last_orientation = get_orientation();
1921
// Because the orientationchange event doesn't exist, simulate the
1922
// event by testing window dimensions on resize.
1923
win.bind( "throttledresize", handler );
1925
teardown: function() {
1926
// If the event is not supported natively, return false so that
1927
// jQuery will unbind the event using DOM methods.
1928
if ( $.support.orientation && !$.event.special.orientationchange.disabled ) {
1932
// Because the orientationchange event doesn't exist, unbind the
1933
// resize event handler.
1934
win.unbind( "throttledresize", handler );
1936
add: function( handleObj ) {
1937
// Save a reference to the bound event handler.
1938
var old_handler = handleObj.handler;
1941
handleObj.handler = function( event ) {
1942
// Modify event object, adding the .orientation property.
1943
event.orientation = get_orientation();
1945
// Call the originally-bound event handler and return its result.
1946
return old_handler.apply( this, arguments );
1951
// If the event is not supported natively, this handler will be bound to
1952
// the window resize event to simulate the orientationchange event.
1953
function handler() {
1954
// Get the current orientation.
1955
var orientation = get_orientation();
1957
if ( orientation !== last_orientation ) {
1958
// The orientation has changed, so trigger the orientationchange event.
1959
last_orientation = orientation;
1960
win.trigger( event_name );
1964
// Get the current page orientation. This method is exposed publicly, should it
1965
// be needed, as jQuery.event.special.orientationchange.orientation()
1966
$.event.special.orientationchange.orientation = get_orientation = function() {
1967
var isPortrait = true, elem = document.documentElement;
1969
// prefer window orientation to the calculation based on screensize as
1970
// the actual screen resize takes place before or after the orientation change event
1971
// has been fired depending on implementation (eg android 2.3 is before, iphone after).
1972
// More testing is required to determine if a more reliable method of determining the new screensize
1973
// is possible when orientationchange is fired. (eg, use media queries + element + opacity)
1974
if ( $.support.orientation ) {
1975
// if the window orientation registers as 0 or 180 degrees report
1976
// portrait, otherwise landscape
1977
isPortrait = portrait_map[ window.orientation ];
1979
isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1;
1982
return isPortrait ? "portrait" : "landscape";
1985
$.fn[ event_name ] = function( fn ) {
1986
return fn ? this.bind( event_name, fn ) : this.trigger( event_name );
1991
$.attrFn[ event_name ] = true;
1997
(function( $, undefined ) {
1999
var $window = $( window ),
2000
$html = $( "html" );
2002
/* $.mobile.media method: pass a CSS media type or query and get a bool return
2003
note: this feature relies on actual media query support for media queries, though types will work most anywhere
2005
$.mobile.media('screen') // tests for screen media type
2006
$.mobile.media('screen and (min-width: 480px)') // tests for screen media type with window width > 480px
2007
$.mobile.media('@media screen and (-webkit-min-device-pixel-ratio: 2)') // tests for webkit 2x pixel ratio (iPhone 4)
2009
$.mobile.media = (function() {
2010
// TODO: use window.matchMedia once at least one UA implements it
2012
testDiv = $( "<div id='jquery-mediatest'></div>" ),
2013
fakeBody = $( "<body>" ).append( testDiv );
2015
return function( query ) {
2016
if ( !( query in cache ) ) {
2017
var styleBlock = document.createElement( "style" ),
2018
cssrule = "@media " + query + " { #jquery-mediatest { position:absolute; } }";
2020
//must set type for IE!
2021
styleBlock.type = "text/css";
2023
if ( styleBlock.styleSheet ) {
2024
styleBlock.styleSheet.cssText = cssrule;
2026
styleBlock.appendChild( document.createTextNode(cssrule) );
2029
$html.prepend( fakeBody ).prepend( styleBlock );
2030
cache[ query ] = testDiv.css( "position" ) === "absolute";
2031
fakeBody.add( styleBlock ).remove();
2033
return cache[ query ];
2039
(function( $, undefined ) {
2042
function propExists( prop ) {
2043
var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ),
2044
props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " );
2046
for ( var v in props ) {
2047
if ( fbCSS[ props[ v ] ] !== undefined ) {
2053
var fakeBody = $( "<body>" ).prependTo( "html" ),
2054
fbCSS = fakeBody[ 0 ].style,
2055
vendors = [ "Webkit", "Moz", "O" ],
2056
webos = "palmGetResource" in window, //only used to rule out scrollTop
2057
opera = window.opera,
2058
operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]",
2059
bb = window.blackberry && !propExists( "-webkit-transform" ); //only used to rule out box shadow, as it's filled opaque on BB 5 and lower
2062
function validStyle( prop, value, check_vend ) {
2063
var div = document.createElement( 'div' ),
2064
uc = function( txt ) {
2065
return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 );
2067
vend_pref = function( vend ) {
2068
return "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-";
2070
check_style = function( vend ) {
2071
var vend_prop = vend_pref( vend ) + prop + ": " + value + ";",
2072
uc_vend = uc( vend ),
2073
propStyle = uc_vend + uc( prop );
2075
div.setAttribute( "style", vend_prop );
2077
if ( !!div.style[ propStyle ] ) {
2081
check_vends = check_vend ? [ check_vend ] : vendors,
2084
for( var i = 0; i < check_vends.length; i++ ) {
2085
check_style( check_vends[i] );
2090
// Thanks to Modernizr src for this test idea. `perspective` check is limited to Moz to prevent a false positive for 3D transforms on Android.
2091
function transform3dTest() {
2092
var prop = "transform-3d";
2093
return validStyle( 'perspective', '10px', 'moz' ) || $.mobile.media( "(-" + vendors.join( "-" + prop + "),(-" ) + "-" + prop + "),(" + prop + ")" );
2096
// Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting )
2097
function baseTagTest() {
2098
var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/",
2099
base = $( "head base" ),
2104
if ( !base.length ) {
2105
base = fauxEle = $( "<base>", { "href": fauxBase }).appendTo( "head" );
2107
href = base.attr( "href" );
2110
link = $( "<a href='testurl' />" ).prependTo( fakeBody );
2111
rebase = link[ 0 ].href;
2112
base[ 0 ].href = href || location.pathname;
2117
return rebase.indexOf( fauxBase ) === 0;
2121
function cssPointerEventsTest() {
2122
var element = document.createElement( 'x' ),
2123
documentElement = document.documentElement,
2124
getComputedStyle = window.getComputedStyle,
2127
if ( !( 'pointerEvents' in element.style ) ) {
2131
element.style.pointerEvents = 'auto';
2132
element.style.pointerEvents = 'x';
2133
documentElement.appendChild( element );
2134
supports = getComputedStyle &&
2135
getComputedStyle( element, '' ).pointerEvents === 'auto';
2136
documentElement.removeChild( element );
2140
function boundingRect() {
2141
var div = document.createElement( "div" );
2142
return typeof div.getBoundingClientRect !== "undefined";
2145
// non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683
2146
// allows for inclusion of IE 6+, including Windows Mobile 7
2147
$.extend( $.mobile, { browser: {} } );
2148
$.mobile.browser.ie = (function() {
2150
div = document.createElement( "div" ),
2154
div.innerHTML = "<!--[if gt IE " + ( ++v ) + "]><br><![endif]-->";
2157
return v > 4 ? v : !v;
2161
$.extend( $.support, {
2162
cssTransitions: "WebKitTransitionEvent" in window || validStyle( 'transition', 'height 100ms linear' ) && !opera,
2163
pushState: "pushState" in history && "replaceState" in history,
2164
mediaquery: $.mobile.media( "only all" ),
2165
cssPseudoElement: !!propExists( "content" ),
2166
touchOverflow: !!propExists( "overflowScrolling" ),
2167
cssTransform3d: transform3dTest(),
2168
boxShadow: !!propExists( "boxShadow" ) && !bb,
2169
scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos && !operamini,
2170
dynamicBaseTag: baseTagTest(),
2171
cssPointerEvents: cssPointerEventsTest(),
2172
boundingRect: boundingRect()
2178
// $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian)
2179
// or that generally work better browsing in regular http for full page refreshes (Opera Mini)
2180
// Note: This detection below is used as a last resort.
2181
// We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible
2182
var nokiaLTE7_3 = (function() {
2184
var ua = window.navigator.userAgent;
2186
//The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older
2187
return ua.indexOf( "Nokia" ) > -1 &&
2188
( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) &&
2189
ua.indexOf( "AppleWebKit" ) > -1 &&
2190
ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ );
2193
// Support conditions that must be met in order to proceed
2194
// default enhanced qualifications are media query support OR IE 7+
2196
$.mobile.gradeA = function() {
2197
return ( $.support.mediaquery || $.mobile.browser.ie && $.mobile.browser.ie >= 7 ) && ( $.support.boundingRect || $.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null );
2200
$.mobile.ajaxBlacklist =
2201
// BlackBerry browsers, pre-webkit
2202
window.blackberry && !window.WebKitPoint ||
2205
// Symbian webkits pre 7.3
2208
// Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices
2209
// to render the stylesheets when they're referenced before this script, as we'd recommend doing.
2210
// This simply reappends the CSS in place, which for some reason makes it apply
2211
if ( nokiaLTE7_3 ) {
2213
$( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" );
2217
// For ruling out shadows via css
2218
if ( !$.support.boxShadow ) {
2219
$( "html" ).addClass( "ui-mobile-nosupport-boxshadow" );
2224
(function( $, undefined ) {
2226
$.widget( "mobile.page", $.mobile.widget, {
2230
keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')"
2233
_create: function() {
2237
// if false is returned by the callbacks do not create the page
2238
if ( self._trigger( "beforecreate" ) === false ) {
2243
.attr( "tabindex", "0" )
2244
.addClass( "ui-page ui-body-" + self.options.theme )
2245
.bind( "pagebeforehide", function() {
2246
self.removeContainerBackground();
2248
.bind( "pagebeforeshow", function() {
2249
self.setContainerBackground();
2254
removeContainerBackground: function() {
2255
$.mobile.pageContainer.removeClass( "ui-overlay-" + $.mobile.getInheritedTheme( this.element.parent() ) );
2258
// set the page container background to the page theme
2259
setContainerBackground: function( theme ) {
2260
if ( this.options.theme ) {
2261
$.mobile.pageContainer.addClass( "ui-overlay-" + ( theme || this.options.theme ) );
2265
keepNativeSelector: function() {
2266
var options = this.options,
2267
keepNativeDefined = options.keepNative && $.trim( options.keepNative );
2269
if ( keepNativeDefined && options.keepNative !== options.keepNativeDefault ) {
2270
return [options.keepNative, options.keepNativeDefault].join( ", " );
2273
return options.keepNativeDefault;
2278
// Script: jQuery hashchange event
2280
// *Version: 1.3, Last updated: 7/21/2010*
2282
// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
2283
// GitHub - http://github.com/cowboy/jquery-hashchange/
2284
// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
2285
// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
2289
// Copyright (c) 2010 "Cowboy" Ben Alman,
2290
// Dual licensed under the MIT and GPL licenses.
2291
// http://benalman.com/about/license/
2295
// These working examples, complete with fully commented code, illustrate a few
2296
// ways in which this plugin can be used.
2298
// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
2299
// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
2301
// About: Support and Testing
2303
// Information about what version or versions of jQuery this plugin has been
2304
// tested with, what browsers it has been tested in, and where the unit tests
2305
// reside (so you can test it yourself).
2307
// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
2308
// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
2309
// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
2310
// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/
2312
// About: Known issues
2314
// While this jQuery hashchange event implementation is quite stable and
2315
// robust, there are a few unfortunate browser bugs surrounding expected
2316
// hashchange event-based behaviors, independent of any JavaScript
2317
// window.onhashchange abstraction. See the following examples for more
2320
// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
2321
// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
2322
// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
2323
// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
2325
// Also note that should a browser natively support the window.onhashchange
2326
// event, but not report that it does, the fallback polling loop will be used.
2328
// About: Release History
2330
// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
2331
// "removable" for mobile-only development. Added IE6/7 document.title
2332
// support. Attempted to make Iframe as hidden as possible by using
2333
// techniques from http://www.paciellogroup.com/blog/?p=604. Added
2334
// support for the "shortcut" format $(window).hashchange( fn ) and
2335
// $(window).hashchange() like jQuery provides for built-in events.
2336
// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
2337
// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
2338
// and <jQuery.fn.hashchange.src> properties plus document-domain.html
2339
// file to address access denied issues when setting document.domain in
2341
// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
2342
// from a page on another domain would cause an error in Safari 4. Also,
2343
// IE6/7 Iframe is now inserted after the body (this actually works),
2344
// which prevents the page from scrolling when the event is first bound.
2345
// Event can also now be bound before DOM ready, but it won't be usable
2346
// before then in IE6/7.
2347
// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
2348
// where browser version is incorrectly reported as 8.0, despite
2349
// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
2350
// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
2351
// window.onhashchange functionality into a separate plugin for users
2352
// who want just the basic event & back button support, without all the
2353
// extra awesomeness that BBQ provides. This plugin will be included as
2354
// part of jQuery BBQ, but also be available separately.
2356
(function( $, window, undefined ) {
2358
var str_hashchange = 'hashchange',
2360
// Method / object references.
2363
special = $.event.special,
2365
// Does the browser support window.onhashchange? Note that IE8 running in
2366
// IE7 compatibility mode reports true for 'onhashchange' in window, even
2367
// though the event isn't supported, so also test document.documentMode.
2368
doc_mode = doc.documentMode,
2369
supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
2371
// Get location.hash (or what you'd expect location.hash to be) sans any
2372
// leading #. Thanks for making this necessary, Firefox!
2373
function get_fragment( url ) {
2374
url = url || location.href;
2375
return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
2378
// Method: jQuery.fn.hashchange
2380
// Bind a handler to the window.onhashchange event or trigger all bound
2381
// window.onhashchange event handlers. This behavior is consistent with
2382
// jQuery's built-in event handlers.
2386
// > jQuery(window).hashchange( [ handler ] );
2390
// handler - (Function) Optional handler to be bound to the hashchange
2391
// event. This is a "shortcut" for the more verbose form:
2392
// jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
2393
// all bound window.onhashchange event handlers will be triggered. This
2394
// is a shortcut for the more verbose
2395
// jQuery(window).trigger( 'hashchange' ). These forms are described in
2396
// the <hashchange event> section.
2400
// (jQuery) The initial jQuery collection of elements.
2402
// Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
2403
// $(elem).hashchange() for triggering, like jQuery does for built-in events.
2404
$.fn[ str_hashchange ] = function( fn ) {
2405
return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
2408
// Property: jQuery.fn.hashchange.delay
2410
// The numeric interval (in milliseconds) at which the <hashchange event>
2411
// polling loop executes. Defaults to 50.
2413
// Property: jQuery.fn.hashchange.domain
2415
// If you're setting document.domain in your JavaScript, and you want hash
2416
// history to work in IE6/7, not only must this property be set, but you must
2417
// also set document.domain BEFORE jQuery is loaded into the page. This
2418
// property is only applicable if you are supporting IE6/7 (or IE8 operating
2419
// in "IE7 compatibility" mode).
2421
// In addition, the <jQuery.fn.hashchange.src> property must be set to the
2422
// path of the included "document-domain.html" file, which can be renamed or
2423
// modified if necessary (note that the document.domain specified must be the
2424
// same in both your main JavaScript as well as in this file).
2428
// jQuery.fn.hashchange.domain = document.domain;
2430
// Property: jQuery.fn.hashchange.src
2432
// If, for some reason, you need to specify an Iframe src file (for example,
2433
// when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
2434
// do so using this property. Note that when using this property, history
2435
// won't be recorded in IE6/7 until the Iframe src file loads. This property
2436
// is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
2437
// compatibility" mode).
2441
// jQuery.fn.hashchange.src = 'path/to/file.html';
2443
$.fn[ str_hashchange ].delay = 50;
2445
$.fn[ str_hashchange ].domain = null;
2446
$.fn[ str_hashchange ].src = null;
2449
// Event: hashchange event
2451
// Fired when location.hash changes. In browsers that support it, the native
2452
// HTML5 window.onhashchange event is used, otherwise a polling loop is
2453
// initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
2454
// see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
2455
// compatibility" mode), a hidden Iframe is created to allow the back button
2456
// and hash-based history to work.
2458
// Usage as described in <jQuery.fn.hashchange>:
2460
// > // Bind an event handler.
2461
// > jQuery(window).hashchange( function(e) {
2462
// > var hash = location.hash;
2466
// > // Manually trigger the event handler.
2467
// > jQuery(window).hashchange();
2469
// A more verbose usage that allows for event namespacing:
2471
// > // Bind an event handler.
2472
// > jQuery(window).bind( 'hashchange', function(e) {
2473
// > var hash = location.hash;
2477
// > // Manually trigger the event handler.
2478
// > jQuery(window).trigger( 'hashchange' );
2480
// Additional Notes:
2482
// * The polling loop and Iframe are not created until at least one handler
2483
// is actually bound to the 'hashchange' event.
2484
// * If you need the bound handler(s) to execute immediately, in cases where
2485
// a location.hash exists on page load, via bookmark or page refresh for
2486
// example, use jQuery(window).hashchange() or the more verbose
2487
// jQuery(window).trigger( 'hashchange' ).
2488
// * The event can be bound before DOM ready, but since it won't be usable
2489
// before then in IE6/7 (due to the necessary Iframe), recommended usage is
2490
// to bind it inside a DOM ready handler.
2492
// Override existing $.event.special.hashchange methods (allowing this plugin
2493
// to be defined after jQuery BBQ in BBQ's source code).
2494
special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
2496
// Called only when the first 'hashchange' event is bound to window.
2498
// If window.onhashchange is supported natively, there's nothing to do..
2499
if ( supports_onhashchange ) { return false; }
2501
// Otherwise, we need to create our own. And we don't want to call this
2502
// until the user binds to the event, just in case they never do, since it
2503
// will create a polling loop and possibly even a hidden Iframe.
2504
$( fake_onhashchange.start );
2507
// Called only when the last 'hashchange' event is unbound from window.
2508
teardown: function() {
2509
// If window.onhashchange is supported natively, there's nothing to do..
2510
if ( supports_onhashchange ) { return false; }
2512
// Otherwise, we need to stop ours (if possible).
2513
$( fake_onhashchange.stop );
2518
// fake_onhashchange does all the work of triggering the window.onhashchange
2519
// event for browsers that don't natively support it, including creating a
2520
// polling loop to watch for hash changes and in IE 6/7 creating a hidden
2521
// Iframe to enable back and forward.
2522
fake_onhashchange = (function() {
2526
// Remember the initial hash so it doesn't get triggered immediately.
2527
last_hash = get_fragment(),
2529
fn_retval = function( val ) { return val; },
2530
history_set = fn_retval,
2531
history_get = fn_retval;
2533
// Start the polling loop.
2534
self.start = function() {
2535
timeout_id || poll();
2538
// Stop the polling loop.
2539
self.stop = function() {
2540
timeout_id && clearTimeout( timeout_id );
2541
timeout_id = undefined;
2544
// This polling loop checks every $.fn.hashchange.delay milliseconds to see
2545
// if location.hash has changed, and triggers the 'hashchange' event on
2546
// window when necessary.
2548
var hash = get_fragment(),
2549
history_hash = history_get( last_hash );
2551
if ( hash !== last_hash ) {
2552
history_set( last_hash = hash, history_hash );
2554
$(window).trigger( str_hashchange );
2556
} else if ( history_hash !== last_hash ) {
2557
location.href = location.href.replace( /#.*/, '' ) + history_hash;
2560
timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
2563
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
2564
// vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
2565
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
2566
$.browser.msie && !supports_onhashchange && (function() {
2567
// Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
2568
// when running in "IE7 compatibility" mode.
2573
// When the event is bound and polling starts in IE 6/7, create a hidden
2574
// Iframe for history handling.
2575
self.start = function() {
2577
iframe_src = $.fn[ str_hashchange ].src;
2578
iframe_src = iframe_src && iframe_src + get_fragment();
2580
// Create hidden Iframe. Attempt to make Iframe as hidden as possible
2581
// by using techniques from http://www.paciellogroup.com/blog/?p=604.
2582
iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
2584
// When Iframe has completely loaded, initialize the history and
2586
.one( 'load', function() {
2587
iframe_src || history_set( get_fragment() );
2591
// Load Iframe src if specified, otherwise nothing.
2592
.attr( 'src', iframe_src || 'javascript:0' )
2594
// Append Iframe after the end of the body to prevent unnecessary
2595
// initial page scrolling (yes, this works).
2596
.insertAfter( 'body' )[0].contentWindow;
2598
// Whenever `document.title` changes, update the Iframe's title to
2599
// prettify the back/next history menu entries. Since IE sometimes
2600
// errors with "Unspecified error" the very first time this is set
2601
// (yes, very useful) wrap this with a try/catch block.
2602
doc.onpropertychange = function() {
2604
if ( event.propertyName === 'title' ) {
2605
iframe.document.title = doc.title;
2613
// Override the "stop" method since an IE6/7 Iframe was created. Even
2614
// if there are no longer any bound event handlers, the polling loop
2615
// is still necessary for back/next to work at all!
2616
self.stop = fn_retval;
2618
// Get history by looking at the hidden Iframe's location.hash.
2619
history_get = function() {
2620
return get_fragment( iframe.location.href );
2623
// Set a new history item by opening and then closing the Iframe
2624
// document, *then* setting its location.hash. If document.domain has
2625
// been set, update that as well.
2626
history_set = function( hash, history_hash ) {
2627
var iframe_doc = iframe.document,
2628
domain = $.fn[ str_hashchange ].domain;
2630
if ( hash !== history_hash ) {
2631
// Update Iframe with any initial `document.title` that might be set.
2632
iframe_doc.title = doc.title;
2634
// Opening the Iframe's document after it has been closed is what
2635
// actually adds a history entry.
2638
// Set document.domain for the Iframe document as well, if necessary.
2639
domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
2643
// Update the Iframe's hash, for great justice.
2644
iframe.location.hash = hash;
2649
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2650
// ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
2651
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2659
(function( $, window, undefined ) {
2661
var createHandler = function( sequential ) {
2663
// Default to sequential
2664
if ( sequential === undefined ) {
2668
return function( name, reverse, $to, $from ) {
2670
var deferred = new $.Deferred(),
2671
reverseClass = reverse ? " reverse" : "",
2672
active = $.mobile.urlHistory.getActive(),
2673
toScroll = active.lastScroll || $.mobile.defaultHomeScroll,
2674
screenHeight = $.mobile.getScreenHeight(),
2675
maxTransitionOverride = $.mobile.maxTransitionWidth !== false && $( window ).width() > $.mobile.maxTransitionWidth,
2676
none = !$.support.cssTransitions || maxTransitionOverride || !name || name === "none" || Math.max( $( window ).scrollTop(), toScroll ) > $.mobile.getMaxScrollForTransition(),
2677
toPreClass = " ui-page-pre-in",
2678
toggleViewportClass = function() {
2679
$.mobile.pageContainer.toggleClass( "ui-mobile-viewport-transitioning viewport-" + name );
2681
scrollPage = function() {
2682
// By using scrollTo instead of silentScroll, we can keep things better in order
2683
// Just to be precautios, disable scrollstart listening like silentScroll would
2684
$.event.special.scrollstart.enabled = false;
2686
window.scrollTo( 0, toScroll );
2688
// reenable scrollstart listening like silentScroll would
2689
setTimeout( function() {
2690
$.event.special.scrollstart.enabled = true;
2693
cleanFrom = function() {
2695
.removeClass( $.mobile.activePageClass + " out in reverse " + name )
2698
startOut = function() {
2699
// if it's not sequential, call the doneOut transition to start the TO page animating in simultaneously
2700
if ( !sequential ) {
2704
$from.animationComplete( doneOut );
2707
// Set the from page's height and start it transitioning out
2708
// Note: setting an explicit height helps eliminate tiling in the transitions
2710
.height( screenHeight + $( window ).scrollTop() )
2711
.addClass( name + " out" + reverseClass );
2714
doneOut = function() {
2716
if ( $from && sequential ) {
2723
startIn = function() {
2725
// Prevent flickering in phonegap container: see comments at #4024 regarding iOS
2726
$to.css( "z-index", -10 );
2728
$to.addClass( $.mobile.activePageClass + toPreClass );
2730
// Send focus to page as it is now display: block
2731
$.mobile.focusPage( $to );
2733
// Set to page height
2734
$to.height( screenHeight + toScroll );
2738
// Restores visibility of the new page: added together with $to.css( "z-index", -10 );
2739
$to.css( "z-index", "" );
2742
$to.animationComplete( doneIn );
2746
.removeClass( toPreClass )
2747
.addClass( name + " in" + reverseClass );
2755
doneIn = function() {
2757
if ( !sequential ) {
2765
.removeClass( "out in reverse " + name )
2768
toggleViewportClass();
2770
// In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition
2771
// This ensures we jump to that spot after the fact, if we aren't there already.
2772
if ( $( window ).scrollTop() !== toScroll ) {
2776
deferred.resolve( name, reverse, $to, $from, true );
2779
toggleViewportClass();
2781
if ( $from && !none ) {
2788
return deferred.promise();
2792
// generate the handlers from the above
2793
var sequentialHandler = createHandler(),
2794
simultaneousHandler = createHandler( false ),
2795
defaultGetMaxScrollForTransition = function() {
2796
return $.mobile.getScreenHeight() * 3;
2799
// Make our transition handler the public default.
2800
$.mobile.defaultTransitionHandler = sequentialHandler;
2802
//transition handler dictionary for 3rd party transitions
2803
$.mobile.transitionHandlers = {
2804
"default": $.mobile.defaultTransitionHandler,
2805
"sequential": sequentialHandler,
2806
"simultaneous": simultaneousHandler
2809
$.mobile.transitionFallbacks = {};
2811
// If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified
2812
$.mobile._maybeDegradeTransition = function( transition ) {
2813
if ( transition && !$.support.cssTransform3d && $.mobile.transitionFallbacks[ transition ] ) {
2814
transition = $.mobile.transitionFallbacks[ transition ];
2820
// Set the getMaxScrollForTransition to default if no implementation was set by user
2821
$.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition;
2824
(function( $, undefined ) {
2826
//define vars for interal use
2827
var $window = $( window ),
2828
$html = $( 'html' ),
2829
$head = $( 'head' ),
2831
//url path helpers for use in relative url management
2834
// This scary looking regular expression parses an absolute URL or its relative
2835
// variants (protocol, site, document, query, and hash), into the various
2836
// components (protocol, host, path, query, fragment, etc that make up the
2837
// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
2838
// or String.match, it parses the URL into a results array that looks like this:
2840
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
2841
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
2842
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
2843
// [3]: http://jblas:password@mycompany.com:8080
2846
// [6]: jblas:password@mycompany.com:8080
2847
// [7]: jblas:password
2850
// [10]: mycompany.com:8080
2851
// [11]: mycompany.com
2853
// [13]: /mail/inbox
2856
// [16]: ?msg=1234&type=unread
2857
// [17]: #msg-content
2859
urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
2861
// Abstraction to address xss (Issue #4787) by removing the authority in
2862
// browsers that auto decode it. All references to location.href should be
2863
// replaced with a call to this method so that it can be dealt with properly here
2864
getLocation: function( url ) {
2865
var uri = url ? this.parseUrl( url ) : location,
2866
hash = this.parseUrl( url || location.href ).hash;
2868
// mimic the browser with an empty string when the hash is empty
2869
hash = hash === "#" ? "" : hash;
2871
// Make sure to parse the url or the location object for the hash because using location.hash
2872
// is autodecoded in firefox, the rest of the url should be from the object (location unless
2873
// we're testing) to avoid the inclusion of the authority
2874
return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash;
2877
parseLocation: function() {
2878
return this.parseUrl( this.getLocation() );
2881
//Parse a URL into a structure that allows easy access to
2882
//all of the URL components by name.
2883
parseUrl: function( url ) {
2884
// If we're passed an object, we'll assume that it is
2885
// a parsed url object and just return it back to the caller.
2886
if ( $.type( url ) === "object" ) {
2890
var matches = path.urlParseRE.exec( url || "" ) || [];
2892
// Create an object that allows the caller to access the sub-matches
2893
// by name. Note that IE returns an empty string instead of undefined,
2894
// like all other browsers do, so we normalize everything so its consistent
2895
// no matter what browser we're running on.
2897
href: matches[ 0 ] || "",
2898
hrefNoHash: matches[ 1 ] || "",
2899
hrefNoSearch: matches[ 2 ] || "",
2900
domain: matches[ 3 ] || "",
2901
protocol: matches[ 4 ] || "",
2902
doubleSlash: matches[ 5 ] || "",
2903
authority: matches[ 6 ] || "",
2904
username: matches[ 8 ] || "",
2905
password: matches[ 9 ] || "",
2906
host: matches[ 10 ] || "",
2907
hostname: matches[ 11 ] || "",
2908
port: matches[ 12 ] || "",
2909
pathname: matches[ 13 ] || "",
2910
directory: matches[ 14 ] || "",
2911
filename: matches[ 15 ] || "",
2912
search: matches[ 16 ] || "",
2913
hash: matches[ 17 ] || ""
2917
//Turn relPath into an asbolute path. absPath is
2918
//an optional absolute path which describes what
2919
//relPath is relative to.
2920
makePathAbsolute: function( relPath, absPath ) {
2921
if ( relPath && relPath.charAt( 0 ) === "/" ) {
2925
relPath = relPath || "";
2926
absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : "";
2928
var absStack = absPath ? absPath.split( "/" ) : [],
2929
relStack = relPath.split( "/" );
2930
for ( var i = 0; i < relStack.length; i++ ) {
2931
var d = relStack[ i ];
2936
if ( absStack.length ) {
2945
return "/" + absStack.join( "/" );
2948
//Returns true if both urls have the same domain.
2949
isSameDomain: function( absUrl1, absUrl2 ) {
2950
return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain;
2953
//Returns true for any relative variant.
2954
isRelativeUrl: function( url ) {
2955
// All relative Url variants have one thing in common, no protocol.
2956
return path.parseUrl( url ).protocol === "";
2959
//Returns true for an absolute url.
2960
isAbsoluteUrl: function( url ) {
2961
return path.parseUrl( url ).protocol !== "";
2964
//Turn the specified realtive URL into an absolute one. This function
2965
//can handle all relative variants (protocol, site, document, query, fragment).
2966
makeUrlAbsolute: function( relUrl, absUrl ) {
2967
if ( !path.isRelativeUrl( relUrl ) ) {
2971
if ( absUrl === undefined ) {
2972
absUrl = documentBase;
2975
var relObj = path.parseUrl( relUrl ),
2976
absObj = path.parseUrl( absUrl ),
2977
protocol = relObj.protocol || absObj.protocol,
2978
doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ),
2979
authority = relObj.authority || absObj.authority,
2980
hasPath = relObj.pathname !== "",
2981
pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ),
2982
search = relObj.search || ( !hasPath && absObj.search ) || "",
2985
return protocol + doubleSlash + authority + pathname + search + hash;
2988
//Add search (aka query) params to the specified url.
2989
addSearchParams: function( url, params ) {
2990
var u = path.parseUrl( url ),
2991
p = ( typeof params === "object" ) ? $.param( params ) : params,
2992
s = u.search || "?";
2993
return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" );
2996
convertUrlToDataUrl: function( absUrl ) {
2997
var u = path.parseUrl( absUrl );
2998
if ( path.isEmbeddedPage( u ) ) {
2999
// For embedded pages, remove the dialog hash key as in getFilePath(),
3000
// otherwise the Data Url won't match the id of the embedded Page.
3001
return u.hash.split( dialogHashKey )[0].replace( /^#/, "" );
3002
} else if ( path.isSameDomain( u, documentBase ) ) {
3003
return u.hrefNoHash.replace( documentBase.domain, "" ).split( dialogHashKey )[0];
3006
return window.decodeURIComponent(absUrl);
3009
//get path from current hash, or from a file path
3010
get: function( newPath ) {
3011
if ( newPath === undefined ) {
3012
newPath = path.parseLocation().hash;
3014
return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' );
3017
//return the substring of a filepath before the sub-page key, for making a server request
3018
getFilePath: function( path ) {
3019
var splitkey = '&' + $.mobile.subPageUrlKey;
3020
return path && path.split( splitkey )[0].split( dialogHashKey )[0];
3023
//set location hash to path
3024
set: function( path ) {
3025
location.hash = path;
3028
//test if a given url (string) is a path
3029
//NOTE might be exceptionally naive
3030
isPath: function( url ) {
3031
return ( /\// ).test( url );
3034
//return a url path with the window's location protocol/hostname/pathname removed
3035
clean: function( url ) {
3036
return url.replace( documentBase.domain, "" );
3039
//just return the url without an initial #
3040
stripHash: function( url ) {
3041
return url.replace( /^#/, "" );
3044
//remove the preceding hash, any query params, and dialog notations
3045
cleanHash: function( hash ) {
3046
return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
3049
isHashValid: function( hash ) {
3050
return ( /^#[^#]+$/ ).test( hash );
3053
//check whether a url is referencing the same domain, or an external domain or different protocol
3054
//could be mailto, etc
3055
isExternal: function( url ) {
3056
var u = path.parseUrl( url );
3057
return u.protocol && u.domain !== documentUrl.domain ? true : false;
3060
hasProtocol: function( url ) {
3061
return ( /^(:?\w+:)/ ).test( url );
3064
//check if the specified url refers to the first page in the main application document.
3065
isFirstPageUrl: function( url ) {
3066
// We only deal with absolute paths.
3067
var u = path.parseUrl( path.makeUrlAbsolute( url, documentBase ) ),
3069
// Does the url have the same path as the document?
3070
samePath = u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ),
3072
// Get the first page element.
3073
fp = $.mobile.firstPage,
3075
// Get the id of the first page element if it has one.
3076
fpId = fp && fp[0] ? fp[0].id : undefined;
3078
// The url refers to the first page if the path matches the document and
3079
// it either has no hash value, or the hash is exactly equal to the id of the
3080
// first page element.
3081
return samePath && ( !u.hash || u.hash === "#" || ( fpId && u.hash.replace( /^#/, "" ) === fpId ) );
3084
isEmbeddedPage: function( url ) {
3085
var u = path.parseUrl( url );
3087
//if the path is absolute, then we need to compare the url against
3088
//both the documentUrl and the documentBase. The main reason for this
3089
//is that links embedded within external documents will refer to the
3090
//application document, whereas links embedded within the application
3091
//document will be resolved against the document base.
3092
if ( u.protocol !== "" ) {
3093
return ( u.hash && ( u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ) ) );
3095
return ( /^#/ ).test( u.href );
3099
// Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
3100
// requests if the document doing the request was loaded via the file:// protocol.
3101
// This is usually to allow the application to "phone home" and fetch app specific
3102
// data. We normally let the browser handle external/cross-domain urls, but if the
3103
// allowCrossDomainPages option is true, we will allow cross-domain http/https
3104
// requests to go through our page loading logic.
3105
isPermittedCrossDomainRequest: function( docUrl, reqUrl ) {
3106
return $.mobile.allowCrossDomainPages &&
3107
docUrl.protocol === "file:" &&
3108
reqUrl.search( /^https?:/ ) !== -1;
3112
//will be defined when a link is clicked and given an active class
3113
$activeClickedLink = null,
3115
//urlHistory is purely here to make guesses at whether the back or forward button was clicked
3116
//and provide an appropriate transition
3118
// Array of pages that are visited during a single page load.
3119
// Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs)
3122
//maintain an index number for the active page in the stack
3126
getActive: function() {
3127
return urlHistory.stack[ urlHistory.activeIndex ];
3130
getPrev: function() {
3131
return urlHistory.stack[ urlHistory.activeIndex - 1 ];
3134
getNext: function() {
3135
return urlHistory.stack[ urlHistory.activeIndex + 1 ];
3138
// addNew is used whenever a new page is added
3139
addNew: function( url, transition, title, pageUrl, role ) {
3140
//if there's forward history, wipe it
3141
if ( urlHistory.getNext() ) {
3142
urlHistory.clearForward();
3145
urlHistory.stack.push( {url : url, transition: transition, title: title, pageUrl: pageUrl, role: role } );
3147
urlHistory.activeIndex = urlHistory.stack.length - 1;
3150
//wipe urls ahead of active index
3151
clearForward: function() {
3152
urlHistory.stack = urlHistory.stack.slice( 0, urlHistory.activeIndex + 1 );
3155
directHashChange: function( opts ) {
3156
var back , forward, newActiveIndex, prev = this.getActive();
3158
// check if url is in history and if it's ahead or behind current page
3159
$.each( urlHistory.stack, function( i, historyEntry ) {
3161
//if the url is in the stack, it's a forward or a back
3162
if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) {
3163
//define back and forward by whether url is older or newer than current page
3164
back = i < urlHistory.activeIndex;
3170
// save new page index, null check to prevent falsey 0 result
3171
this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex;
3174
( opts.either || opts.isBack )( true );
3175
} else if ( forward ) {
3176
( opts.either || opts.isForward )( false );
3180
//disable hashchange event listener internally to ignore one change
3181
//toggled internally when location.hash is updated to match the url of a successful page load
3182
ignoreNextHashChange: false
3185
//define first selector to receive focus when a page is shown
3186
focusable = "[tabindex],a,button:visible,select:visible,input",
3188
//queue to hold simultanious page transitions
3189
pageTransitionQueue = [],
3191
//indicates whether or not page is in process of transitioning
3192
isPageTransitioning = false,
3194
//nonsense hash change key for dialogs, so they create a history entry
3195
dialogHashKey = "&ui-state=dialog",
3197
//existing base tag?
3198
$base = $head.children( "base" ),
3200
//tuck away the original document URL minus any fragment.
3201
documentUrl = path.parseLocation(),
3203
//if the document has an embedded base tag, documentBase is set to its
3204
//initial value. If a base tag does not exist, then we default to the documentUrl.
3205
documentBase = $base.length ? path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), documentUrl.href ) ) : documentUrl,
3207
//cache the comparison once.
3208
documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ),
3210
getScreenHeight = $.mobile.getScreenHeight;
3212
//base element management, defined depending on dynamic base tag support
3213
var base = $.support.dynamicBaseTag ? {
3215
//define base element, for use in routing asset urls that are referenced in Ajax-requested markup
3216
element: ( $base.length ? $base : $( "<base>", { href: documentBase.hrefNoHash } ).prependTo( $head ) ),
3218
//set the generated BASE element's href attribute to a new page's base path
3219
set: function( href ) {
3220
base.element.attr( "href", path.makeUrlAbsolute( href, documentBase ) );
3223
//set the generated BASE element's href attribute to a new page's base path
3225
base.element.attr( "href", documentBase.hrefNoHash );
3230
/* internal utility functions */
3232
// NOTE Issue #4950 Android phonegap doesn't navigate back properly
3233
// when a full page refresh has taken place. It appears that hashchange
3234
// and replacestate history alterations work fine but we need to support
3235
// both forms of history traversal in our code that uses backward history
3237
$.mobile.back = function() {
3238
var nav = window.navigator;
3240
// if the setting is on and the navigator object is
3241
// available use the phonegap navigation capability
3242
if( this.phonegapNavigationEnabled &&
3245
nav.app.backHistory ){
3246
nav.app.backHistory();
3248
window.history.back();
3252
//direct focus to the page title, or otherwise first focusable element
3253
$.mobile.focusPage = function ( page ) {
3254
var autofocus = page.find( "[autofocus]" ),
3255
pageTitle = page.find( ".ui-title:eq(0)" );
3257
if ( autofocus.length ) {
3262
if ( pageTitle.length ) {
3269
//remove active classes after page transition or error
3270
function removeActiveLinkClass( forceRemoval ) {
3271
if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) {
3272
$activeClickedLink.removeClass( $.mobile.activeBtnClass );
3274
$activeClickedLink = null;
3277
function releasePageTransitionLock() {
3278
isPageTransitioning = false;
3279
if ( pageTransitionQueue.length > 0 ) {
3280
$.mobile.changePage.apply( null, pageTransitionQueue.pop() );
3284
// Save the last scroll distance per page, before it is hidden
3285
var setLastScrollEnabled = true,
3286
setLastScroll, delayedSetLastScroll;
3288
setLastScroll = function() {
3289
// this barrier prevents setting the scroll value based on the browser
3290
// scrolling the window based on a hashchange
3291
if ( !setLastScrollEnabled ) {
3295
var active = $.mobile.urlHistory.getActive();
3298
var lastScroll = $window.scrollTop();
3300
// Set active page's lastScroll prop.
3301
// If the location we're scrolling to is less than minScrollBack, let it go.
3302
active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll;
3306
// bind to scrollstop to gather scroll position. The delay allows for the hashchange
3307
// event to fire and disable scroll recording in the case where the browser scrolls
3308
// to the hash targets location (sometimes the top of the page). once pagechange fires
3309
// getLastScroll is again permitted to operate
3310
delayedSetLastScroll = function() {
3311
setTimeout( setLastScroll, 100 );
3314
// disable an scroll setting when a hashchange has been fired, this only works
3315
// because the recording of the scroll position is delayed for 100ms after
3316
// the browser might have changed the position because of the hashchange
3317
$window.bind( $.support.pushState ? "popstate" : "hashchange", function() {
3318
setLastScrollEnabled = false;
3321
// handle initial hashchange from chrome :(
3322
$window.one( $.support.pushState ? "popstate" : "hashchange", function() {
3323
setLastScrollEnabled = true;
3326
// wait until the mobile page container has been determined to bind to pagechange
3327
$window.one( "pagecontainercreate", function() {
3328
// once the page has changed, re-enable the scroll recording
3329
$.mobile.pageContainer.bind( "pagechange", function() {
3331
setLastScrollEnabled = true;
3333
// remove any binding that previously existed on the get scroll
3334
// which may or may not be different than the scroll element determined for
3335
// this page previously
3336
$window.unbind( "scrollstop", delayedSetLastScroll );
3338
// determine and bind to the current scoll element which may be the window
3339
// or in the case of touch overflow the element with touch overflow
3340
$window.bind( "scrollstop", delayedSetLastScroll );
3344
// bind to scrollstop for the first page as "pagechange" won't be fired in that case
3345
$window.bind( "scrollstop", delayedSetLastScroll );
3347
// No-op implementation of transition degradation
3348
$.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) {
3352
//function for transitioning between two existing pages
3353
function transitionPages( toPage, fromPage, transition, reverse ) {
3356
//trigger before show/hide events
3357
fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } );
3360
toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } );
3363
$.mobile.hidePageLoadingMsg();
3365
transition = $.mobile._maybeDegradeTransition( transition );
3367
//find the transition handler for the specified transition. If there
3368
//isn't one in our transitionHandlers dictionary, use the default one.
3369
//call the handler immediately to kick-off the transition.
3370
var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler,
3371
promise = th( transition, reverse, toPage, fromPage );
3373
promise.done(function() {
3375
//trigger show/hide events
3377
fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } );
3380
//trigger pageshow, define prevPage as either fromPage or empty jQuery obj
3381
toPage.data( "page" )._trigger( "show", null, { prevPage: fromPage || $( "" ) } );
3387
//simply set the active page's minimum height to screen height, depending on orientation
3388
function resetActivePageHeight() {
3389
var aPage = $( "." + $.mobile.activePageClass ),
3390
aPagePadT = parseFloat( aPage.css( "padding-top" ) ),
3391
aPagePadB = parseFloat( aPage.css( "padding-bottom" ) ),
3392
aPageBorderT = parseFloat( aPage.css( "border-top-width" ) ),
3393
aPageBorderB = parseFloat( aPage.css( "border-bottom-width" ) );
3395
aPage.css( "min-height", getScreenHeight() - aPagePadT - aPagePadB - aPageBorderT - aPageBorderB );
3398
//shared page enhancements
3399
function enhancePage( $page, role ) {
3400
// If a role was specified, make sure the data-role attribute
3401
// on the page element is in sync.
3403
$page.attr( "data-" + $.mobile.ns + "role", role );
3410
/* exposed $.mobile methods */
3412
//animation complete callback
3413
$.fn.animationComplete = function( callback ) {
3414
if ( $.support.cssTransitions ) {
3415
return $( this ).one( 'webkitAnimationEnd animationend', callback );
3418
// defer execution for consistency between webkit/non webkit
3419
setTimeout( callback, 0 );
3424
//expose path object on $.mobile
3425
$.mobile.path = path;
3427
//expose base object on $.mobile
3428
$.mobile.base = base;
3431
$.mobile.urlHistory = urlHistory;
3433
$.mobile.dialogHashKey = dialogHashKey;
3437
//enable cross-domain page support
3438
$.mobile.allowCrossDomainPages = false;
3440
//return the original document url
3441
$.mobile.getDocumentUrl = function( asParsedObject ) {
3442
return asParsedObject ? $.extend( {}, documentUrl ) : documentUrl.href;
3445
//return the original document base url
3446
$.mobile.getDocumentBase = function( asParsedObject ) {
3447
return asParsedObject ? $.extend( {}, documentBase ) : documentBase.href;
3450
$.mobile._bindPageRemove = function() {
3451
var page = $( this );
3453
// when dom caching is not enabled or the page is embedded bind to remove the page on hide
3454
if ( !page.data( "page" ).options.domCache &&
3455
page.is( ":jqmData(external-page='true')" ) ) {
3457
page.bind( 'pagehide.remove', function() {
3458
var $this = $( this ),
3459
prEvent = new $.Event( "pageremove" );
3461
$this.trigger( prEvent );
3463
if ( !prEvent.isDefaultPrevented() ) {
3464
$this.removeWithDependents();
3470
// Load a page into the DOM.
3471
$.mobile.loadPage = function( url, options ) {
3472
// This function uses deferred notifications to let callers
3473
// know when the page is done loading, or if an error has occurred.
3474
var deferred = $.Deferred(),
3476
// The default loadPage options with overrides specified by
3478
settings = $.extend( {}, $.mobile.loadPage.defaults, options ),
3480
// The DOM element for the page after it has been loaded.
3483
// If the reloadPage option is true, and the page is already
3484
// in the DOM, dupCachedPage will be set to the page element
3485
// so that it can be removed after the new version of the
3486
// page is loaded off the network.
3487
dupCachedPage = null,
3489
// determine the current base url
3490
findBaseWithDefault = function() {
3491
var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) );
3492
return closestBase || documentBase.hrefNoHash;
3495
// The absolute version of the URL passed into the function. This
3496
// version of the URL may contain dialog/subpage params in it.
3497
absUrl = path.makeUrlAbsolute( url, findBaseWithDefault() );
3500
// If the caller provided data, and we're using "get" request,
3501
// append the data to the URL.
3502
if ( settings.data && settings.type === "get" ) {
3503
absUrl = path.addSearchParams( absUrl, settings.data );
3504
settings.data = undefined;
3507
// If the caller is using a "post" request, reloadPage must be true
3508
if ( settings.data && settings.type === "post" ) {
3509
settings.reloadPage = true;
3512
// The absolute version of the URL minus any dialog/subpage params.
3513
// In otherwords the real URL of the page to be loaded.
3514
var fileUrl = path.getFilePath( absUrl ),
3516
// The version of the Url actually stored in the data-url attribute of
3517
// the page. For embedded pages, it is just the id of the page. For pages
3518
// within the same domain as the document base, it is the site relative
3519
// path. For cross-domain pages (Phone Gap only) the entire absolute Url
3520
// used to load the page.
3521
dataUrl = path.convertUrlToDataUrl( absUrl );
3523
// Make sure we have a pageContainer to work with.
3524
settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
3526
// Check to see if the page already exists in the DOM.
3527
// NOTE do _not_ use the :jqmData psuedo selector because parenthesis
3528
// are a valid url char and it breaks on the first occurence
3529
page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
3531
// If we failed to find the page, check to see if the url is a
3532
// reference to an embedded page. If so, it may have been dynamically
3533
// injected by a developer, in which case it would be lacking a data-url
3534
// attribute and in need of enhancement.
3535
if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) {
3536
page = settings.pageContainer.children( "#" + dataUrl )
3537
.attr( "data-" + $.mobile.ns + "url", dataUrl )
3538
.jqmData( "url", dataUrl );
3542
// If we failed to find a page in the DOM, check the URL to see if it
3543
// refers to the first page in the application. If it isn't a reference
3544
// to the first page and refers to non-existent embedded page, error out.
3545
if ( page.length === 0 ) {
3546
if ( $.mobile.firstPage && path.isFirstPageUrl( fileUrl ) ) {
3547
// Check to make sure our cached-first-page is actually
3548
// in the DOM. Some user deployed apps are pruning the first
3549
// page from the DOM for various reasons, we check for this
3550
// case here because we don't want a first-page with an id
3551
// falling through to the non-existent embedded page error
3552
// case. If the first-page is not in the DOM, then we let
3553
// things fall through to the ajax loading code below so
3554
// that it gets reloaded.
3555
if ( $.mobile.firstPage.parent().length ) {
3556
page = $( $.mobile.firstPage );
3558
} else if ( path.isEmbeddedPage( fileUrl ) ) {
3559
deferred.reject( absUrl, options );
3560
return deferred.promise();
3564
// If the page we are interested in is already in the DOM,
3565
// and the caller did not indicate that we should force a
3566
// reload of the file, we are done. Otherwise, track the
3567
// existing page as a duplicated.
3568
if ( page.length ) {
3569
if ( !settings.reloadPage ) {
3570
enhancePage( page, settings.role );
3571
deferred.resolve( absUrl, options, page );
3572
//if we are reloading the page make sure we update the base if its not a prefetch
3573
if( base && !options.prefetch ){
3576
return deferred.promise();
3578
dupCachedPage = page;
3580
var mpc = settings.pageContainer,
3581
pblEvent = new $.Event( "pagebeforeload" ),
3582
triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings };
3584
// Let listeners know we're about to load a page.
3585
mpc.trigger( pblEvent, triggerData );
3587
// If the default behavior is prevented, stop here!
3588
if ( pblEvent.isDefaultPrevented() ) {
3589
return deferred.promise();
3592
if ( settings.showLoadMsg ) {
3594
// This configurable timeout allows cached pages a brief delay to load without showing a message
3595
var loadMsgDelay = setTimeout(function() {
3596
$.mobile.showPageLoadingMsg();
3597
}, settings.loadMsgDelay ),
3599
// Shared logic for clearing timeout and removing message.
3600
hideMsg = function() {
3602
// Stop message show timer
3603
clearTimeout( loadMsgDelay );
3605
// Hide loading message
3606
$.mobile.hidePageLoadingMsg();
3609
// Reset base to the default document base.
3610
// only reset if we are not prefetching
3611
if ( base && typeof options.prefetch === "undefined" ) {
3615
if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) {
3616
deferred.reject( absUrl, options );
3618
// Load the new page.
3621
type: settings.type,
3622
data: settings.data,
3624
success: function( html, textStatus, xhr ) {
3625
//pre-parse html to check for a data-url,
3626
//use it as the new fileUrl, base path, etc
3627
var all = $( "<div></div>" ),
3630
newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1,
3632
// TODO handle dialogs again
3633
pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ),
3634
dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" );
3637
// data-url must be provided for the base tag so resource requests can be directed to the
3638
// correct url. loading into a temprorary element makes these requests immediately
3639
if ( pageElemRegex.test( html ) &&
3641
dataUrlRegex.test( RegExp.$1 ) &&
3643
url = fileUrl = path.getFilePath( $( "<div>" + RegExp.$1 + "</div>" ).text() );
3645
//dont update the base tag if we are prefetching
3646
if ( base && typeof options.prefetch === "undefined") {
3647
base.set( fileUrl );
3650
//workaround to allow scripts to execute when included in page divs
3651
all.get( 0 ).innerHTML = html;
3652
page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first();
3654
//if page elem couldn't be found, create one and insert the body element's contents
3655
if ( !page.length ) {
3656
page = $( "<div data-" + $.mobile.ns + "role='page'>" + html.split( /<\/?body[^>]*>/gmi )[1] + "</div>" );
3659
if ( newPageTitle && !page.jqmData( "title" ) ) {
3660
if ( ~newPageTitle.indexOf( "&" ) ) {
3661
newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text();
3663
page.jqmData( "title", newPageTitle );
3666
//rewrite src and href attrs to use a base url
3667
if ( !$.support.dynamicBaseTag ) {
3668
var newPath = path.get( fileUrl );
3669
page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() {
3670
var thisAttr = $( this ).is( '[href]' ) ? 'href' :
3671
$( this ).is( '[src]' ) ? 'src' : 'action',
3672
thisUrl = $( this ).attr( thisAttr );
3674
// XXX_jblas: We need to fix this so that it removes the document
3675
// base URL, and then prepends with the new page URL.
3676
//if full path exists and is same, chop it - helps IE out
3677
thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' );
3679
if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) {
3680
$( this ).attr( thisAttr, newPath + thisUrl );
3685
//append to page and enhance
3686
// TODO taging a page with external to make sure that embedded pages aren't removed
3687
// by the various page handling code is bad. Having page handling code in many
3688
// places is bad. Solutions post 1.0
3690
.attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) )
3691
.attr( "data-" + $.mobile.ns + "external-page", true )
3692
.appendTo( settings.pageContainer );
3694
// wait for page creation to leverage options defined on widget
3695
page.one( 'pagecreate', $.mobile._bindPageRemove );
3697
enhancePage( page, settings.role );
3699
// Enhancing the page may result in new dialogs/sub pages being inserted
3700
// into the DOM. If the original absUrl refers to a sub-page, that is the
3701
// real page we are interested in.
3702
if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) {
3703
page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
3706
//bind pageHide to removePage after it's hidden, if the page options specify to do so
3708
// Remove loading message.
3709
if ( settings.showLoadMsg ) {
3713
// Add the page reference and xhr to our triggerData.
3714
triggerData.xhr = xhr;
3715
triggerData.textStatus = textStatus;
3716
triggerData.page = page;
3718
// Let listeners know the page loaded successfully.
3719
settings.pageContainer.trigger( "pageload", triggerData );
3721
deferred.resolve( absUrl, options, page, dupCachedPage );
3723
error: function( xhr, textStatus, errorThrown ) {
3724
//set base back to current path
3726
base.set( path.get() );
3729
// Add error info to our triggerData.
3730
triggerData.xhr = xhr;
3731
triggerData.textStatus = textStatus;
3732
triggerData.errorThrown = errorThrown;
3734
var plfEvent = new $.Event( "pageloadfailed" );
3736
// Let listeners know the page load failed.
3737
settings.pageContainer.trigger( plfEvent, triggerData );
3739
// If the default behavior is prevented, stop here!
3740
// Note that it is the responsibility of the listener/handler
3741
// that called preventDefault(), to resolve/reject the
3742
// deferred object within the triggerData.
3743
if ( plfEvent.isDefaultPrevented() ) {
3747
// Remove loading message.
3748
if ( settings.showLoadMsg ) {
3750
// Remove loading message.
3753
// show error message
3754
$.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true );
3757
setTimeout( $.mobile.hidePageLoadingMsg, 1500 );
3760
deferred.reject( absUrl, options );
3765
return deferred.promise();
3768
$.mobile.loadPage.defaults = {
3772
role: undefined, // By default we rely on the role defined by the @data-role attribute.
3774
pageContainer: undefined,
3775
loadMsgDelay: 50 // This delay allows loads that pull from browser cache to occur without showing the loading message.
3778
// Show a specific page in the page container.
3779
$.mobile.changePage = function( toPage, options ) {
3780
// If we are in the midst of a transition, queue the current request.
3781
// We'll call changePage() once we're done with the current transition to
3782
// service the request.
3783
if ( isPageTransitioning ) {
3784
pageTransitionQueue.unshift( arguments );
3788
var settings = $.extend( {}, $.mobile.changePage.defaults, options );
3790
// Make sure we have a pageContainer to work with.
3791
settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
3793
// Make sure we have a fromPage.
3794
settings.fromPage = settings.fromPage || $.mobile.activePage;
3796
var mpc = settings.pageContainer,
3797
pbcEvent = new $.Event( "pagebeforechange" ),
3798
triggerData = { toPage: toPage, options: settings };
3800
// Let listeners know we're about to change the current page.
3801
mpc.trigger( pbcEvent, triggerData );
3803
// If the default behavior is prevented, stop here!
3804
if ( pbcEvent.isDefaultPrevented() ) {
3808
// We allow "pagebeforechange" observers to modify the toPage in the trigger
3809
// data to allow for redirects. Make sure our toPage is updated.
3811
toPage = triggerData.toPage;
3813
// Set the isPageTransitioning flag to prevent any requests from
3814
// entering this method while we are in the midst of loading a page
3815
// or transitioning.
3817
isPageTransitioning = true;
3819
// If the caller passed us a url, call loadPage()
3820
// to make sure it is loaded into the DOM. We'll listen
3821
// to the promise object it returns so we know when
3822
// it is done loading or if an error ocurred.
3823
if ( typeof toPage === "string" ) {
3824
$.mobile.loadPage( toPage, settings )
3825
.done(function( url, options, newPage, dupCachedPage ) {
3826
isPageTransitioning = false;
3827
options.duplicateCachedPage = dupCachedPage;
3828
$.mobile.changePage( newPage, options );
3830
.fail(function( url, options ) {
3832
//clear out the active button state
3833
removeActiveLinkClass( true );
3835
//release transition lock so navigation is free again
3836
releasePageTransitionLock();
3837
settings.pageContainer.trigger( "pagechangefailed", triggerData );
3842
// If we are going to the first-page of the application, we need to make
3843
// sure settings.dataUrl is set to the application document url. This allows
3844
// us to avoid generating a document url with an id hash in the case where the
3845
// first-page of the document has an id attribute specified.
3846
if ( toPage[ 0 ] === $.mobile.firstPage[ 0 ] && !settings.dataUrl ) {
3847
settings.dataUrl = documentUrl.hrefNoHash;
3850
// The caller passed us a real page DOM element. Update our
3851
// internal state and then trigger a transition to the page.
3852
var fromPage = settings.fromPage,
3853
url = ( settings.dataUrl && path.convertUrlToDataUrl( settings.dataUrl ) ) || toPage.jqmData( "url" ),
3854
// The pageUrl var is usually the same as url, except when url is obscured as a dialog url. pageUrl always contains the file path
3856
fileUrl = path.getFilePath( url ),
3857
active = urlHistory.getActive(),
3858
activeIsInitialPage = urlHistory.activeIndex === 0,
3860
pageTitle = document.title,
3861
isDialog = settings.role === "dialog" || toPage.jqmData( "role" ) === "dialog";
3863
// By default, we prevent changePage requests when the fromPage and toPage
3864
// are the same element, but folks that generate content manually/dynamically
3865
// and reuse pages want to be able to transition to the same page. To allow
3866
// this, they will need to change the default value of allowSamePageTransition
3867
// to true, *OR*, pass it in as an option when they manually call changePage().
3868
// It should be noted that our default transition animations assume that the
3869
// formPage and toPage are different elements, so they may behave unexpectedly.
3870
// It is up to the developer that turns on the allowSamePageTransitiona option
3871
// to either turn off transition animations, or make sure that an appropriate
3872
// animation transition is used.
3873
if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) {
3874
isPageTransitioning = false;
3875
mpc.trigger( "pagechange", triggerData );
3877
// Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes
3878
if ( settings.fromHashChange ) {
3879
urlHistory.directHashChange({
3881
isBack: function() {},
3882
isForward: function() {}
3889
// We need to make sure the page we are given has already been enhanced.
3890
enhancePage( toPage, settings.role );
3892
// If the changePage request was sent from a hashChange event, check to see if the
3893
// page is already within the urlHistory stack. If so, we'll assume the user hit
3894
// the forward/back button and will try to match the transition accordingly.
3895
if ( settings.fromHashChange ) {
3896
urlHistory.directHashChange({
3898
isBack: function() { historyDir = -1; },
3899
isForward: function() { historyDir = 1; }
3903
// Kill the keyboard.
3904
// XXX_jblas: We need to stop crawling the entire document to kill focus. Instead,
3905
// we should be tracking focus with a delegate() handler so we already have
3906
// the element in hand at this point.
3907
// Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement
3908
// is undefined when we are in an IFrame.
3910
if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) {
3911
$( document.activeElement ).blur();
3913
$( "input:focus, textarea:focus, select:focus" ).blur();
3917
// 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
3918
var alreadyThere = false;
3920
// If we're displaying the page as a dialog, we don't want the url
3921
// for the dialog content to be used in the hash. Instead, we want
3922
// to append the dialogHashKey to the url of the current page.
3923
if ( isDialog && active ) {
3924
// on the initial page load active.url is undefined and in that case should
3925
// be an empty string. Moving the undefined -> empty string back into
3926
// urlHistory.addNew seemed imprudent given undefined better represents
3929
// If we are at a place in history that once belonged to a dialog, reuse
3930
// this state without adding to urlHistory and without modifying the hash.
3931
// However, if a dialog is already displayed at this point, and we're
3932
// about to display another dialog, then we must add another hash and
3933
// history entry on top so that one may navigate back to the original dialog
3934
if ( active.url && active.url.indexOf( dialogHashKey ) > -1 && !$.mobile.activePage.is( ".ui-dialog" ) ) {
3935
settings.changeHash = false;
3936
alreadyThere = true;
3939
// Normally, we tack on a dialog hash key, but if this is the location of a stale dialog,
3940
// we reuse the URL from the entry
3941
url = ( active.url || "" ) + ( alreadyThere ? "" : dialogHashKey );
3943
// tack on another dialogHashKey if this is the same as the initial hash
3944
// this makes sure that a history entry is created for this dialog
3945
if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) {
3946
url += dialogHashKey;
3950
// Set the location hash.
3951
if ( settings.changeHash !== false && url ) {
3952
//disable hash listening temporarily
3953
urlHistory.ignoreNextHashChange = true;
3954
//update hash and history
3958
// if title element wasn't found, try the page div data attr too
3959
// If this is a deep-link or a reload ( active === undefined ) then just use pageTitle
3960
var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText();
3961
if ( !!newPageTitle && pageTitle === document.title ) {
3962
pageTitle = newPageTitle;
3964
if ( !toPage.jqmData( "title" ) ) {
3965
toPage.jqmData( "title", pageTitle );
3968
// Make sure we have a transition defined.
3969
settings.transition = settings.transition ||
3970
( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) ||
3971
( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition );
3973
//add page to history stack if it's not back or forward
3974
if ( !historyDir ) {
3975
// Overwrite the current entry if it's a leftover from a dialog
3976
if ( alreadyThere ) {
3977
urlHistory.activeIndex = Math.max( 0, urlHistory.activeIndex - 1 );
3979
urlHistory.addNew( url, settings.transition, pageTitle, pageUrl, settings.role );
3983
document.title = urlHistory.getActive().title;
3985
//set "toPage" as activePage
3986
$.mobile.activePage = toPage;
3988
// If we're navigating back in the URL history, set reverse accordingly.
3989
settings.reverse = settings.reverse || historyDir < 0;
3991
transitionPages( toPage, fromPage, settings.transition, settings.reverse )
3992
.done(function( name, reverse, $to, $from, alreadyFocused ) {
3993
removeActiveLinkClass();
3995
//if there's a duplicateCachedPage, remove it from the DOM now that it's hidden
3996
if ( settings.duplicateCachedPage ) {
3997
settings.duplicateCachedPage.remove();
4000
// Send focus to the newly shown page. Moved from promise .done binding in transitionPages
4001
// itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility)
4002
// despite visibility: hidden addresses issue #2965
4003
// https://github.com/jquery/jquery-mobile/issues/2965
4004
if ( !alreadyFocused ) {
4005
$.mobile.focusPage( toPage );
4008
releasePageTransitionLock();
4010
// Let listeners know we're all done changing the current page.
4011
mpc.trigger( "pagechange", triggerData );
4015
$.mobile.changePage.defaults = {
4016
transition: undefined,
4019
fromHashChange: false,
4020
role: undefined, // By default we rely on the role defined by the @data-role attribute.
4021
duplicateCachedPage: undefined,
4022
pageContainer: undefined,
4023
showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage
4025
fromPage: undefined,
4026
allowSamePageTransition: false
4029
/* Event Bindings - hashchange, submit, and click */
4030
function findClosestLink( ele )
4033
// Look for the closest element with a nodeName of "a".
4034
// Note that we are checking if we have a valid nodeName
4035
// before attempting to access it. This is because the
4036
// node we get called with could have originated from within
4037
// an embedded SVG document where some symbol instance elements
4038
// don't have nodeName defined on them, or strings are of type
4039
// SVGAnimatedString.
4040
if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) {
4043
ele = ele.parentNode;
4048
// The base URL for any given element depends on the page it resides in.
4049
function getClosestBaseUrl( ele )
4051
// Find the closest page and extract out its url.
4052
var url = $( ele ).closest( ".ui-page" ).jqmData( "url" ),
4053
base = documentBase.hrefNoHash;
4055
if ( !url || !path.isPath( url ) ) {
4059
return path.makeUrlAbsolute( url, base);
4062
//The following event bindings should be bound after mobileinit has been triggered
4063
//the following deferred is resolved in the init file
4064
$.mobile.navreadyDeferred = $.Deferred();
4065
$.mobile._registerInternalEvents = function() {
4066
//bind to form submit events, handle with Ajax
4067
$( document ).delegate( "form", "submit", function( event ) {
4068
var $this = $( this );
4070
if ( !$.mobile.ajaxEnabled ||
4071
// test that the form is, itself, ajax false
4072
$this.is( ":jqmData(ajax='false')" ) ||
4073
// test that $.mobile.ignoreContentEnabled is set and
4074
// the form or one of it's parents is ajax=false
4075
!$this.jqmHijackable().length ) {
4079
var type = $this.attr( "method" ),
4080
target = $this.attr( "target" ),
4081
url = $this.attr( "action" );
4083
// If no action is specified, browsers default to using the
4084
// URL of the document containing the form. Since we dynamically
4085
// pull in pages from external documents, the form should submit
4086
// to the URL for the source document of the page containing
4089
// Get the @data-url for the page containing the form.
4090
url = getClosestBaseUrl( $this );
4091
if ( url === documentBase.hrefNoHash ) {
4092
// The url we got back matches the document base,
4093
// which means the page must be an internal/embedded page,
4094
// so default to using the actual document url as a browser
4096
url = documentUrl.hrefNoSearch;
4100
url = path.makeUrlAbsolute( url, getClosestBaseUrl( $this ) );
4102
if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) {
4106
$.mobile.changePage(
4109
type: type && type.length && type.toLowerCase() || "get",
4110
data: $this.serialize(),
4111
transition: $this.jqmData( "transition" ),
4112
reverse: $this.jqmData( "direction" ) === "reverse",
4116
event.preventDefault();
4119
//add active state on vclick
4120
$( document ).bind( "vclick", function( event ) {
4121
// if this isn't a left click we don't care. Its important to note
4122
// that when the virtual event is generated it will create the which attr
4123
if ( event.which > 1 || !$.mobile.linkBindingEnabled ) {
4127
var link = findClosestLink( event.target );
4129
// split from the previous return logic to avoid find closest where possible
4130
// TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
4132
if ( !$( link ).jqmHijackable().length ) {
4137
if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) {
4138
removeActiveLinkClass( true );
4139
$activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" );
4140
$activeClickedLink.addClass( $.mobile.activeBtnClass );
4145
// click routing - direct to HTTP or Ajax, accordingly
4146
$( document ).bind( "click", function( event ) {
4147
if ( !$.mobile.linkBindingEnabled ) {
4151
var link = findClosestLink( event.target ), $link = $( link ), httpCleanup;
4153
// If there is no link associated with the click or its not a left
4154
// click we want to ignore the click
4155
// TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
4157
if ( !link || event.which > 1 || !$link.jqmHijackable().length ) {
4161
//remove active link class if external (then it won't be there if you come back)
4162
httpCleanup = function() {
4163
window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 );
4166
//if there's a data-rel=back attr, go back in history
4167
if ( $link.is( ":jqmData(rel='back')" ) ) {
4172
var baseUrl = getClosestBaseUrl( $link ),
4174
//get href, if defined, otherwise default to empty hash
4175
href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl );
4177
//if ajax is disabled, exit early
4178
if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) {
4180
//use default click handling
4184
// XXX_jblas: Ideally links to application pages should be specified as
4185
// an url to the application document with a hash that is either
4186
// the site relative path or id to the page. But some of the
4187
// internal code that dynamically generates sub-pages for nested
4188
// lists and select dialogs, just write a hash in the link they
4189
// create. This means the actual URL path is based on whatever
4190
// the current value of the base tag is at the time this code
4191
// is called. For now we are just assuming that any url with a
4192
// hash in it is an application page reference.
4193
if ( href.search( "#" ) !== -1 ) {
4194
href = href.replace( /[^#]*#/, "" );
4196
//link was an empty hash meant purely
4197
//for interaction, so we ignore it.
4198
event.preventDefault();
4200
} else if ( path.isPath( href ) ) {
4201
//we have apath so make it the href we want to load.
4202
href = path.makeUrlAbsolute( href, baseUrl );
4204
//we have a simple id so use the documentUrl as its base.
4205
href = path.makeUrlAbsolute( "#" + href, documentUrl.hrefNoHash );
4209
// Should we handle this link, or let the browser deal with it?
4210
var useDefaultUrlHandling = $link.is( "[rel='external']" ) || $link.is( ":jqmData(ajax='false')" ) || $link.is( "[target]" ),
4212
// Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
4213
// requests if the document doing the request was loaded via the file:// protocol.
4214
// This is usually to allow the application to "phone home" and fetch app specific
4215
// data. We normally let the browser handle external/cross-domain urls, but if the
4216
// allowCrossDomainPages option is true, we will allow cross-domain http/https
4217
// requests to go through our page loading logic.
4219
//check for protocol or rel and its not an embedded page
4220
//TODO overlap in logic from isExternal, rel=external check should be
4221
// moved into more comprehensive isExternalLink
4222
isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) );
4226
//use default click handling
4231
var transition = $link.jqmData( "transition" ),
4232
reverse = $link.jqmData( "direction" ) === "reverse" ||
4233
// deprecated - remove by 1.0
4234
$link.jqmData( "back" ),
4236
//this may need to be more specific as we use data-rel more
4237
role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined;
4239
$.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } );
4240
event.preventDefault();
4243
//prefetch pages when anchors with data-prefetch are encountered
4244
$( document ).delegate( ".ui-page", "pageshow.prefetch", function() {
4246
$( this ).find( "a:jqmData(prefetch)" ).each(function() {
4247
var $link = $( this ),
4248
url = $link.attr( "href" );
4250
if ( url && $.inArray( url, urls ) === -1 ) {
4253
$.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ),prefetch: true } );
4258
$.mobile._handleHashChange = function( hash ) {
4259
//find first page via hash
4260
var to = path.stripHash( hash ),
4261
//transition is false if it's the first page, undefined otherwise (and may be overridden by default)
4262
transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined,
4264
// "navigate" event fired to allow others to take advantage of the more robust hashchange handling
4265
navEvent = new $.Event( "navigate" ),
4267
// default options for the changPage calls made after examining the current state
4268
// of the page and the hash
4269
changePageOptions = {
4270
transition: transition,
4272
fromHashChange: true
4275
if ( 0 === urlHistory.stack.length ) {
4276
urlHistory.initialDst = to;
4279
// We should probably fire the "navigate" event from those places that make calls to _handleHashChange,
4280
// and have _handleHashChange hook into the "navigate" event instead of triggering it here
4281
$.mobile.pageContainer.trigger( navEvent );
4282
if ( navEvent.isDefaultPrevented() ) {
4286
//if listening is disabled (either globally or temporarily), or it's a dialog hash
4287
if ( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) {
4288
urlHistory.ignoreNextHashChange = false;
4292
// special case for dialogs
4293
if ( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) {
4295
// If current active page is not a dialog skip the dialog and continue
4296
// in the same direction
4297
if ( !$.mobile.activePage.is( ".ui-dialog" ) ) {
4298
//determine if we're heading forward or backward and continue accordingly past
4299
//the current dialog
4300
urlHistory.directHashChange({
4302
isBack: function() { $.mobile.back(); },
4303
isForward: function() { window.history.forward(); }
4306
// prevent changePage()
4309
// if the current active page is a dialog and we're navigating
4310
// to a dialog use the dialog objected saved in the stack
4311
urlHistory.directHashChange({
4314
// regardless of the direction of the history change
4316
either: function( isBack ) {
4317
var active = $.mobile.urlHistory.getActive();
4319
to = active.pageUrl;
4321
// make sure to set the role, transition and reversal
4322
// as most of this is lost by the domCache cleaning
4323
$.extend( changePageOptions, {
4325
transition: active.transition,
4333
//if to is defined, load it
4335
// At this point, 'to' can be one of 3 things, a cached page element from
4336
// a history stack entry, an id, or site-relative/absolute URL. If 'to' is
4337
// an id, we need to resolve it against the documentBase, not the location.href,
4338
// since the hashchange could've been the result of a forward/backward navigation
4339
// that crosses from an external page/dialog to an internal page/dialog.
4340
to = ( typeof to === "string" && !path.isPath( to ) ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to;
4342
// If we're about to go to an initial URL that contains a reference to a non-existent
4343
// internal page, go to the first page instead. We know that the initial hash refers to a
4344
// non-existent page, because the initial hash did not end up in the initial urlHistory entry
4345
if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) &&
4346
urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) {
4347
to = $.mobile.firstPage;
4349
$.mobile.changePage( to, changePageOptions );
4351
//there's no hash, go to the first page in the dom
4352
$.mobile.changePage( $.mobile.firstPage, changePageOptions );
4356
//hashchange event handler
4357
$window.bind( "hashchange", function( e, triggered ) {
4358
// Firefox auto-escapes the location.hash as for v13 but
4359
// leaves the href untouched
4360
$.mobile._handleHashChange( path.parseLocation().hash );
4363
//set page min-heights to be device specific
4364
$( document ).bind( "pageshow", resetActivePageHeight );
4365
$( window ).bind( "throttledresize", resetActivePageHeight );
4367
};//navreadyDeferred done callback
4368
$.mobile.navreadyDeferred.done( function() { $.mobile._registerInternalEvents(); } );
4372
(function( $, window ) {
4373
// For now, let's Monkeypatch this onto the end of $.mobile._registerInternalEvents
4374
// Scope self to pushStateHandler so we can reference it sanely within the
4375
// methods handed off as event handlers
4376
var pushStateHandler = {},
4377
self = pushStateHandler,
4379
url = $.mobile.path.parseLocation(),
4380
mobileinitDeferred = $.Deferred(),
4381
domreadyDeferred = $.Deferred();
4383
$( document ).ready( $.proxy( domreadyDeferred, "resolve" ) );
4385
$( document ).one( "mobileinit", $.proxy( mobileinitDeferred, "resolve" ) );
4387
$.extend( pushStateHandler, {
4388
// TODO move to a path helper, this is rather common functionality
4389
initialFilePath: (function() {
4390
return url.pathname + url.search;
4393
hashChangeTimeout: 200,
4395
hashChangeEnableTimer: undefined,
4397
initialHref: url.hrefNoHash,
4401
// firefox auto decodes the url when using location.hash but not href
4402
hash: $.mobile.path.parseLocation().hash || "#" + self.initialFilePath,
4403
title: document.title,
4405
// persist across refresh
4406
initialHref: self.initialHref
4410
resetUIKeys: function( url ) {
4411
var dialog = $.mobile.dialogHashKey,
4412
subkey = "&" + $.mobile.subPageUrlKey,
4413
dialogIndex = url.indexOf( dialog );
4415
if ( dialogIndex > -1 ) {
4416
url = url.slice( 0, dialogIndex ) + "#" + url.slice( dialogIndex );
4417
} else if ( url.indexOf( subkey ) > -1 ) {
4418
url = url.split( subkey ).join( "#" + subkey );
4424
// TODO sort out a single barrier to hashchange functionality
4425
nextHashChangePrevented: function( value ) {
4426
$.mobile.urlHistory.ignoreNextHashChange = value;
4427
self.onHashChangeDisabled = value;
4430
// on hash change we want to clean up the url
4431
// NOTE this takes place *after* the vanilla navigation hash change
4432
// handling has taken place and set the state of the DOM
4433
onHashChange: function( e ) {
4434
// disable this hash change
4435
if ( self.onHashChangeDisabled ) {
4440
// firefox auto decodes the url when using location.hash but not href
4441
hash = $.mobile.path.parseLocation().hash,
4442
isPath = $.mobile.path.isPath( hash ),
4443
resolutionUrl = isPath ? $.mobile.path.getLocation() : $.mobile.getDocumentUrl();
4445
hash = isPath ? hash.replace( "#", "" ) : hash;
4448
// propulate the hash when its not available
4449
state = self.state();
4451
// make the hash abolute with the current href
4452
href = $.mobile.path.makeUrlAbsolute( hash, resolutionUrl );
4455
href = self.resetUIKeys( href );
4458
// replace the current url with the new href and store the state
4459
// Note that in some cases we might be replacing an url with the
4460
// same url. We do this anyways because we need to make sure that
4461
// all of our history entries have a state object associated with
4462
// them. This allows us to work around the case where $.mobile.back()
4463
// is called to transition from an external page to an embedded page.
4464
// In that particular case, a hashchange event is *NOT* generated by the browser.
4465
// Ensuring each history entry has a state object means that onPopState()
4466
// will always trigger our hashchange callback even when a hashchange event
4468
history.replaceState( state, document.title, href );
4471
// on popstate (ie back or forward) we need to replace the hash that was there previously
4472
// cleaned up by the additional hash handling
4473
onPopState: function( e ) {
4474
var poppedState = e.originalEvent.state,
4475
fromHash, toHash, hashChanged;
4477
// if there's no state its not a popstate we care about, eg chrome's initial popstate
4478
if ( poppedState ) {
4479
// if we get two pop states in under this.hashChangeTimeout
4480
// make sure to clear any timer set for the previous change
4481
clearTimeout( self.hashChangeEnableTimer );
4483
// make sure to enable hash handling for the the _handleHashChange call
4484
self.nextHashChangePrevented( false );
4486
// change the page based on the hash in the popped state
4487
$.mobile._handleHashChange( poppedState.hash );
4489
// prevent any hashchange in the next self.hashChangeTimeout
4490
self.nextHashChangePrevented( true );
4492
// re-enable hash change handling after swallowing a possible hash
4493
// change event that comes on all popstates courtesy of browsers like Android
4494
self.hashChangeEnableTimer = setTimeout( function() {
4495
self.nextHashChangePrevented( false );
4496
}, self.hashChangeTimeout );
4501
$win.bind( "hashchange", self.onHashChange );
4503
// Handle popstate events the occur through history changes
4504
$win.bind( "popstate", self.onPopState );
4506
// if there's no hash, we need to replacestate for returning to home
4507
if ( location.hash === "" ) {
4508
history.replaceState( self.state(), document.title, $.mobile.path.getLocation() );
4513
// We need to init when "mobileinit", "domready", and "navready" have all happened
4514
$.when( domreadyDeferred, mobileinitDeferred, $.mobile.navreadyDeferred ).done(function() {
4515
if ( $.mobile.pushStateEnabled && $.support.pushState ) {
4516
pushStateHandler.init();
4522
* fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4525
(function( $, window, undefined ) {
4527
$.mobile.transitionFallbacks.flip = "fade";
4531
* fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4534
(function( $, window, undefined ) {
4536
$.mobile.transitionFallbacks.flow = "fade";
4540
* fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4543
(function( $, window, undefined ) {
4545
$.mobile.transitionFallbacks.pop = "fade";
4549
* fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4552
(function( $, window, undefined ) {
4554
// Use the simultaneous transitions handler for slide transitions
4555
$.mobile.transitionHandlers.slide = $.mobile.transitionHandlers.simultaneous;
4557
// Set the slide transitions's fallback to "fade"
4558
$.mobile.transitionFallbacks.slide = "fade";
4562
* fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4565
(function( $, window, undefined ) {
4567
$.mobile.transitionFallbacks.slidedown = "fade";
4571
* fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4574
(function( $, window, undefined ) {
4576
// Set the slide transitions's fallback to "fade"
4577
$.mobile.transitionFallbacks.slidefade = "fade";
4581
* fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4584
(function( $, window, undefined ) {
4586
$.mobile.transitionFallbacks.slideup = "fade";
4590
* fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general
4593
(function( $, window, undefined ) {
4595
$.mobile.transitionFallbacks.turn = "fade";
4599
(function( $, undefined ) {
4601
$.mobile.page.prototype.options.degradeInputs = {
4605
"datetime-local": false,
4618
//auto self-init widgets
4619
$( document ).bind( "pagecreate create", function( e ) {
4621
var page = $.mobile.closestPageData( $( e.target ) ), options;
4627
options = page.options;
4629
// degrade inputs to avoid poorly implemented native functionality
4630
$( e.target ).find( "input" ).not( page.keepNativeSelector() ).each(function() {
4631
var $this = $( this ),
4632
type = this.getAttribute( "type" ),
4633
optType = options.degradeInputs[ type ] || "text";
4635
if ( options.degradeInputs[ type ] ) {
4636
var html = $( "<div>" ).html( $this.clone() ).html(),
4637
// In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead
4638
hasType = html.indexOf( " type=" ) > -1,
4639
findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/,
4640
repstr = " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\"" + ( hasType ? "" : ">" );
4642
$this.replaceWith( html.replace( findstr, repstr ) );
4650
(function( $, window, undefined ) {
4652
$.widget( "mobile.dialog", $.mobile.widget, {
4654
closeBtnText: "Close",
4656
initSelector: ":jqmData(role='dialog')"
4658
_create: function() {
4661
headerCloseButton = $( "<a href='#' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" ),
4662
dialogWrap = $( "<div/>", {
4664
"class" : "ui-dialog-contain ui-corner-all ui-overlay-shadow"
4667
$el.addClass( "ui-dialog ui-overlay-" + this.options.overlayTheme );
4669
// Class the markup for dialog styling
4672
.wrapInner( dialogWrap )
4674
.find( ":jqmData(role='header')" ).first()
4675
.prepend( headerCloseButton )
4677
.children( ':first-child')
4678
.addClass( "ui-corner-top" )
4680
.children( ":last-child" )
4681
.addClass( "ui-corner-bottom" );
4683
// this must be an anonymous function so that select menu dialogs can replace
4684
// the close method. This is a change from previously just defining data-rel=back
4685
// on the button and letting nav handle it
4687
// Use click rather than vclick in order to prevent the possibility of unintentionally
4688
// reopening the dialog if the dialog opening item was directly under the close button.
4689
headerCloseButton.bind( "click", function() {
4694
- clicks and submits should use the closing transition that the dialog opened with
4695
unless a data-transition is specified on the link/form
4696
- if the click was on the close button, or the link has a data-rel="back" it'll go back in history naturally
4698
$el.bind( "vclick submit", function( event ) {
4699
var $target = $( event.target ).closest( event.type === "vclick" ? "a" : "form" ),
4702
if ( $target.length && !$target.jqmData( "transition" ) ) {
4704
active = $.mobile.urlHistory.getActive() || {};
4706
$target.attr( "data-" + $.mobile.ns + "transition", ( active.transition || $.mobile.defaultDialogTransition ) )
4707
.attr( "data-" + $.mobile.ns + "direction", "reverse" );
4710
.bind( "pagehide", function( e, ui ) {
4711
$( this ).find( "." + $.mobile.activeBtnClass ).not( ".ui-slider-bg" ).removeClass( $.mobile.activeBtnClass );
4713
// Override the theme set by the page plugin on pageshow
4714
.bind( "pagebeforeshow", function() {
4715
self._isCloseable = true;
4716
if ( self.options.overlayTheme ) {
4718
.page( "removeContainerBackground" )
4719
.page( "setContainerBackground", self.options.overlayTheme );
4724
// Close method goes back in history
4728
if ( this._isCloseable ) {
4729
this._isCloseable = false;
4730
if ( $.mobile.hashListeningEnabled ) {
4733
dst = $.mobile.urlHistory.getPrev().url;
4734
if ( !$.mobile.path.isPath( dst ) ) {
4735
dst = $.mobile.path.makeUrlAbsolute( "#" + dst );
4738
$.mobile.changePage( dst, { changeHash: false, fromHashChange: true } );
4744
//auto self-init widgets
4745
$( document ).delegate( $.mobile.dialog.prototype.options.initSelector, "pagecreate", function() {
4746
$.mobile.dialog.prototype.enhance( this );
4751
(function( $, undefined ) {
4753
$.mobile.page.prototype.options.backBtnText = "Back";
4754
$.mobile.page.prototype.options.addBackBtn = false;
4755
$.mobile.page.prototype.options.backBtnTheme = null;
4756
$.mobile.page.prototype.options.headerTheme = "a";
4757
$.mobile.page.prototype.options.footerTheme = "a";
4758
$.mobile.page.prototype.options.contentTheme = null;
4760
// NOTE bind used to force this binding to run before the buttonMarkup binding
4761
// which expects .ui-footer top be applied in its gigantic selector
4762
// TODO remove the buttonMarkup giant selector and move it to the various modules
4763
// on which it depends
4764
$( document ).bind( "pagecreate", function( e ) {
4765
var $page = $( e.target ),
4766
o = $page.data( "page" ).options,
4767
pageRole = $page.jqmData( "role" ),
4768
pageTheme = o.theme;
4770
$( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", $page )
4774
var $this = $( this ),
4775
role = $this.jqmData( "role" ),
4776
theme = $this.jqmData( "theme" ),
4777
contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ),
4783
$this.addClass( "ui-" + role );
4785
//apply theming and markup modifications to page,header,content,footer
4786
if ( role === "header" || role === "footer" ) {
4788
var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme;
4792
.addClass( "ui-bar-" + thisTheme )
4794
.attr( "role", role === "header" ? "banner" : "contentinfo" );
4796
if ( role === "header") {
4797
// Right,left buttons
4798
$headeranchors = $this.children( "a, button" );
4799
leftbtn = $headeranchors.hasClass( "ui-btn-left" );
4800
rightbtn = $headeranchors.hasClass( "ui-btn-right" );
4802
leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length;
4804
rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length;
4807
// Auto-add back btn on pages beyond first view
4808
if ( o.addBackBtn &&
4809
role === "header" &&
4810
$( ".ui-page" ).length > 1 &&
4811
$page.jqmData( "url" ) !== $.mobile.path.stripHash( location.hash ) &&
4814
backBtn = $( "<a href='javascript:void(0);' class='ui-btn-left' data-"+ $.mobile.ns +"rel='back' data-"+ $.mobile.ns +"icon='arrow-l'>"+ o.backBtnText +"</a>" )
4815
// If theme is provided, override default inheritance
4816
.attr( "data-"+ $.mobile.ns +"theme", o.backBtnTheme || thisTheme )
4817
.prependTo( $this );
4821
$this.children( "h1, h2, h3, h4, h5, h6" )
4822
.addClass( "ui-title" )
4823
// Regardless of h element number in src, it becomes h1 for the enhanced page
4829
} else if ( role === "content" ) {
4830
if ( contentTheme ) {
4831
$this.addClass( "ui-body-" + ( contentTheme ) );
4835
$this.attr( "role", "main" );
4842
(function( $, undefined ) {
4844
// filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text)
4845
$.fn.fieldcontain = function( options ) {
4847
.addClass( "ui-field-contain ui-body ui-br" )
4848
.contents().filter( function() {
4849
return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) );
4853
//auto self-init widgets
4854
$( document ).bind( "pagecreate create", function( e ) {
4855
$( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain();
4860
(function( $, undefined ) {
4862
$.fn.grid = function( options ) {
4863
return this.each(function() {
4865
var $this = $( this ),
4869
$kids = $this.children(),
4870
gridCols = { solo:1, a:2, b:3, c:4, d:5 },
4875
if ( $kids.length <= 5 ) {
4876
for ( var letter in gridCols ) {
4877
if ( gridCols[ letter ] === $kids.length ) {
4883
$this.addClass( "ui-grid-duo" );
4886
iterator = gridCols[grid];
4888
$this.addClass( "ui-grid-" + grid );
4890
$kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" );
4892
if ( iterator > 1 ) {
4893
$kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" );
4895
if ( iterator > 2 ) {
4896
$kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" );
4898
if ( iterator > 3 ) {
4899
$kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" );
4901
if ( iterator > 4 ) {
4902
$kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" );
4908
(function( $, undefined ) {
4910
$( document ).bind( "pagecreate create", function( e ) {
4911
$( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" );
4917
(function( $, undefined ) {
4919
$.mobile.behaviors.formReset = {
4920
_handleFormReset: function() {
4921
this._on( this.element.closest( "form" ), {
4923
this._delay( "_reset" );
4931
(function( $, undefined ) {
4933
$.fn.buttonMarkup = function( options ) {
4934
var $workingSet = this,
4935
mapToDataAttr = function( key, value ) {
4936
e.setAttribute( "data-" + $.mobile.ns + key, value );
4937
el.jqmData( key, value );
4940
// Enforce options to be of type string
4941
options = ( options && ( $.type( options ) === "object" ) )? options : {};
4942
for ( var i = 0; i < $workingSet.length; i++ ) {
4943
var el = $workingSet.eq( i ),
4945
o = $.extend( {}, $.fn.buttonMarkup.defaults, {
4946
icon: options.icon !== undefined ? options.icon : el.jqmData( "icon" ),
4947
iconpos: options.iconpos !== undefined ? options.iconpos : el.jqmData( "iconpos" ),
4948
theme: options.theme !== undefined ? options.theme : el.jqmData( "theme" ) || $.mobile.getInheritedTheme( el, "c" ),
4949
inline: options.inline !== undefined ? options.inline : el.jqmData( "inline" ),
4950
shadow: options.shadow !== undefined ? options.shadow : el.jqmData( "shadow" ),
4951
corners: options.corners !== undefined ? options.corners : el.jqmData( "corners" ),
4952
iconshadow: options.iconshadow !== undefined ? options.iconshadow : el.jqmData( "iconshadow" ),
4953
mini: options.mini !== undefined ? options.mini : el.jqmData( "mini" )
4957
innerClass = "ui-btn-inner",
4958
textClass = "ui-btn-text",
4959
buttonClass, iconClass,
4960
// Button inner markup
4966
$.each( o, mapToDataAttr );
4968
if ( el.jqmData( "rel" ) === "popup" && el.attr( "href" ) ) {
4969
e.setAttribute( "aria-haspopup", true );
4970
e.setAttribute( "aria-owns", e.getAttribute( "href" ) );
4973
// Check if this element is already enhanced
4974
buttonElements = $.data( ( ( e.tagName === "INPUT" || e.tagName === "BUTTON" ) ? e.parentNode : e ), "buttonElements" );
4976
if ( buttonElements ) {
4977
e = buttonElements.outer;
4979
buttonInner = buttonElements.inner;
4980
buttonText = buttonElements.text;
4981
// We will recreate this icon below
4982
$( buttonElements.icon ).remove();
4983
buttonElements.icon = null;
4986
buttonInner = document.createElement( o.wrapperEls );
4987
buttonText = document.createElement( o.wrapperEls );
4989
buttonIcon = o.icon ? document.createElement( "span" ) : null;
4991
if ( attachEvents && !buttonElements ) {
4995
// if not, try to find closest theme container
4997
o.theme = $.mobile.getInheritedTheme( el, "c" );
5000
buttonClass = "ui-btn ui-btn-up-" + o.theme;
5001
buttonClass += o.shadow ? " ui-shadow" : "";
5002
buttonClass += o.corners ? " ui-btn-corner-all" : "";
5004
if ( o.mini !== undefined ) {
5005
// Used to control styling in headers/footers, where buttons default to `mini` style.
5006
buttonClass += o.mini === true ? " ui-mini" : " ui-fullsize";
5009
if ( o.inline !== undefined ) {
5010
// Used to control styling in headers/footers, where buttons default to `inline` style.
5011
buttonClass += o.inline === true ? " ui-btn-inline" : " ui-btn-block";
5015
o.icon = "ui-icon-" + o.icon;
5016
o.iconpos = o.iconpos || "left";
5018
iconClass = "ui-icon " + o.icon;
5020
if ( o.iconshadow ) {
5021
iconClass += " ui-icon-shadow";
5026
buttonClass += " ui-btn-icon-" + o.iconpos;
5028
if ( o.iconpos === "notext" && !el.attr( "title" ) ) {
5029
el.attr( "title", el.getEncodedText() );
5033
innerClass += o.corners ? " ui-btn-corner-all" : "";
5035
if ( o.iconpos && o.iconpos === "notext" && !el.attr( "title" ) ) {
5036
el.attr( "title", el.getEncodedText() );
5039
if ( buttonElements ) {
5040
el.removeClass( buttonElements.bcls || "" );
5042
el.removeClass( "ui-link" ).addClass( buttonClass );
5044
buttonInner.className = innerClass;
5046
buttonText.className = textClass;
5047
if ( !buttonElements ) {
5048
buttonInner.appendChild( buttonText );
5051
buttonIcon.className = iconClass;
5052
if ( !( buttonElements && buttonElements.icon ) ) {
5053
buttonIcon.innerHTML = " ";
5054
buttonInner.appendChild( buttonIcon );
5058
while ( e.firstChild && !buttonElements ) {
5059
buttonText.appendChild( e.firstChild );
5062
if ( !buttonElements ) {
5063
e.appendChild( buttonInner );
5066
// Assign a structure containing the elements of this button to the elements of this button. This
5067
// will allow us to recognize this as an already-enhanced button in future calls to buttonMarkup().
5071
inner : buttonInner,
5076
$.data( e, 'buttonElements', buttonElements );
5077
$.data( buttonInner, 'buttonElements', buttonElements );
5078
$.data( buttonText, 'buttonElements', buttonElements );
5080
$.data( buttonIcon, 'buttonElements', buttonElements );
5087
$.fn.buttonMarkup.defaults = {
5094
function closestEnabledButton( element ) {
5098
// Note that we check for typeof className below because the element we
5099
// handed could be in an SVG DOM where className on SVG elements is defined to
5100
// be of a different type (SVGAnimatedString). We only operate on HTML DOM
5101
// elements, so we look for plain "string".
5102
cname = ( typeof element.className === 'string' ) && ( element.className + ' ' );
5103
if ( cname && cname.indexOf( "ui-btn " ) > -1 && cname.indexOf( "ui-disabled " ) < 0 ) {
5107
element = element.parentNode;
5113
var attachEvents = function() {
5114
var hoverDelay = $.mobile.buttonMarkup.hoverDelay, hov, foc;
5116
$( document ).bind( {
5117
"vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart": function( event ) {
5119
$btn = $( closestEnabledButton( event.target ) ),
5120
isTouchEvent = event.originalEvent && /^touch/.test( event.originalEvent.type ),
5123
if ( $btn.length ) {
5124
theme = $btn.attr( "data-" + $.mobile.ns + "theme" );
5126
if ( evt === "vmousedown" ) {
5127
if ( isTouchEvent ) {
5128
// Use a short delay to determine if the user is scrolling before highlighting
5129
hov = setTimeout( function() {
5130
$btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme );
5133
$btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme );
5135
} else if ( evt === "vmousecancel" || evt === "vmouseup" ) {
5136
$btn.removeClass( "ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme );
5137
} else if ( evt === "vmouseover" || evt === "focus" ) {
5138
if ( isTouchEvent ) {
5139
// Use a short delay to determine if the user is scrolling before highlighting
5140
foc = setTimeout( function() {
5141
$btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme );
5144
$btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme );
5146
} else if ( evt === "vmouseout" || evt === "blur" || evt === "scrollstart" ) {
5147
$btn.removeClass( "ui-btn-hover-" + theme + " ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme );
5149
clearTimeout( hov );
5152
clearTimeout( foc );
5157
"focusin focus": function( event ) {
5158
$( closestEnabledButton( event.target ) ).addClass( $.mobile.focusClass );
5160
"focusout blur": function( event ) {
5161
$( closestEnabledButton( event.target ) ).removeClass( $.mobile.focusClass );
5165
attachEvents = null;
5168
//links in bars, or those with data-role become buttons
5169
//auto self-init widgets
5170
$( document ).bind( "pagecreate create", function( e ) {
5172
$( ":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a", e.target )
5174
.not( "button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')" )
5181
(function( $, undefined ) {
5183
$.widget( "mobile.collapsible", $.mobile.widget, {
5185
expandCueText: " click to expand contents",
5186
collapseCueText: " click to collapse contents",
5188
heading: "h1,h2,h3,h4,h5,h6,legend",
5193
initSelector: ":jqmData(role='collapsible')"
5195
_create: function() {
5197
var $el = this.element,
5199
collapsible = $el.addClass( "ui-collapsible" ),
5200
collapsibleHeading = $el.children( o.heading ).first(),
5201
collapsedIcon = $el.jqmData( "collapsed-icon" ) || o.collapsedIcon,
5202
expandedIcon = $el.jqmData( "expanded-icon" ) || o.expandedIcon,
5203
collapsibleContent = collapsible.wrapInner( "<div class='ui-collapsible-content'></div>" ).children( ".ui-collapsible-content" ),
5204
collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" );
5206
// Replace collapsibleHeading if it's a legend
5207
if ( collapsibleHeading.is( "legend" ) ) {
5208
collapsibleHeading = $( "<div role='heading'>"+ collapsibleHeading.html() +"</div>" ).insertBefore( collapsibleHeading );
5209
collapsibleHeading.next().remove();
5212
// If we are in a collapsible set
5213
if ( collapsibleSet.length ) {
5214
// Inherit the theme from collapsible-set
5216
o.theme = collapsibleSet.jqmData( "theme" ) || $.mobile.getInheritedTheme( collapsibleSet, "c" );
5218
// Inherit the content-theme from collapsible-set
5219
if ( !o.contentTheme ) {
5220
o.contentTheme = collapsibleSet.jqmData( "content-theme" );
5223
// Get the preference for collapsed icon in the set
5224
if ( !o.collapsedIcon ) {
5225
o.collapsedIcon = collapsibleSet.jqmData( "collapsed-icon" );
5227
// Get the preference for expanded icon in the set
5228
if ( !o.expandedIcon ) {
5229
o.expandedIcon = collapsibleSet.jqmData( "expanded-icon" );
5231
// Gets the preference icon position in the set
5233
o.iconpos = collapsibleSet.jqmData( "iconpos" );
5235
// Inherit the preference for inset from collapsible-set or set the default value to ensure equalty within a set
5236
if ( collapsibleSet.jqmData( "inset" ) !== undefined ) {
5237
o.inset = collapsibleSet.jqmData( "inset" );
5241
// Gets the preference for mini in the set
5243
o.mini = collapsibleSet.jqmData( "mini" );
5246
// get inherited theme if not a set and no theme has been set
5248
o.theme = $.mobile.getInheritedTheme( $el, "c" );
5253
collapsible.addClass( "ui-collapsible-inset" );
5256
collapsibleContent.addClass( ( o.contentTheme ) ? ( "ui-body-" + o.contentTheme ) : "");
5258
collapsedIcon = $el.jqmData( "collapsed-icon" ) || o.collapsedIcon || "plus";
5259
expandedIcon = $el.jqmData( "expanded-icon" ) || o.expandedIcon || "minus";
5262
//drop heading in before content
5263
.insertBefore( collapsibleContent )
5264
//modify markup & attributes
5265
.addClass( "ui-collapsible-heading" )
5266
.append( "<span class='ui-collapsible-heading-status'></span>" )
5267
.wrapInner( "<a href='#' class='ui-collapsible-heading-toggle'></a>" )
5273
iconpos: $el.jqmData( "iconpos" ) || o.iconpos || "left",
5274
icon: collapsedIcon,
5281
.find( "a" ).first().add( ".ui-btn-inner", $el )
5282
.addClass( "ui-corner-top ui-corner-bottom" );
5287
.bind( "expand collapse", function( event ) {
5288
if ( !event.isDefaultPrevented() ) {
5289
var $this = $( this ),
5290
isCollapse = ( event.type === "collapse" ),
5291
contentTheme = o.contentTheme;
5293
event.preventDefault();
5296
.toggleClass( "ui-collapsible-heading-collapsed", isCollapse )
5297
.find( ".ui-collapsible-heading-status" )
5298
.text( isCollapse ? o.expandCueText : o.collapseCueText )
5301
.toggleClass( "ui-icon-" + expandedIcon, !isCollapse )
5302
// logic or cause same icon for expanded/collapsed state would remove the ui-icon-class
5303
.toggleClass( "ui-icon-" + collapsedIcon, ( isCollapse || expandedIcon === collapsedIcon ) )
5305
.find( "a" ).first().removeClass( $.mobile.activeBtnClass );
5307
$this.toggleClass( "ui-collapsible-collapsed", isCollapse );
5308
collapsibleContent.toggleClass( "ui-collapsible-content-collapsed", isCollapse ).attr( "aria-hidden", isCollapse );
5310
if ( contentTheme && !!o.inset && ( !collapsibleSet.length || collapsible.jqmData( "collapsible-last" ) ) ) {
5312
.find( "a" ).first().add( collapsibleHeading.find( ".ui-btn-inner" ) )
5313
.toggleClass( "ui-corner-bottom", isCollapse );
5314
collapsibleContent.toggleClass( "ui-corner-bottom", !isCollapse );
5316
collapsibleContent.trigger( "updatelayout" );
5319
.trigger( o.collapsed ? "collapse" : "expand" );
5322
.bind( "tap", function( event ) {
5323
collapsibleHeading.find( "a" ).first().addClass( $.mobile.activeBtnClass );
5325
.bind( "click", function( event ) {
5327
var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? "expand" : "collapse";
5329
collapsible.trigger( type );
5331
event.preventDefault();
5332
event.stopPropagation();
5337
//auto self-init widgets
5338
$( document ).bind( "pagecreate create", function( e ) {
5339
$.mobile.collapsible.prototype.enhanceWithin( e.target );
5344
(function( $, undefined ) {
5346
$.widget( "mobile.collapsibleset", $.mobile.widget, {
5348
initSelector: ":jqmData(role='collapsible-set')"
5350
_create: function() {
5351
var $el = this.element.addClass( "ui-collapsible-set" ),
5354
// Inherit the theme from collapsible-set
5356
o.theme = $.mobile.getInheritedTheme( $el, "c" );
5358
// Inherit the content-theme from collapsible-set
5359
if ( !o.contentTheme ) {
5360
o.contentTheme = $el.jqmData( "content-theme" );
5363
if ( $el.jqmData( "inset" ) !== undefined ) {
5364
o.inset = $el.jqmData( "inset" );
5366
o.inset = o.inset !== undefined ? o.inset : true;
5368
// Initialize the collapsible set if it's not already initialized
5369
if ( !$el.jqmData( "collapsiblebound" ) ) {
5371
.jqmData( "collapsiblebound", true )
5372
.bind( "expand collapse", function( event ) {
5373
var isCollapse = ( event.type === "collapse" ),
5374
collapsible = $( event.target ).closest( ".ui-collapsible" ),
5375
widget = collapsible.data( "collapsible" );
5376
if ( collapsible.jqmData( "collapsible-last" ) && !!o.inset ) {
5377
collapsible.find( ".ui-collapsible-heading" ).first()
5378
.find( "a" ).first()
5379
.toggleClass( "ui-corner-bottom", isCollapse )
5380
.find( ".ui-btn-inner" )
5381
.toggleClass( "ui-corner-bottom", isCollapse );
5382
collapsible.find( ".ui-collapsible-content" ).toggleClass( "ui-corner-bottom", !isCollapse );
5385
.bind( "expand", function( event ) {
5386
var closestCollapsible = $( event.target )
5387
.closest( ".ui-collapsible" );
5388
if ( closestCollapsible.parent().is( ":jqmData(role='collapsible-set')" ) ) {
5390
.siblings( ".ui-collapsible" )
5391
.trigger( "collapse" );
5398
var $el = this.element,
5399
collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ),
5400
expanded = collapsiblesInSet.filter( ":jqmData(collapsed='false')" );
5403
// Because the corners are handled by the collapsible itself and the default state is collapsed
5404
// That was causing https://github.com/jquery/jquery-mobile/issues/4116
5405
expanded.trigger( "expand" );
5408
refresh: function() {
5409
var $el = this.element,
5411
collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" );
5413
$.mobile.collapsible.prototype.enhance( collapsiblesInSet.not( ".ui-collapsible" ) );
5417
collapsiblesInSet.each(function() {
5418
$( this ).jqmRemoveData( "collapsible-last" )
5419
.find( ".ui-collapsible-heading" )
5420
.find( "a" ).first()
5421
.removeClass( "ui-corner-top ui-corner-bottom" )
5422
.find( ".ui-btn-inner" )
5423
.removeClass( "ui-corner-top ui-corner-bottom" );
5426
collapsiblesInSet.first()
5429
.addClass( "ui-corner-top" )
5430
.find( ".ui-btn-inner" )
5431
.addClass( "ui-corner-top" );
5433
collapsiblesInSet.last()
5434
.jqmData( "collapsible-last", true )
5437
.addClass( "ui-corner-bottom" )
5438
.find( ".ui-btn-inner" )
5439
.addClass( "ui-corner-bottom" );
5444
//auto self-init widgets
5445
$( document ).bind( "pagecreate create", function( e ) {
5446
$.mobile.collapsibleset.prototype.enhanceWithin( e.target );
5451
(function( $, undefined ) {
5453
$.widget( "mobile.navbar", $.mobile.widget, {
5457
initSelector: ":jqmData(role='navbar')"
5460
_create: function() {
5462
var $navbar = this.element,
5463
$navbtns = $navbar.find( "a" ),
5464
iconpos = $navbtns.filter( ":jqmData(icon)" ).length ?
5465
this.options.iconpos : undefined;
5467
$navbar.addClass( "ui-navbar ui-mini" )
5468
.attr( "role", "navigation" )
5471
.grid({ grid: this.options.grid });
5473
$navbtns.buttonMarkup({
5480
$navbar.delegate( "a", "vclick", function( event ) {
5481
if ( !$(event.target).hasClass( "ui-disabled" ) ) {
5482
$navbtns.removeClass( $.mobile.activeBtnClass );
5483
$( this ).addClass( $.mobile.activeBtnClass );
5487
// Buttons in the navbar with ui-state-persist class should regain their active state before page show
5488
$navbar.closest( ".ui-page" ).bind( "pagebeforeshow", function() {
5489
$navbtns.filter( ".ui-state-persist" ).addClass( $.mobile.activeBtnClass );
5494
//auto self-init widgets
5495
$( document ).bind( "pagecreate create", function( e ) {
5496
$.mobile.navbar.prototype.enhanceWithin( e.target );
5501
(function( $, undefined ) {
5503
//Keeps track of the number of lists per page UID
5504
//This allows support for multiple nested list in the same page
5505
//https://github.com/jquery/jquery-mobile/issues/1617
5506
var listCountPerPage = {};
5508
$.widget( "mobile.listview", $.mobile.widget, {
5516
splitIcon: "arrow-r",
5519
initSelector: ":jqmData(role='listview')"
5522
_create: function() {
5524
listviewClasses = "";
5526
listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "";
5528
// create listview markup
5529
t.element.addClass(function( i, orig ) {
5530
return orig + " ui-listview " + listviewClasses;
5536
_removeCorners: function( li, which ) {
5537
var top = "ui-corner-top ui-corner-tr ui-corner-tl",
5538
bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
5540
li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
5542
if ( which === "top" ) {
5543
li.removeClass( top );
5544
} else if ( which === "bottom" ) {
5545
li.removeClass( bot );
5547
li.removeClass( top + " " + bot );
5551
_refreshCorners: function( create ) {
5557
$li = this.element.children( "li" );
5558
// At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden
5559
$visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
5561
// ui-li-last is used for setting border-bottom on the last li
5562
$li.filter( ".ui-li-last" ).removeClass( "ui-li-last" );
5564
if ( this.options.inset ) {
5565
this._removeCorners( $li );
5567
// Select the first visible li element
5568
$topli = $visibleli.first()
5569
.addClass( "ui-corner-top" );
5571
$topli.add( $topli.find( ".ui-btn-inner" )
5572
.not( ".ui-li-link-alt span:first-child" ) )
5573
.addClass( "ui-corner-top" )
5575
.find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" )
5576
.addClass( "ui-corner-tr" )
5578
.find( ".ui-li-thumb" )
5579
.not( ".ui-li-icon" )
5580
.addClass( "ui-corner-tl" );
5582
// Select the last visible li element
5583
$bottomli = $visibleli.last()
5584
.addClass( "ui-corner-bottom ui-li-last" );
5586
$bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
5587
.find( ".ui-li-link-alt" )
5588
.addClass( "ui-corner-br" )
5590
.find( ".ui-li-thumb" )
5591
.not( ".ui-li-icon" )
5592
.addClass( "ui-corner-bl" );
5594
$visibleli.last().addClass( "ui-li-last" );
5597
this.element.trigger( "updatelayout" );
5601
// This is a generic utility method for finding the first
5602
// node with a given nodeName. It uses basic DOM traversal
5603
// to be fast and is meant to be a substitute for simple
5604
// $.fn.closest() and $.fn.children() calls on a single
5605
// element. Note that callers must pass both the lowerCase
5606
// and upperCase version of the nodeName they are looking for.
5607
// The main reason for this is that this function will be
5608
// called many times and we want to avoid having to lowercase
5609
// the nodeName from the element every time to ensure we have
5610
// a match. Note that this function lives here for now, but may
5611
// be moved into $.mobile if other components need a similar method.
5612
_findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) {
5614
dict[ lcName ] = dict[ ucName ] = true;
5616
if ( dict[ ele.nodeName ] ) {
5619
ele = ele[ nextProp ];
5623
_getChildrenByTagName: function( ele, lcName, ucName ) {
5626
dict[ lcName ] = dict[ ucName ] = true;
5627
ele = ele.firstChild;
5629
if ( dict[ ele.nodeName ] ) {
5630
results.push( ele );
5632
ele = ele.nextSibling;
5634
return $( results );
5637
_addThumbClasses: function( containers ) {
5638
var i, img, len = containers.length;
5639
for ( i = 0; i < len; i++ ) {
5640
img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
5642
img.addClass( "ui-li-thumb" );
5643
$( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
5648
refresh: function( create ) {
5649
this.parentPage = this.element.closest( ".ui-page" );
5650
this._createSubPages();
5652
var o = this.options,
5653
$list = this.element,
5655
dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
5656
listsplittheme = $list.jqmData( "splittheme" ),
5657
listspliticon = $list.jqmData( "spliticon" ),
5658
listicon = $list.jqmData( "icon" ),
5659
li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
5660
ol = !!$.nodeName( $list[ 0 ], "ol" ),
5661
jsCount = !$.support.cssPseudoElement,
5662
start = $list.attr( "start" ),
5664
item, itemClass, itemTheme,
5665
a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon;
5667
if ( ol && jsCount ) {
5668
$list.find( ".ui-li-dec" ).remove();
5672
// Check if a start attribute has been set while taking a value of 0 into account
5673
if ( start || start === 0 ) {
5675
startCount = parseFloat( start ) - 1;
5676
$list.css( "counter-reset", "listnumbering " + startCount );
5678
counter = parseFloat( start );
5680
} else if ( jsCount ) {
5686
o.theme = $.mobile.getInheritedTheme( this.element, "c" );
5689
for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
5690
item = li.eq( pos );
5691
itemClass = "ui-li";
5693
// If we're creating the element, we update it regardless
5694
if ( create || !item.hasClass( "ui-li" ) ) {
5695
itemTheme = item.jqmData( "theme" ) || o.theme;
5696
a = this._getChildrenByTagName( item[ 0 ], "a", "A" );
5697
var isDivider = ( item.jqmData( "role" ) === "list-divider" );
5699
if ( a.length && !isDivider ) {
5700
icon = item.jqmData( "icon" );
5707
icon: a.length > 1 || icon === false ? false : icon || listicon || o.icon,
5711
if ( ( icon !== false ) && ( a.length === 1 ) ) {
5712
item.addClass( "ui-li-has-arrow" );
5715
a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
5717
if ( a.length > 1 ) {
5718
itemClass += " ui-li-has-alt";
5721
splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
5722
linkIcon = last.jqmData( "icon" );
5724
last.appendTo( item )
5725
.attr( "title", last.getEncodedText() )
5726
.addClass( "ui-li-link-alt" )
5735
.find( ".ui-btn-inner" )
5737
$( document.createElement( "span" ) ).buttonMarkup({
5742
// link icon overrides list item icon overrides ul element overrides options
5743
icon: linkIcon || icon || listspliticon || o.splitIcon
5747
} else if ( isDivider ) {
5749
itemClass += " ui-li-divider ui-bar-" + dividertheme;
5750
item.attr( "role", "heading" );
5753
//reset counter when a divider heading is encountered
5754
if ( start || start === 0 ) {
5756
newStartCount = parseFloat( start ) - 1;
5757
item.css( "counter-reset", "listnumbering " + newStartCount );
5759
counter = parseFloat( start );
5761
} else if ( jsCount ) {
5767
itemClass += " ui-li-static ui-btn-up-" + itemTheme;
5771
if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
5772
countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" );
5774
countParent.addClass( "ui-li-jsnumbering" )
5775
.prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" );
5778
// Instead of setting item class directly on the list item and its
5779
// btn-inner at this point in time, push the item into a dictionary
5780
// that tells us what class to set on it so we can do this after this
5781
// processing loop is finished.
5783
if ( !itemClassDict[ itemClass ] ) {
5784
itemClassDict[ itemClass ] = [];
5787
itemClassDict[ itemClass ].push( item[ 0 ] );
5790
// Set the appropriate listview item classes on each list item
5791
// and their btn-inner elements. The main reason we didn't do this
5792
// in the for-loop above is because we can eliminate per-item function overhead
5793
// by calling addClass() and children() once or twice afterwards. This
5794
// can give us a significant boost on platforms like WP7.5.
5796
for ( itemClass in itemClassDict ) {
5797
$( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
5800
$list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
5803
.find( "p, dl" ).addClass( "ui-li-desc" )
5806
.find( ".ui-li-aside" ).each(function() {
5807
var $this = $( this );
5808
$this.prependTo( $this.parent() ); //shift aside to front for css float
5812
.find( ".ui-li-count" ).each(function() {
5813
$( this ).closest( "li" ).addClass( "ui-li-has-count" );
5814
}).addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
5816
// The idea here is to look at the first image in the list item
5817
// itself, and any .ui-link-inherit element it may contain, so we
5818
// can place the appropriate classes on the image and list item.
5819
// Note that we used to use something like:
5821
// li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
5823
// But executing a find() like that on Windows Phone 7.5 took a
5824
// really long time. Walking things manually with the code below
5825
// allows the 400 listview item page to load in about 3 seconds as
5826
// opposed to 30 seconds.
5828
this._addThumbClasses( li );
5829
this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
5831
this._refreshCorners( create );
5833
// autodividers binds to this to redraw dividers after the listview refresh
5834
this._trigger( "afterrefresh" );
5837
//create a string for ID/subpage url creation
5838
_idStringEscape: function( str ) {
5839
return str.replace(/[^a-zA-Z0-9]/g, '-');
5842
_createSubPages: function() {
5843
var parentList = this.element,
5844
parentPage = parentList.closest( ".ui-page" ),
5845
parentUrl = parentPage.jqmData( "url" ),
5846
parentId = parentUrl || parentPage[ 0 ][ $.expando ],
5847
parentListId = parentList.attr( "id" ),
5849
dns = "data-" + $.mobile.ns,
5851
persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
5854
if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
5855
listCountPerPage[ parentId ] = -1;
5858
parentListId = parentListId || ++listCountPerPage[ parentId ];
5860
$( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
5863
listId = list.attr( "id" ) || parentListId + "-" + i,
5864
parent = list.parent(),
5865
nodeElsFull = $( list.prevAll().toArray().reverse() ),
5866
nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
5867
title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
5868
id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
5869
theme = list.jqmData( "theme" ) || o.theme,
5870
countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
5873
//define hasSubPages for use in later removal
5876
newPage = list.detach()
5877
.wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
5879
.before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
5880
.after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" )
5882
.appendTo( $.mobile.pageContainer );
5886
anchor = parent.find( 'a:first' );
5888
if ( !anchor.length ) {
5889
anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
5892
anchor.attr( "href", "#" + id );
5896
// on pagehide, remove any nested pages along with the parent page, as long as they aren't active
5897
// and aren't embedded
5899
parentPage.is( ":jqmData(external-page='true')" ) &&
5900
parentPage.data( "page" ).options.domCache === false ) {
5902
var newRemove = function( e, ui ) {
5903
var nextPage = ui.nextPage, npURL,
5904
prEvent = new $.Event( "pageremove" );
5906
if ( ui.nextPage ) {
5907
npURL = nextPage.jqmData( "url" );
5908
if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
5909
self.childPages().remove();
5910
parentPage.trigger( prEvent );
5911
if ( !prEvent.isDefaultPrevented() ) {
5912
parentPage.removeWithDependents();
5918
// unbind the original page remove and replace with our specialized version
5920
.unbind( "pagehide.remove" )
5921
.bind( "pagehide.remove", newRemove);
5925
// TODO sort out a better way to track sub pages of the listview this is brittle
5926
childPages: function() {
5927
var parentUrl = this.parentPage.jqmData( "url" );
5929
return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
5933
//auto self-init widgets
5934
$( document ).bind( "pagecreate create", function( e ) {
5935
$.mobile.listview.prototype.enhanceWithin( e.target );
5940
(function( $, undefined ) {
5942
$.mobile.listview.prototype.options.autodividers = false;
5943
$.mobile.listview.prototype.options.autodividersSelector = function( elt ) {
5944
// look for the text in the given element
5945
var text = $.trim( elt.text() ) || null;
5951
// create the text for the divider (first uppercased letter)
5952
text = text.slice( 0, 1 ).toUpperCase();
5957
$( document ).delegate( "ul,ol", "listviewcreate", function() {
5959
var list = $( this ),
5960
listview = list.data( "listview" );
5962
if ( !listview || !listview.options.autodividers ) {
5966
var replaceDividers = function () {
5967
list.find( "li:jqmData(role='list-divider')" ).remove();
5969
var lis = list.find( 'li' ),
5970
lastDividerText = null, li, dividerText;
5972
for ( var i = 0; i < lis.length ; i++ ) {
5974
dividerText = listview.options.autodividersSelector( $( li ) );
5976
if ( dividerText && lastDividerText !== dividerText ) {
5977
var divider = document.createElement( 'li' );
5978
divider.appendChild( document.createTextNode( dividerText ) );
5979
divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' );
5980
li.parentNode.insertBefore( divider, li );
5983
lastDividerText = dividerText;
5987
var afterListviewRefresh = function () {
5988
list.unbind( 'listviewafterrefresh', afterListviewRefresh );
5991
list.bind( 'listviewafterrefresh', afterListviewRefresh );
5994
afterListviewRefresh();
6000
* "checkboxradio" plugin
6003
(function( $, undefined ) {
6005
$.widget( "mobile.checkboxradio", $.mobile.widget, {
6009
initSelector: "input[type='checkbox'],input[type='radio']"
6011
_create: function() {
6013
input = this.element,
6015
inheritAttr = function( input, dataAttr ) {
6016
return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr );
6018
// NOTE: Windows Phone could not find the label through a selector
6019
// filter works though.
6020
parentLabel = $( input ).closest( "label" ),
6021
label = parentLabel.length ? parentLabel : $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ).first(),
6022
inputtype = input[0].type,
6023
mini = inheritAttr( input, "mini" ) || o.mini,
6024
checkedState = inputtype + "-on",
6025
uncheckedState = inputtype + "-off",
6026
icon = input.parents( ":jqmData(type='horizontal')" ).length ? undefined : uncheckedState,
6027
iconpos = inheritAttr( input, "iconpos" ),
6028
activeBtn = icon ? "" : " " + $.mobile.activeBtnClass,
6029
checkedClass = "ui-" + checkedState + activeBtn,
6030
uncheckedClass = "ui-" + uncheckedState,
6031
checkedicon = "ui-icon-" + checkedState,
6032
uncheckedicon = "ui-icon-" + uncheckedState;
6034
if ( inputtype !== "checkbox" && inputtype !== "radio" ) {
6038
// Expose for other methods
6041
inputtype: inputtype,
6042
checkedClass: checkedClass,
6043
uncheckedClass: uncheckedClass,
6044
checkedicon: checkedicon,
6045
uncheckedicon: uncheckedicon
6048
// If there's no selected theme check the data attr
6050
o.theme = $.mobile.getInheritedTheme( this.element, "c" );
6053
label.buttonMarkup({
6061
// Wrap the input + label in a div
6062
var wrapper = document.createElement('div');
6063
wrapper.className = 'ui-' + inputtype;
6065
input.add( label ).wrapAll( wrapper );
6068
vmouseover: function( event ) {
6069
if ( $( this ).parent().is( ".ui-disabled" ) ) {
6070
event.stopPropagation();
6074
vclick: function( event ) {
6075
if ( input.is( ":disabled" ) ) {
6076
event.preventDefault();
6082
input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) );
6084
// trigger click handler's bound directly to the input as a substitute for
6085
// how label clicks behave normally in the browsers
6086
// TODO: it would be nice to let the browser's handle the clicks and pass them
6087
// through to the associate input. we can swallow that click at the parent
6088
// wrapper element level
6089
input.triggerHandler( 'click' );
6091
// Input set for common radio buttons will contain all the radio
6092
// buttons, but will not for checkboxes. clearing the checked status
6093
// of other radios ensures the active button state is applied properly
6094
self._getInputSet().not( input ).prop( "checked", false );
6103
vmousedown: function() {
6107
vclick: function() {
6108
var $this = $( this );
6110
// Adds checked attribute to checked input when keyboard is used
6111
if ( $this.is( ":checked" ) ) {
6113
$this.prop( "checked", true);
6114
self._getInputSet().not( $this ).prop( "checked", false );
6117
$this.prop( "checked", false );
6124
label.addClass( $.mobile.focusClass );
6128
label.removeClass( $.mobile.focusClass );
6132
if ( this._handleFormReset ) {
6133
this._handleFormReset();
6138
_cacheVals: function() {
6139
this._getInputSet().each(function() {
6140
$( this ).jqmData( "cacheVal", this.checked );
6144
//returns either a set of radios with the same name attribute, or a single checkbox
6145
_getInputSet: function() {
6146
if ( this.inputtype === "checkbox" ) {
6147
return this.element;
6150
return this.element.closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" )
6151
.find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" );
6154
_updateAll: function() {
6157
this._getInputSet().each(function() {
6158
var $this = $( this );
6160
if ( this.checked || self.inputtype === "checkbox" ) {
6161
$this.trigger( "change" );
6164
.checkboxradio( "refresh" );
6167
_reset: function() {
6171
refresh: function() {
6172
var input = this.element[0],
6174
icon = label.find( ".ui-icon" );
6176
if ( input.checked ) {
6177
label.addClass( this.checkedClass ).removeClass( this.uncheckedClass );
6178
icon.addClass( this.checkedicon ).removeClass( this.uncheckedicon );
6180
label.removeClass( this.checkedClass ).addClass( this.uncheckedClass );
6181
icon.removeClass( this.checkedicon ).addClass( this.uncheckedicon );
6184
if ( input.disabled ) {
6191
disable: function() {
6192
this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" );
6195
enable: function() {
6196
this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" );
6200
$.widget( "mobile.checkboxradio", $.mobile.checkboxradio, $.mobile.behaviors.formReset );
6202
//auto self-init widgets
6203
$( document ).bind( "pagecreate create", function( e ) {
6204
$.mobile.checkboxradio.prototype.enhanceWithin( e.target, true );
6209
(function( $, undefined ) {
6211
$.widget( "mobile.button", $.mobile.widget, {
6219
initSelector: "button, [type='button'], [type='submit'], [type='reset']"
6221
_create: function() {
6222
var $el = this.element,
6227
inline = o.inline || $el.jqmData( "inline" ),
6228
mini = o.mini || $el.jqmData( "mini" ),
6232
// if this is a link, check if it's been enhanced and, if not, use the right function
6233
if ( $el[ 0 ].tagName === "A" ) {
6234
if ( !$el.hasClass( "ui-btn" ) ) {
6241
// get the inherited theme
6242
// TODO centralize for all widgets
6243
if ( !this.options.theme ) {
6244
this.options.theme = $.mobile.getInheritedTheme( this.element, "c" );
6247
// 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
6248
/* if ( $el[0].className.length ) {
6249
classes = $el[0].className;
6251
if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) {
6252
classes = "ui-btn-left";
6255
if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) {
6256
classes = "ui-btn-right";
6259
if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) {
6260
classes ? classes += " ui-submit" : classes = "ui-submit";
6262
$( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" );
6265
this.button = $( "<div></div>" )
6266
[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() )
6267
.insertBefore( $el )
6275
iconshadow: o.iconshadow,
6278
.addClass( classes )
6279
.append( $el.addClass( "ui-btn-hidden" ) );
6281
$button = this.button;
6282
type = $el.attr( "type" );
6283
name = $el.attr( "name" );
6285
// Add hidden input during submit if input type="submit" has a name.
6286
if ( type !== "button" && type !== "reset" && name ) {
6287
$el.bind( "vclick", function() {
6288
// Add hidden input if it doesn't already exist.
6289
if ( $buttonPlaceholder === undefined ) {
6290
$buttonPlaceholder = $( "<input>", {
6292
name: $el.attr( "name" ),
6293
value: $el.attr( "value" )
6294
}).insertBefore( $el );
6296
// Bind to doc to remove after submit handling
6297
$( document ).one( "submit", function() {
6298
$buttonPlaceholder.remove();
6300
// reset the local var so that the hidden input
6301
// will be re-added on subsequent clicks
6302
$buttonPlaceholder = undefined;
6310
$button.addClass( $.mobile.focusClass );
6314
$button.removeClass( $.mobile.focusClass );
6321
enable: function() {
6322
this.element.attr( "disabled", false );
6323
this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
6324
return this._setOption( "disabled", false );
6327
disable: function() {
6328
this.element.attr( "disabled", true );
6329
this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true );
6330
return this._setOption( "disabled", true );
6333
refresh: function() {
6334
var $el = this.element;
6336
if ( $el.prop("disabled") ) {
6342
// Grab the button's text element from its implementation-independent data item
6343
$( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() );
6347
//auto self-init widgets
6348
$( document ).bind( "pagecreate create", function( e ) {
6349
$.mobile.button.prototype.enhanceWithin( e.target, true );
6354
(function( $, undefined ) {
6356
$.fn.controlgroup = function( options ) {
6357
function flipClasses( els, flCorners ) {
6358
els.removeClass( "ui-btn-corner-all ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-controlgroup-last ui-shadow" )
6359
.eq( 0 ).addClass( flCorners[ 0 ] )
6361
.last().addClass( flCorners[ 1 ] ).addClass( "ui-controlgroup-last" );
6364
return this.each(function() {
6365
var $el = $( this ),
6367
direction: $el.jqmData( "type" ) || "vertical",
6369
excludeInvisible: true,
6370
mini: $el.jqmData( "mini" )
6372
grouplegend = $el.children( "legend" ),
6373
groupheading = $el.children( ".ui-controlgroup-label" ),
6374
groupcontrols = $el.children( ".ui-controlgroup-controls" ),
6375
flCorners = o.direction === "horizontal" ? [ "ui-corner-left", "ui-corner-right" ] : [ "ui-corner-top", "ui-corner-bottom" ],
6376
type = $el.find( "input" ).first().attr( "type" );
6378
// First unwrap the controls if the controlgroup was already enhanced
6379
if ( groupcontrols.length ) {
6380
groupcontrols.contents().unwrap();
6382
$el.wrapInner( "<div class='ui-controlgroup-controls'></div>" );
6384
if ( grouplegend.length ) {
6385
// Replace legend with more stylable replacement div
6386
$( "<div role='heading' class='ui-controlgroup-label'>" + grouplegend.html() + "</div>" ).insertBefore( $el.children( 0 ) );
6387
grouplegend.remove();
6388
} else if ( groupheading.length ) {
6389
// Just move the heading if the controlgroup was already enhanced
6390
$el.prepend( groupheading );
6393
$el.addClass( "ui-corner-all ui-controlgroup ui-controlgroup-" + o.direction );
6395
flipClasses( $el.find( ".ui-btn" + ( o.excludeInvisible ? ":visible" : "" ) ).not( '.ui-slider-handle' ), flCorners );
6396
flipClasses( $el.find( ".ui-btn-inner" ), flCorners );
6399
$el.addClass( "ui-shadow" );
6403
$el.addClass( "ui-mini" );
6409
// The pagecreate handler for controlgroup is in jquery.mobile.init because of the soft-dependency on the wrapped widgets
6413
(function( $, undefined ) {
6415
$( document ).bind( "pagecreate create", function( e ) {
6417
//links within content areas, tests included with page
6421
.not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" )
6422
.addClass( "ui-link" );
6429
(function( $, undefined ) {
6431
function fitSegmentInsideSegment( winSize, segSize, offset, desired ) {
6434
if ( winSize < segSize ) {
6435
// Center segment if it's bigger than the window
6436
ret = offset + ( winSize - segSize ) / 2;
6438
// Otherwise center it at the desired coordinate while keeping it completely inside the window
6439
ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize );
6445
function windowCoords() {
6446
var $win = $( window );
6449
x: $win.scrollLeft(),
6450
y: $win.scrollTop(),
6451
cx: ( window.innerWidth || $win.width() ),
6452
cy: ( window.innerHeight || $win.height() )
6456
$.widget( "mobile.popup", $.mobile.widget, {
6463
positionTo: "origin",
6465
initSelector: ":jqmData(role='popup')",
6466
closeLinkSelector: "a:jqmData(rel='back')",
6467
closeLinkEvents: "click.popup",
6468
navigateEvents: "navigate.popup",
6469
closeEvents: "navigate.popup pagebeforechange.popup",
6471
// NOTE Windows Phone 7 has a scroll position caching issue that
6472
// requires us to disable popup history management by default
6473
// https://github.com/jquery/jquery-mobile/issues/4784
6475
// NOTE this option is modified in _create!
6476
history: !$.mobile.browser.ie
6479
_eatEventAndClose: function( e ) {
6481
e.stopImmediatePropagation();
6486
// Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height
6487
_resizeScreen: function() {
6488
var popupHeight = this._ui.container.outerHeight( true );
6490
this._ui.screen.removeAttr( "style" );
6491
if ( popupHeight > this._ui.screen.height() ) {
6492
this._ui.screen.height( popupHeight );
6496
_handleWindowKeyUp: function( e ) {
6497
if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) {
6498
return this._eatEventAndClose( e );
6502
_expectResizeEvent: function() {
6503
var winCoords = windowCoords();
6505
if ( this._resizeData ) {
6506
if ( winCoords.x === this._resizeData.winCoords.x &&
6507
winCoords.y === this._resizeData.winCoords.y &&
6508
winCoords.cx === this._resizeData.winCoords.cx &&
6509
winCoords.cy === this._resizeData.winCoords.cy ) {
6510
// timeout not refreshed
6513
// clear existing timeout - it will be refreshed below
6514
clearTimeout( this._resizeData.timeoutId );
6518
this._resizeData = {
6519
timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ),
6520
winCoords: winCoords
6526
_resizeTimeout: function() {
6527
if ( this._isOpen ) {
6528
if ( !this._expectResizeEvent() ) {
6529
if ( this._ui.container.hasClass( "ui-popup-hidden" ) ) {
6530
// effectively rapid-open the popup while leaving the screen intact
6531
this._trigger( "beforeposition" );
6533
.removeClass( "ui-popup-hidden" )
6534
.offset( this._placementCoords( this._desiredCoords( undefined, undefined, "window" ) ) );
6537
this._resizeScreen();
6538
this._resizeData = null;
6539
this._orientationchangeInProgress = false;
6542
this._resizeData = null;
6543
this._orientationchangeInProgress = false;
6547
_handleWindowResize: function( e ) {
6548
if ( this._isOpen ) {
6549
if ( ( this._expectResizeEvent() || this._orientationchangeInProgress ) &&
6550
!this._ui.container.hasClass( "ui-popup-hidden" ) ) {
6551
// effectively rapid-close the popup while leaving the screen intact
6553
.addClass( "ui-popup-hidden" )
6554
.removeAttr( "style" );
6559
_handleWindowOrientationchange: function( e ) {
6560
if ( !this._orientationchangeInProgress && this._isOpen ) {
6561
this._expectResizeEvent();
6562
this._orientationchangeInProgress = true;
6566
_create: function() {
6568
screen: $( "<div class='ui-screen-hidden ui-popup-screen'></div>" ),
6569
placeholder: $( "<div style='display: none;'><!-- placeholder --></div>" ),
6570
container: $( "<div class='ui-popup-container ui-popup-hidden'></div>" )
6572
thisPage = this.element.closest( ".ui-page" ),
6573
myId = this.element.attr( "id" ),
6576
// We need to adjust the history option to be false if there's no AJAX nav.
6577
// We can't do it in the option declarations because those are run before
6578
// it is determined whether there shall be AJAX nav.
6579
this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled;
6581
if ( thisPage.length === 0 ) {
6582
thisPage = $( "body" );
6585
// define the container for navigation event bindings
6586
// TODO this would be nice at the the mobile widget level
6587
this.options.container = this.options.container || $.mobile.pageContainer;
6590
thisPage.append( ui.screen );
6591
ui.container.insertAfter( ui.screen );
6592
// Leave a placeholder where the element used to be
6593
ui.placeholder.insertAfter( this.element );
6595
ui.screen.attr( "id", myId + "-screen" );
6596
ui.container.attr( "id", myId + "-popup" );
6597
ui.placeholder.html( "<!-- placeholder for " + myId + " -->" );
6599
ui.container.append( this.element );
6601
// Add class to popup element
6602
this.element.addClass( "ui-popup" );
6604
// Define instance variables
6609
_fallbackTransition: "",
6610
_currentTransition: false,
6615
_orientationchangeInProgress: false,
6620
orientationchange: $.proxy( this, "_handleWindowOrientationchange" ),
6621
resize: $.proxy( this, "_handleWindowResize" ),
6622
keyup: $.proxy( this, "_handleWindowKeyUp" )
6628
$.each( this.options, function( key, value ) {
6629
// Cause initial options to be applied by their handler by temporarily setting the option to undefined
6630
// - the handler then sets it to the initial value
6631
self.options[ key ] = undefined;
6632
self._setOption( key, value, true );
6635
ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) );
6637
$.each( this._globalHandlers, function( idx, value ) {
6638
value.src.bind( value.handler );
6642
_applyTheme: function( dst, theme, prefix ) {
6643
var classes = ( dst.attr( "class" ) || "").split( " " ),
6644
alreadyAdded = true,
6645
currentTheme = null,
6647
themeStr = String( theme );
6649
while ( classes.length > 0 ) {
6650
currentTheme = classes.pop();
6651
matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme );
6652
if ( matches && matches.length > 1 ) {
6653
currentTheme = matches[ 1 ];
6656
currentTheme = null;
6660
if ( theme !== currentTheme ) {
6661
dst.removeClass( "ui-" + prefix + "-" + currentTheme );
6662
if ( ! ( theme === null || theme === "none" ) ) {
6663
dst.addClass( "ui-" + prefix + "-" + themeStr );
6668
_setTheme: function( value ) {
6669
this._applyTheme( this.element, value, "body" );
6672
_setOverlayTheme: function( value ) {
6673
this._applyTheme( this._ui.screen, value, "overlay" );
6675
if ( this._isOpen ) {
6676
this._ui.screen.addClass( "in" );
6680
_setShadow: function( value ) {
6681
this.element.toggleClass( "ui-overlay-shadow", value );
6684
_setCorners: function( value ) {
6685
this.element.toggleClass( "ui-corner-all", value );
6688
_applyTransition: function( value ) {
6689
this._ui.container.removeClass( this._fallbackTransition );
6690
if ( value && value !== "none" ) {
6691
this._fallbackTransition = $.mobile._maybeDegradeTransition( value );
6692
if ( this._fallbackTransition === "none" ) {
6693
this._fallbackTransition = "";
6695
this._ui.container.addClass( this._fallbackTransition );
6699
_setTransition: function( value ) {
6700
if ( !this._currentTransition ) {
6701
this._applyTransition( value );
6705
_setTolerance: function( value ) {
6706
var tol = { t: 30, r: 15, b: 30, l: 15 };
6709
var ar = String( value ).split( "," );
6711
$.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } );
6713
switch( ar.length ) {
6714
// All values are to be the same
6716
if ( !isNaN( ar[ 0 ] ) ) {
6717
tol.t = tol.r = tol.b = tol.l = ar[ 0 ];
6721
// The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance
6723
if ( !isNaN( ar[ 0 ] ) ) {
6724
tol.t = tol.b = ar[ 0 ];
6726
if ( !isNaN( ar[ 1 ] ) ) {
6727
tol.l = tol.r = ar[ 1 ];
6731
// The array contains values in the order top, right, bottom, left
6733
if ( !isNaN( ar[ 0 ] ) ) {
6736
if ( !isNaN( ar[ 1 ] ) ) {
6739
if ( !isNaN( ar[ 2 ] ) ) {
6742
if ( !isNaN( ar[ 3 ] ) ) {
6752
this._tolerance = tol;
6755
_setOption: function( key, value ) {
6756
var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 );
6758
if ( this[ setter ] !== undefined ) {
6759
this[ setter ]( value );
6762
// TODO REMOVE FOR 1.2.1 by moving them out to a default options object
6765
"closeLinkSelector",
6773
$.mobile.widget.prototype._setOption.apply( this, arguments );
6774
if ( $.inArray( key, exclusions ) === -1 ) {
6775
// Record the option change in the options and in the DOM data-* attributes
6776
this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value );
6780
// Try and center the overlay over the given coordinates
6781
_placementCoords: function( desired ) {
6782
// rectangle within which the popup must fit
6784
winCoords = windowCoords(),
6786
x: this._tolerance.l,
6787
y: winCoords.y + this._tolerance.t,
6788
cx: winCoords.cx - this._tolerance.l - this._tolerance.r,
6789
cy: winCoords.cy - this._tolerance.t - this._tolerance.b
6793
// Clamp the width of the menu before grabbing its size
6794
this._ui.container.css( "max-width", rc.cx );
6796
cx: this._ui.container.outerWidth( true ),
6797
cy: this._ui.container.outerHeight( true )
6800
// Center the menu over the desired coordinates, while not going outside
6801
// the window tolerances. This will center wrt. the window if the popup is too large.
6803
x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ),
6804
y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y )
6807
// Make sure the top of the menu is visible
6808
ret.y = Math.max( 0, ret.y );
6810
// If the height of the menu is smaller than the height of the document
6811
// align the bottom with the bottom of the document
6813
// fix for $( document ).height() bug in core 1.7.2.
6814
var docEl = document.documentElement, docBody = document.body,
6815
docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight );
6817
ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) );
6819
return { left: ret.x, top: ret.y };
6822
_createPrereqs: function( screenPrereq, containerPrereq, whenDone ) {
6823
var self = this, prereqs;
6825
// It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in
6826
// the closure of the functions which call the callbacks passed in. The comparison between the local variable and
6827
// self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called
6828
// next time an animation completes, even if that's not the animation whose end the function was supposed to catch
6829
// (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for
6830
// that animation anymore, but the handler remains attached, so it is called the next time the popup is opened
6831
// - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that
6832
// callbacks triggered by a stale .animationComplete will be ignored.
6835
screen: $.Deferred(),
6836
container: $.Deferred()
6839
prereqs.screen.then( function() {
6840
if ( prereqs === self._prereqs ) {
6845
prereqs.container.then( function() {
6846
if ( prereqs === self._prereqs ) {
6851
$.when( prereqs.screen, prereqs.container ).done( function() {
6852
if ( prereqs === self._prereqs ) {
6853
self._prereqs = null;
6858
self._prereqs = prereqs;
6861
_animate: function( args ) {
6862
// NOTE before removing the default animation of the screen
6863
// this had an animate callback that would resolve the deferred
6864
// now the deferred is resolved immediately
6865
// TODO remove the dependency on the screen deferred
6867
.removeClass( args.classToRemove )
6868
.addClass( args.screenClassToAdd );
6870
args.prereqs.screen.resolve();
6872
if ( args.transition && args.transition !== "none" ) {
6873
if ( args.applyTransition ) {
6874
this._applyTransition( args.transition );
6876
if ( this._fallbackTransition ) {
6878
.animationComplete( $.proxy( args.prereqs.container, "resolve" ) )
6879
.addClass( args.containerClassToAdd )
6880
.removeClass( args.classToRemove );
6884
this._ui.container.removeClass( args.classToRemove );
6885
args.prereqs.container.resolve();
6888
// The desired coordinates passed in will be returned untouched if no reference element can be identified via
6889
// desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid
6890
// x and y coordinates by specifying the center middle of the window if the coordinates are absent.
6891
_desiredCoords: function( x, y, positionTo ) {
6892
var dst = null, offset, winCoords = windowCoords();
6894
// Establish which element will serve as the reference
6895
if ( positionTo && positionTo !== "origin" ) {
6896
if ( positionTo === "window" ) {
6897
x = winCoords.cx / 2 + winCoords.x;
6898
y = winCoords.cy / 2 + winCoords.y;
6901
dst = $( positionTo );
6906
dst.filter( ":visible" );
6907
if ( dst.length === 0 ) {
6914
// If an element was found, center over it
6916
offset = dst.offset();
6917
x = offset.left + dst.outerWidth() / 2;
6918
y = offset.top + dst.outerHeight() / 2;
6921
// Make sure x and y are valid numbers - center over the window
6922
if ( $.type( x ) !== "number" || isNaN( x ) ) {
6923
x = winCoords.cx / 2 + winCoords.x;
6925
if ( $.type( y ) !== "number" || isNaN( y ) ) {
6926
y = winCoords.cy / 2 + winCoords.y;
6929
return { x: x, y: y };
6932
_openPrereqsComplete: function() {
6935
self._ui.container.addClass( "ui-popup-active" );
6936
self._isOpen = true;
6937
self._resizeScreen();
6939
// Android appears to trigger the animation complete before the popup
6940
// is visible. Allowing the stack to unwind before applying focus prevents
6941
// the "blue flash" of element focus in android 4.0
6942
setTimeout(function(){
6943
self._ui.container.attr( "tabindex", "0" ).focus();
6944
self._expectResizeEvent();
6945
self._trigger( "afteropen" );
6949
_open: function( options ) {
6950
var coords, transition,
6951
androidBlacklist = ( function() {
6953
ua = navigator.userAgent,
6954
// Rendering engine is Webkit, and capture major version
6955
wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ),
6956
wkversion = !!wkmatch && wkmatch[ 1 ],
6957
androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ),
6958
andversion = !!androidmatch && androidmatch[ 1 ],
6959
chromematch = ua.indexOf( "Chrome" ) > -1;
6961
// Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome.
6962
if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) {
6968
// Make sure options is defined
6969
options = ( options || {} );
6971
// Copy out the transition, because we may be overwriting it later and we don't want to pass that change back to the caller
6972
transition = options.transition || this.options.transition;
6974
// Give applications a chance to modify the contents of the container before it appears
6975
this._trigger( "beforeposition" );
6977
coords = this._placementCoords( this._desiredCoords( options.x, options.y, options.positionTo || this.options.positionTo || "origin" ) );
6979
// Count down to triggering "popupafteropen" - we have two prerequisites:
6980
// 1. The popup window animation completes (container())
6981
// 2. The screen opacity animation completes (screen())
6982
this._createPrereqs(
6985
$.proxy( this, "_openPrereqsComplete" ) );
6988
this._currentTransition = transition;
6989
this._applyTransition( transition );
6991
transition = this.options.transition;
6994
if ( !this.options.theme ) {
6995
this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) );
6998
this._ui.screen.removeClass( "ui-screen-hidden" );
7001
.removeClass( "ui-popup-hidden" )
7004
if ( this.options.overlayTheme && androidBlacklist ) {
7006
The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed
7007
above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain
7008
types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser:
7009
https://github.com/scottjehl/Device-Bugs/issues/3
7011
This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ):
7013
https://github.com/jquery/jquery-mobile/issues/4816
7014
https://github.com/jquery/jquery-mobile/issues/4844
7015
https://github.com/jquery/jquery-mobile/issues/4874
7018
// TODO sort out why this._page isn't working
7019
this.element.closest( ".ui-page" ).addClass( "ui-popup-open" );
7022
additionalCondition: true,
7023
transition: transition,
7025
screenClassToAdd: "in",
7026
containerClassToAdd: "in",
7027
applyTransition: false,
7028
prereqs: this._prereqs
7032
_closePrereqScreen: function() {
7034
.removeClass( "out" )
7035
.addClass( "ui-screen-hidden" );
7038
_closePrereqContainer: function() {
7040
.removeClass( "reverse out" )
7041
.addClass( "ui-popup-hidden" )
7042
.removeAttr( "style" );
7045
_closePrereqsDone: function() {
7046
var self = this, opts = self.options;
7048
self._ui.container.removeAttr( "tabindex" );
7050
// remove nav bindings if they are still present
7051
opts.container.unbind( opts.closeEvents );
7053
// unbind click handlers added when history is disabled
7054
self.element.undelegate( opts.closeLinkSelector, opts.closeLinkEvents );
7056
// remove the global mutex for popups
7057
$.mobile.popup.active = undefined;
7059
// alert users that the popup is closed
7060
self._trigger( "afterclose" );
7063
_close: function( immediate ) {
7064
this._ui.container.removeClass( "ui-popup-active" );
7065
this._page.removeClass( "ui-popup-open" );
7067
this._isOpen = false;
7069
// Count down to triggering "popupafterclose" - we have two prerequisites:
7070
// 1. The popup window reverse animation completes (container())
7071
// 2. The screen opacity animation completes (screen())
7072
this._createPrereqs(
7073
$.proxy( this, "_closePrereqScreen" ),
7074
$.proxy( this, "_closePrereqContainer" ),
7075
$.proxy( this, "_closePrereqsDone" ) );
7078
additionalCondition: this._ui.screen.hasClass( "in" ),
7079
transition: ( immediate ? "none" : ( this._currentTransition || this.options.transition ) ),
7080
classToRemove: "in",
7081
screenClassToAdd: "out",
7082
containerClassToAdd: "reverse out",
7083
applyTransition: true,
7084
prereqs: this._prereqs
7088
_unenhance: function() {
7091
// Put the element back to where the placeholder was and remove the "ui-popup" class
7092
self._setTheme( "none" );
7094
// Cannot directly insertAfter() - we need to detach() first, because
7095
// insertAfter() will do nothing if the payload div was not attached
7096
// to the DOM at the time the widget was created, and so the payload
7097
// will remain inside the container even after we call insertAfter().
7098
// If that happens and we remove the container a few lines below, we
7099
// will cause an infinite recursion - #5244
7101
.insertAfter( self._ui.placeholder )
7102
.removeClass( "ui-popup ui-overlay-shadow ui-corner-all" );
7103
self._ui.screen.remove();
7104
self._ui.container.remove();
7105
self._ui.placeholder.remove();
7107
// Unbind handlers that were bound to elements outside self.element (the window, in self case)
7108
$.each( self._globalHandlers, function( idx, oneSrc ) {
7109
$.each( oneSrc.handler, function( eventType, handler ) {
7110
oneSrc.src.unbind( eventType, handler );
7115
_destroy: function() {
7116
if ( $.mobile.popup.active === this ) {
7117
this.element.one( "popupafterclose", $.proxy( this, "_unenhance" ) );
7124
_closePopup: function( e, data ) {
7125
var parsedDst, toUrl;
7127
window.scrollTo( 0, this._scrollTop );
7129
if ( e.type === "pagebeforechange" && data ) {
7130
// Determine whether we need to rapid-close the popup, or whether we can
7131
// take the time to run the closing transition
7132
if ( typeof data.toPage === "string" ) {
7133
parsedDst = data.toPage;
7135
parsedDst = data.toPage.jqmData( "url" );
7137
parsedDst = $.mobile.path.parseUrl( parsedDst );
7138
toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash;
7140
if ( this._myUrl !== toUrl ) {
7141
// Going to a different page - close immediately
7142
this.options.container.unbind( this.options.closeEvents );
7143
this._close( true );
7155
// any navigation event after a popup is opened should close the popup
7156
// NOTE the pagebeforechange is bound to catch navigation events that don't
7157
// alter the url (eg, dialogs from popups)
7158
_bindContainerClose: function() {
7161
self.options.container
7162
.one( self.options.closeEvents, $.proxy( self, "_closePopup" ) );
7165
// TODO no clear deliniation of what should be here and
7166
// what should be in _open. Seems to be "visual" vs "history" for now
7167
open: function( options ) {
7168
var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory;
7170
// make sure open is idempotent
7171
if( $.mobile.popup.active ) {
7175
// set the global popup mutex
7176
$.mobile.popup.active = this;
7177
this._scrollTop = $( window ).scrollTop();
7179
// if history alteration is disabled close on navigate events
7180
// and leave the url as is
7181
if( !( opts.history ) ) {
7182
self._open( options );
7183
self._bindContainerClose();
7185
// When histoy is disabled we have to grab the data-rel
7186
// back link clicks so we can close the popup instead of
7187
// relying on history to do it for us
7189
.delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) {
7192
// NOTE prevent the browser and navigation handlers from
7193
// working with the link's rel=back. This may cause
7194
// issues for developers expecting the event to bubble
7201
// cache some values for min/readability
7202
hashkey = $.mobile.dialogHashKey;
7203
activePage = $.mobile.activePage;
7204
currentIsDialog = activePage.is( ".ui-dialog" );
7205
this._myUrl = url = $.mobile.urlHistory.getActive().url;
7206
hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog;
7207
urlHistory = $.mobile.urlHistory;
7210
self._open( options );
7211
self._bindContainerClose();
7215
url = url + hashkey;
7217
// Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash
7218
if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) {
7222
// swallow the the initial navigation event, and bind for the next
7223
opts.container.one( opts.navigateEvents, function( e ) {
7225
self._open( options );
7226
self._bindContainerClose();
7229
urlHistory.ignoreNextHashChange = currentIsDialog;
7231
// Gotta love methods with 1mm args :(
7232
urlHistory.addNew( url, undefined, undefined, undefined, "dialog" );
7234
// set the new url with (or without) the new dialog hash key
7235
$.mobile.path.set( url );
7239
// make sure close is idempotent
7240
if( !$.mobile.popup.active ){
7244
this._scrollTop = $( window ).scrollTop();
7246
if( this.options.history ) {
7255
// TODO this can be moved inside the widget
7256
$.mobile.popup.handleLink = function( $link ) {
7257
var closestPage = $link.closest( ":jqmData(role='page')" ),
7258
scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ),
7259
// NOTE make sure to get only the hash, ie7 (wp7) return the absolute href
7260
// in this case ruining the element selection
7261
popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ),
7264
if ( popup.data( "popup" ) ) {
7265
offset = $link.offset();
7266
popup.popup( "open", {
7267
x: offset.left + $link.outerWidth() / 2,
7268
y: offset.top + $link.outerHeight() / 2,
7269
transition: $link.jqmData( "transition" ),
7270
positionTo: $link.jqmData( "position-to" ),
7275
//remove after delay
7276
setTimeout( function() {
7277
// Check if we are in a listview
7278
var $parent = $link.parent().parent();
7279
if ($parent.hasClass("ui-li")) {
7280
$link = $parent.parent();
7282
$link.removeClass( $.mobile.activeBtnClass );
7286
// TODO move inside _create
7287
$( document ).bind( "pagebeforechange", function( e, data ) {
7288
if ( data.options.role === "popup" ) {
7289
$.mobile.popup.handleLink( data.options.link );
7294
$( document ).bind( "pagecreate create", function( e ) {
7295
$.mobile.popup.prototype.enhanceWithin( e.target, true );
7301
var meta = $( "meta[name=viewport]" ),
7302
initialContent = meta.attr( "content" ),
7303
disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no",
7304
enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes",
7305
disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent );
7307
$.mobile.zoom = $.extend( {}, {
7308
enabled: !disabledInitially,
7310
disable: function( lock ) {
7311
if ( !disabledInitially && !$.mobile.zoom.locked ) {
7312
meta.attr( "content", disabledZoom );
7313
$.mobile.zoom.enabled = false;
7314
$.mobile.zoom.locked = lock || false;
7317
enable: function( unlock ) {
7318
if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) {
7319
meta.attr( "content", enabledZoom );
7320
$.mobile.zoom.enabled = true;
7321
$.mobile.zoom.locked = false;
7324
restore: function() {
7325
if ( !disabledInitially ) {
7326
meta.attr( "content", initialContent );
7327
$.mobile.zoom.enabled = true;
7334
(function( $, undefined ) {
7336
$.widget( "mobile.textinput", $.mobile.widget, {
7340
// This option defaults to true on iOS devices.
7341
preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1,
7342
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])",
7343
clearSearchButtonText: "clear text",
7347
_create: function() {
7350
input = this.element,
7352
theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ),
7353
themeclass = " ui-body-" + theme,
7354
miniclass = o.mini ? " ui-mini" : "",
7355
focusedEl, clearbtn;
7357
function toggleClear() {
7358
setTimeout( function() {
7359
clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() );
7363
$( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" );
7365
focusedEl = input.addClass("ui-input-text ui-body-"+ theme );
7367
// XXX: Temporary workaround for issue 785 (Apple bug 8910589).
7368
// Turn off autocorrect and autocomplete on non-iOS 5 devices
7369
// since the popup they use can't be dismissed by the user. Note
7370
// that we test for the presence of the feature by looking for
7371
// the autocorrect property on the input element. We currently
7372
// have no test for iOS 5 or newer so we're temporarily using
7373
// the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas
7374
if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) {
7375
// Set the attribute instead of the property just in case there
7376
// is code that attempts to make modifications via HTML.
7377
input[0].setAttribute( "autocorrect", "off" );
7378
input[0].setAttribute( "autocomplete", "off" );
7382
//"search" input widget
7383
if ( input.is( "[type='search'],:jqmData(type='search')" ) ) {
7385
focusedEl = input.wrap( "<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield" + themeclass + miniclass + "'></div>" ).parent();
7386
clearbtn = $( "<a href='#' class='ui-input-clear' title='" + o.clearSearchButtonText + "'>" + o.clearSearchButtonText + "</a>" )
7387
.bind('click', function( event ) {
7391
.trigger( "change" );
7392
clearbtn.addClass( "ui-input-clear-hidden" );
7393
event.preventDefault();
7395
.appendTo( focusedEl )
7406
input.bind( 'paste cut keyup focus change blur', toggleClear );
7409
input.addClass( "ui-corner-all ui-shadow-inset" + themeclass + miniclass );
7412
input.focus(function() {
7413
focusedEl.addClass( $.mobile.focusClass );
7416
focusedEl.removeClass( $.mobile.focusClass );
7418
// In many situations, iOS will zoom into the select upon tap, this prevents that from happening
7419
.bind( "focus", function() {
7420
if ( o.preventFocusZoom ) {
7421
$.mobile.zoom.disable( true );
7424
.bind( "blur", function() {
7425
if ( o.preventFocusZoom ) {
7426
$.mobile.zoom.enable( true );
7431
if ( input.is( "textarea" ) ) {
7432
var extraLineHeight = 15,
7433
keyupTimeoutBuffer = 100,
7436
this._keyup = function() {
7437
var scrollHeight = input[ 0 ].scrollHeight,
7438
clientHeight = input[ 0 ].clientHeight;
7440
if ( clientHeight < scrollHeight ) {
7441
input.height(scrollHeight + extraLineHeight);
7445
input.keyup(function() {
7446
clearTimeout( keyupTimeout );
7447
keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer );
7450
// binding to pagechange here ensures that for pages loaded via
7451
// ajax the height is recalculated without user input
7452
this._on( $(document), {"pagechange": "_keyup" });
7454
// Issue 509: the browser is not providing scrollHeight properly until the styles load
7455
if ( $.trim( input.val() ) ) {
7456
// bind to the window load to make sure the height is calculated based on BOTH
7458
this._on( $(window), {"load": "_keyup"});
7461
if ( input.attr( "disabled" ) ) {
7466
disable: function() {
7468
if ( this.element.attr( "disabled", true ).is( "[type='search'], :jqmData(type='search')" ) ) {
7469
$el = this.element.parent();
7473
$el.addClass( "ui-disabled" );
7474
return this._setOption( "disabled", true );
7477
enable: function() {
7480
// TODO using more than one line of code is acceptable ;)
7481
if ( this.element.attr( "disabled", false ).is( "[type='search'], :jqmData(type='search')" ) ) {
7482
$el = this.element.parent();
7486
$el.removeClass( "ui-disabled" );
7487
return this._setOption( "disabled", false );
7491
//auto self-init widgets
7492
$( document ).bind( "pagecreate create", function( e ) {
7493
$.mobile.textinput.prototype.enhanceWithin( e.target, true );
7498
(function( $, undefined ) {
7500
$.mobile.listview.prototype.options.filter = false;
7501
$.mobile.listview.prototype.options.filterPlaceholder = "Filter items...";
7502
$.mobile.listview.prototype.options.filterTheme = "c";
7503
// TODO rename callback/deprecate and default to the item itself as the first argument
7504
var defaultFilterCallback = function( text, searchValue, item ) {
7505
return text.toString().toLowerCase().indexOf( searchValue ) === -1;
7508
$.mobile.listview.prototype.options.filterCallback = defaultFilterCallback;
7510
$( document ).delegate( "ul, ol", "listviewcreate", function() {
7512
var list = $( this ),
7513
listview = list.data( "listview" );
7515
if ( !listview.options.filter ) {
7519
var wrapper = $( "<form>", {
7520
"class": "ui-listview-filter ui-bar-" + listview.options.filterTheme,
7522
}).submit( function( e ) {
7526
search = $( "<input>", {
7527
placeholder: listview.options.filterPlaceholder
7529
.attr( "data-" + $.mobile.ns + "type", "search" )
7530
.jqmData( "lastval", "" )
7531
.bind( "keyup change", function() {
7533
var $this = $( this ),
7534
val = this.value.toLowerCase(),
7536
lastval = $this.jqmData( "lastval" ) + "",
7540
// Check if a custom filter callback applies
7541
isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback;
7543
listview._trigger( "beforefilter", "beforefilter", { input: this } );
7545
// Change val as lastval for next execution
7546
$this.jqmData( "lastval" , val );
7547
if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) {
7549
// Custom filter callback applies or removed chars or pasted something totally different, check all items
7550
listItems = list.children();
7553
// Only chars added, not removed, only use visible subset
7554
listItems = list.children( ":not(.ui-screen-hidden)" );
7559
// This handles hiding regular rows without the text we search for
7560
// and any list dividers without regular rows shown under it
7562
for ( var i = listItems.length - 1; i >= 0; i-- ) {
7563
item = $( listItems[ i ] );
7564
itemtext = item.jqmData( "filtertext" ) || item.text();
7566
if ( item.is( "li:jqmData(role=list-divider)" ) ) {
7568
item.toggleClass( "ui-filter-hidequeue" , !childItems );
7573
} else if ( listview.options.filterCallback( itemtext, val, item ) ) {
7576
item.toggleClass( "ui-filter-hidequeue" , true );
7579
// There's a shown item in the bucket
7584
// Show items, not marked to be hidden
7586
.filter( ":not(.ui-filter-hidequeue)" )
7587
.toggleClass( "ui-screen-hidden", false );
7589
// Hide items, marked to be hidden
7591
.filter( ".ui-filter-hidequeue" )
7592
.toggleClass( "ui-screen-hidden", true )
7593
.toggleClass( "ui-filter-hidequeue", false );
7597
//filtervalue is empty => show all
7598
listItems.toggleClass( "ui-screen-hidden", false );
7600
listview._refreshCorners();
7602
.appendTo( wrapper )
7605
if ( listview.options.inset ) {
7606
wrapper.addClass( "ui-listview-filter-inset" );
7609
wrapper.bind( "submit", function() {
7612
.insertBefore( list );
7617
(function( $, undefined ) {
7619
$.widget( "mobile.slider", $.mobile.widget, {
7624
initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",
7628
_create: function() {
7630
// TODO: Each of these should have comments explain what they're for
7633
control = this.element,
7635
parentTheme = $.mobile.getInheritedTheme( control, "c" ),
7637
theme = this.options.theme || parentTheme,
7639
trackTheme = this.options.trackTheme || parentTheme,
7641
cType = control[ 0 ].nodeName.toLowerCase(),
7643
selectClass = ( cType === "select" ) ? "ui-slider-switch" : "",
7645
controlID = control.attr( "id" ),
7647
$label = $( "[for='" + controlID + "']" ),
7649
labelID = $label.attr( "id" ) || controlID + "-label",
7651
label = $label.attr( "id", labelID ),
7654
return cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex;
7657
min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0,
7659
max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1,
7661
step = window.parseFloat( control.attr( "step" ) || 1 ),
7663
inlineClass = ( this.options.inline || control.jqmData( "inline" ) === true ) ? " ui-slider-inline" : "",
7665
miniClass = ( this.options.mini || control.jqmData( "mini" ) ) ? " ui-slider-mini" : "",
7668
domHandle = document.createElement( 'a' ),
7669
handle = $( domHandle ),
7670
domSlider = document.createElement( 'div' ),
7671
slider = $( domSlider ),
7673
valuebg = control.jqmData( "highlight" ) && cType !== "select" ? (function() {
7674
var bg = document.createElement('div');
7675
bg.className = 'ui-slider-bg ' + $.mobile.activeBtnClass + ' ui-btn-corner-all';
7676
return $( bg ).prependTo( slider );
7683
domHandle.setAttribute( 'href', "#" );
7684
domSlider.setAttribute('role','application');
7685
domSlider.className = ['ui-slider ',selectClass," ui-btn-down-",trackTheme,' ui-btn-corner-all', inlineClass, miniClass].join( "" );
7686
domHandle.className = 'ui-slider-handle';
7687
domSlider.appendChild( domHandle );
7689
handle.buttonMarkup({ corners: true, theme: theme, shadow: true })
7692
"aria-valuemin": min,
7693
"aria-valuemax": max,
7694
"aria-valuenow": val(),
7695
"aria-valuetext": val(),
7697
"aria-labelledby": labelID
7706
userModified: false,
7710
if ( cType === "select" ) {
7711
var wrapper = document.createElement('div');
7712
wrapper.className = 'ui-slider-inneroffset';
7714
for ( var j = 0,length = domSlider.childNodes.length;j < length;j++ ) {
7715
wrapper.appendChild( domSlider.childNodes[j] );
7718
domSlider.appendChild( wrapper );
7720
// slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" );
7722
// make the handle move with a smooth transition
7723
handle.addClass( "ui-slider-handle-snapping" );
7725
options = control.find( "option" );
7727
for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) {
7728
var side = !i ? "b" : "a",
7729
sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ),
7730
sliderLabel = document.createElement( 'div' ),
7731
sliderImg = document.createElement( 'span' );
7733
sliderImg.className = ['ui-slider-label ui-slider-label-',side,sliderTheme," ui-btn-corner-all"].join( "" );
7734
sliderImg.setAttribute('role','img');
7735
sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) );
7736
$(sliderImg).prependTo( slider );
7739
self._labels = $( ".ui-slider-label", slider );
7743
label.addClass( "ui-slider" );
7745
// monitor the input for updated values
7746
control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" )
7747
.change(function() {
7748
// if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again
7749
if ( !self.mouseMoved ) {
7750
self.refresh( val(), true );
7753
.keyup(function() { // necessary?
7754
self.refresh( val(), true, true );
7757
self.refresh( val(), true );
7760
this._preventDocumentDrag = function( event ) {
7761
// NOTE: we don't do this in refresh because we still want to
7762
// support programmatic alteration of disabled inputs
7763
if ( self.dragging && !self.options.disabled ) {
7765
// self.mouseMoved must be updated before refresh() because it will be used in the control "change" event
7766
self.mouseMoved = true;
7768
if ( cType === "select" ) {
7769
// make the handle move in sync with the mouse
7770
handle.removeClass( "ui-slider-handle-snapping" );
7773
self.refresh( event );
7775
// only after refresh() you can calculate self.userModified
7776
self.userModified = self.beforeStart !== control[0].selectedIndex;
7781
this._on( $( document ), { "vmousemove": this._preventDocumentDrag });
7783
// it appears the clicking the up and down buttons in chrome on
7784
// range/number inputs doesn't trigger a change until the field is
7785
// blurred. Here we check thif the value has changed and refresh
7786
control.bind( "vmouseup", $.proxy( self._checkedRefresh, self));
7788
slider.bind( "vmousedown", function( event ) {
7789
// NOTE: we don't do this in refresh because we still want to
7790
// support programmatic alteration of disabled inputs
7791
if ( self.options.disabled ) {
7795
self.dragging = true;
7796
self.userModified = false;
7797
self.mouseMoved = false;
7799
if ( cType === "select" ) {
7800
self.beforeStart = control[0].selectedIndex;
7803
self.refresh( event );
7804
self._trigger( "start" );
7807
.bind( "vclick", false );
7809
this._sliderMouseUp = function() {
7810
if ( self.dragging ) {
7811
self.dragging = false;
7813
if ( cType === "select") {
7814
// make the handle move with a smooth transition
7815
handle.addClass( "ui-slider-handle-snapping" );
7817
if ( self.mouseMoved ) {
7818
// this is a drag, change the value only if user dragged enough
7819
if ( self.userModified ) {
7820
self.refresh( self.beforeStart === 0 ? 1 : 0 );
7823
self.refresh( self.beforeStart );
7827
// this is just a click, change the value
7828
self.refresh( self.beforeStart === 0 ? 1 : 0 );
7832
self.mouseMoved = false;
7833
self._trigger( "stop" );
7838
this._on( slider.add( document ), { "vmouseup": this._sliderMouseUp });
7839
slider.insertAfter( control );
7841
// Only add focus class to toggle switch, sliders get it automatically from ui-btn
7842
if ( cType === 'select' ) {
7845
slider.addClass( $.mobile.focusClass );
7849
slider.removeClass( $.mobile.focusClass );
7855
// NOTE force focus on handle
7856
vmousedown: function() {
7862
keydown: function( event ) {
7865
if ( self.options.disabled ) {
7869
// In all cases prevent the default and mark the handle as active
7870
switch ( event.keyCode ) {
7871
case $.mobile.keyCode.HOME:
7872
case $.mobile.keyCode.END:
7873
case $.mobile.keyCode.PAGE_UP:
7874
case $.mobile.keyCode.PAGE_DOWN:
7875
case $.mobile.keyCode.UP:
7876
case $.mobile.keyCode.RIGHT:
7877
case $.mobile.keyCode.DOWN:
7878
case $.mobile.keyCode.LEFT:
7879
event.preventDefault();
7881
if ( !self._keySliding ) {
7882
self._keySliding = true;
7883
$( this ).addClass( "ui-state-active" );
7888
// move the slider according to the keypress
7889
switch ( event.keyCode ) {
7890
case $.mobile.keyCode.HOME:
7891
self.refresh( min );
7893
case $.mobile.keyCode.END:
7894
self.refresh( max );
7896
case $.mobile.keyCode.PAGE_UP:
7897
case $.mobile.keyCode.UP:
7898
case $.mobile.keyCode.RIGHT:
7899
self.refresh( index + step );
7901
case $.mobile.keyCode.PAGE_DOWN:
7902
case $.mobile.keyCode.DOWN:
7903
case $.mobile.keyCode.LEFT:
7904
self.refresh( index - step );
7907
}, // remove active mark
7909
keyup: function( event ) {
7910
if ( self._keySliding ) {
7911
self._keySliding = false;
7912
$( this ).removeClass( "ui-state-active" );
7917
if ( this._handleFormReset ) {
7918
this._handleFormReset();
7920
this.refresh( undefined, undefined, true );
7923
_checkedRefresh: function() {
7924
if( this.value != this._value() ){
7925
this.refresh( this._value() );
7929
_value: function() {
7930
return this._type === "input" ?
7931
parseFloat( this.element.val() ) : this.element[0].selectedIndex;
7935
_reset: function() {
7936
this.refresh( undefined, false, true );
7939
refresh: function( val, isfromControl, preventInputUpdate ) {
7941
// NOTE: we don't return here because we want to support programmatic
7942
// alteration of the input value, which should still update the slider
7943
if ( this.options.disabled || this.element.attr('disabled')) {
7947
// set the stored value for comparison later
7948
this.value = this._value();
7950
var control = this.element, percent,
7951
cType = control[0].nodeName.toLowerCase(),
7952
min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0,
7953
max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1,
7954
step = ( cType === "input" && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1;
7956
if ( typeof val === "object" ) {
7958
// a slight tolerance helped get to the ends of the slider
7960
if ( !this.dragging ||
7961
data.pageX < this.slider.offset().left - tol ||
7962
data.pageX > this.slider.offset().left + this.slider.width() + tol ) {
7965
percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 );
7967
if ( val == null ) {
7968
val = cType === "input" ? parseFloat( control.val() || 0 ) : control[0].selectedIndex;
7970
percent = ( parseFloat( val ) - min ) / ( max - min ) * 100;
7973
if ( isNaN( percent ) ) {
7977
if ( percent < 0 ) {
7981
if ( percent > 100 ) {
7985
var newval = ( percent / 100 ) * ( max - min ) + min;
7987
//from jQuery UI slider, the following source will round to the nearest step
7988
var valModStep = ( newval - min ) % step;
7989
var alignValue = newval - valModStep;
7991
if ( Math.abs( valModStep ) * 2 >= step ) {
7992
alignValue += ( valModStep > 0 ) ? step : ( -step );
7994
// Since JavaScript has problems with large floats, round
7995
// the final value to 5 digits after the decimal point (see jQueryUI: #4124)
7996
newval = parseFloat( alignValue.toFixed(5) );
7998
if ( newval < min ) {
8002
if ( newval > max ) {
8006
this.handle.css( "left", percent + "%" );
8008
"aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ),
8009
"aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(),
8010
title: cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText()
8013
if ( this.valuebg ) {
8014
this.valuebg.css( "width", percent + "%" );
8017
// drag the label widths
8018
if ( this._labels ) {
8019
var handlePercent = this.handle.width() / this.slider.width() * 100,
8020
aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100,
8021
bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 );
8023
this._labels.each(function() {
8024
var ab = $( this ).is( ".ui-slider-label-a" );
8025
$( this ).width( ( ab ? aPercent : bPercent ) + "%" );
8029
if ( !preventInputUpdate ) {
8030
var valueChanged = false;
8032
// update control"s value
8033
if ( cType === "input" ) {
8034
valueChanged = control.val() !== newval;
8035
control.val( newval );
8037
valueChanged = control[ 0 ].selectedIndex !== newval;
8038
control[ 0 ].selectedIndex = newval;
8040
if ( !isfromControl && valueChanged ) {
8041
control.trigger( "change" );
8046
enable: function() {
8047
this.element.attr( "disabled", false );
8048
this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
8049
return this._setOption( "disabled", false );
8052
disable: function() {
8053
this.element.attr( "disabled", true );
8054
this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
8055
return this._setOption( "disabled", true );
8060
$.widget( "mobile.slider", $.mobile.slider, $.mobile.behaviors.formReset );
8062
// FIXME: Move the declaration of widgetEventPrefix back to the top of the
8063
// initial declaration of the slider widget once we start using a version of
8064
// the widget factory that includes a fix for http://bugs.jqueryui.com/ticket/8724
8065
$.widget( "mobile.slider", $.mobile.slider, { widgetEventPrefix: "slide" } );
8067
//auto self-init widgets
8068
$( document ).bind( "pagecreate create", function( e ) {
8069
$.mobile.slider.prototype.enhanceWithin( e.target, true );
8074
(function( $, undefined ) {
8076
$.widget( "mobile.selectmenu", $.mobile.widget, {
8087
hidePlaceholderMenuItems: true,
8090
// This option defaults to true on iOS devices.
8091
preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1,
8092
initSelector: "select:not( :jqmData(role='slider') )",
8096
_button: function() {
8097
return $( "<div/>" );
8100
_setDisabled: function( value ) {
8101
this.element.attr( "disabled", value );
8102
this.button.attr( "aria-disabled", value );
8103
return this._setOption( "disabled", value );
8106
_focusButton : function() {
8109
setTimeout( function() {
8110
self.button.focus();
8114
_selectOptions: function() {
8115
return this.select.find( "option" );
8118
// setup items that are generally necessary for select menu extension
8119
_preExtension: function() {
8121
// 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
8122
/* if ( $el[0].className.length ) {
8123
classes = $el[0].className;
8125
if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) {
8126
classes = " ui-btn-left";
8129
if ( !!~this.element[0].className.indexOf( "ui-btn-right" ) ) {
8130
classes = " ui-btn-right";
8133
this.select = this.element.wrap( "<div class='ui-select" + classes + "'>" );
8134
this.selectID = this.select.attr( "id" );
8135
this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" );
8136
this.isMultiple = this.select[ 0 ].multiple;
8137
if ( !this.options.theme ) {
8138
this.options.theme = $.mobile.getInheritedTheme( this.select, "c" );
8142
_destroy: function() {
8143
var wrapper = this.element.parents( ".ui-select" );
8144
if ( wrapper.length > 0 ) {
8145
this.element.insertAfter( wrapper );
8150
_create: function() {
8151
this._preExtension();
8153
// Allows for extension of the native select for custom selects and other plugins
8154
// see select.custom for example extension
8155
// TODO explore plugin registration
8156
this._trigger( "beforeCreate" );
8158
this.button = this._button();
8162
options = this.options,
8164
inline = options.inline || this.select.jqmData( "inline" ),
8165
mini = options.mini || this.select.jqmData( "mini" ),
8166
iconpos = options.icon ? ( options.iconpos || this.select.jqmData( "iconpos" ) ) : false,
8168
// IE throws an exception at options.item() function when
8169
// there is no selected item
8170
// select first in this case
8171
selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex,
8173
// TODO values buttonId and menuId are undefined here
8174
button = this.button
8175
.insertBefore( this.select )
8177
theme: options.theme,
8181
corners: options.corners,
8182
shadow: options.shadow,
8183
iconshadow: options.iconshadow,
8187
this.setButtonText();
8189
// Opera does not properly support opacity on select elements
8190
// In Mini, it hides the element, but not its text
8191
// On the desktop,it seems to do the opposite
8192
// for these reasons, using the nativeMenu option results in a full native select in Opera
8193
if ( options.nativeMenu && window.opera && window.opera.version ) {
8194
button.addClass( "ui-select-nativeonly" );
8197
// Add counter for multi selects
8198
if ( this.isMultiple ) {
8199
this.buttonCount = $( "<span>" )
8200
.addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" )
8202
.appendTo( button.addClass('ui-li-has-count') );
8205
// Disable if specified
8206
if ( options.disabled || this.element.attr('disabled')) {
8210
// Events on native select
8211
this.select.change(function() {
8215
if ( this._handleFormReset ) {
8216
this._handleFormReset();
8225
.appendTo( self.button )
8226
.bind( "vmousedown", function() {
8227
// Add active class to button
8228
self.button.addClass( $.mobile.activeBtnClass );
8230
.bind( "focus", function() {
8231
self.button.addClass( $.mobile.focusClass );
8233
.bind( "blur", function() {
8234
self.button.removeClass( $.mobile.focusClass );
8236
.bind( "focus vmouseover", function() {
8237
self.button.trigger( "vmouseover" );
8239
.bind( "vmousemove", function() {
8240
// Remove active class on scroll/touchmove
8241
self.button.removeClass( $.mobile.activeBtnClass );
8243
.bind( "change blur vmouseout", function() {
8244
self.button.trigger( "vmouseout" )
8245
.removeClass( $.mobile.activeBtnClass );
8247
.bind( "change blur", function() {
8248
self.button.removeClass( "ui-btn-down-" + self.options.theme );
8251
// In many situations, iOS will zoom into the select upon tap, this prevents that from happening
8252
self.button.bind( "vmousedown", function() {
8253
if ( self.options.preventFocusZoom ) {
8254
$.mobile.zoom.disable( true );
8257
self.label.bind( "click focus", function() {
8258
if ( self.options.preventFocusZoom ) {
8259
$.mobile.zoom.disable( true );
8262
self.select.bind( "focus", function() {
8263
if ( self.options.preventFocusZoom ) {
8264
$.mobile.zoom.disable( true );
8267
self.button.bind( "mouseup", function() {
8268
if ( self.options.preventFocusZoom ) {
8269
setTimeout(function() {
8270
$.mobile.zoom.enable( true );
8274
self.select.bind( "blur", function() {
8275
if ( self.options.preventFocusZoom ) {
8276
$.mobile.zoom.enable( true );
8282
selected: function() {
8283
return this._selectOptions().filter( ":selected" );
8286
selectedIndices: function() {
8289
return this.selected().map(function() {
8290
return self._selectOptions().index( this );
8294
setButtonText: function() {
8296
selected = this.selected(),
8297
text = this.placeholder,
8298
span = $( document.createElement( "span" ) );
8300
this.button.find( ".ui-btn-text" ).html(function() {
8301
if ( selected.length ) {
8302
text = selected.map(function() {
8303
return $( this ).text();
8304
}).get().join( ", " );
8306
text = self.placeholder;
8309
// TODO possibly aggregate multiple select option classes
8310
return span.text( text )
8311
.addClass( self.select.attr( "class" ) )
8312
.addClass( selected.attr( "class" ) );
8316
setButtonCount: function() {
8317
var selected = this.selected();
8319
// multiple count inside button
8320
if ( this.isMultiple ) {
8321
this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length );
8325
_reset: function() {
8329
refresh: function() {
8330
this.setButtonText();
8331
this.setButtonCount();
8334
// open and close preserved in native selects
8335
// to simplify users code when looping over selects
8339
disable: function() {
8340
this._setDisabled( true );
8341
this.button.addClass( "ui-disabled" );
8344
enable: function() {
8345
this._setDisabled( false );
8346
this.button.removeClass( "ui-disabled" );
8350
$.widget( "mobile.selectmenu", $.mobile.selectmenu, $.mobile.behaviors.formReset );
8352
//auto self-init widgets
8353
$( document ).bind( "pagecreate create", function( e ) {
8354
$.mobile.selectmenu.prototype.enhanceWithin( e.target, true );
8359
* custom "selectmenu" plugin
8362
(function( $, undefined ) {
8363
var extendSelect = function( widget ) {
8365
var select = widget.select,
8366
origDestroy = widget._destroy,
8367
selectID = widget.selectID,
8368
label = widget.label,
8369
thisPage = widget.select.closest( ".ui-page" ),
8370
selectOptions = widget._selectOptions(),
8371
isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
8372
buttonId = selectID + "-button",
8373
menuId = selectID + "-menu",
8374
menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
8375
"<div data-" + $.mobile.ns + "role='header'>" +
8376
"<div class='ui-title'>" + label.getEncodedText() + "</div>"+
8378
"<div data-" + $.mobile.ns + "role='content'></div>"+
8381
listbox = $( "<div>", { "class": "ui-selectmenu" } ).insertAfter( widget.select ).popup( { theme: widget.options.overlayTheme } ),
8384
"class": "ui-selectmenu-list",
8387
"aria-labelledby": buttonId
8388
}).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ),
8390
header = $( "<div>", {
8391
"class": "ui-header ui-bar-" + widget.options.theme
8392
}).prependTo( listbox ),
8394
headerTitle = $( "<h1>", {
8396
}).appendTo( header ),
8402
if ( widget.isMultiple ) {
8403
headerClose = $( "<a>", {
8404
"text": widget.options.closeText,
8406
"class": "ui-btn-left"
8407
}).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
8411
select: widget.select,
8418
selectOptions: selectOptions,
8419
isMultiple: isMultiple,
8420
theme: widget.options.theme,
8424
headerTitle: headerTitle,
8425
headerClose: headerClose,
8426
menuPageContent: menuPageContent,
8427
menuPageClose: menuPageClose,
8433
// Create list from select, update state
8436
if ( self._origTabIndex === undefined ) {
8437
self._origTabIndex = self.select.attr( "tabindex" );
8438
// Map undefined to false, because self._origTabIndex === undefined
8439
// indicates that we have not yet checked whether the select has
8440
// originally had a tabindex attribute, whereas false indicates that
8441
// we have checked the select for such an attribute, and have found
8443
if ( self._origTabIndex === undefined ) {
8444
self._origTabIndex = false;
8447
self.select.attr( "tabindex", "-1" ).focus(function() {
8449
self.button.focus();
8453
self.button.bind( "vclick keydown" , function( event ) {
8454
if (event.type === "vclick" ||
8455
event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER ||
8456
event.keyCode === $.mobile.keyCode.SPACE)) {
8459
event.preventDefault();
8463
// Events for list items
8464
self.list.attr( "role", "listbox" )
8465
.bind( "focusin", function( e ) {
8467
.attr( "tabindex", "0" )
8468
.trigger( "vmouseover" );
8471
.bind( "focusout", function( e ) {
8473
.attr( "tabindex", "-1" )
8474
.trigger( "vmouseout" );
8476
.delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
8478
// index of option tag to be selected
8479
var oldIndex = self.select[ 0 ].selectedIndex,
8480
newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
8481
option = self._selectOptions().eq( newIndex )[ 0 ];
8483
// toggle selected status on the tag for multi selects
8484
option.selected = self.isMultiple ? !option.selected : true;
8486
// toggle checkbox class for multiple selects
8487
if ( self.isMultiple ) {
8488
$( this ).find( ".ui-icon" )
8489
.toggleClass( "ui-icon-checkbox-on", option.selected )
8490
.toggleClass( "ui-icon-checkbox-off", !option.selected );
8493
// trigger change if value changed
8494
if ( self.isMultiple || oldIndex !== newIndex ) {
8495
self.select.trigger( "change" );
8498
// hide custom select for single selects only - otherwise focus clicked item
8499
// We need to grab the clicked item the hard way, because the list may have been rebuilt
8500
if ( self.isMultiple ) {
8501
self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex )
8502
.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
8508
event.preventDefault();
8510
.keydown(function( event ) { //keyboard events for menu items
8511
var target = $( event.target ),
8512
li = target.closest( "li" ),
8515
// switch logic based on which key was pressed
8516
switch ( event.keyCode ) {
8517
// up or left arrow keys
8519
prev = li.prev().not( ".ui-selectmenu-placeholder" );
8521
if ( prev.is( ".ui-li-divider" ) ) {
8525
// if there's a previous option, focus it
8526
if ( prev.length ) {
8529
.attr( "tabindex", "-1" );
8531
prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
8535
// down or right arrow keys
8539
if ( next.is( ".ui-li-divider" ) ) {
8543
// if there's a next option, focus it
8544
if ( next.length ) {
8547
.attr( "tabindex", "-1" );
8549
next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
8553
// If enter or space is pressed, trigger click
8556
target.trigger( "click" );
8562
// button refocus ensures proper height calculation
8563
// by removing the inline style and ensuring page inclusion
8564
self.menuPage.bind( "pagehide", function() {
8565
self.list.appendTo( self.listbox );
8566
self._focusButton();
8568
// TODO centralize page removal binding / handling in the page plugin.
8569
// Suggestion from @jblas to do refcounting
8571
// TODO extremely confusing dependency on the open method where the pagehide.remove
8572
// bindings are stripped to prevent the parent page from disappearing. The way
8573
// we're keeping pages in the DOM right now sucks
8575
// rebind the page remove that was unbound in the open function
8576
// to allow for the parent page removal from actions other than the use
8577
// of a dialog sized custom select
8579
// doing this here provides for the back button on the custom select dialog
8580
$.mobile._bindPageRemove.call( self.thisPage );
8583
// Events on the popup
8584
self.listbox.bind( "popupafterclose", function( event ) {
8588
// Close button on small overlays
8589
if ( self.isMultiple ) {
8590
self.headerClose.click(function() {
8591
if ( self.menuType === "overlay" ) {
8598
// track this dependency so that when the parent page
8599
// is removed on pagehide it will also remove the menupage
8600
self.thisPage.addDependents( this.menuPage );
8603
_isRebuildRequired: function() {
8604
var list = this.list.find( "li" ),
8605
options = this._selectOptions();
8607
// TODO exceedingly naive method to determine difference
8608
// ignores value changes etc in favor of a forcedRebuild
8609
// from the user in the refresh method
8610
return options.text() !== list.text();
8613
selected: function() {
8614
return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" );
8617
refresh: function( forceRebuild , foo ) {
8619
select = this.element,
8620
isMultiple = this.isMultiple,
8623
if ( forceRebuild || this._isRebuildRequired() ) {
8627
indicies = this.selectedIndices();
8629
self.setButtonText();
8630
self.setButtonCount();
8632
self.list.find( "li:not(.ui-li-divider)" )
8633
.removeClass( $.mobile.activeBtnClass )
8634
.attr( "aria-selected", false )
8635
.each(function( i ) {
8637
if ( $.inArray( i, indicies ) > -1 ) {
8638
var item = $( this );
8640
// Aria selected attr
8641
item.attr( "aria-selected", true );
8643
// Multiple selects: add the "on" checkbox state to the icon
8644
if ( self.isMultiple ) {
8645
item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
8647
if ( item.is( ".ui-selectmenu-placeholder" ) ) {
8648
item.next().addClass( $.mobile.activeBtnClass );
8650
item.addClass( $.mobile.activeBtnClass );
8658
if ( this.options.disabled || !this.isOpen ) {
8664
if ( self.menuType === "page" ) {
8665
// doesn't solve the possible issue with calling change page
8666
// where the objects don't define data urls which prevents dialog key
8667
// stripping - changePage has incoming refactor
8670
self.listbox.popup( "close" );
8671
self.list.appendTo( self.listbox );
8672
self._focusButton();
8675
// allow the dialog to be closed again
8676
self.isOpen = false;
8680
if ( this.options.disabled ) {
8685
$window = $( window ),
8686
selfListParent = self.list.parent(),
8687
menuHeight = selfListParent.outerHeight(),
8688
menuWidth = selfListParent.outerWidth(),
8689
activePage = $( "." + $.mobile.activePageClass ),
8690
scrollTop = $window.scrollTop(),
8691
btnOffset = self.button.offset().top,
8692
screenHeight = $window.height(),
8693
screenWidth = $window.width();
8695
//add active class to button
8696
self.button.addClass( $.mobile.activeBtnClass );
8698
//remove after delay
8699
setTimeout( function() {
8700
self.button.removeClass( $.mobile.activeBtnClass );
8703
function focusMenuItem() {
8704
var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" );
8705
if ( selector.length === 0 ) {
8706
selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" );
8708
selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme );
8711
if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
8713
self.menuPage.appendTo( $.mobile.pageContainer ).page();
8714
self.menuPageContent = menuPage.find( ".ui-content" );
8715
self.menuPageClose = menuPage.find( ".ui-header a" );
8717
// prevent the parent page from being removed from the DOM,
8718
// otherwise the results of selecting a list item in the dialog
8719
// fall into a black hole
8720
self.thisPage.unbind( "pagehide.remove" );
8722
//for WebOS/Opera Mini (set lastscroll using button offset)
8723
if ( scrollTop === 0 && btnOffset > screenHeight ) {
8724
self.thisPage.one( "pagehide", function() {
8725
$( this ).jqmData( "lastScroll", btnOffset );
8730
.one( "pageshow", function() {
8734
.one( "pagehide", function() {
8735
self.isOpen = false;
8738
self.menuType = "page";
8739
self.menuPageContent.append( self.list );
8740
self.menuPage.find("div .ui-title").text(self.label.text());
8741
$.mobile.changePage( self.menuPage, {
8742
transition: $.mobile.defaultDialogTransition
8745
self.menuType = "overlay";
8748
.one( "popupafteropen", focusMenuItem )
8750
x: self.button.offset().left + self.button.outerWidth() / 2,
8751
y: self.button.offset().top + self.button.outerHeight() / 2
8754
// duplicate with value set in page show for dialog sized selects
8759
_buildList: function() {
8762
placeholder = this.placeholder,
8763
needPlaceholder = true,
8766
dataIcon = self.isMultiple ? "checkbox-off" : "false";
8768
self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
8770
var $options = self.select.find( "option" ),
8771
numOptions = $options.length,
8772
select = this.select[ 0 ],
8773
dataPrefix = 'data-' + $.mobile.ns,
8774
dataIndexAttr = dataPrefix + 'option-index',
8775
dataIconAttr = dataPrefix + 'icon',
8776
dataRoleAttr = dataPrefix + 'role',
8777
dataPlaceholderAttr = dataPrefix + 'placeholder',
8778
fragment = document.createDocumentFragment(),
8779
isPlaceholderItem = false,
8782
for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) {
8783
var option = $options[i],
8784
$option = $( option ),
8785
parent = option.parentNode,
8786
text = $option.text(),
8787
anchor = document.createElement( 'a' ),
8790
anchor.setAttribute( 'href', '#' );
8791
anchor.appendChild( document.createTextNode( text ) );
8793
// Are we inside an optgroup?
8794
if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) {
8795
var optLabel = parent.getAttribute( 'label' );
8796
if ( optLabel !== optGroup ) {
8797
var divider = document.createElement( 'li' );
8798
divider.setAttribute( dataRoleAttr, 'list-divider' );
8799
divider.setAttribute( 'role', 'option' );
8800
divider.setAttribute( 'tabindex', '-1' );
8801
divider.appendChild( document.createTextNode( optLabel ) );
8802
fragment.appendChild( divider );
8803
optGroup = optLabel;
8807
if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) {
8808
needPlaceholder = false;
8809
isPlaceholderItem = true;
8811
// If we have identified a placeholder, record the fact that it was
8812
// us who have added the placeholder to the option and mark it
8813
// retroactively in the select as well
8814
if ( !option.hasAttribute( dataPlaceholderAttr ) ) {
8815
this._removePlaceholderAttr = true;
8817
option.setAttribute( dataPlaceholderAttr, true );
8818
if ( o.hidePlaceholderMenuItems ) {
8819
classes.push( "ui-selectmenu-placeholder" );
8821
if ( placeholder !== text ) {
8822
placeholder = self.placeholder = text;
8826
var item = document.createElement('li');
8827
if ( option.disabled ) {
8828
classes.push( "ui-disabled" );
8829
item.setAttribute('aria-disabled',true);
8831
item.setAttribute( dataIndexAttr,i );
8832
item.setAttribute( dataIconAttr, dataIcon );
8833
if ( isPlaceholderItem ) {
8834
item.setAttribute( dataPlaceholderAttr, true );
8836
item.className = classes.join( " " );
8837
item.setAttribute( 'role', 'option' );
8838
anchor.setAttribute( 'tabindex', '-1' );
8839
item.appendChild( anchor );
8840
fragment.appendChild( item );
8843
self.list[0].appendChild( fragment );
8845
// Hide header if it's not a multiselect and there's no placeholder
8846
if ( !this.isMultiple && !placeholder.length ) {
8849
this.headerTitle.text( this.placeholder );
8852
// Now populated, create listview
8853
self.list.listview();
8856
_button: function() {
8860
// TODO value is undefined at creation
8861
"id": this.buttonId,
8862
"aria-haspopup": "true",
8864
// TODO value is undefined at creation
8865
"aria-owns": this.menuId
8869
_destroy: function() {
8872
// Restore the tabindex attribute to its original value
8873
if ( this._origTabIndex !== undefined ) {
8874
if ( this._origTabIndex !== false ) {
8875
this.select.attr( "tabindex", this._origTabIndex );
8877
this.select.removeAttr( "tabindex" );
8881
// Remove the placeholder attribute if we were the ones to add it
8882
if ( this._removePlaceholderAttr ) {
8883
this._selectOptions().removeAttr( "data-" + $.mobile.ns + "placeholder" );
8887
this.listbox.remove();
8890
origDestroy.apply( this, arguments );
8895
// issue #3894 - core doesn't trigger events on disabled delegates
8896
$( document ).bind( "selectmenubeforecreate", function( event ) {
8897
var selectmenuWidget = $( event.target ).data( "selectmenu" );
8899
if ( !selectmenuWidget.options.nativeMenu &&
8900
selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) {
8901
extendSelect( selectmenuWidget );
8906
(function( $, undefined ) {
8909
$.widget( "mobile.fixedtoolbar", $.mobile.widget, {
8911
visibleOnPageShow: true,
8912
disablePageZoom: true,
8913
transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown)
8916
tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup",
8917
hideDuringFocus: "input, textarea, select",
8918
updatePagePadding: true,
8919
trackPersistentToolbars: true,
8921
// Browser detection! Weeee, here we go...
8922
// Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately.
8923
// Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience.
8924
// Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window
8925
// The following function serves to rule out some popular browsers with known fixed-positioning issues
8926
// This is a plugin option like any other, so feel free to improve or overwrite it
8927
supportBlacklist: function() {
8929
ua = navigator.userAgent,
8930
platform = navigator.platform,
8931
// Rendering engine is Webkit, and capture major version
8932
wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ),
8933
wkversion = !!wkmatch && wkmatch[ 1 ],
8934
ffmatch = ua.match( /Fennec\/([0-9]+)/ ),
8935
ffversion = !!ffmatch && ffmatch[ 1 ],
8936
operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ),
8937
omversion = !!operammobilematch && operammobilematch[ 1 ];
8940
// iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
8941
( ( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534 ) ||
8943
( w.operamini && ({}).toString.call( w.operamini ) === "[object OperaMini]" ) ||
8944
( operammobilematch && omversion < 7458 ) ||
8945
//Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
8946
( ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533 ) ||
8947
// Firefox Mobile before 6.0 -
8948
( ffversion && ffversion < 6 ) ||
8949
// WebOS less than 3
8950
( "palmGetResource" in window && wkversion && wkversion < 534 ) ||
8952
( ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1 ) ) {
8958
initSelector: ":jqmData(position='fixed')"
8961
_create: function() {
8966
tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer",
8967
$page = $el.closest( ".ui-page" );
8969
// Feature detecting support for
8970
if ( o.supportBlacklist() ) {
8975
$el.addClass( "ui-"+ tbtype +"-fixed" );
8977
// "fullscreen" overlay positioning
8978
if ( o.fullscreen ) {
8979
$el.addClass( "ui-"+ tbtype +"-fullscreen" );
8980
$page.addClass( "ui-page-" + tbtype + "-fullscreen" );
8982
// If not fullscreen, add class to page to set top or bottom padding
8984
$page.addClass( "ui-page-" + tbtype + "-fixed" );
8987
self._addTransitionClass();
8988
self._bindPageEvents();
8989
self._bindToggleHandlers();
8992
_addTransitionClass: function() {
8993
var tclass = this.options.transition;
8995
if ( tclass && tclass !== "none" ) {
8996
// use appropriate slide for header or footer
8997
if ( tclass === "slide" ) {
8998
tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup";
9001
this.element.addClass( tclass );
9005
_bindPageEvents: function() {
9010
//page event bindings
9011
// Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up
9012
// This method is meant to disable zoom while a fixed-positioned toolbar page is visible
9013
$el.closest( ".ui-page" )
9014
.bind( "pagebeforeshow", function() {
9015
if ( o.disablePageZoom ) {
9016
$.mobile.zoom.disable( true );
9018
if ( !o.visibleOnPageShow ) {
9022
.bind( "webkitAnimationStart animationstart updatelayout", function() {
9023
var thisPage = this;
9024
if ( o.updatePagePadding ) {
9025
self.updatePagePadding( thisPage );
9028
.bind( "pageshow", function() {
9029
var thisPage = this;
9030
self.updatePagePadding( thisPage );
9031
if ( o.updatePagePadding ) {
9032
$( window ).bind( "throttledresize." + self.widgetName, function() {
9033
self.updatePagePadding( thisPage );
9037
.bind( "pagebeforehide", function( e, ui ) {
9038
if ( o.disablePageZoom ) {
9039
$.mobile.zoom.enable( true );
9041
if ( o.updatePagePadding ) {
9042
$( window ).unbind( "throttledresize." + self.widgetName );
9045
if ( o.trackPersistentToolbars ) {
9046
var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this ),
9047
thisHeader = $( ".ui-header-fixed:jqmData(id)", this ),
9048
nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(),
9049
nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $();
9051
if ( nextFooter.length || nextHeader.length ) {
9053
nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer );
9055
ui.nextPage.one( "pageshow", function() {
9056
nextFooter.add( nextHeader ).appendTo( this );
9065
// This will set the content element's top or bottom padding equal to the toolbar's height
9066
updatePagePadding: function( tbPage ) {
9067
var $el = this.element,
9068
header = $el.is( ".ui-header" );
9070
// This behavior only applies to "fixed", not "fullscreen"
9071
if ( this.options.fullscreen ) { return; }
9073
tbPage = tbPage || $el.closest( ".ui-page" );
9074
$( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() );
9077
_useTransition: function( notransition ) {
9078
var $win = $( window ),
9080
scroll = $win.scrollTop(),
9081
elHeight = $el.height(),
9082
pHeight = $el.closest( ".ui-page" ).height(),
9083
viewportHeight = $.mobile.getScreenHeight(),
9084
tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer";
9086
return !notransition &&
9087
( this.options.transition && this.options.transition !== "none" &&
9089
( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) ||
9090
( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight )
9091
) || this.options.fullscreen
9095
show: function( notransition ) {
9096
var hideClass = "ui-fixed-hidden",
9099
if ( this._useTransition( notransition ) ) {
9101
.removeClass( "out " + hideClass )
9105
$el.removeClass( hideClass );
9107
this._visible = true;
9110
hide: function( notransition ) {
9111
var hideClass = "ui-fixed-hidden",
9113
// if it's a slide transition, our new transitions need the reverse class as well to slide outward
9114
outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" );
9116
if( this._useTransition( notransition ) ) {
9118
.addClass( outclass )
9119
.removeClass( "in" )
9120
.animationComplete(function() {
9121
$el.addClass( hideClass ).removeClass( outclass );
9125
$el.addClass( hideClass ).removeClass( outclass );
9127
this._visible = false;
9130
toggle: function() {
9131
this[ this._visible ? "hide" : "show" ]();
9134
_bindToggleHandlers: function() {
9140
$el.closest( ".ui-page" )
9141
.bind( "vclick", function( e ) {
9142
if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) {
9146
.bind( "focusin focusout", function( e ) {
9147
if ( screen.width < 500 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) {
9148
self[ ( e.type === "focusin" && self._visible ) ? "hide" : "show" ]();
9153
_destroy: function() {
9154
var $el = this.element,
9155
header = $el.is( ".ui-header" );
9157
$el.closest( ".ui-page" ).css( "padding-" + ( header ? "top" : "bottom" ), "" );
9158
$el.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" );
9159
$el.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" );
9164
//auto self-init widgets
9166
.bind( "pagecreate create", function( e ) {
9168
// DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element.
9169
// This line ensures it still works, but we recommend moving the attribute to the toolbars themselves.
9170
if ( $( e.target ).jqmData( "fullscreen" ) ) {
9171
$( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true );
9174
$.mobile.fixedtoolbar.prototype.enhanceWithin( e.target );
9179
(function( $, window ) {
9181
// This fix addresses an iOS bug, so return early if the UA claims it's something else.
9182
if ( !(/iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1 ) ) {
9186
var zoom = $.mobile.zoom,
9189
function checkTilt( e ) {
9190
evt = e.originalEvent;
9191
aig = evt.accelerationIncludingGravity;
9193
x = Math.abs( aig.x );
9194
y = Math.abs( aig.y );
9195
z = Math.abs( aig.z );
9197
// If portrait orientation and in one of the danger zones
9198
if ( !window.orientation && ( x > 7 || ( ( z > 6 && y < 8 || z < 8 && y > 6 ) && x > 5 ) ) ) {
9199
if ( zoom.enabled ) {
9202
} else if ( !zoom.enabled ) {
9208
.bind( "orientationchange.iosorientationfix", zoom.enable )
9209
.bind( "devicemotion.iosorientationfix", checkTilt );
9213
(function( $, window, undefined ) {
9214
var $html = $( "html" ),
9215
$head = $( "head" ),
9216
$window = $( window );
9218
//remove initial build class (only present on first pageshow)
9219
function hideRenderingClass() {
9220
$html.removeClass( "ui-mobile-rendering" );
9223
// trigger mobileinit event - useful hook for configuring $.mobile settings before they're used
9224
$( window.document ).trigger( "mobileinit" );
9226
// support conditions
9227
// if device support condition(s) aren't met, leave things as they are -> a basic, usable experience,
9228
// otherwise, proceed with the enhancements
9229
if ( !$.mobile.gradeA() ) {
9233
// override ajaxEnabled on platforms that have known conflicts with hash history updates
9234
// or generally work better browsing in regular http for full page refreshes (BB5, Opera Mini)
9235
if ( $.mobile.ajaxBlacklist ) {
9236
$.mobile.ajaxEnabled = false;
9239
// Add mobile, initial load "rendering" classes to docEl
9240
$html.addClass( "ui-mobile ui-mobile-rendering" );
9242
// This is a fallback. If anything goes wrong (JS errors, etc), or events don't fire,
9243
// this ensures the rendering class is removed after 5 seconds, so content is visible and accessible
9244
setTimeout( hideRenderingClass, 5000 );
9246
$.extend( $.mobile, {
9247
// find and enhance the pages in the dom and transition to the first page.
9248
initializePage: function() {
9249
// find present pages
9250
var $pages = $( ":jqmData(role='page'), :jqmData(role='dialog')" ),
9251
hash = $.mobile.path.parseLocation().hash.replace("#", ""),
9252
hashPage = document.getElementById( hash );
9254
// if no pages are found, create one with body's inner html
9255
if ( !$pages.length ) {
9256
$pages = $( "body" ).wrapInner( "<div data-" + $.mobile.ns + "role='page'></div>" ).children( 0 );
9259
// add dialogs, set data-url attrs
9260
$pages.each(function() {
9261
var $this = $( this );
9263
// unless the data url is already set set it to the pathname
9264
if ( !$this.jqmData( "url" ) ) {
9265
$this.attr( "data-" + $.mobile.ns + "url", $this.attr( "id" ) || location.pathname + location.search );
9269
// define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback)
9270
$.mobile.firstPage = $pages.first();
9272
// define page container
9273
$.mobile.pageContainer = $pages.first().parent().addClass( "ui-mobile-viewport" );
9275
// alert listeners that the pagecontainer has been determined for binding
9276
// to events triggered on it
9277
$window.trigger( "pagecontainercreate" );
9279
// cue page loading message
9280
$.mobile.showPageLoadingMsg();
9282
//remove initial build class (only present on first pageshow)
9283
hideRenderingClass();
9285
// if hashchange listening is disabled, there's no hash deeplink,
9286
// the hash is not valid (contains more than one # or does not start with #)
9287
// or there is no page with that hash, change to the first page in the DOM
9288
// Remember, however, that the hash can also be a path!
9289
if ( ! ( $.mobile.hashListeningEnabled &&
9290
$.mobile.path.isHashValid( location.hash ) &&
9291
( $( hashPage ).is( ':jqmData(role="page")' ) ||
9292
$.mobile.path.isPath( hash ) ||
9293
hash === $.mobile.dialogHashKey ) ) ) {
9295
// Store the initial destination
9296
if ( $.mobile.path.isHashValid( location.hash ) ) {
9297
$.mobile.urlHistory.initialDst = hash.replace( "#", "" );
9299
$.mobile.changePage( $.mobile.firstPage, { transition: "none", reverse: true, changeHash: false, fromHashChange: true } );
9301
// otherwise, trigger a hashchange to load a deeplink
9303
$window.trigger( "hashchange", [ true ] );
9308
// initialize events now, after mobileinit has occurred
9309
$.mobile.navreadyDeferred.resolve();
9311
// check which scrollTop value should be used by scrolling to 1 immediately at domready
9312
// then check what the scroll top is. Android will report 0... others 1
9313
// note that this initial scroll won't hide the address bar. It's just for the check.
9315
window.scrollTo( 0, 1 );
9317
// if defaultHomeScroll hasn't been set yet, see if scrollTop is 1
9318
// it should be 1 in most browsers, but android treats 1 as 0 (for hiding addr bar)
9319
// so if it's 1, use 0 from now on
9320
$.mobile.defaultHomeScroll = ( !$.support.scrollTop || $( window ).scrollTop() === 1 ) ? 0 : 1;
9323
// TODO: Implement a proper registration mechanism with dependency handling in order to not have exceptions like the one below
9324
//auto self-init widgets for those widgets that have a soft dependency on others
9325
if ( $.fn.controlgroup ) {
9326
$( document ).bind( "pagecreate create", function( e ) {
9327
$( ":jqmData(role='controlgroup')", e.target )
9329
.controlgroup({ excludeInvisible: false });
9334
if ( $.mobile.autoInitializePage ) {
9335
$.mobile.initializePage();
9338
// window load event
9339
// hide iOS browser chrome on load
9340
$window.load( $.mobile.silentScroll );
9342
if ( !$.support.cssPointerEvents ) {
9343
// IE and Opera don't support CSS pointer-events: none that we use to disable link-based buttons
9344
// by adding the 'ui-disabled' class to them. Using a JavaScript workaround for those browser.
9345
// https://github.com/jquery/jquery-mobile/issues/3558
9347
$( document ).delegate( ".ui-disabled", "vclick",
9350
e.stopImmediatePropagation();