| /* |
| * jQuery Mobile Framework Git Build: SHA1: 4e0055a4eaa6c052f6ff1f6869a6473cd14c00fc <> Date: Mon Jul 15 15:58:34 2013 +0900 |
| * http://jquerymobile.com |
| * |
| * Copyright 2012 jQuery Foundation and other contributors |
| * Released under the MIT license. |
| * http://jquery.org/license |
| * |
| */ |
| |
| |
| (function ( root, doc, factory ) { |
| if ( typeof define === "function" && define.amd ) { |
| // AMD. Register as an anonymous module. |
| define( [ "jquery" ], function ( $ ) { |
| factory( $, root, doc ); |
| return $.mobile; |
| }); |
| } else { |
| // Browser globals |
| factory( root.jQuery, root, doc ); |
| } |
| }( this, document, function ( jQuery, window, document, undefined ) { |
| (function( $, window, undefined ) { |
| |
| var nsNormalizeDict = {}; |
| |
| // jQuery.mobile configurable options |
| $.mobile = $.extend( {}, { |
| |
| // Version of the jQuery Mobile Framework |
| version: "1.2.0", |
| |
| // Namespace used framework-wide for data-attrs. Default is no namespace |
| ns: "", |
| |
| // Define the url parameter used for referencing widget-generated sub-pages. |
| // Translates to to example.html&ui-page=subpageIdentifier |
| // hash segment before &ui-page= is used to make Ajax request |
| subPageUrlKey: "ui-page", |
| |
| // Class assigned to page currently in view, and during transitions |
| activePageClass: "ui-page-active", |
| |
| // Class used for "active" button state, from CSS framework |
| activeBtnClass: "ui-btn-active", |
| |
| // Class used for "focus" form element state, from CSS framework |
| focusClass: "ui-focus", |
| |
| // Automatically handle clicks and form submissions through Ajax, when same-domain |
| ajaxEnabled: true, |
| |
| // Automatically load and show pages based on location.hash |
| hashListeningEnabled: true, |
| |
| // disable to prevent jquery from bothering with links |
| linkBindingEnabled: true, |
| |
| // Set default page transition - 'none' for no transitions |
| defaultPageTransition: "fade", |
| |
| // Set maximum window width for transitions to apply - 'false' for no limit |
| maxTransitionWidth: false, |
| |
| // Minimum scroll distance that will be remembered when returning to a page |
| minScrollBack: 250, |
| |
| // DEPRECATED: the following property is no longer in use, but defined until 2.0 to prevent conflicts |
| touchOverflowEnabled: false, |
| |
| // Set default dialog transition - 'none' for no transitions |
| defaultDialogTransition: "pop", |
| |
| // Error response message - appears when an Ajax page request fails |
| pageLoadErrorMessage: "Error Loading Page", |
| |
| // For error messages, which theme does the box uses? |
| pageLoadErrorMessageTheme: "e", |
| |
| // replace calls to window.history.back with phonegaps navigation helper |
| // where it is provided on the window object |
| phonegapNavigationEnabled: false, |
| |
| //automatically initialize the DOM when it's ready |
| autoInitializePage: true, |
| |
| pushStateEnabled: true, |
| |
| // allows users to opt in to ignoring content by marking a parent element as |
| // data-ignored |
| ignoreContentEnabled: false, |
| |
| // turn of binding to the native orientationchange due to android orientation behavior |
| orientationChangeEnabled: true, |
| |
| buttonMarkup: { |
| hoverDelay: 200 |
| }, |
| |
| // define the window and the document objects |
| $window: $( window ), |
| $document: $( document ), |
| |
| getAttrFixed : function( e, key ) { |
| var value = e.getAttribute( key ); |
| |
| return value === "true" ? true : |
| value === "false" ? false : |
| value === null ? undefined : value; |
| }, |
| |
| // TODO might be useful upstream in jquery itself ? |
| keyCode: { |
| ALT: 18, |
| BACKSPACE: 8, |
| CAPS_LOCK: 20, |
| COMMA: 188, |
| COMMAND: 91, |
| COMMAND_LEFT: 91, // COMMAND |
| COMMAND_RIGHT: 93, |
| CONTROL: 17, |
| DELETE: 46, |
| DOWN: 40, |
| END: 35, |
| ENTER: 13, |
| ESCAPE: 27, |
| HOME: 36, |
| INSERT: 45, |
| LEFT: 37, |
| MENU: 93, // COMMAND_RIGHT |
| NUMPAD_ADD: 107, |
| NUMPAD_DECIMAL: 110, |
| NUMPAD_DIVIDE: 111, |
| NUMPAD_ENTER: 108, |
| NUMPAD_MULTIPLY: 106, |
| NUMPAD_SUBTRACT: 109, |
| PAGE_DOWN: 34, |
| PAGE_UP: 33, |
| PERIOD: 190, |
| RIGHT: 39, |
| SHIFT: 16, |
| SPACE: 32, |
| TAB: 9, |
| UP: 38, |
| WINDOWS: 91 // COMMAND |
| }, |
| |
| // Scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value |
| silentScroll: function( ypos ) { |
| if ( $.type( ypos ) !== "number" ) { |
| ypos = $.mobile.defaultHomeScroll; |
| } |
| |
| // prevent scrollstart and scrollstop events |
| $.event.special.scrollstart.enabled = false; |
| |
| setTimeout( function() { |
| window.scrollTo( 0, ypos ); |
| $.mobile.$document.trigger( "silentscroll", { x: 0, y: ypos }); |
| }, 20 ); |
| |
| setTimeout( function() { |
| $.event.special.scrollstart.enabled = true; |
| }, 150 ); |
| }, |
| |
| // Expose our cache for testing purposes. |
| nsNormalizeDict: nsNormalizeDict, |
| |
| // Take a data attribute property, prepend the namespace |
| // and then camel case the attribute string. Add the result |
| // to our nsNormalizeDict so we don't have to do this again. |
| nsNormalize: function( prop ) { |
| if ( !prop ) { |
| return; |
| } |
| |
| return nsNormalizeDict[ prop ] || ( nsNormalizeDict[ prop ] = $.camelCase( $.mobile.ns + prop ) ); |
| }, |
| |
| // Find the closest parent with a theme class on it. Note that |
| // we are not using $.fn.closest() on purpose here because this |
| // method gets called quite a bit and we need it to be as fast |
| // as possible. |
| getInheritedTheme: function( el, defaultTheme ) { |
| var e = el[ 0 ], |
| ltr = "", |
| re = /ui-(bar|body|overlay)-([a-z])\b/, |
| c, m; |
| |
| while ( e ) { |
| c = e.className || ""; |
| if ( c && ( m = re.exec( c ) ) && ( ltr = m[ 2 ] ) ) { |
| // We found a parent with a theme class |
| // on it so bail from this loop. |
| break; |
| } |
| |
| e = e.parentNode; |
| } |
| |
| // Return the theme letter we found, if none, return the |
| // specified default. |
| |
| return ltr || defaultTheme || "a"; |
| }, |
| |
| // TODO the following $ and $.fn extensions can/probably should be moved into jquery.mobile.core.helpers |
| // |
| // Find the closest javascript page element to gather settings data jsperf test |
| // http://jsperf.com/single-complex-selector-vs-many-complex-selectors/edit |
| // possibly naive, but it shows that the parsing overhead for *just* the page selector vs |
| // the page and dialog selector is negligable. This could probably be speed up by |
| // doing a similar parent node traversal to the one found in the inherited theme code above |
| closestPageData: function( $target ) { |
| return $target |
| .closest( ':jqmData(role="page"), :jqmData(role="dialog")' ) |
| .data( "page" ); |
| }, |
| |
| enhanceable: function( $set ) { |
| return this.haveParents( $set, "enhance" ); |
| }, |
| |
| hijackable: function( $set ) { |
| return this.haveParents( $set, "ajax" ); |
| }, |
| |
| haveParents: function( $set, attr ) { |
| if ( !$.mobile.ignoreContentEnabled ) { |
| return $set; |
| } |
| |
| var count = $set.length, |
| $newSet = $(), |
| e, $element, excluded; |
| |
| for ( var i = 0; i < count; i++ ) { |
| $element = $set.eq( i ); |
| excluded = false; |
| e = $set[ i ]; |
| |
| while ( e ) { |
| var c = e.getAttribute ? e.getAttribute( "data-" + $.mobile.ns + attr ) : ""; |
| |
| if ( c === "false" ) { |
| excluded = true; |
| break; |
| } |
| |
| e = e.parentNode; |
| } |
| |
| if ( !excluded ) { |
| $newSet = $newSet.add( $element ); |
| } |
| } |
| |
| return $newSet; |
| }, |
| |
| getScreenHeight: function() { |
| // Native innerHeight returns more accurate value for this across platforms, |
| // jQuery version is here as a normalized fallback for platforms like Symbian |
| return window.innerHeight || $.mobile.$window.height(); |
| } |
| }, $.mobile ); |
| |
| // Mobile version of data and removeData and hasData methods |
| // ensures all data is set and retrieved using jQuery Mobile's data namespace |
| $.fn.jqmData = function( prop, value ) { |
| var result; |
| if ( typeof prop !== "undefined" ) { |
| if ( prop ) { |
| prop = $.mobile.nsNormalize( prop ); |
| } |
| |
| // undefined is permitted as an explicit input for the second param |
| // in this case it returns the value and does not set it to undefined |
| if( arguments.length < 2 || value === undefined ){ |
| result = this.data( prop ); |
| } else { |
| result = this.data( prop, value ); |
| } |
| } |
| return result; |
| }; |
| |
| $.jqmData = function( elem, prop, value ) { |
| var result; |
| if ( typeof prop !== "undefined" ) { |
| result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value ); |
| } |
| return result; |
| }; |
| |
| $.fn.jqmRemoveData = function( prop ) { |
| return this.removeData( $.mobile.nsNormalize( prop ) ); |
| }; |
| |
| $.jqmRemoveData = function( elem, prop ) { |
| return $.removeData( elem, $.mobile.nsNormalize( prop ) ); |
| }; |
| |
| $.fn.removeWithDependents = function() { |
| $.removeWithDependents( this ); |
| }; |
| |
| $.removeWithDependents = function( elem ) { |
| var $elem = $( elem ); |
| |
| ( $elem.jqmData( 'dependents' ) || $() ).remove(); |
| $elem.remove(); |
| }; |
| |
| $.fn.addDependents = function( newDependents ) { |
| $.addDependents( $( this ), newDependents ); |
| }; |
| |
| $.addDependents = function( elem, newDependents ) { |
| var dependents = $( elem ).jqmData( 'dependents' ) || $(); |
| |
| $( elem ).jqmData( 'dependents', $.merge( dependents, newDependents ) ); |
| }; |
| |
| // note that this helper doesn't attempt to handle the callback |
| // or setting of an html elements text, its only purpose is |
| // to return the html encoded version of the text in all cases. (thus the name) |
| $.fn.getEncodedText = function() { |
| return $( "<div/>" ).text( $( this ).text() ).html(); |
| }; |
| |
| // fluent helper function for the mobile namespaced equivalent |
| $.fn.jqmEnhanceable = function() { |
| return $.mobile.enhanceable( this ); |
| }; |
| |
| $.fn.jqmHijackable = function() { |
| return $.mobile.hijackable( this ); |
| }; |
| |
| // Monkey-patching Sizzle to filter the :jqmData selector |
| var oldFind = $.find, |
| jqmDataRE = /:jqmData\(([^)]*)\)/g; |
| |
| $.find = function( selector, context, ret, extra ) { |
| selector = selector.replace( jqmDataRE, "[data-" + ( $.mobile.ns || "" ) + "$1]" ); |
| |
| return oldFind.call( this, selector, context, ret, extra ); |
| }; |
| |
| $.extend( $.find, oldFind ); |
| |
| $.find.matches = function( expr, set ) { |
| return $.find( expr, null, null, set ); |
| }; |
| |
| $.find.matchesSelector = function( node, expr ) { |
| return $.find( expr, null, null, [ node ] ).length > 0; |
| }; |
| |
| $.extend({ |
| creatorDict: {}, |
| |
| delegateSelfInitWithSingleSelector: function( target, useKeepNative ) { |
| if ( typeof target !== 'function' ) { |
| return false; |
| } |
| var selector = target.prototype.options.initSelector; |
| var selectorRE = /:jqmData\(role='[A-z\-]+'\)$/; |
| if ( selectorRE.test(selector) ) { |
| var firstIdx = selector.indexOf( "'" ) + 1; |
| var lastIdx = selector.lastIndexOf( "'" ); |
| var key = selector.substring( firstIdx, lastIdx ); |
| if ( !$.creatorDict.hasOwnProperty( key ) ) { |
| $.creatorDict[key] = {}; |
| $.creatorDict[key].target = target; |
| if ( useKeepNative === true ) { |
| $.creatorDict[key].useKeepNative = useKeepNative; |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| |
| //auto self-init widgets |
| $( document ).bind( "pagecreate create", function( e ) { |
| var selector = "*[data-" + $.mobile.ns + "role]"; |
| $( selector, e.target ).each( function () { |
| var dataRoleValue = this.getAttribute( "data-role" ), |
| matchedObj = $.creatorDict[dataRoleValue]; |
| if ( matchedObj ) { |
| matchedObj.target.prototype.enhance( this, matchedObj.useKeepNative ); |
| } |
| }); |
| }); |
| })( jQuery, this ); |
| |
| |
| /*! |
| * jQuery UI Widget v1.9.0-beta.1 |
| * |
| * Copyright 2012, https://github.com/jquery/jquery-ui/blob/1.9.0-beta.1/AUTHORS.txt (http://jqueryui.com/about) |
| * Dual licensed under the MIT or GPL Version 2 licenses. |
| * http://jquery.org/license |
| * |
| * http://docs.jquery.com/UI/Widget |
| */ |
| |
| |
| (function( $, undefined ) { |
| |
| var uuid = 0, |
| slice = Array.prototype.slice, |
| _cleanData = $.cleanData; |
| $.cleanData = function( elems ) { |
| for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { |
| try { |
| $( elem ).triggerHandler( "remove" ); |
| // http://bugs.jquery.com/ticket/8235 |
| } catch( e ) {} |
| } |
| _cleanData( elems ); |
| }; |
| |
| $.widget = function( name, base, prototype ) { |
| var fullName, existingConstructor, constructor, basePrototype, |
| namespace = name.split( "." )[ 0 ]; |
| |
| name = name.split( "." )[ 1 ]; |
| fullName = namespace + "-" + name; |
| |
| if ( !prototype ) { |
| prototype = base; |
| base = $.Widget; |
| } |
| |
| // create selector for plugin |
| $.expr[ ":" ][ fullName ] = function( elem ) { |
| return !!$.data( elem, fullName ); |
| }; |
| |
| $[ namespace ] = $[ namespace ] || {}; |
| existingConstructor = $[ namespace ][ name ]; |
| constructor = $[ namespace ][ name ] = function( options, element ) { |
| // allow instantiation without "new" keyword |
| if ( !this._createWidget ) { |
| return new constructor( options, element ); |
| } |
| |
| // allow instantiation without initializing for simple inheritance |
| // must use "new" keyword (the code above always passes args) |
| if ( arguments.length ) { |
| this._createWidget( options, element ); |
| } |
| }; |
| // extend with the existing constructor to carry over any static properties |
| $.extend( constructor, existingConstructor, { |
| version: prototype.version, |
| // copy the object used to create the prototype in case we need to |
| // redefine the widget later |
| _proto: $.extend( {}, prototype ), |
| // track widgets that inherit from this widget in case this widget is |
| // redefined after a widget inherits from it |
| _childConstructors: [] |
| }); |
| |
| basePrototype = new base(); |
| // we need to make the options hash a property directly on the new instance |
| // otherwise we'll modify the options hash on the prototype that we're |
| // inheriting from |
| basePrototype.options = $.widget.extend( {}, basePrototype.options ); |
| $.each( prototype, function( prop, value ) { |
| if ( $.isFunction( value ) ) { |
| prototype[ prop ] = (function() { |
| var _super = function() { |
| return base.prototype[ prop ].apply( this, arguments ); |
| }, |
| _superApply = function( args ) { |
| return base.prototype[ prop ].apply( this, args ); |
| }; |
| return function() { |
| var __super = this._super, |
| __superApply = this._superApply, |
| returnValue; |
| |
| this._super = _super; |
| this._superApply = _superApply; |
| |
| returnValue = value.apply( this, arguments ); |
| |
| this._super = __super; |
| this._superApply = __superApply; |
| |
| return returnValue; |
| }; |
| })(); |
| } |
| }); |
| constructor.prototype = $.widget.extend( basePrototype, { |
| // TODO: remove support for widgetEventPrefix |
| // always use the name + a colon as the prefix, e.g., draggable:start |
| // don't prefix for widgets that aren't DOM-based |
| widgetEventPrefix: name |
| }, prototype, { |
| constructor: constructor, |
| namespace: namespace, |
| widgetName: name, |
| // TODO remove widgetBaseClass, see #8155 |
| widgetBaseClass: fullName, |
| widgetFullName: fullName |
| }); |
| |
| // If this widget is being redefined then we need to find all widgets that |
| // are inheriting from it and redefine all of them so that they inherit from |
| // the new version of this widget. We're essentially trying to replace one |
| // level in the prototype chain. |
| if ( existingConstructor ) { |
| $.each( existingConstructor._childConstructors, function( i, child ) { |
| var childPrototype = child.prototype; |
| |
| // redefine the child widget using the same prototype that was |
| // originally used, but inherit from the new version of the base |
| $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); |
| }); |
| // remove the list of existing child constructors from the old constructor |
| // so the old child constructors can be garbage collected |
| delete existingConstructor._childConstructors; |
| } else { |
| base._childConstructors.push( constructor ); |
| } |
| |
| $.widget.bridge( name, constructor ); |
| }; |
| |
| $.widget.extend = function( target ) { |
| var input = slice.call( arguments, 1 ), |
| inputIndex = 0, |
| inputLength = input.length, |
| key, |
| value; |
| for ( ; inputIndex < inputLength; inputIndex++ ) { |
| for ( key in input[ inputIndex ] ) { |
| value = input[ inputIndex ][ key ]; |
| if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { |
| target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value; |
| } |
| } |
| } |
| return target; |
| }; |
| |
| $.widget.bridge = function( name, object ) { |
| var fullName = object.prototype.widgetFullName; |
| $.fn[ name ] = function( options ) { |
| var isMethodCall = typeof options === "string", |
| args = slice.call( arguments, 1 ), |
| returnValue = this; |
| |
| // allow multiple hashes to be passed on init |
| options = !isMethodCall && args.length ? |
| $.widget.extend.apply( null, [ options ].concat(args) ) : |
| options; |
| |
| if ( isMethodCall ) { |
| this.each(function() { |
| var methodValue, |
| instance = $.data( this, fullName ); |
| if ( !instance ) { |
| return $.error( "cannot call methods on " + name + " prior to initialization; " + |
| "attempted to call method '" + options + "'" ); |
| } |
| if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { |
| return $.error( "no such method '" + options + "' for " + name + " widget instance" ); |
| } |
| methodValue = instance[ options ].apply( instance, args ); |
| if ( methodValue !== instance && methodValue !== undefined ) { |
| returnValue = methodValue && methodValue.jquery ? |
| returnValue.pushStack( methodValue.get() ) : |
| methodValue; |
| return false; |
| } |
| }); |
| } else { |
| this.each(function() { |
| var instance = $.data( this, fullName ); |
| if ( instance ) { |
| instance.option( options || {} )._init(); |
| } else { |
| new object( options, this ); |
| } |
| }); |
| } |
| |
| return returnValue; |
| }; |
| }; |
| |
| $.Widget = function( options, element ) {}; |
| $.Widget._childConstructors = []; |
| |
| $.Widget.prototype = { |
| widgetName: "widget", |
| widgetEventPrefix: "", |
| defaultElement: "<div>", |
| options: { |
| disabled: false, |
| |
| // callbacks |
| create: null |
| }, |
| _createWidget: function( options, element ) { |
| element = $( element || this.defaultElement || this )[ 0 ]; |
| this.element = $( element ); |
| this.uuid = uuid++; |
| this.eventNamespace = "." + this.widgetName + this.uuid; |
| this.options = $.widget.extend( {}, |
| this.options, |
| this._getCreateOptions(), |
| options ); |
| |
| this.bindings = $(); |
| this.hoverable = $(); |
| this.focusable = $(); |
| |
| if ( element !== this ) { |
| // 1.9 BC for #7810 |
| // TODO remove dual storage |
| $.data( element, this.widgetName, this ); |
| $.data( element, this.widgetFullName, this ); |
| this._on({ remove: "destroy" }); |
| this.document = $( element.style ? |
| // element within the document |
| element.ownerDocument : |
| // element is window or document |
| element.document || element ); |
| this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); |
| } |
| |
| this._create(); |
| this._trigger( "create", null, this._getCreateEventData() ); |
| this._init(); |
| }, |
| _getCreateOptions: $.noop, |
| _getCreateEventData: $.noop, |
| _create: $.noop, |
| _init: $.noop, |
| |
| destroy: function() { |
| this._destroy(); |
| // we can probably remove the unbind calls in 2.0 |
| // all event bindings should go through this._on() |
| this.element |
| .unbind( this.eventNamespace ) |
| // 1.9 BC for #7810 |
| // TODO remove dual storage |
| .removeData( this.widgetName ) |
| .removeData( this.widgetFullName ) |
| // support: jquery <1.6.3 |
| // http://bugs.jquery.com/ticket/9413 |
| .removeData( $.camelCase( this.widgetFullName ) ); |
| this.widget() |
| .unbind( this.eventNamespace ) |
| .removeAttr( "aria-disabled" ) |
| .removeClass( |
| this.widgetFullName + "-disabled " + |
| "ui-state-disabled" ); |
| |
| // clean up events and states |
| this.bindings.unbind( this.eventNamespace ); |
| this.hoverable.removeClass( "ui-state-hover" ); |
| this.focusable.removeClass( "ui-state-focus" ); |
| }, |
| _destroy: $.noop, |
| |
| widget: function() { |
| return this.element; |
| }, |
| |
| option: function( key, value ) { |
| var options = key, |
| parts, |
| curOption, |
| i; |
| |
| if ( arguments.length === 0 ) { |
| // don't return a reference to the internal hash |
| return $.widget.extend( {}, this.options ); |
| } |
| |
| if ( typeof key === "string" ) { |
| // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } |
| options = {}; |
| parts = key.split( "." ); |
| key = parts.shift(); |
| if ( parts.length ) { |
| curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); |
| for ( i = 0; i < parts.length - 1; i++ ) { |
| curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; |
| curOption = curOption[ parts[ i ] ]; |
| } |
| key = parts.pop(); |
| if ( value === undefined ) { |
| return curOption[ key ] === undefined ? null : curOption[ key ]; |
| } |
| curOption[ key ] = value; |
| } else { |
| if ( value === undefined ) { |
| return this.options[ key ] === undefined ? null : this.options[ key ]; |
| } |
| options[ key ] = value; |
| } |
| } |
| |
| this._setOptions( options ); |
| |
| return this; |
| }, |
| _setOptions: function( options ) { |
| var key; |
| |
| for ( key in options ) { |
| this._setOption( key, options[ key ] ); |
| } |
| |
| return this; |
| }, |
| _setOption: function( key, value ) { |
| this.options[ key ] = value; |
| |
| if ( key === "disabled" ) { |
| this.widget() |
| .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) |
| .attr( "aria-disabled", value ); |
| this.hoverable.removeClass( "ui-state-hover" ); |
| this.focusable.removeClass( "ui-state-focus" ); |
| } |
| |
| return this; |
| }, |
| |
| enable: function() { |
| return this._setOption( "disabled", false ); |
| }, |
| disable: function() { |
| return this._setOption( "disabled", true ); |
| }, |
| |
| _on: function( element, handlers ) { |
| // no element argument, shuffle and use this.element |
| if ( !handlers ) { |
| handlers = element; |
| element = this.element; |
| } else { |
| // accept selectors, DOM elements |
| element = $( element ); |
| this.bindings = this.bindings.add( element ); |
| } |
| |
| var instance = this; |
| $.each( handlers, function( event, handler ) { |
| function handlerProxy() { |
| // allow widgets to customize the disabled handling |
| // - disabled as an array instead of boolean |
| // - disabled class as method for disabling individual parts |
| if ( instance.options.disabled === true || |
| $( this ).hasClass( "ui-state-disabled" ) ) { |
| return; |
| } |
| return ( typeof handler === "string" ? instance[ handler ] : handler ) |
| .apply( instance, arguments ); |
| } |
| |
| // copy the guid so direct unbinding works |
| if ( typeof handler !== "string" ) { |
| handlerProxy.guid = handler.guid = |
| handler.guid || handlerProxy.guid || $.guid++; |
| } |
| |
| var match = event.match( /^(\w+)\s*(.*)$/ ), |
| eventName = match[1] + instance.eventNamespace, |
| selector = match[2]; |
| if ( selector ) { |
| instance.widget().delegate( selector, eventName, handlerProxy ); |
| } else { |
| element.bind( eventName, handlerProxy ); |
| } |
| }); |
| }, |
| |
| _off: function( element, eventName ) { |
| eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; |
| element.unbind( eventName ).undelegate( eventName ); |
| }, |
| |
| _delay: function( handler, delay ) { |
| function handlerProxy() { |
| return ( typeof handler === "string" ? instance[ handler ] : handler ) |
| .apply( instance, arguments ); |
| } |
| var instance = this; |
| return setTimeout( handlerProxy, delay || 0 ); |
| }, |
| |
| _hoverable: function( element ) { |
| this.hoverable = this.hoverable.add( element ); |
| this._on( element, { |
| mouseenter: function( event ) { |
| $( event.currentTarget ).addClass( "ui-state-hover" ); |
| }, |
| mouseleave: function( event ) { |
| $( event.currentTarget ).removeClass( "ui-state-hover" ); |
| } |
| }); |
| }, |
| |
| _focusable: function( element ) { |
| this.focusable = this.focusable.add( element ); |
| this._on( element, { |
| focusin: function( event ) { |
| $( event.currentTarget ).addClass( "ui-state-focus" ); |
| }, |
| focusout: function( event ) { |
| $( event.currentTarget ).removeClass( "ui-state-focus" ); |
| } |
| }); |
| }, |
| |
| _trigger: function( type, event, data ) { |
| var prop, orig, |
| callback = this.options[ type ]; |
| |
| data = data || {}; |
| event = $.Event( event ); |
| event.type = ( type === this.widgetEventPrefix ? |
| type : |
| this.widgetEventPrefix + type ).toLowerCase(); |
| // the original event may come from any element |
| // so we need to reset the target on the new event |
| event.target = this.element[ 0 ]; |
| |
| // copy original event properties over to the new event |
| orig = event.originalEvent; |
| if ( orig ) { |
| for ( prop in orig ) { |
| if ( !( prop in event ) ) { |
| event[ prop ] = orig[ prop ]; |
| } |
| } |
| } |
| |
| this.element.trigger( event, data ); |
| return !( $.isFunction( callback ) && |
| callback.apply( this.element[0], [ event ].concat( data ) ) === false || |
| event.isDefaultPrevented() ); |
| } |
| }; |
| |
| $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { |
| $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { |
| if ( typeof options === "string" ) { |
| options = { effect: options }; |
| } |
| var hasOptions, |
| effectName = !options ? |
| method : |
| options === true || typeof options === "number" ? |
| defaultEffect : |
| options.effect || defaultEffect; |
| options = options || {}; |
| if ( typeof options === "number" ) { |
| options = { duration: options }; |
| } |
| hasOptions = !$.isEmptyObject( options ); |
| options.complete = callback; |
| if ( options.delay ) { |
| element.delay( options.delay ); |
| } |
| if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) { |
| element[ method ]( options ); |
| } else if ( effectName !== method && element[ effectName ] ) { |
| element[ effectName ]( options.duration, options.easing, callback ); |
| } else { |
| element.queue(function( next ) { |
| $( this )[ method ](); |
| if ( callback ) { |
| callback.call( element[ 0 ] ); |
| } |
| next(); |
| }); |
| } |
| }; |
| }); |
| |
| // DEPRECATED |
| if ( $.uiBackCompat !== false ) { |
| $.Widget.prototype._getCreateOptions = function() { |
| return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ]; |
| }; |
| } |
| |
| })( jQuery ); |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.widget", { |
| // decorate the parent _createWidget to trigger `widgetinit` for users |
| // who wish to do post post `widgetcreate` alterations/additions |
| // |
| // TODO create a pull request for jquery ui to trigger this event |
| // in the original _createWidget |
| _createWidget: function() { |
| $.Widget.prototype._createWidget.apply( this, arguments ); |
| this._trigger( 'init' ); |
| }, |
| |
| _getCreateOptions: function() { |
| |
| var elem = this.element, |
| options = {}; |
| |
| $.each( this.options, function( option ) { |
| |
| var value = elem.jqmData( option.replace( /[A-Z]/g, function( c ) { |
| return "-" + c.toLowerCase(); |
| }) |
| ); |
| |
| if ( value !== undefined ) { |
| options[ option ] = value; |
| } |
| }); |
| |
| return options; |
| }, |
| |
| enhanceWithin: function( target, useKeepNative ) { |
| this.enhance( $( this.options.initSelector, $( target )), useKeepNative ); |
| }, |
| |
| enhance: function( targets, useKeepNative ) { |
| var page, keepNative, $widgetElements = $( targets ), self = this; |
| |
| // if ignoreContentEnabled is set to true the framework should |
| // only enhance the selected elements when they do NOT have a |
| // parent with the data-namespace-ignore attribute |
| $widgetElements = $.mobile.enhanceable( $widgetElements ); |
| |
| if ( useKeepNative && $widgetElements.length ) { |
| // TODO remove dependency on the page widget for the keepNative. |
| // Currently the keepNative value is defined on the page prototype so |
| // the method is as well |
| page = $.mobile.closestPageData( $widgetElements ); |
| keepNative = ( page && page.keepNativeSelector()) || ""; |
| |
| $widgetElements = $widgetElements.not( keepNative ); |
| } |
| |
| $widgetElements[ this.widgetName ](); |
| }, |
| |
| raise: function( msg ) { |
| throw "Widget [" + this.widgetName + "]: " + msg; |
| } |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, window ) { |
| // DEPRECATED |
| // NOTE global mobile object settings |
| $.extend( $.mobile, { |
| // DEPRECATED Should the text be visble in the loading message? |
| loadingMessageTextVisible: undefined, |
| |
| // DEPRECATED When the text is visible, what theme does the loading box use? |
| loadingMessageTheme: undefined, |
| |
| // DEPRECATED default message setting |
| loadingMessage: undefined, |
| |
| // DEPRECATED |
| // Turn on/off page loading message. Theme doubles as an object argument |
| // with the following shape: { theme: '', text: '', html: '', textVisible: '' } |
| // NOTE that the $.mobile.loading* settings and params past the first are deprecated |
| showPageLoadingMsg: function( theme, msgText, textonly ) { |
| $.mobile.loading( 'show', theme, msgText, textonly ); |
| }, |
| |
| // DEPRECATED |
| hidePageLoadingMsg: function() { |
| $.mobile.loading( 'hide' ); |
| }, |
| |
| loading: function() { |
| this.loaderWidget.loader.apply( this.loaderWidget, arguments ); |
| } |
| }); |
| |
| // TODO move loader class down into the widget settings |
| var loaderClass = "ui-loader", $html = $( "html" ), $window = $.mobile.$window; |
| |
| $.widget( "mobile.loader", { |
| // NOTE if the global config settings are defined they will override these |
| // options |
| options: { |
| // the theme for the loading message |
| theme: "a", |
| |
| // whether the text in the loading message is shown |
| textVisible: false, |
| |
| // custom html for the inner content of the loading message |
| html: "", |
| |
| // the text to be displayed when the popup is shown |
| text: "loading" |
| }, |
| |
| defaultHtml: "<div class='" + loaderClass + "'>" + |
| "<span class='ui-icon ui-icon-loading'></span>" + |
| "<h1></h1>" + |
| "</div>", |
| |
| // For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top |
| fakeFixLoader: function() { |
| var activeBtn = $( "." + $.mobile.activeBtnClass ).first(); |
| |
| this.element |
| .css({ |
| top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 || |
| activeBtn.length && activeBtn.offset().top || 100 |
| }); |
| }, |
| |
| // check position of loader to see if it appears to be "fixed" to center |
| // if not, use abs positioning |
| checkLoaderPosition: function() { |
| var offset = this.element.offset(), |
| scrollTop = $window.scrollTop(), |
| screenHeight = $.mobile.getScreenHeight(); |
| |
| if ( offset.top < scrollTop || ( offset.top - scrollTop ) > screenHeight ) { |
| this.element.addClass( "ui-loader-fakefix" ); |
| this.fakeFixLoader(); |
| $window |
| .unbind( "scroll", this.checkLoaderPosition ) |
| .bind( "scroll", this.fakeFixLoader ); |
| } |
| }, |
| |
| resetHtml: function() { |
| this.element.html( $( this.defaultHtml ).html() ); |
| }, |
| |
| // Turn on/off page loading message. Theme doubles as an object argument |
| // with the following shape: { theme: '', text: '', html: '', textVisible: '' } |
| // NOTE that the $.mobile.loading* settings and params past the first are deprecated |
| // TODO sweet jesus we need to break some of this out |
| show: function( theme, msgText, textonly ) { |
| var textVisible, message, $header, loadSettings; |
| |
| this.resetHtml(); |
| |
| // use the prototype options so that people can set them globally at |
| // mobile init. Consistency, it's what's for dinner |
| if ( $.type(theme) === "object" ) { |
| loadSettings = $.extend( {}, this.options, theme ); |
| |
| // prefer object property from the param then the old theme setting |
| theme = loadSettings.theme || $.mobile.loadingMessageTheme; |
| } else { |
| loadSettings = this.options; |
| |
| // here we prefer the them value passed as a string argument, then |
| // we prefer the global option because we can't use undefined default |
| // prototype options, then the prototype option |
| theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme; |
| } |
| |
| // set the message text, prefer the param, then the settings object |
| // then loading message |
| message = msgText || $.mobile.loadingMessage || loadSettings.text; |
| |
| // prepare the dom |
| $html.addClass( "ui-loading" ); |
| |
| if ( $.mobile.loadingMessage !== false || loadSettings.html ) { |
| // boolean values require a bit more work :P, supports object properties |
| // and old settings |
| if ( $.mobile.loadingMessageTextVisible !== undefined ) { |
| textVisible = $.mobile.loadingMessageTextVisible; |
| } else { |
| textVisible = loadSettings.textVisible; |
| } |
| |
| // add the proper css given the options (theme, text, etc) |
| // Force text visibility if the second argument was supplied, or |
| // if the text was explicitly set in the object args |
| this.element.attr("class", loaderClass + |
| " ui-corner-all ui-body-" + theme + |
| " ui-loader-" + ( textVisible || msgText || theme.text ? "verbose" : "default" ) + |
| ( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) ); |
| |
| // TODO verify that jquery.fn.html is ok to use in both cases here |
| // this might be overly defensive in preventing unknowing xss |
| // if the html attribute is defined on the loading settings, use that |
| // otherwise use the fallbacks from above |
| if ( loadSettings.html ) { |
| this.element.html( loadSettings.html ); |
| } else { |
| this.element.find( "h1" ).text( message ); |
| } |
| |
| // attach the loader to the DOM |
| this.element.appendTo( $.mobile.pageContainer ); |
| |
| // check that the loader is visible |
| this.checkLoaderPosition(); |
| |
| // on scroll check the loader position |
| $window.bind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); |
| } |
| }, |
| |
| hide: function() { |
| $html.removeClass( "ui-loading" ); |
| |
| if ( $.mobile.loadingMessage ) { |
| this.element.removeClass( "ui-loader-fakefix" ); |
| } |
| |
| $.mobile.$window.unbind( "scroll", $.proxy( this.fakeFixLoader, this) ); |
| $.mobile.$window.unbind( "scroll", $.proxy( this.checkLoaderPosition, this ) ); |
| } |
| }); |
| |
| $window.bind( 'pagecontainercreate', function() { |
| $.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader(); |
| }); |
| })(jQuery, this); |
| |
| |
| |
| // This plugin is an experiment for abstracting away the touch and mouse |
| // events so that developers don't have to worry about which method of input |
| // the device their document is loaded on supports. |
| // |
| // The idea here is to allow the developer to register listeners for the |
| // basic mouse events, such as mousedown, mousemove, mouseup, and click, |
| // and the plugin will take care of registering the correct listeners |
| // behind the scenes to invoke the listener at the fastest possible time |
| // for that device, while still retaining the order of event firing in |
| // the traditional mouse environment, should multiple handlers be registered |
| // on the same element for different events. |
| // |
| // The current version exposes the following virtual events to jQuery bind methods: |
| // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel" |
| |
| (function( $, window, document, undefined ) { |
| |
| var dataPropertyName = "virtualMouseBindings", |
| touchTargetPropertyName = "virtualTouchID", |
| virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ), |
| touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ), |
| mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [], |
| mouseEventProps = $.event.props.concat( mouseHookProps ), |
| activeDocHandlers = {}, |
| resetTimerID = 0, |
| startX = 0, |
| startY = 0, |
| didScroll = false, |
| clickBlockList = [], |
| blockMouseTriggers = false, |
| blockTouchTriggers = false, |
| eventCaptureSupported = "addEventListener" in document, |
| $document = $.mobile.$document, |
| nextTouchID = 1, |
| lastTouchID = 0, threshold; |
| |
| $.vmouse = { |
| moveDistanceThreshold: 10, |
| clickDistanceThreshold: 10, |
| resetTimerDuration: 1500 |
| }; |
| |
| function getNativeEvent( event ) { |
| |
| while ( event && typeof event.originalEvent !== "undefined" ) { |
| event = event.originalEvent; |
| } |
| return event; |
| } |
| |
| function createVirtualEvent( event, eventType ) { |
| |
| var t = event.type, |
| oe, props, ne, prop, ct, touch, i, j, len; |
| |
| event = $.Event( event ); |
| event.type = eventType; |
| |
| oe = event.originalEvent; |
| props = $.event.props; |
| |
| // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280 |
| // https://github.com/jquery/jquery-mobile/issues/3280 |
| if ( t.search( /^(mouse|click)/ ) > -1 ) { |
| props = mouseEventProps; |
| } |
| |
| // copy original event properties over to the new event |
| // this would happen if we could call $.event.fix instead of $.Event |
| // but we don't have a way to force an event to be fixed multiple times |
| if ( oe ) { |
| for ( i = props.length, prop; i; ) { |
| prop = props[ --i ]; |
| event[ prop ] = oe[ prop ]; |
| } |
| } |
| |
| // make sure that if the mouse and click virtual events are generated |
| // without a .which one is defined |
| if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) { |
| event.which = 1; |
| } |
| |
| if ( t.search(/^touch/) !== -1 ) { |
| ne = getNativeEvent( oe ); |
| t = ne.touches; |
| ct = ne.changedTouches; |
| touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined ); |
| |
| if ( touch ) { |
| for ( j = 0, len = touchEventProps.length; j < len; j++) { |
| prop = touchEventProps[ j ]; |
| event[ prop ] = touch[ prop ]; |
| } |
| } |
| } |
| |
| return event; |
| } |
| |
| function getVirtualBindingFlags( element ) { |
| |
| var flags = {}, |
| b, k; |
| |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| for ( k in b ) { |
| if ( b[ k ] ) { |
| flags[ k ] = flags.hasVirtualBinding = true; |
| } |
| } |
| element = element.parentNode; |
| } |
| return flags; |
| } |
| |
| function getClosestElementWithVirtualBinding( element, eventType ) { |
| var b; |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| if ( b && ( !eventType || b[ eventType ] ) ) { |
| return element; |
| } |
| element = element.parentNode; |
| } |
| return null; |
| } |
| |
| function enableTouchBindings() { |
| blockTouchTriggers = false; |
| } |
| |
| function disableTouchBindings() { |
| blockTouchTriggers = true; |
| } |
| |
| function enableMouseBindings() { |
| lastTouchID = 0; |
| clickBlockList.length = 0; |
| blockMouseTriggers = false; |
| |
| // When mouse bindings are enabled, our |
| // touch bindings are disabled. |
| disableTouchBindings(); |
| } |
| |
| function disableMouseBindings() { |
| // When mouse bindings are disabled, our |
| // touch bindings are enabled. |
| enableTouchBindings(); |
| } |
| |
| function startResetTimer() { |
| clearResetTimer(); |
| resetTimerID = setTimeout( function() { |
| resetTimerID = 0; |
| enableMouseBindings(); |
| }, $.vmouse.resetTimerDuration ); |
| } |
| |
| function clearResetTimer() { |
| if ( resetTimerID ) { |
| clearTimeout( resetTimerID ); |
| resetTimerID = 0; |
| } |
| } |
| |
| function triggerVirtualEvent( eventType, event, flags ) { |
| var ve; |
| |
| if ( ( flags && flags[ eventType ] ) || |
| ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) { |
| |
| ve = createVirtualEvent( event, eventType ); |
| |
| $( event.target).trigger( ve ); |
| } |
| |
| return ve; |
| } |
| |
| function mouseEventCallback( event ) { |
| var touchID = $.data( event.target, touchTargetPropertyName ); |
| |
| if ( ( $.support.touch === true ) && ( touchID === undefined ) ) { |
| return; |
| } |
| |
| if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) { |
| var ve = triggerVirtualEvent( "v" + event.type, event ); |
| if ( ve ) { |
| if ( ve.isDefaultPrevented() ) { |
| event.preventDefault(); |
| } |
| if ( ve.isPropagationStopped() ) { |
| event.stopPropagation(); |
| } |
| if ( ve.isImmediatePropagationStopped() ) { |
| event.stopImmediatePropagation(); |
| } |
| } |
| } |
| } |
| |
| function handleTouchStart( event ) { |
| |
| var touches = getNativeEvent( event ).touches, |
| target, flags; |
| |
| if ( touches && touches.length === 1 ) { |
| |
| target = event.target; |
| flags = getVirtualBindingFlags( target ); |
| |
| if ( flags.hasVirtualBinding ) { |
| |
| lastTouchID = nextTouchID++; |
| $.data( target, touchTargetPropertyName, lastTouchID ); |
| |
| clearResetTimer(); |
| |
| disableMouseBindings(); |
| didScroll = false; |
| |
| var t = getNativeEvent( event ).touches[ 0 ]; |
| startX = t.pageX; |
| startY = t.pageY; |
| |
| triggerVirtualEvent( "vmouseover", event, flags ); |
| triggerVirtualEvent( "vmousedown", event, flags ); |
| } |
| } |
| } |
| |
| function handleScroll( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| if ( !didScroll ) { |
| triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) ); |
| } |
| |
| didScroll = true; |
| startResetTimer(); |
| } |
| |
| function handleTouchMove( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| var t = getNativeEvent( event ).touches[ 0 ], |
| didCancel = didScroll, |
| moveThreshold = $.vmouse.moveDistanceThreshold, |
| flags = getVirtualBindingFlags( event.target ); |
| |
| didScroll = didScroll || |
| ( Math.abs( t.pageX - startX ) > moveThreshold || |
| Math.abs( t.pageY - startY ) > moveThreshold ); |
| |
| |
| if ( didScroll && !didCancel ) { |
| triggerVirtualEvent( "vmousecancel", event, flags ); |
| } |
| |
| triggerVirtualEvent( "vmousemove", event, flags ); |
| startResetTimer(); |
| } |
| |
| function handleTouchEnd( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| disableTouchBindings(); |
| |
| var flags = getVirtualBindingFlags( event.target ), |
| t; |
| triggerVirtualEvent( "vmouseup", event, flags ); |
| |
| if ( !didScroll ) { |
| var ve = triggerVirtualEvent( "vclick", event, flags ); |
| if ( ve && ve.isDefaultPrevented() ) { |
| // The target of the mouse events that follow the touchend |
| // event don't necessarily match the target used during the |
| // touch. This means we need to rely on coordinates for blocking |
| // any click that is generated. |
| t = getNativeEvent( event ).changedTouches[ 0 ]; |
| clickBlockList.push({ |
| touchID: lastTouchID, |
| target: event.target, |
| x: t.clientX, |
| y: t.clientY |
| }); |
| |
| // Prevent any mouse events that follow from triggering |
| // virtual event notifications. |
| blockMouseTriggers = true; |
| } |
| } |
| triggerVirtualEvent( "vmouseout", event, flags); |
| didScroll = false; |
| |
| startResetTimer(); |
| } |
| |
| function hasVirtualBindings( ele ) { |
| var bindings = $.data( ele, dataPropertyName ), |
| k; |
| |
| if ( bindings ) { |
| for ( k in bindings ) { |
| if ( bindings[ k ] ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| function dummyMouseHandler() {} |
| |
| function getSpecialEventObject( eventType ) { |
| var realType = eventType.substr( 1 ); |
| |
| return { |
| setup: function( data, namespace ) { |
| // If this is the first virtual mouse binding for this element, |
| // add a bindings object to its data. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $.data( this, dataPropertyName, {} ); |
| } |
| |
| // If setup is called, we know it is the first binding for this |
| // eventType, so initialize the count for the eventType to zero. |
| var bindings = $.data( this, dataPropertyName ); |
| bindings[ eventType ] = true; |
| |
| // If this is the first virtual mouse event for this type, |
| // register a global handler on the document. |
| |
| activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1; |
| |
| if ( activeDocHandlers[ eventType ] === 1 ) { |
| $document.bind( realType, mouseEventCallback ); |
| } |
| |
| // Some browsers, like Opera Mini, won't dispatch mouse/click events |
| // for elements unless they actually have handlers registered on them. |
| // To get around this, we register dummy handlers on the elements. |
| |
| $( this ).bind( realType, dummyMouseHandler ); |
| |
| // For now, if event capture is not supported, we rely on mouse handlers. |
| if ( eventCaptureSupported ) { |
| // If this is the first virtual mouse binding for the document, |
| // register our touchstart handler on the document. |
| |
| activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1; |
| |
| if ( activeDocHandlers[ "touchstart" ] === 1 ) { |
| $document.bind( "touchstart", handleTouchStart ) |
| .bind( "touchend", handleTouchEnd ) |
| |
| // On touch platforms, touching the screen and then dragging your finger |
| // causes the window content to scroll after some distance threshold is |
| // exceeded. On these platforms, a scroll prevents a click event from being |
| // dispatched, and on some platforms, even the touchend is suppressed. To |
| // mimic the suppression of the click event, we need to watch for a scroll |
| // event. Unfortunately, some platforms like iOS don't dispatch scroll |
| // events until *AFTER* the user lifts their finger (touchend). This means |
| // we need to watch both scroll and touchmove events to figure out whether |
| // or not a scroll happenens before the touchend event is fired. |
| |
| .bind( "touchmove", handleTouchMove ) |
| .bind( "scroll", handleScroll ); |
| } |
| } |
| }, |
| |
| teardown: function( data, namespace ) { |
| // If this is the last virtual binding for this eventType, |
| // remove its global handler from the document. |
| |
| --activeDocHandlers[ eventType ]; |
| |
| if ( !activeDocHandlers[ eventType ] ) { |
| $document.unbind( realType, mouseEventCallback ); |
| } |
| |
| if ( eventCaptureSupported ) { |
| // If this is the last virtual mouse binding in existence, |
| // remove our document touchstart listener. |
| |
| --activeDocHandlers[ "touchstart" ]; |
| |
| if ( !activeDocHandlers[ "touchstart" ] ) { |
| $document.unbind( "touchstart", handleTouchStart ) |
| .unbind( "touchmove", handleTouchMove ) |
| .unbind( "touchend", handleTouchEnd ) |
| .unbind( "scroll", handleScroll ); |
| } |
| } |
| |
| var $this = $( this ), |
| bindings = $.data( this, dataPropertyName ); |
| |
| // teardown may be called when an element was |
| // removed from the DOM. If this is the case, |
| // jQuery core may have already stripped the element |
| // of any data bindings so we need to check it before |
| // using it. |
| if ( bindings ) { |
| bindings[ eventType ] = false; |
| } |
| |
| // Unregister the dummy event handler. |
| |
| $this.unbind( realType, dummyMouseHandler ); |
| |
| // If this is the last virtual mouse binding on the |
| // element, remove the binding data from the element. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $this.removeData( dataPropertyName ); |
| } |
| } |
| }; |
| } |
| |
| // Expose our custom events to the jQuery bind/unbind mechanism. |
| |
| for ( var i = 0; i < virtualEventNames.length; i++ ) { |
| $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] ); |
| } |
| |
| // Add a capture click handler to block clicks. |
| // Note that we require event capture support for this so if the device |
| // doesn't support it, we punt for now and rely solely on mouse events. |
| if ( eventCaptureSupported ) { |
| document.addEventListener( "click", function( e ) { |
| var cnt = clickBlockList.length, |
| target = e.target, |
| x, y, ele, i, o, touchID; |
| |
| if ( cnt ) { |
| x = e.clientX; |
| y = e.clientY; |
| threshold = $.vmouse.clickDistanceThreshold; |
| |
| // The idea here is to run through the clickBlockList to see if |
| // the current click event is in the proximity of one of our |
| // vclick events that had preventDefault() called on it. If we find |
| // one, then we block the click. |
| // |
| // Why do we have to rely on proximity? |
| // |
| // Because the target of the touch event that triggered the vclick |
| // can be different from the target of the click event synthesized |
| // by the browser. The target of a mouse/click event that is syntehsized |
| // from a touch event seems to be implementation specific. For example, |
| // some browsers will fire mouse/click events for a link that is near |
| // a touch event, even though the target of the touchstart/touchend event |
| // says the user touched outside the link. Also, it seems that with most |
| // browsers, the target of the mouse/click event is not calculated until the |
| // time it is dispatched, so if you replace an element that you touched |
| // with another element, the target of the mouse/click will be the new |
| // element underneath that point. |
| // |
| // Aside from proximity, we also check to see if the target and any |
| // of its ancestors were the ones that blocked a click. This is necessary |
| // because of the strange mouse/click target calculation done in the |
| // Android 2.1 browser, where if you click on an element, and there is a |
| // mouse/click handler on one of its ancestors, the target will be the |
| // innermost child of the touched element, even if that child is no where |
| // near the point of touch. |
| |
| ele = target; |
| |
| while ( ele ) { |
| for ( i = 0; i < cnt; i++ ) { |
| o = clickBlockList[ i ]; |
| touchID = 0; |
| |
| if ( ( ele === target && target === o.target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) || |
| $.data( ele, touchTargetPropertyName ) === o.touchID ) { |
| // XXX: We may want to consider removing matches from the block list |
| // instead of waiting for the reset timer to fire. |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
| } |
| ele = ele.parentNode; |
| } |
| } |
| }, true); |
| } |
| })( jQuery, window, document ); |
| |
| |
| // throttled resize event |
| (function( $ ) { |
| $.event.special.throttledresize = { |
| setup: function() { |
| $( this ).bind( "resize", handler ); |
| }, |
| teardown: function() { |
| $( this ).unbind( "resize", handler ); |
| } |
| }; |
| |
| var throttle = 250, |
| handler = function() { |
| curr = ( new Date() ).getTime(); |
| diff = curr - lastCall; |
| |
| if ( diff >= throttle ) { |
| |
| lastCall = curr; |
| $( this ).trigger( "throttledresize" ); |
| |
| } else { |
| |
| if ( heldCall ) { |
| clearTimeout( heldCall ); |
| } |
| |
| // Promise a held call will still execute |
| heldCall = setTimeout( handler, throttle - diff ); |
| } |
| }, |
| lastCall = 0, |
| heldCall, |
| curr, |
| diff; |
| })( jQuery ); |
| (function( $, undefined ) { |
| |
| var $window = $.mobile.$window, |
| $html = $( "html" ); |
| |
| /* $.mobile.media method: pass a CSS media type or query and get a bool return |
| note: this feature relies on actual media query support for media queries, though types will work most anywhere |
| examples: |
| $.mobile.media('screen') // tests for screen media type |
| $.mobile.media('screen and (min-width: 480px)') // tests for screen media type with window width > 480px |
| $.mobile.media('@media screen and (-webkit-min-device-pixel-ratio: 2)') // tests for webkit 2x pixel ratio (iPhone 4) |
| */ |
| $.mobile.media = (function() { |
| // TODO: use window.matchMedia once at least one UA implements it |
| var cache = {}, |
| testDiv = $( "<div id='jquery-mediatest'></div>" ), |
| fakeBody = $( "<body>" ).append( testDiv ); |
| |
| return function( query ) { |
| if ( !( query in cache ) ) { |
| var styleBlock = document.createElement( "style" ), |
| cssrule = "@media " + query + " { #jquery-mediatest { position:absolute; } }"; |
| |
| //must set type for IE! |
| styleBlock.type = "text/css"; |
| |
| if ( styleBlock.styleSheet ) { |
| styleBlock.styleSheet.cssText = cssrule; |
| } else { |
| styleBlock.appendChild( document.createTextNode(cssrule) ); |
| } |
| |
| $html.prepend( fakeBody ).prepend( styleBlock ); |
| cache[ query ] = testDiv.css( "position" ) === "absolute"; |
| fakeBody.add( styleBlock ).remove(); |
| } |
| return cache[ query ]; |
| }; |
| })(); |
| |
| })(jQuery); |
| |
| (function( $, undefined ) { |
| |
| // thx Modernizr |
| function propExists( prop ) { |
| var uc_prop = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), |
| props = ( prop + " " + vendors.join( uc_prop + " " ) + uc_prop ).split( " " ); |
| |
| for ( var v in props ) { |
| if ( fbCSS[ props[ v ] ] !== undefined ) { |
| return true; |
| } |
| } |
| } |
| |
| var fakeBody = $( "<body>" ).prependTo( "html" ), |
| fbCSS = fakeBody[ 0 ].style, |
| vendors = [ "Webkit", "Moz", "O" ], |
| webos = "palmGetResource" in window, //only used to rule out scrollTop |
| opera = window.opera, |
| operamini = window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]", |
| bb = window.blackberry && !propExists( "-webkit-transform" ); //only used to rule out box shadow, as it's filled opaque on BB 5 and lower |
| |
| |
| function validStyle( prop, value, check_vend ) { |
| var div = document.createElement( 'div' ), |
| uc = function( txt ) { |
| return txt.charAt( 0 ).toUpperCase() + txt.substr( 1 ); |
| }, |
| vend_pref = function( vend ) { |
| return "-" + vend.charAt( 0 ).toLowerCase() + vend.substr( 1 ) + "-"; |
| }, |
| check_style = function( vend ) { |
| var vend_prop = vend_pref( vend ) + prop + ": " + value + ";", |
| uc_vend = uc( vend ), |
| propStyle = uc_vend + uc( prop ); |
| |
| div.setAttribute( "style", vend_prop ); |
| |
| if ( !!div.style[ propStyle ] ) { |
| ret = true; |
| } |
| }, |
| check_vends = check_vend ? [ check_vend ] : vendors, |
| ret; |
| |
| for( var i = 0; i < check_vends.length; i++ ) { |
| check_style( check_vends[i] ); |
| } |
| return !!ret; |
| } |
| |
| // Thanks to Modernizr src for this test idea. `perspective` check is limited to Moz to prevent a false positive for 3D transforms on Android. |
| function transform3dTest() { |
| var prop = "transform-3d"; |
| return validStyle( 'perspective', '10px', 'moz' ) || $.mobile.media( "(-" + vendors.join( "-" + prop + "),(-" ) + "-" + prop + "),(" + prop + ")" ); |
| } |
| |
| // Test for dynamic-updating base tag support ( allows us to avoid href,src attr rewriting ) |
| function baseTagTest() { |
| var fauxBase = location.protocol + "//" + location.host + location.pathname + "ui-dir/", |
| base = $( "head base" ), |
| fauxEle = null, |
| href = "", |
| link, rebase; |
| |
| if ( !base.length ) { |
| base = fauxEle = $( "<base>", { "href": fauxBase }).appendTo( "head" ); |
| } else { |
| href = base.attr( "href" ); |
| } |
| |
| link = $( "<a href='testurl' />" ).prependTo( fakeBody ); |
| rebase = link[ 0 ].href; |
| base[ 0 ].href = href || location.pathname; |
| |
| if ( fauxEle ) { |
| fauxEle.remove(); |
| } |
| return rebase.indexOf( fauxBase ) === 0; |
| } |
| |
| // Thanks Modernizr |
| function cssPointerEventsTest() { |
| var element = document.createElement( 'x' ), |
| documentElement = document.documentElement, |
| getComputedStyle = window.getComputedStyle, |
| supports; |
| |
| if ( !( 'pointerEvents' in element.style ) ) { |
| return false; |
| } |
| |
| element.style.pointerEvents = 'auto'; |
| element.style.pointerEvents = 'x'; |
| documentElement.appendChild( element ); |
| supports = getComputedStyle && |
| getComputedStyle( element, '' ).pointerEvents === 'auto'; |
| documentElement.removeChild( element ); |
| return !!supports; |
| } |
| |
| function boundingRect() { |
| var div = document.createElement( "div" ); |
| return typeof div.getBoundingClientRect !== "undefined"; |
| } |
| |
| // non-UA-based IE version check by James Padolsey, modified by jdalton - from http://gist.github.com/527683 |
| // allows for inclusion of IE 6+, including Windows Mobile 7 |
| $.extend( $.mobile, { browser: {} } ); |
| $.mobile.browser.ie = (function() { |
| var v = 3, |
| div = document.createElement( "div" ), |
| a = div.all || []; |
| |
| do { |
| div.innerHTML = "<!--[if gt IE " + ( ++v ) + "]><br><![endif]-->"; |
| } while( a[0] ); |
| |
| return v > 4 ? v : !v; |
| })(); |
| |
| |
| $.extend( $.support, { |
| cssTransitions: "WebKitTransitionEvent" in window || validStyle( 'transition', 'height 100ms linear' ) && !opera, |
| pushState: "pushState" in history && "replaceState" in history, |
| mediaquery: $.mobile.media( "only all" ), |
| cssPseudoElement: !!propExists( "content" ), |
| touchOverflow: !!propExists( "overflowScrolling" ), |
| cssTransform3d: transform3dTest(), |
| boxShadow: !!propExists( "boxShadow" ) && !bb, |
| scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos && !operamini, |
| dynamicBaseTag: baseTagTest(), |
| cssPointerEvents: cssPointerEventsTest(), |
| boundingRect: boundingRect() |
| }); |
| |
| fakeBody.remove(); |
| |
| |
| // $.mobile.ajaxBlacklist is used to override ajaxEnabled on platforms that have known conflicts with hash history updates (BB5, Symbian) |
| // or that generally work better browsing in regular http for full page refreshes (Opera Mini) |
| // Note: This detection below is used as a last resort. |
| // We recommend only using these detection methods when all other more reliable/forward-looking approaches are not possible |
| var nokiaLTE7_3 = (function() { |
| |
| var ua = window.navigator.userAgent; |
| |
| //The following is an attempt to match Nokia browsers that are running Symbian/s60, with webkit, version 7.3 or older |
| return ua.indexOf( "Nokia" ) > -1 && |
| ( ua.indexOf( "Symbian/3" ) > -1 || ua.indexOf( "Series60/5" ) > -1 ) && |
| ua.indexOf( "AppleWebKit" ) > -1 && |
| ua.match( /(BrowserNG|NokiaBrowser)\/7\.[0-3]/ ); |
| })(); |
| |
| // Support conditions that must be met in order to proceed |
| // default enhanced qualifications are media query support OR IE 7+ |
| |
| $.mobile.gradeA = function() { |
| return ( $.support.mediaquery || $.mobile.browser.ie && $.mobile.browser.ie >= 7 ) && ( $.support.boundingRect || $.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/) !== null ); |
| }; |
| |
| $.mobile.ajaxBlacklist = |
| // BlackBerry browsers, pre-webkit |
| window.blackberry && !window.WebKitPoint || |
| // Opera Mini |
| operamini || |
| // Symbian webkits pre 7.3 |
| nokiaLTE7_3; |
| |
| // Lastly, this workaround is the only way we've found so far to get pre 7.3 Symbian webkit devices |
| // to render the stylesheets when they're referenced before this script, as we'd recommend doing. |
| // This simply reappends the CSS in place, which for some reason makes it apply |
| if ( nokiaLTE7_3 ) { |
| $(function() { |
| $( "head link[rel='stylesheet']" ).attr( "rel", "alternate stylesheet" ).attr( "rel", "stylesheet" ); |
| }); |
| } |
| |
| // For ruling out shadows via css |
| if ( !$.support.boxShadow ) { |
| $( "html" ).addClass( "ui-mobile-nosupport-boxshadow" ); |
| } |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| var support = { |
| touch: "ontouchend" in document |
| }; |
| |
| $.mobile = $.mobile || {}; |
| $.mobile.support = $.mobile.support || {}; |
| $.extend( $.support, support ); |
| $.extend( $.mobile.support, support ); |
| }( jQuery )); |
| |
| |
| (function( $, window, undefined ) { |
| // add new event shortcuts |
| $.each( ( "touchstart touchmove touchend " + |
| "tap taphold " + |
| "swipe swipeleft swiperight " + |
| "scrollstart scrollstop" ).split( " " ), function( i, name ) { |
| |
| $.fn[ name ] = function( fn ) { |
| return fn ? this.bind( name, fn ) : this.trigger( name ); |
| }; |
| |
| // jQuery < 1.8 |
| if ( $.attrFn ) { |
| $.attrFn[ name ] = true; |
| } |
| }); |
| |
| var supportTouch = $.mobile.support.touch, |
| scrollEvent = "touchmove scroll", |
| touchStartEvent = supportTouch ? "touchstart" : "mousedown", |
| touchStopEvent = supportTouch ? "touchend" : "mouseup", |
| touchMoveEvent = supportTouch ? "touchmove" : "mousemove"; |
| |
| function triggerCustomEvent( obj, eventType, event ) { |
| var originalType = event.type; |
| event.type = eventType; |
| // event.liveFired is already set by basic events, e.g. vclick, which is fired already. |
| // To fire this custom event, event.liveFired must be cleared. |
| event.liveFired = undefined; |
| |
| $.event.handle.call( obj, event ); |
| event.type = originalType; |
| } |
| |
| // also handles scrollstop |
| $.event.special.scrollstart = { |
| |
| enabled: true, |
| |
| setup: function() { |
| |
| var thisObject = this, |
| $this = $( thisObject ), |
| scrolling, |
| timer; |
| |
| function trigger( event, state ) { |
| scrolling = state; |
| triggerCustomEvent( thisObject, scrolling ? "scrollstart" : "scrollstop", event ); |
| } |
| |
| // iPhone triggers scroll after a small delay; use touchmove instead |
| $this.bind( scrollEvent, function( event ) { |
| |
| if ( !$.event.special.scrollstart.enabled ) { |
| return; |
| } |
| |
| if ( !scrolling ) { |
| trigger( event, true ); |
| } |
| |
| clearTimeout( timer ); |
| timer = setTimeout( function() { |
| trigger( event, false ); |
| }, 50 ); |
| }); |
| } |
| }; |
| |
| // also handles taphold |
| $.event.special.tap = { |
| tapholdThreshold: 750, |
| |
| setup: function() { |
| var thisObject = this, |
| $this = $( thisObject ); |
| |
| $this.bind( "vmousedown", function( event ) { |
| |
| if ( event.which && event.which !== 1 ) { |
| return false; |
| } |
| |
| var origTarget = event.target, |
| origEvent = event.originalEvent, |
| timer; |
| |
| function clearTapTimer() { |
| clearTimeout( timer ); |
| } |
| |
| function clearTapHandlers() { |
| clearTapTimer(); |
| |
| $this.unbind( "vclick", clickHandler ) |
| .unbind( "vmouseup", clearTapTimer ); |
| $.mobile.$document.unbind( "vmousecancel", clearTapHandlers ); |
| } |
| |
| function clickHandler( event ) { |
| clearTapHandlers(); |
| |
| // ONLY trigger a 'tap' event if the start target is |
| // the same as the stop target. |
| if ( origTarget === event.target ) { |
| triggerCustomEvent( thisObject, "tap", event ); |
| } |
| } |
| |
| $this.bind( "vmouseup", clearTapTimer ) |
| .bind( "vclick", clickHandler ); |
| $.mobile.$document.bind( "vmousecancel", clearTapHandlers ); |
| |
| timer = setTimeout( function() { |
| triggerCustomEvent( thisObject, "taphold", $.Event( "taphold", { target: origTarget } ) ); |
| }, $.event.special.tap.tapholdThreshold ); |
| }); |
| } |
| }; |
| |
| // also handles swipeleft, swiperight |
| $.event.special.swipe = { |
| scrollSupressionThreshold: 30, // More than this horizontal displacement, and we will suppress scrolling. |
| |
| durationThreshold: 1000, // More time than this, and it isn't a swipe. |
| |
| horizontalDistanceThreshold: 30, // Swipe horizontal displacement must be more than this. |
| |
| verticalDistanceThreshold: 75, // Swipe vertical displacement must be less than this. |
| |
| setup: function() { |
| var thisObject = this, |
| $this = $( thisObject ); |
| |
| $this.bind( touchStartEvent, function( event ) { |
| var data = event.originalEvent.touches ? |
| event.originalEvent.touches[ 0 ] : event, |
| start = { |
| time: ( new Date() ).getTime(), |
| coords: [ data.pageX, data.pageY ], |
| origin: $( event.target ) |
| }, |
| stop; |
| |
| function moveHandler( event ) { |
| |
| if ( !start ) { |
| return; |
| } |
| |
| var data = event.originalEvent.touches ? |
| event.originalEvent.touches[ 0 ] : event; |
| |
| stop = { |
| time: ( new Date() ).getTime(), |
| coords: [ data.pageX, data.pageY ] |
| }; |
| |
| // prevent scrolling |
| if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) { |
| event.preventDefault(); |
| } |
| } |
| |
| $this.bind( touchMoveEvent, moveHandler ) |
| .one( touchStopEvent, function( event ) { |
| $this.unbind( touchMoveEvent, moveHandler ); |
| |
| if ( start && stop ) { |
| if ( stop.time - start.time < $.event.special.swipe.durationThreshold && |
| Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontalDistanceThreshold && |
| Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) { |
| |
| start.origin.trigger( "swipe" ) |
| .trigger( start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight" ); |
| } |
| } |
| start = stop = undefined; |
| }); |
| }); |
| } |
| }; |
| $.each({ |
| scrollstop: "scrollstart", |
| taphold: "tap", |
| swipeleft: "swipe", |
| swiperight: "swipe" |
| }, function( event, sourceEvent ) { |
| |
| $.event.special[ event ] = { |
| setup: function() { |
| $( this ).bind( sourceEvent, $.noop ); |
| } |
| }; |
| }); |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| $.extend( $.support, { |
| orientation: "orientation" in window && "onorientationchange" in window |
| }); |
| }( jQuery )); |
| |
| |
| (function( $, window ) { |
| var win = $.mobile.$window, |
| event_name = "orientationchange", |
| special_event, |
| get_orientation, |
| last_orientation, |
| initial_orientation_is_landscape, |
| initial_orientation_is_default, |
| portrait_map = { "0": true, "180": true }; |
| |
| // It seems that some device/browser vendors use window.orientation values 0 and 180 to |
| // denote the "default" orientation. For iOS devices, and most other smart-phones tested, |
| // the default orientation is always "portrait", but in some Android and RIM based tablets, |
| // the default orientation is "landscape". The following code attempts to use the window |
| // dimensions to figure out what the current orientation is, and then makes adjustments |
| // to the to the portrait_map if necessary, so that we can properly decode the |
| // window.orientation value whenever get_orientation() is called. |
| // |
| // Note that we used to use a media query to figure out what the orientation the browser |
| // thinks it is in: |
| // |
| // initial_orientation_is_landscape = $.mobile.media("all and (orientation: landscape)"); |
| // |
| // but there was an iPhone/iPod Touch bug beginning with iOS 4.2, up through iOS 5.1, |
| // where the browser *ALWAYS* applied the landscape media query. This bug does not |
| // happen on iPad. |
| |
| if ( $.support.orientation ) { |
| |
| // Check the window width and height to figure out what the current orientation |
| // of the device is at this moment. Note that we've initialized the portrait map |
| // values to 0 and 180, *AND* we purposely check for landscape so that if we guess |
| // wrong, , we default to the assumption that portrait is the default orientation. |
| // We use a threshold check below because on some platforms like iOS, the iPhone |
| // form-factor can report a larger width than height if the user turns on the |
| // developer console. The actual threshold value is somewhat arbitrary, we just |
| // need to make sure it is large enough to exclude the developer console case. |
| |
| var ww = window.innerWidth || $.mobile.$window.width(), |
| wh = window.innerHeight || $.mobile.$window.height(), |
| landscape_threshold = 50; |
| |
| initial_orientation_is_landscape = ww > wh && ( ww - wh ) > landscape_threshold; |
| |
| |
| // Now check to see if the current window.orientation is 0 or 180. |
| initial_orientation_is_default = portrait_map[ window.orientation ]; |
| |
| // If the initial orientation is landscape, but window.orientation reports 0 or 180, *OR* |
| // if the initial orientation is portrait, but window.orientation reports 90 or -90, we |
| // need to flip our portrait_map values because landscape is the default orientation for |
| // this device/browser. |
| if ( ( initial_orientation_is_landscape && initial_orientation_is_default ) || ( !initial_orientation_is_landscape && !initial_orientation_is_default ) ) { |
| portrait_map = { "-90": true, "90": true }; |
| } |
| } |
| |
| $.event.special.orientationchange = $.extend( {}, $.event.special.orientationchange, { |
| setup: function() { |
| // If the event is supported natively, return false so that jQuery |
| // will bind to the event using DOM methods. |
| if ( $.support.orientation && |
| $.event.special.orientationchange.disabled === false ) { |
| return false; |
| } |
| |
| // Get the current orientation to avoid initial double-triggering. |
| last_orientation = get_orientation(); |
| |
| // Because the orientationchange event doesn't exist, simulate the |
| // event by testing window dimensions on resize. |
| win.bind( "throttledresize", handler ); |
| }, |
| teardown: function() { |
| // If the event is not supported natively, return false so that |
| // jQuery will unbind the event using DOM methods. |
| if ( $.support.orientation && !$.event.special.orientationchange.disabled ) { |
| return false; |
| } |
| |
| // Because the orientationchange event doesn't exist, unbind the |
| // resize event handler. |
| win.unbind( "throttledresize", handler ); |
| }, |
| add: function( handleObj ) { |
| // Save a reference to the bound event handler. |
| var old_handler = handleObj.handler; |
| |
| |
| handleObj.handler = function( event ) { |
| // Modify event object, adding the .orientation property. |
| event.orientation = get_orientation(); |
| |
| // Call the originally-bound event handler and return its result. |
| return old_handler.apply( this, arguments ); |
| }; |
| } |
| }); |
| |
| // If the event is not supported natively, this handler will be bound to |
| // the window resize event to simulate the orientationchange event. |
| function handler() { |
| // Get the current orientation. |
| var orientation = get_orientation(); |
| |
| if ( orientation !== last_orientation ) { |
| // The orientation has changed, so trigger the orientationchange event. |
| last_orientation = orientation; |
| win.trigger( event_name ); |
| } |
| } |
| |
| // Get the current page orientation. This method is exposed publicly, should it |
| // be needed, as jQuery.event.special.orientationchange.orientation() |
| $.event.special.orientationchange.orientation = get_orientation = function() { |
| var isPortrait = true, elem = document.documentElement; |
| |
| // prefer window orientation to the calculation based on screensize as |
| // the actual screen resize takes place before or after the orientation change event |
| // has been fired depending on implementation (eg android 2.3 is before, iphone after). |
| // More testing is required to determine if a more reliable method of determining the new screensize |
| // is possible when orientationchange is fired. (eg, use media queries + element + opacity) |
| if ( $.support.orientation ) { |
| // if the window orientation registers as 0 or 180 degrees report |
| // portrait, otherwise landscape |
| isPortrait = portrait_map[ window.orientation ]; |
| } else { |
| isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; |
| } |
| |
| return isPortrait ? "portrait" : "landscape"; |
| }; |
| |
| $.fn[ event_name ] = function( fn ) { |
| return fn ? this.bind( event_name, fn ) : this.trigger( event_name ); |
| }; |
| |
| // jQuery < 1.8 |
| if ( $.attrFn ) { |
| $.attrFn[ event_name ] = true; |
| } |
| |
| }( jQuery, this )); |
| |
| |
| |
| // Script: jQuery hashchange event |
| // |
| // *Version: 1.3, Last updated: 7/21/2010* |
| // |
| // Project Home - http://benalman.com/projects/jquery-hashchange-plugin/ |
| // GitHub - http://github.com/cowboy/jquery-hashchange/ |
| // Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js |
| // (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped) |
| // |
| // About: License |
| // |
| // Copyright (c) 2010 "Cowboy" Ben Alman, |
| // Dual licensed under the MIT and GPL licenses. |
| // http://benalman.com/about/license/ |
| // |
| // About: Examples |
| // |
| // These working examples, complete with fully commented code, illustrate a few |
| // ways in which this plugin can be used. |
| // |
| // hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/ |
| // document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/ |
| // |
| // About: Support and Testing |
| // |
| // Information about what version or versions of jQuery this plugin has been |
| // tested with, what browsers it has been tested in, and where the unit tests |
| // reside (so you can test it yourself). |
| // |
| // jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2 |
| // Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5, |
| // Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5. |
| // Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/ |
| // |
| // About: Known issues |
| // |
| // While this jQuery hashchange event implementation is quite stable and |
| // robust, there are a few unfortunate browser bugs surrounding expected |
| // hashchange event-based behaviors, independent of any JavaScript |
| // window.onhashchange abstraction. See the following examples for more |
| // information: |
| // |
| // Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/ |
| // Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/ |
| // WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/ |
| // Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/ |
| // |
| // Also note that should a browser natively support the window.onhashchange |
| // event, but not report that it does, the fallback polling loop will be used. |
| // |
| // About: Release History |
| // |
| // 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more |
| // "removable" for mobile-only development. Added IE6/7 document.title |
| // support. Attempted to make Iframe as hidden as possible by using |
| // techniques from http://www.paciellogroup.com/blog/?p=604. Added |
| // support for the "shortcut" format $(window).hashchange( fn ) and |
| // $(window).hashchange() like jQuery provides for built-in events. |
| // Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and |
| // lowered its default value to 50. Added <jQuery.fn.hashchange.domain> |
| // and <jQuery.fn.hashchange.src> properties plus document-domain.html |
| // file to address access denied issues when setting document.domain in |
| // IE6/7. |
| // 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin |
| // from a page on another domain would cause an error in Safari 4. Also, |
| // IE6/7 Iframe is now inserted after the body (this actually works), |
| // which prevents the page from scrolling when the event is first bound. |
| // Event can also now be bound before DOM ready, but it won't be usable |
| // before then in IE6/7. |
| // 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug |
| // where browser version is incorrectly reported as 8.0, despite |
| // inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag. |
| // 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special |
| // window.onhashchange functionality into a separate plugin for users |
| // who want just the basic event & back button support, without all the |
| // extra awesomeness that BBQ provides. This plugin will be included as |
| // part of jQuery BBQ, but also be available separately. |
| |
| |
| |
| (function( $, window, undefined ) { |
| // Reused string. |
| var str_hashchange = 'hashchange', |
| |
| // Method / object references. |
| doc = document, |
| fake_onhashchange, |
| special = $.event.special, |
| |
| // Does the browser support window.onhashchange? Note that IE8 running in |
| // IE7 compatibility mode reports true for 'onhashchange' in window, even |
| // though the event isn't supported, so also test document.documentMode. |
| doc_mode = doc.documentMode, |
| supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 ); |
| |
| // Get location.hash (or what you'd expect location.hash to be) sans any |
| // leading #. Thanks for making this necessary, Firefox! |
| function get_fragment( url ) { |
| url = url || location.href; |
| return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' ); |
| }; |
| |
| // Method: jQuery.fn.hashchange |
| // |
| // Bind a handler to the window.onhashchange event or trigger all bound |
| // window.onhashchange event handlers. This behavior is consistent with |
| // jQuery's built-in event handlers. |
| // |
| // Usage: |
| // |
| // > jQuery(window).hashchange( [ handler ] ); |
| // |
| // Arguments: |
| // |
| // handler - (Function) Optional handler to be bound to the hashchange |
| // event. This is a "shortcut" for the more verbose form: |
| // jQuery(window).bind( 'hashchange', handler ). If handler is omitted, |
| // all bound window.onhashchange event handlers will be triggered. This |
| // is a shortcut for the more verbose |
| // jQuery(window).trigger( 'hashchange' ). These forms are described in |
| // the <hashchange event> section. |
| // |
| // Returns: |
| // |
| // (jQuery) The initial jQuery collection of elements. |
| |
| // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and |
| // $(elem).hashchange() for triggering, like jQuery does for built-in events. |
| $.fn[ str_hashchange ] = function( fn ) { |
| return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange ); |
| }; |
| |
| // Property: jQuery.fn.hashchange.delay |
| // |
| // The numeric interval (in milliseconds) at which the <hashchange event> |
| // polling loop executes. Defaults to 50. |
| |
| // Property: jQuery.fn.hashchange.domain |
| // |
| // If you're setting document.domain in your JavaScript, and you want hash |
| // history to work in IE6/7, not only must this property be set, but you must |
| // also set document.domain BEFORE jQuery is loaded into the page. This |
| // property is only applicable if you are supporting IE6/7 (or IE8 operating |
| // in "IE7 compatibility" mode). |
| // |
| // In addition, the <jQuery.fn.hashchange.src> property must be set to the |
| // path of the included "document-domain.html" file, which can be renamed or |
| // modified if necessary (note that the document.domain specified must be the |
| // same in both your main JavaScript as well as in this file). |
| // |
| // Usage: |
| // |
| // jQuery.fn.hashchange.domain = document.domain; |
| |
| // Property: jQuery.fn.hashchange.src |
| // |
| // If, for some reason, you need to specify an Iframe src file (for example, |
| // when setting document.domain as in <jQuery.fn.hashchange.domain>), you can |
| // do so using this property. Note that when using this property, history |
| // won't be recorded in IE6/7 until the Iframe src file loads. This property |
| // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7 |
| // compatibility" mode). |
| // |
| // Usage: |
| // |
| // jQuery.fn.hashchange.src = 'path/to/file.html'; |
| |
| $.fn[ str_hashchange ].delay = 50; |
| /* |
| $.fn[ str_hashchange ].domain = null; |
| $.fn[ str_hashchange ].src = null; |
| */ |
| |
| // Event: hashchange event |
| // |
| // Fired when location.hash changes. In browsers that support it, the native |
| // HTML5 window.onhashchange event is used, otherwise a polling loop is |
| // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to |
| // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7 |
| // compatibility" mode), a hidden Iframe is created to allow the back button |
| // and hash-based history to work. |
| // |
| // Usage as described in <jQuery.fn.hashchange>: |
| // |
| // > // Bind an event handler. |
| // > jQuery(window).hashchange( function(e) { |
| // > var hash = location.hash; |
| // > ... |
| // > }); |
| // > |
| // > // Manually trigger the event handler. |
| // > jQuery(window).hashchange(); |
| // |
| // A more verbose usage that allows for event namespacing: |
| // |
| // > // Bind an event handler. |
| // > jQuery(window).bind( 'hashchange', function(e) { |
| // > var hash = location.hash; |
| // > ... |
| // > }); |
| // > |
| // > // Manually trigger the event handler. |
| // > jQuery(window).trigger( 'hashchange' ); |
| // |
| // Additional Notes: |
| // |
| // * The polling loop and Iframe are not created until at least one handler |
| // is actually bound to the 'hashchange' event. |
| // * If you need the bound handler(s) to execute immediately, in cases where |
| // a location.hash exists on page load, via bookmark or page refresh for |
| // example, use jQuery(window).hashchange() or the more verbose |
| // jQuery(window).trigger( 'hashchange' ). |
| // * The event can be bound before DOM ready, but since it won't be usable |
| // before then in IE6/7 (due to the necessary Iframe), recommended usage is |
| // to bind it inside a DOM ready handler. |
| |
| // Override existing $.event.special.hashchange methods (allowing this plugin |
| // to be defined after jQuery BBQ in BBQ's source code). |
| special[ str_hashchange ] = $.extend( special[ str_hashchange ], { |
| |
| // Called only when the first 'hashchange' event is bound to window. |
| setup: function() { |
| // If window.onhashchange is supported natively, there's nothing to do.. |
| if ( supports_onhashchange ) { return false; } |
| |
| // Otherwise, we need to create our own. And we don't want to call this |
| // until the user binds to the event, just in case they never do, since it |
| // will create a polling loop and possibly even a hidden Iframe. |
| $( fake_onhashchange.start ); |
| }, |
| |
| // Called only when the last 'hashchange' event is unbound from window. |
| teardown: function() { |
| // If window.onhashchange is supported natively, there's nothing to do.. |
| if ( supports_onhashchange ) { return false; } |
| |
| // Otherwise, we need to stop ours (if possible). |
| $( fake_onhashchange.stop ); |
| } |
| |
| }); |
| |
| // fake_onhashchange does all the work of triggering the window.onhashchange |
| // event for browsers that don't natively support it, including creating a |
| // polling loop to watch for hash changes and in IE 6/7 creating a hidden |
| // Iframe to enable back and forward. |
| fake_onhashchange = (function() { |
| var self = {}, |
| timeout_id, |
| |
| // Remember the initial hash so it doesn't get triggered immediately. |
| last_hash = get_fragment(), |
| |
| fn_retval = function( val ) { return val; }, |
| history_set = fn_retval, |
| history_get = fn_retval; |
| |
| // Start the polling loop. |
| self.start = function() { |
| timeout_id || poll(); |
| }; |
| |
| // Stop the polling loop. |
| self.stop = function() { |
| timeout_id && clearTimeout( timeout_id ); |
| timeout_id = undefined; |
| }; |
| |
| // This polling loop checks every $.fn.hashchange.delay milliseconds to see |
| // if location.hash has changed, and triggers the 'hashchange' event on |
| // window when necessary. |
| function poll() { |
| var hash = get_fragment(), |
| history_hash = history_get( last_hash ); |
| |
| if ( hash !== last_hash ) { |
| history_set( last_hash = hash, history_hash ); |
| |
| $(window).trigger( str_hashchange ); |
| |
| } else if ( history_hash !== last_hash ) { |
| location.href = location.href.replace( /#.*/, '' ) + history_hash; |
| } |
| |
| timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay ); |
| }; |
| |
| // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv |
| // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv |
| // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv |
| $.browser.msie && !supports_onhashchange && (function() { |
| // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 |
| // when running in "IE7 compatibility" mode. |
| |
| var iframe, |
| iframe_src; |
| |
| // When the event is bound and polling starts in IE 6/7, create a hidden |
| // Iframe for history handling. |
| self.start = function() { |
| if ( !iframe ) { |
| iframe_src = $.fn[ str_hashchange ].src; |
| iframe_src = iframe_src && iframe_src + get_fragment(); |
| |
| // Create hidden Iframe. Attempt to make Iframe as hidden as possible |
| // by using techniques from http://www.paciellogroup.com/blog/?p=604. |
| iframe = $('<iframe tabindex="-1" title="empty"/>').hide() |
| |
| // When Iframe has completely loaded, initialize the history and |
| // start polling. |
| .one( 'load', function() { |
| iframe_src || history_set( get_fragment() ); |
| poll(); |
| }) |
| |
| // Load Iframe src if specified, otherwise nothing. |
| .attr( 'src', iframe_src || 'javascript:0' ) |
| |
| // Append Iframe after the end of the body to prevent unnecessary |
| // initial page scrolling (yes, this works). |
| .insertAfter( 'body' )[0].contentWindow; |
| |
| // Whenever `document.title` changes, update the Iframe's title to |
| // prettify the back/next history menu entries. Since IE sometimes |
| // errors with "Unspecified error" the very first time this is set |
| // (yes, very useful) wrap this with a try/catch block. |
| doc.onpropertychange = function() { |
| try { |
| if ( event.propertyName === 'title' ) { |
| iframe.document.title = doc.title; |
| } |
| } catch(e) {} |
| }; |
| |
| } |
| }; |
| |
| // Override the "stop" method since an IE6/7 Iframe was created. Even |
| // if there are no longer any bound event handlers, the polling loop |
| // is still necessary for back/next to work at all! |
| self.stop = fn_retval; |
| |
| // Get history by looking at the hidden Iframe's location.hash. |
| history_get = function() { |
| return get_fragment( iframe.location.href ); |
| }; |
| |
| // Set a new history item by opening and then closing the Iframe |
| // document, *then* setting its location.hash. If document.domain has |
| // been set, update that as well. |
| history_set = function( hash, history_hash ) { |
| var iframe_doc = iframe.document, |
| domain = $.fn[ str_hashchange ].domain; |
| |
| if ( hash !== history_hash ) { |
| // Update Iframe with any initial `document.title` that might be set. |
| iframe_doc.title = doc.title; |
| |
| // Opening the Iframe's document after it has been closed is what |
| // actually adds a history entry. |
| iframe_doc.open(); |
| |
| // Set document.domain for the Iframe document as well, if necessary. |
| domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' ); |
| |
| iframe_doc.close(); |
| |
| // Update the Iframe's hash, for great justice. |
| iframe.location.hash = hash; |
| } |
| }; |
| |
| })(); |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^ |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| return self; |
| })(); |
| |
| })(jQuery,this); |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.page", $.mobile.widget, { |
| options: { |
| theme: "c", |
| domCache: false, |
| keepNativeDefault: ":jqmData(role='none'), :jqmData(role='nojs')" |
| }, |
| |
| _create: function() { |
| |
| var self = this; |
| |
| // if false is returned by the callbacks do not create the page |
| if ( self._trigger( "beforecreate" ) === false ) { |
| return false; |
| } |
| |
| self.element |
| .addClass( "ui-page ui-body-" + self.options.theme ) |
| .bind( "pagebeforehide", function() { |
| self.removeContainerBackground(); |
| } ) |
| .bind( "pagebeforeshow", function() { |
| self.setContainerBackground(); |
| } ); |
| |
| }, |
| |
| refresh: function() { |
| $( this.element ).children( ".ui-content" ).trigger("updatelayout", ["external"]); |
| }, |
| |
| /* GUI Builder only : redesign page when user drag&drop header, footer */ |
| setToolbar: function () { |
| $( this.element ).trigger( "pagebeforeshow" ); |
| }, |
| |
| removeContainerBackground: function() { |
| $.mobile.pageContainer.removeClass( "ui-overlay-" + $.mobile.getInheritedTheme( this.element.parent() ) ); |
| }, |
| |
| // set the page container background to the page theme |
| setContainerBackground: function( theme ) { |
| if ( this.options.theme ) { |
| $.mobile.pageContainer.addClass( "ui-overlay-" + ( theme || this.options.theme ) ); |
| } |
| }, |
| |
| addBackBtn : function ( target ) { |
| var $dest = $( ".ui-page-active .ui-footer" ); |
| |
| if ( target == "header" ) { |
| $dest = $( ".ui-page-active .ui-header" ); |
| } |
| backBtn = $( "<a href='#' class='ui-btn-back' data-" + $.mobile.ns + "rel='back'></a>" ) |
| .buttonMarkup( {icon: "header-back-btn", theme : "s"} ); |
| if ( !$dest.find( ".ui-btn-back").length ) { |
| backBtn.prependTo( $dest ); |
| } |
| }, |
| |
| keepNativeSelector: function() { |
| var options = this.options, |
| keepNativeDefined = options.keepNative && $.trim( options.keepNative ); |
| |
| if ( keepNativeDefined && options.keepNative !== options.keepNativeDefault ) { |
| return [options.keepNative, options.keepNativeDefault].join( ", " ); |
| } |
| |
| return options.keepNativeDefault; |
| } |
| }); |
| })( jQuery ); |
| |
| |
| (function( $, window, undefined ) { |
| |
| var createHandler = function( sequential ) { |
| |
| // Default to sequential |
| if ( sequential === undefined ) { |
| sequential = true; |
| } |
| |
| return function( name, reverse, $to, $from ) { |
| |
| var deferred = new $.Deferred(), |
| reverseClass = reverse ? " reverse" : "", |
| active = $.mobile.urlHistory.getActive(), |
| toScroll = active.lastScroll || $.mobile.defaultHomeScroll, |
| screenHeight = $.mobile.getScreenHeight(), |
| maxTransitionOverride = $.mobile.maxTransitionWidth !== false && $.mobile.$window.width() > $.mobile.maxTransitionWidth, |
| none = !$.support.cssTransitions || maxTransitionOverride || !name || name === "none" || Math.max( $.mobile.$window.scrollTop(), toScroll ) > $.mobile.getMaxScrollForTransition(), |
| toPreClass = " ui-page-pre-in", |
| toggleViewportClass = function() { |
| $.mobile.pageContainer.toggleClass( "ui-mobile-viewport-transitioning viewport-" + name ); |
| }, |
| scrollPage = function() { |
| // Prevent blinking on page scrolling in Tizen/Android devices. |
| // Don't scoll window, when current scroll top(scrollTop()) is already at toScroll, |
| // or when current scroll top is 0 and toScroll is same to defaultHomeScroll |
| // (which means the top position of page). In these case, page scrolling is not needed. |
| var st = $.mobile.$window.scrollTop(); |
| if( st === toScroll || ( $.mobile.defaultHomeScroll === toScroll && st == 0 ) ) { |
| return; |
| } |
| |
| // By using scrollTo instead of silentScroll, we can keep things better in order |
| // Just to be precautios, disable scrollstart listening like silentScroll would |
| $.event.special.scrollstart.enabled = false; |
| |
| window.scrollTo( 0, toScroll ); |
| |
| // reenable scrollstart listening like silentScroll would |
| setTimeout( function() { |
| $.event.special.scrollstart.enabled = true; |
| }, 150 ); |
| }, |
| cleanFrom = function() { |
| $from |
| .removeClass( $.mobile.activePageClass + " out in reverse " + name ) |
| .height( "" ); |
| }, |
| startOut = function() { |
| // if it's not sequential, call the doneOut transition to start the TO page animating in simultaneously |
| if ( !sequential ) { |
| doneOut(); |
| } |
| else { |
| $from.animationComplete( doneOut ); |
| } |
| |
| // Set the from page's height and start it transitioning out |
| // Note: setting an explicit height helps eliminate tiling in the transitions |
| $from |
| .height( screenHeight + $.mobile.$window.scrollTop() ) |
| .addClass( name + " out" + reverseClass ); |
| }, |
| |
| doneOut = function() { |
| |
| if ( $from && sequential ) { |
| cleanFrom(); |
| } |
| |
| startIn(); |
| }, |
| |
| startIn = function() { |
| |
| // Prevent flickering in phonegap container: see comments at #4024 regarding iOS |
| $to.css( "z-index", -10 ); |
| |
| $to.addClass( $.mobile.activePageClass + toPreClass ); |
| |
| // Send focus to page as it is now display: block |
| $.mobile.focusPage( $to ); |
| |
| // Set to page height |
| $to.height( screenHeight + toScroll ); |
| |
| scrollPage(); |
| |
| // Restores visibility of the new page: added together with $to.css( "z-index", -10 ); |
| $to.css( "z-index", "" ); |
| |
| if ( !none ) { |
| $to.animationComplete( doneIn ); |
| } |
| |
| $to |
| .removeClass( toPreClass ) |
| .addClass( name + " in" + reverseClass ); |
| |
| if ( none ) { |
| setTimeout( doneIn, 0 ); |
| } |
| |
| }, |
| |
| doneIn = function() { |
| |
| if ( !sequential ) { |
| |
| if ( $from ) { |
| cleanFrom(); |
| } |
| } |
| |
| $to |
| .removeClass( "out in reverse " + name ) |
| .height( "" ); |
| |
| toggleViewportClass(); |
| |
| // In some browsers (iOS5), 3D transitions block the ability to scroll to the desired location during transition |
| // This ensures we jump to that spot after the fact, if we aren't there already. |
| if ( $.mobile.$window.scrollTop() !== toScroll ) { |
| scrollPage(); |
| } |
| |
| deferred.resolve( name, reverse, $to, $from, true ); |
| }; |
| |
| toggleViewportClass(); |
| |
| if ( $from && !none ) { |
| startOut(); |
| } |
| else { |
| doneOut(); |
| } |
| |
| return deferred.promise(); |
| }; |
| }; |
| |
| // generate the handlers from the above |
| var sequentialHandler = createHandler(), |
| simultaneousHandler = createHandler( false ), |
| defaultGetMaxScrollForTransition = function() { |
| return $.mobile.getScreenHeight() * 3; |
| }; |
| |
| // Make our transition handler the public default. |
| $.mobile.defaultTransitionHandler = sequentialHandler; |
| |
| //transition handler dictionary for 3rd party transitions |
| $.mobile.transitionHandlers = { |
| "default": $.mobile.defaultTransitionHandler, |
| "sequential": sequentialHandler, |
| "simultaneous": simultaneousHandler |
| }; |
| |
| $.mobile.transitionFallbacks = {}; |
| |
| // If transition is defined, check if css 3D transforms are supported, and if not, if a fallback is specified |
| $.mobile._maybeDegradeTransition = function( transition ) { |
| if ( transition && !$.support.cssTransform3d && $.mobile.transitionFallbacks[ transition ] ) { |
| transition = $.mobile.transitionFallbacks[ transition ]; |
| } |
| |
| return transition; |
| }; |
| |
| // Set the getMaxScrollForTransition to default if no implementation was set by user |
| $.mobile.getMaxScrollForTransition = $.mobile.getMaxScrollForTransition || defaultGetMaxScrollForTransition; |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| //define vars for interal use |
| var $window = $.mobile.$window, |
| $html = $( 'html' ), |
| $head = $( 'head' ), |
| |
| //url path helpers for use in relative url management |
| path = { |
| |
| // This scary looking regular expression parses an absolute URL or its relative |
| // variants (protocol, site, document, query, and hash), into the various |
| // components (protocol, host, path, query, fragment, etc that make up the |
| // URL as well as some other commonly used sub-parts. When used with RegExp.exec() |
| // or String.match, it parses the URL into a results array that looks like this: |
| // |
| // [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content |
| // [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread |
| // [2]: http://jblas:password@mycompany.com:8080/mail/inbox |
| // [3]: http://jblas:password@mycompany.com:8080 |
| // [4]: http: |
| // [5]: // |
| // [6]: jblas:password@mycompany.com:8080 |
| // [7]: jblas:password |
| // [8]: jblas |
| // [9]: password |
| // [10]: mycompany.com:8080 |
| // [11]: mycompany.com |
| // [12]: 8080 |
| // [13]: /mail/inbox |
| // [14]: /mail/ |
| // [15]: inbox |
| // [16]: ?msg=1234&type=unread |
| // [17]: #msg-content |
| // |
| urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, |
| |
| // Abstraction to address xss (Issue #4787) by removing the authority in |
| // browsers that auto decode it. All references to location.href should be |
| // replaced with a call to this method so that it can be dealt with properly here |
| getLocation: function( url ) { |
| var uri = url ? this.parseUrl( url ) : location, |
| hash = this.parseUrl( url || location.href ).hash; |
| |
| // mimic the browser with an empty string when the hash is empty |
| hash = hash === "#" ? "" : hash; |
| |
| // Make sure to parse the url or the location object for the hash because using location.hash |
| // is autodecoded in firefox, the rest of the url should be from the object (location unless |
| // we're testing) to avoid the inclusion of the authority |
| return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash; |
| }, |
| |
| parseLocation: function() { |
| return this.parseUrl( this.getLocation() ); |
| }, |
| |
| //Parse a URL into a structure that allows easy access to |
| //all of the URL components by name. |
| parseUrl: function( url ) { |
| // If we're passed an object, we'll assume that it is |
| // a parsed url object and just return it back to the caller. |
| if ( $.type( url ) === "object" ) { |
| return url; |
| } |
| |
| var matches = path.urlParseRE.exec( url || "" ) || []; |
| |
| // Create an object that allows the caller to access the sub-matches |
| // by name. Note that IE returns an empty string instead of undefined, |
| // like all other browsers do, so we normalize everything so its consistent |
| // no matter what browser we're running on. |
| return { |
| href: matches[ 0 ] || "", |
| hrefNoHash: matches[ 1 ] || "", |
| hrefNoSearch: matches[ 2 ] || "", |
| domain: matches[ 3 ] || "", |
| protocol: matches[ 4 ] || "", |
| doubleSlash: matches[ 5 ] || "", |
| authority: matches[ 6 ] || "", |
| username: matches[ 8 ] || "", |
| password: matches[ 9 ] || "", |
| host: matches[ 10 ] || "", |
| hostname: matches[ 11 ] || "", |
| port: matches[ 12 ] || "", |
| pathname: matches[ 13 ] || "", |
| directory: matches[ 14 ] || "", |
| filename: matches[ 15 ] || "", |
| search: matches[ 16 ] || "", |
| hash: matches[ 17 ] || "" |
| }; |
| }, |
| |
| //Turn relPath into an asbolute path. absPath is |
| //an optional absolute path which describes what |
| //relPath is relative to. |
| makePathAbsolute: function( relPath, absPath ) { |
| if ( relPath && relPath.charAt( 0 ) === "/" ) { |
| return relPath; |
| } |
| |
| relPath = relPath || ""; |
| absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : ""; |
| |
| var absStack = absPath ? absPath.split( "/" ) : [], |
| relStack = relPath.split( "/" ); |
| for ( var i = 0; i < relStack.length; i++ ) { |
| var d = relStack[ i ]; |
| switch ( d ) { |
| case ".": |
| break; |
| case "..": |
| if ( absStack.length ) { |
| absStack.pop(); |
| } |
| break; |
| default: |
| absStack.push( d ); |
| break; |
| } |
| } |
| return "/" + absStack.join( "/" ); |
| }, |
| |
| //Returns true if both urls have the same domain. |
| isSameDomain: function( absUrl1, absUrl2 ) { |
| return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain; |
| }, |
| |
| //Returns true for any relative variant. |
| isRelativeUrl: function( url ) { |
| // All relative Url variants have one thing in common, no protocol. |
| return path.parseUrl( url ).protocol === ""; |
| }, |
| |
| //Returns true for an absolute url. |
| isAbsoluteUrl: function( url ) { |
| return path.parseUrl( url ).protocol !== ""; |
| }, |
| |
| //Turn the specified realtive URL into an absolute one. This function |
| //can handle all relative variants (protocol, site, document, query, fragment). |
| makeUrlAbsolute: function( relUrl, absUrl ) { |
| if ( !path.isRelativeUrl( relUrl ) ) { |
| return relUrl; |
| } |
| |
| if ( absUrl === undefined ) { |
| absUrl = documentBase; |
| } |
| |
| var relObj = path.parseUrl( relUrl ), |
| absObj = path.parseUrl( absUrl ), |
| protocol = relObj.protocol || absObj.protocol, |
| doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ), |
| authority = relObj.authority || absObj.authority, |
| hasPath = relObj.pathname !== "", |
| pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ), |
| search = relObj.search || ( !hasPath && absObj.search ) || "", |
| hash = relObj.hash; |
| |
| return protocol + doubleSlash + authority + pathname + search + hash; |
| }, |
| |
| //Add search (aka query) params to the specified url. |
| addSearchParams: function( url, params ) { |
| var u = path.parseUrl( url ), |
| p = ( typeof params === "object" ) ? $.param( params ) : params, |
| s = u.search || "?"; |
| return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" ); |
| }, |
| |
| convertUrlToDataUrl: function( absUrl ) { |
| var u = path.parseUrl( absUrl ); |
| if ( path.isEmbeddedPage( u ) ) { |
| // For embedded pages, remove the dialog hash key as in getFilePath(), |
| // otherwise the Data Url won't match the id of the embedded Page. |
| return u.hash.split( dialogHashKey )[0].replace( /^#/, "" ); |
| } else if ( path.isSameDomain( u, documentBase ) ) { |
| return u.hrefNoHash.replace( documentBase.domain, "" ).split( dialogHashKey )[0]; |
| } |
| |
| return window.decodeURIComponent(absUrl); |
| }, |
| |
| //get path from current hash, or from a file path |
| get: function( newPath ) { |
| if ( newPath === undefined ) { |
| newPath = path.parseLocation().hash; |
| } |
| return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' ); |
| }, |
| |
| //return the substring of a filepath before the sub-page key, for making a server request |
| getFilePath: function( path ) { |
| var splitkey = '&' + $.mobile.subPageUrlKey; |
| return path && path.split( splitkey )[0].split( dialogHashKey )[0]; |
| }, |
| |
| //set location hash to path |
| set: function( path ) { |
| location.hash = path; |
| }, |
| |
| //test if a given url (string) is a path |
| //NOTE might be exceptionally naive |
| isPath: function( url ) { |
| return ( /\// ).test( url ); |
| }, |
| |
| //return a url path with the window's location protocol/hostname/pathname removed |
| clean: function( url ) { |
| return url.replace( documentBase.domain, "" ); |
| }, |
| |
| //just return the url without an initial # |
| stripHash: function( url ) { |
| return url.replace( /^#/, "" ); |
| }, |
| |
| //remove the preceding hash, any query params, and dialog notations |
| cleanHash: function( hash ) { |
| return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) ); |
| }, |
| |
| isHashValid: function( hash ) { |
| return ( /^#[^#]+$/ ).test( hash ); |
| }, |
| |
| //check whether a url is referencing the same domain, or an external domain or different protocol |
| //could be mailto, etc |
| isExternal: function( url ) { |
| var u = path.parseUrl( url ); |
| return u.protocol && u.domain !== documentUrl.domain ? true : false; |
| }, |
| |
| hasProtocol: function( url ) { |
| return ( /^(:?\w+:)/ ).test( url ); |
| }, |
| |
| //check if the specified url refers to the first page in the main application document. |
| isFirstPageUrl: function( url ) { |
| // We only deal with absolute paths. |
| var u = path.parseUrl( path.makeUrlAbsolute( url, documentBase ) ), |
| |
| // Does the url have the same path as the document? |
| samePath = u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ), |
| |
| // Get the first page element. |
| fp = $.mobile.firstPage, |
| |
| // Get the id of the first page element if it has one. |
| fpId = fp && fp[0] ? fp[0].id : undefined; |
| |
| // The url refers to the first page if the path matches the document and |
| // it either has no hash value, or the hash is exactly equal to the id of the |
| // first page element. |
| return samePath && ( !u.hash || u.hash === "#" || ( fpId && u.hash.replace( /^#/, "" ) === fpId ) ); |
| }, |
| |
| isEmbeddedPage: function( url ) { |
| var u = path.parseUrl( url ); |
| |
| //if the path is absolute, then we need to compare the url against |
| //both the documentUrl and the documentBase. The main reason for this |
| //is that links embedded within external documents will refer to the |
| //application document, whereas links embedded within the application |
| //document will be resolved against the document base. |
| if ( u.protocol !== "" ) { |
| return ( u.hash && ( u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ) ) ); |
| } |
| return ( /^#/ ).test( u.href ); |
| }, |
| |
| |
| // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR |
| // requests if the document doing the request was loaded via the file:// protocol. |
| // This is usually to allow the application to "phone home" and fetch app specific |
| // data. We normally let the browser handle external/cross-domain urls, but if the |
| // allowCrossDomainPages option is true, we will allow cross-domain http/https |
| // requests to go through our page loading logic. |
| isPermittedCrossDomainRequest: function( docUrl, reqUrl ) { |
| return $.mobile.allowCrossDomainPages && |
| docUrl.protocol === "file:" && |
| reqUrl.search( /^https?:/ ) !== -1; |
| } |
| }, |
| |
| //will be defined when a link is clicked and given an active class |
| $activeClickedLink = null, |
| |
| //urlHistory is purely here to make guesses at whether the back or forward button was clicked |
| //and provide an appropriate transition |
| urlHistory = { |
| // Array of pages that are visited during a single page load. |
| // Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs) |
| stack: [], |
| |
| //maintain an index number for the active page in the stack |
| activeIndex: 0, |
| |
| //get active |
| getActive: function() { |
| return urlHistory.stack[ urlHistory.activeIndex ]; |
| }, |
| |
| getPrev: function() { |
| return urlHistory.stack[ urlHistory.activeIndex - 1 ]; |
| }, |
| |
| getNext: function() { |
| return urlHistory.stack[ urlHistory.activeIndex + 1 ]; |
| }, |
| |
| // addNew is used whenever a new page is added |
| addNew: function( url, transition, title, pageUrl, role ) { |
| //if there's forward history, wipe it |
| if ( urlHistory.getNext() ) { |
| urlHistory.clearForward(); |
| } |
| |
| urlHistory.stack.push( {url : url, transition: transition, title: title, pageUrl: pageUrl, role: role } ); |
| |
| urlHistory.activeIndex = urlHistory.stack.length - 1; |
| }, |
| |
| //wipe urls ahead of active index |
| clearForward: function() { |
| urlHistory.stack = urlHistory.stack.slice( 0, urlHistory.activeIndex + 1 ); |
| }, |
| |
| directHashChange: function( opts ) { |
| var back , forward, newActiveIndex, prev = this.getActive(); |
| |
| // check if url is in history and if it's ahead or behind current page |
| $.each( urlHistory.stack, function( i, historyEntry ) { |
| |
| //if the url is in the stack, it's a forward or a back |
| if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) { |
| //define back and forward by whether url is older or newer than current page |
| back = i < urlHistory.activeIndex; |
| forward = !back; |
| newActiveIndex = i; |
| } |
| }); |
| |
| // save new page index, null check to prevent falsey 0 result |
| this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex; |
| |
| if ( back ) { |
| ( opts.either || opts.isBack )( true ); |
| } else if ( forward ) { |
| ( opts.either || opts.isForward )( false ); |
| } |
| }, |
| |
| //disable hashchange event listener internally to ignore one change |
| //toggled internally when location.hash is updated to match the url of a successful page load |
| ignoreNextHashChange: false |
| }, |
| |
| //define first selector to receive focus when a page is shown |
| focusable = "[tabindex],a,button:visible,select:visible,input", |
| |
| //queue to hold simultanious page transitions |
| pageTransitionQueue = [], |
| |
| //indicates whether or not page is in process of transitioning |
| isPageTransitioning = false, |
| |
| //nonsense hash change key for dialogs, so they create a history entry |
| dialogHashKey = "&ui-state=dialog", |
| |
| //existing base tag? |
| $base = $head.children( "base" ), |
| |
| //tuck away the original document URL minus any fragment. |
| documentUrl = path.parseLocation(), |
| |
| //if the document has an embedded base tag, documentBase is set to its |
| //initial value. If a base tag does not exist, then we default to the documentUrl. |
| documentBase = $base.length ? path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), documentUrl.href ) ) : documentUrl, |
| |
| //cache the comparison once. |
| documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ), |
| |
| getScreenHeight = $.mobile.getScreenHeight; |
| |
| //base element management, defined depending on dynamic base tag support |
| var base = $.support.dynamicBaseTag ? { |
| |
| //define base element, for use in routing asset urls that are referenced in Ajax-requested markup |
| element: ( $base.length ? $base : $( "<base>", { href: documentBase.hrefNoHash } ).prependTo( $head ) ), |
| |
| //set the generated BASE element's href attribute to a new page's base path |
| set: function( href ) { |
| base.element.attr( "href", path.makeUrlAbsolute( href, documentBase ) ); |
| }, |
| |
| //set the generated BASE element's href attribute to a new page's base path |
| reset: function() { |
| base.element.attr( "href", documentBase.hrefNoHash ); |
| } |
| |
| } : undefined; |
| |
| /* internal utility functions */ |
| |
| // NOTE Issue #4950 Android phonegap doesn't navigate back properly |
| // when a full page refresh has taken place. It appears that hashchange |
| // and replacestate history alterations work fine but we need to support |
| // both forms of history traversal in our code that uses backward history |
| // movement |
| $.mobile.back = function() { |
| var nav = window.navigator; |
| |
| // if the setting is on and the navigator object is |
| // available use the phonegap navigation capability |
| if( this.phonegapNavigationEnabled && |
| nav && |
| nav.app && |
| nav.app.backHistory ){ |
| nav.app.backHistory(); |
| } else { |
| window.history.back(); |
| } |
| }; |
| |
| //direct focus to the page title, or otherwise first focusable element |
| $.mobile.focusPage = function ( page ) { |
| var autofocus = page.find( "[autofocus]" ), |
| pageTitle = page.find( ".ui-title:eq(0)" ); |
| |
| if ( autofocus.length ) { |
| autofocus.focus(); |
| return; |
| } |
| |
| if ( pageTitle.length ) { |
| pageTitle.focus(); |
| } else{ |
| page.focus(); |
| } |
| }; |
| |
| //remove active classes after page transition or error |
| function removeActiveLinkClass( forceRemoval ) { |
| if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) { |
| $activeClickedLink.removeClass( $.mobile.activeBtnClass ); |
| } |
| $activeClickedLink = null; |
| } |
| |
| function releasePageTransitionLock() { |
| isPageTransitioning = false; |
| if ( pageTransitionQueue.length > 0 ) { |
| $.mobile.changePage.apply( null, pageTransitionQueue.pop() ); |
| } |
| } |
| |
| // Save the last scroll distance per page, before it is hidden |
| var setLastScrollEnabled = true, |
| setLastScroll, delayedSetLastScroll; |
| |
| setLastScroll = function() { |
| // this barrier prevents setting the scroll value based on the browser |
| // scrolling the window based on a hashchange |
| if ( !setLastScrollEnabled ) { |
| return; |
| } |
| |
| var active = $.mobile.urlHistory.getActive(); |
| |
| if ( active ) { |
| var lastScroll = $window.scrollTop(); |
| |
| // Set active page's lastScroll prop. |
| // If the location we're scrolling to is less than minScrollBack, let it go. |
| active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll; |
| } |
| }; |
| |
| // bind to scrollstop to gather scroll position. The delay allows for the hashchange |
| // event to fire and disable scroll recording in the case where the browser scrolls |
| // to the hash targets location (sometimes the top of the page). once pagechange fires |
| // getLastScroll is again permitted to operate |
| delayedSetLastScroll = function() { |
| setTimeout( setLastScroll, 100 ); |
| }; |
| |
| // disable an scroll setting when a hashchange has been fired, this only works |
| // because the recording of the scroll position is delayed for 100ms after |
| // the browser might have changed the position because of the hashchange |
| $window.bind( $.support.pushState ? "popstate" : "hashchange", function() { |
| setLastScrollEnabled = false; |
| }); |
| |
| // handle initial hashchange from chrome :( |
| $window.one( $.support.pushState ? "popstate" : "hashchange", function() { |
| setLastScrollEnabled = true; |
| }); |
| |
| // wait until the mobile page container has been determined to bind to pagechange |
| $window.one( "pagecontainercreate", function() { |
| // once the page has changed, re-enable the scroll recording |
| $.mobile.pageContainer.bind( "pagechange", function() { |
| |
| setLastScrollEnabled = true; |
| |
| // remove any binding that previously existed on the get scroll |
| // which may or may not be different than the scroll element determined for |
| // this page previously |
| $window.unbind( "scrollstop", delayedSetLastScroll ); |
| |
| // determine and bind to the current scoll element which may be the window |
| // or in the case of touch overflow the element with touch overflow |
| $window.bind( "scrollstop", delayedSetLastScroll ); |
| }); |
| }); |
| |
| // bind to scrollstop for the first page as "pagechange" won't be fired in that case |
| $window.bind( "scrollstop", delayedSetLastScroll ); |
| |
| // No-op implementation of transition degradation |
| $.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) { |
| return transition; |
| }; |
| |
| //function for transitioning between two existing pages |
| function transitionPages( toPage, fromPage, transition, reverse ) { |
| |
| if ( fromPage ) { |
| //trigger before show/hide events |
| fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } ); |
| } |
| |
| toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } ); |
| |
| //clear page loader |
| $.mobile.hidePageLoadingMsg(); |
| |
| transition = $.mobile._maybeDegradeTransition( transition ); |
| |
| //find the transition handler for the specified transition. If there |
| //isn't one in our transitionHandlers dictionary, use the default one. |
| //call the handler immediately to kick-off the transition. |
| var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler, |
| promise = th( transition, reverse, toPage, fromPage ); |
| |
| promise.done(function() { |
| |
| //trigger show/hide events |
| if ( fromPage ) { |
| fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } ); |
| } |
| |
| //trigger pageshow, define prevPage as either fromPage or empty jQuery obj |
| toPage.data( "page" )._trigger( "show", null, { prevPage: fromPage || $( "" ) } ); |
| |
| setTimeout( function () { |
| $.mobile.removeEventBlocker(); |
| }, 0 ); |
| }); |
| |
| return promise; |
| } |
| |
| //shared page enhancements |
| function enhancePage( $page, role ) { |
| // If a role was specified, make sure the data-role attribute |
| // on the page element is in sync. |
| if ( role ) { |
| $page.attr( "data-" + $.mobile.ns + "role", role ); |
| } |
| |
| //run page plugin |
| $page.page(); |
| } |
| |
| /* exposed $.mobile methods */ |
| |
| //animation complete callback |
| $.fn.animationComplete = function( callback ) { |
| if ( $.support.cssTransitions ) { |
| return $( this ).one( 'webkitAnimationEnd animationend', callback ); |
| } |
| else{ |
| // defer execution for consistency between webkit/non webkit |
| setTimeout( callback, 0 ); |
| return $( this ); |
| } |
| }; |
| |
| //expose path object on $.mobile |
| $.mobile.path = path; |
| |
| //expose base object on $.mobile |
| $.mobile.base = base; |
| |
| //history stack |
| $.mobile.urlHistory = urlHistory; |
| |
| $.mobile.dialogHashKey = dialogHashKey; |
| |
| |
| |
| //enable cross-domain page support |
| $.mobile.allowCrossDomainPages = false; |
| |
| //return the original document url |
| $.mobile.getDocumentUrl = function( asParsedObject ) { |
| return asParsedObject ? $.extend( {}, documentUrl ) : documentUrl.href; |
| }; |
| |
| //return the original document base url |
| $.mobile.getDocumentBase = function( asParsedObject ) { |
| return asParsedObject ? $.extend( {}, documentBase ) : documentBase.href; |
| }; |
| |
| $.mobile._bindPageRemove = function() { |
| var page = $( this ); |
| |
| // when dom caching is not enabled or the page is embedded bind to remove the page on hide |
| if ( !page.data( "page" ).options.domCache && |
| page.is( ":jqmData(external-page='true')" ) ) { |
| |
| page.bind( 'pagehide.remove', function() { |
| var $this = $( this ), |
| prEvent = new $.Event( "pageremove" ); |
| |
| $this.trigger( prEvent ); |
| |
| if ( !prEvent.isDefaultPrevented() ) { |
| $this.removeWithDependents(); |
| } |
| }); |
| } |
| }; |
| |
| // Load a page into the DOM. |
| $.mobile.loadPage = function( url, options ) { |
| // This function uses deferred notifications to let callers |
| // know when the page is done loading, or if an error has occurred. |
| var deferred = $.Deferred(), |
| |
| // The default loadPage options with overrides specified by |
| // the caller. |
| settings = $.extend( {}, $.mobile.loadPage.defaults, options ), |
| |
| // The DOM element for the page after it has been loaded. |
| page = null, |
| |
| // If the reloadPage option is true, and the page is already |
| // in the DOM, dupCachedPage will be set to the page element |
| // so that it can be removed after the new version of the |
| // page is loaded off the network. |
| dupCachedPage = null, |
| |
| // determine the current base url |
| findBaseWithDefault = function() { |
| var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) ); |
| return closestBase || documentBase.hrefNoHash; |
| }, |
| |
| // The absolute version of the URL passed into the function. This |
| // version of the URL may contain dialog/subpage params in it. |
| absUrl = path.makeUrlAbsolute( url, findBaseWithDefault() ); |
| |
| |
| // If the caller provided data, and we're using "get" request, |
| // append the data to the URL. |
| if ( settings.data && settings.type === "get" ) { |
| absUrl = path.addSearchParams( absUrl, settings.data ); |
| settings.data = undefined; |
| } |
| |
| // If the caller is using a "post" request, reloadPage must be true |
| if ( settings.data && settings.type === "post" ) { |
| settings.reloadPage = true; |
| } |
| |
| // The absolute version of the URL minus any dialog/subpage params. |
| // In otherwords the real URL of the page to be loaded. |
| var fileUrl = path.getFilePath( absUrl ), |
| |
| // The version of the Url actually stored in the data-url attribute of |
| // the page. For embedded pages, it is just the id of the page. For pages |
| // within the same domain as the document base, it is the site relative |
| // path. For cross-domain pages (Phone Gap only) the entire absolute Url |
| // used to load the page. |
| dataUrl = path.convertUrlToDataUrl( absUrl ); |
| |
| // Make sure we have a pageContainer to work with. |
| settings.pageContainer = settings.pageContainer || $.mobile.pageContainer; |
| |
| // Check to see if the page already exists in the DOM. |
| // NOTE do _not_ use the :jqmData psuedo selector because parenthesis |
| // are a valid url char and it breaks on the first occurence |
| page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); |
| |
| // If we failed to find the page, check to see if the url is a |
| // reference to an embedded page. If so, it may have been dynamically |
| // injected by a developer, in which case it would be lacking a data-url |
| // attribute and in need of enhancement. |
| if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) { |
| page = settings.pageContainer.children( "#" + dataUrl ) |
| .attr( "data-" + $.mobile.ns + "url", dataUrl ) |
| .jqmData( "url", dataUrl ); |
| } |
| |
| // If we failed to find a page in the DOM, check the URL to see if it |
| // refers to the first page in the application. If it isn't a reference |
| // to the first page and refers to non-existent embedded page, error out. |
| if ( page.length === 0 ) { |
| if ( $.mobile.firstPage && path.isFirstPageUrl( fileUrl ) ) { |
| // Check to make sure our cached-first-page is actually |
| // in the DOM. Some user deployed apps are pruning the first |
| // page from the DOM for various reasons, we check for this |
| // case here because we don't want a first-page with an id |
| // falling through to the non-existent embedded page error |
| // case. If the first-page is not in the DOM, then we let |
| // things fall through to the ajax loading code below so |
| // that it gets reloaded. |
| if ( $.mobile.firstPage.parent().length ) { |
| page = $( $.mobile.firstPage ); |
| } |
| } else if ( path.isEmbeddedPage( fileUrl ) ) { |
| deferred.reject( absUrl, options ); |
| return deferred.promise(); |
| } |
| } |
| |
| // If the page we are interested in is already in the DOM, |
| // and the caller did not indicate that we should force a |
| // reload of the file, we are done. Otherwise, track the |
| // existing page as a duplicated. |
| if ( page.length ) { |
| if ( !settings.reloadPage ) { |
| enhancePage( page, settings.role ); |
| deferred.resolve( absUrl, options, page ); |
| //if we are reloading the page make sure we update the base if its not a prefetch |
| if( base && ( !options || !options.prefetch ) ){ |
| base.set(url); |
| } |
| return deferred.promise(); |
| } |
| dupCachedPage = page; |
| } |
| |
| var mpc = settings.pageContainer, |
| pblEvent = new $.Event( "pagebeforeload" ), |
| triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings }; |
| |
| // Let listeners know we're about to load a page. |
| mpc.trigger( pblEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| if ( pblEvent.isDefaultPrevented() ) { |
| return deferred.promise(); |
| } |
| |
| if ( settings.showLoadMsg ) { |
| |
| // This configurable timeout allows cached pages a brief delay to load without showing a message |
| var loadMsgDelay = setTimeout(function() { |
| $.mobile.showPageLoadingMsg(); |
| }, settings.loadMsgDelay ), |
| |
| // Shared logic for clearing timeout and removing message. |
| hideMsg = function() { |
| |
| // Stop message show timer |
| clearTimeout( loadMsgDelay ); |
| |
| // Hide loading message |
| $.mobile.hidePageLoadingMsg(); |
| }; |
| } |
| |
| // Reset base to the default document base. |
| // only reset if we are not prefetching |
| if ( base && ( typeof options === "undefined" || typeof options.prefetch === "undefined" ) ) { |
| base.reset(); |
| } |
| |
| if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) { |
| deferred.reject( absUrl, options ); |
| } else { |
| // Load the new page. |
| $.ajax({ |
| url: fileUrl, |
| type: settings.type, |
| data: settings.data, |
| dataType: "html", |
| success: function( html, textStatus, xhr ) { |
| //pre-parse html to check for a data-url, |
| //use it as the new fileUrl, base path, etc |
| var all = $( "<div></div>" ), |
| |
| //page title regexp |
| newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1, |
| |
| // TODO handle dialogs again |
| pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ), |
| dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" ); |
| |
| |
| // data-url must be provided for the base tag so resource requests can be directed to the |
| // correct url. loading into a temprorary element makes these requests immediately |
| if ( pageElemRegex.test( html ) && |
| RegExp.$1 && |
| dataUrlRegex.test( RegExp.$1 ) && |
| RegExp.$1 ) { |
| url = fileUrl = path.getFilePath( $( "<div>" + RegExp.$1 + "</div>" ).text() ); |
| } |
| |
| //dont update the base tag if we are prefetching |
| if ( base && ( typeof options === "undefined" || typeof options.prefetch === "undefined" ) ) { |
| base.set( fileUrl ); |
| } |
| |
| //workaround to allow scripts to execute when included in page divs |
| all.get( 0 ).innerHTML = html; |
| page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first(); |
| |
| //if page elem couldn't be found, create one and insert the body element's contents |
| if ( !page.length ) { |
| page = $( "<div data-" + $.mobile.ns + "role='page'>" + html.split( /<\/?body[^>]*>/gmi )[1] + "</div>" ); |
| } |
| |
| if ( newPageTitle && !page.jqmData( "title" ) ) { |
| if ( ~newPageTitle.indexOf( "&" ) ) { |
| newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text(); |
| } |
| page.jqmData( "title", newPageTitle ); |
| } |
| |
| //rewrite src and href attrs to use a base url |
| if ( !$.support.dynamicBaseTag ) { |
| var newPath = path.get( fileUrl ); |
| page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() { |
| var thisAttr = $( this ).is( '[href]' ) ? 'href' : |
| $( this ).is( '[src]' ) ? 'src' : 'action', |
| thisUrl = $( this ).attr( thisAttr ); |
| |
| // XXX_jblas: We need to fix this so that it removes the document |
| // base URL, and then prepends with the new page URL. |
| //if full path exists and is same, chop it - helps IE out |
| thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' ); |
| |
| if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) { |
| $( this ).attr( thisAttr, newPath + thisUrl ); |
| } |
| }); |
| } |
| |
| //append to page and enhance |
| // TODO taging a page with external to make sure that embedded pages aren't removed |
| // by the various page handling code is bad. Having page handling code in many |
| // places is bad. Solutions post 1.0 |
| page |
| .attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) ) |
| .attr( "data-" + $.mobile.ns + "external-page", true ) |
| .appendTo( settings.pageContainer ); |
| |
| // wait for page creation to leverage options defined on widget |
| page.one( 'pagecreate', $.mobile._bindPageRemove ); |
| |
| enhancePage( page, settings.role ); |
| |
| // Enhancing the page may result in new dialogs/sub pages being inserted |
| // into the DOM. If the original absUrl refers to a sub-page, that is the |
| // real page we are interested in. |
| if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) { |
| page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" ); |
| } |
| |
| //bind pageHide to removePage after it's hidden, if the page options specify to do so |
| |
| // Remove loading message. |
| if ( settings.showLoadMsg ) { |
| hideMsg(); |
| } |
| |
| // Add the page reference and xhr to our triggerData. |
| triggerData.xhr = xhr; |
| triggerData.textStatus = textStatus; |
| triggerData.page = page; |
| |
| // Let listeners know the page loaded successfully. |
| settings.pageContainer.trigger( "pageload", triggerData ); |
| |
| deferred.resolve( absUrl, options, page, dupCachedPage ); |
| }, |
| error: function( xhr, textStatus, errorThrown ) { |
| //set base back to current path |
| if ( base ) { |
| base.set( path.get() ); |
| } |
| |
| // Add error info to our triggerData. |
| triggerData.xhr = xhr; |
| triggerData.textStatus = textStatus; |
| triggerData.errorThrown = errorThrown; |
| |
| var plfEvent = new $.Event( "pageloadfailed" ); |
| |
| // Let listeners know the page load failed. |
| settings.pageContainer.trigger( plfEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| // Note that it is the responsibility of the listener/handler |
| // that called preventDefault(), to resolve/reject the |
| // deferred object within the triggerData. |
| if ( plfEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| // Remove loading message. |
| if ( settings.showLoadMsg ) { |
| |
| // Remove loading message. |
| hideMsg(); |
| |
| // show error message |
| $.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true ); |
| |
| // hide after delay |
| setTimeout( $.mobile.hidePageLoadingMsg, 1500 ); |
| } |
| |
| deferred.reject( absUrl, options ); |
| } |
| }); |
| } |
| |
| return deferred.promise(); |
| }; |
| |
| $.mobile.loadPage.defaults = { |
| type: "get", |
| data: undefined, |
| reloadPage: false, |
| role: undefined, // By default we rely on the role defined by the @data-role attribute. |
| showLoadMsg: false, |
| pageContainer: undefined, |
| loadMsgDelay: 50 // This delay allows loads that pull from browser cache to occur without showing the loading message. |
| }; |
| |
| // Show a specific page in the page container. |
| $.mobile.changePage = function( toPage, options ) { |
| // If we are in the midst of a transition, queue the current request. |
| // We'll call changePage() once we're done with the current transition to |
| // service the request. |
| if ( isPageTransitioning ) { |
| pageTransitionQueue.unshift( arguments ); |
| return; |
| } |
| |
| var settings = $.extend( {}, $.mobile.changePage.defaults, options ); |
| |
| // Make sure we have a pageContainer to work with. |
| settings.pageContainer = settings.pageContainer || $.mobile.pageContainer; |
| |
| // Make sure we have a fromPage. |
| settings.fromPage = settings.fromPage || $.mobile.activePage; |
| |
| var mpc = settings.pageContainer, |
| pbcEvent = new $.Event( "pagebeforechange" ), |
| triggerData = { toPage: toPage, options: settings }; |
| |
| // Let listeners know we're about to change the current page. |
| mpc.trigger( pbcEvent, triggerData ); |
| |
| // If the default behavior is prevented, stop here! |
| if ( pbcEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| // We allow "pagebeforechange" observers to modify the toPage in the trigger |
| // data to allow for redirects. Make sure our toPage is updated. |
| |
| toPage = triggerData.toPage; |
| |
| // Set the isPageTransitioning flag to prevent any requests from |
| // entering this method while we are in the midst of loading a page |
| // or transitioning. |
| |
| isPageTransitioning = true; |
| |
| // If the caller passed us a url, call loadPage() |
| // to make sure it is loaded into the DOM. We'll listen |
| // to the promise object it returns so we know when |
| // it is done loading or if an error ocurred. |
| if ( typeof toPage === "string" ) { |
| $.mobile.loadPage( toPage, settings ) |
| .done(function( url, options, newPage, dupCachedPage ) { |
| isPageTransitioning = false; |
| options.duplicateCachedPage = dupCachedPage; |
| $.mobile.changePage( newPage, options ); |
| }) |
| .fail(function( url, options ) { |
| isPageTransitioning = false; |
| |
| //clear out the active button state |
| removeActiveLinkClass( true ); |
| |
| //release transition lock so navigation is free again |
| releasePageTransitionLock(); |
| settings.pageContainer.trigger( "pagechangefailed", triggerData ); |
| }); |
| return; |
| } |
| |
| // If we are going to the first-page of the application, we need to make |
| // sure settings.dataUrl is set to the application document url. This allows |
| // us to avoid generating a document url with an id hash in the case where the |
| // first-page of the document has an id attribute specified. |
| if ( toPage[ 0 ] === $.mobile.firstPage[ 0 ] && !settings.dataUrl ) { |
| settings.dataUrl = documentUrl.hrefNoHash; |
| } |
| |
| // The caller passed us a real page DOM element. Update our |
| // internal state and then trigger a transition to the page. |
| var fromPage = settings.fromPage, |
| url = ( settings.dataUrl && path.convertUrlToDataUrl( settings.dataUrl ) ) || toPage.jqmData( "url" ), |
| // The pageUrl var is usually the same as url, except when url is obscured as a dialog url. pageUrl always contains the file path |
| pageUrl = url, |
| fileUrl = path.getFilePath( url ), |
| active = urlHistory.getActive(), |
| activeIsInitialPage = urlHistory.activeIndex === 0, |
| historyDir = 0, |
| pageTitle = document.title, |
| isDialog = settings.role === "dialog" || $.mobile.getAttrFixed( toPage [0], "data-" + $.mobile.ns + "role" ) === "dialog"; |
| |
| // By default, we prevent changePage requests when the fromPage and toPage |
| // are the same element, but folks that generate content manually/dynamically |
| // and reuse pages want to be able to transition to the same page. To allow |
| // this, they will need to change the default value of allowSamePageTransition |
| // to true, *OR*, pass it in as an option when they manually call changePage(). |
| // It should be noted that our default transition animations assume that the |
| // formPage and toPage are different elements, so they may behave unexpectedly. |
| // It is up to the developer that turns on the allowSamePageTransitiona option |
| // to either turn off transition animations, or make sure that an appropriate |
| // animation transition is used. |
| if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) { |
| isPageTransitioning = false; |
| mpc.trigger( "pagechange", triggerData ); |
| |
| // Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes |
| if ( settings.fromHashChange ) { |
| urlHistory.directHashChange({ |
| currentUrl: url, |
| isBack: function() {}, |
| isForward: function() {} |
| }); |
| } |
| |
| return; |
| } |
| |
| // We need to make sure the page we are given has already been enhanced. |
| enhancePage( toPage, settings.role ); |
| |
| // If the changePage request was sent from a hashChange event, check to see if the |
| // page is already within the urlHistory stack. If so, we'll assume the user hit |
| // the forward/back button and will try to match the transition accordingly. |
| if ( settings.fromHashChange ) { |
| urlHistory.directHashChange({ |
| currentUrl: url, |
| isBack: function() { historyDir = -1; }, |
| isForward: function() { historyDir = 1; } |
| }); |
| } |
| |
| // Kill the keyboard. |
| // XXX_jblas: We need to stop crawling the entire document to kill focus. Instead, |
| // we should be tracking focus with a delegate() handler so we already have |
| // the element in hand at this point. |
| // Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement |
| // is undefined when we are in an IFrame. |
| try { |
| if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) { |
| $( document.activeElement ).blur(); |
| } else { |
| $( "input:focus, textarea:focus, select:focus" ).blur(); |
| } |
| } catch( e ) {} |
| |
| // Record whether we are at a place in history where a dialog used to be - if so, do not add a new history entry and do not change the hash either |
| var alreadyThere = false; |
| |
| // If we're displaying the page as a dialog, we don't want the url |
| // for the dialog content to be used in the hash. Instead, we want |
| // to append the dialogHashKey to the url of the current page. |
| if ( isDialog && active ) { |
| // on the initial page load active.url is undefined and in that case should |
| // be an empty string. Moving the undefined -> empty string back into |
| // urlHistory.addNew seemed imprudent given undefined better represents |
| // the url state |
| |
| // If we are at a place in history that once belonged to a dialog, reuse |
| // this state without adding to urlHistory and without modifying the hash. |
| // However, if a dialog is already displayed at this point, and we're |
| // about to display another dialog, then we must add another hash and |
| // history entry on top so that one may navigate back to the original dialog |
| if ( active.url.indexOf( dialogHashKey ) > -1 && !$.mobile.activePage.is( ".ui-dialog" ) ) { |
| settings.changeHash = false; |
| alreadyThere = true; |
| } |
| |
| // Normally, we tack on a dialog hash key, but if this is the location of a stale dialog, |
| // we reuse the URL from the entry |
| url = ( active.url || "" ) + ( alreadyThere ? "" : dialogHashKey ); |
| |
| // tack on another dialogHashKey if this is the same as the initial hash |
| // this makes sure that a history entry is created for this dialog |
| if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { |
| url += dialogHashKey; |
| } |
| } |
| |
| // Set the location hash. |
| if ( settings.changeHash !== false && url ) { |
| //disable hash listening temporarily |
| urlHistory.ignoreNextHashChange = true; |
| //update hash and history |
| path.set( url ); |
| } |
| |
| // if title element wasn't found, try the page div data attr too |
| // If this is a deep-link or a reload ( active === undefined ) then just use pageTitle |
| var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText(); |
| if ( !!newPageTitle && pageTitle === document.title ) { |
| pageTitle = newPageTitle; |
| } |
| if ( !toPage.jqmData( "title" ) ) { |
| toPage.jqmData( "title", pageTitle ); |
| } |
| |
| // Make sure we have a transition defined. |
| settings.transition = settings.transition || |
| ( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) || |
| ( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition ); |
| |
| //add page to history stack if it's not back or forward |
| if ( !historyDir ) { |
| // Overwrite the current entry if it's a leftover from a dialog |
| if ( alreadyThere ) { |
| urlHistory.activeIndex = Math.max( 0, urlHistory.activeIndex - 1 ); |
| } |
| urlHistory.addNew( url, settings.transition, pageTitle, pageUrl, settings.role ); |
| } |
| |
| //set page title |
| document.title = urlHistory.getActive().title; |
| |
| //set "toPage" as activePage |
| $.mobile.activePage = toPage; |
| |
| // If we're navigating back in the URL history, set reverse accordingly. |
| settings.reverse = settings.reverse || historyDir < 0; |
| |
| transitionPages( toPage, fromPage, settings.transition, settings.reverse ) |
| .done(function( name, reverse, $to, $from, alreadyFocused ) { |
| removeActiveLinkClass(); |
| |
| //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden |
| if ( settings.duplicateCachedPage ) { |
| settings.duplicateCachedPage.remove(); |
| } |
| |
| // Send focus to the newly shown page. Moved from promise .done binding in transitionPages |
| // itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility) |
| // despite visibility: hidden addresses issue #2965 |
| // https://github.com/jquery/jquery-mobile/issues/2965 |
| if ( !alreadyFocused ) { |
| $.mobile.focusPage( toPage ); |
| } |
| |
| releasePageTransitionLock(); |
| |
| // Let listeners know we're all done changing the current page. |
| mpc.trigger( "pagechange", triggerData ); |
| }); |
| }; |
| |
| $.mobile.changePage.defaults = { |
| transition: undefined, |
| reverse: false, |
| changeHash: true, |
| fromHashChange: false, |
| role: undefined, // By default we rely on the role defined by the @data-role attribute. |
| duplicateCachedPage: undefined, |
| pageContainer: undefined, |
| showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage |
| dataUrl: undefined, |
| fromPage: undefined, |
| allowSamePageTransition: false |
| }; |
| |
| /* Event Bindings - hashchange, submit, and click */ |
| function findClosestLink( ele ) |
| { |
| while ( ele ) { |
| // Look for the closest element with a nodeName of "a". |
| // Note that we are checking if we have a valid nodeName |
| // before attempting to access it. This is because the |
| // node we get called with could have originated from within |
| // an embedded SVG document where some symbol instance elements |
| // don't have nodeName defined on them, or strings are of type |
| // SVGAnimatedString. |
| if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) { |
| break; |
| } |
| ele = ele.parentNode; |
| } |
| return ele; |
| } |
| |
| // The base URL for any given element depends on the page it resides in. |
| function getClosestBaseUrl( ele ) |
| { |
| // Find the closest page and extract out its url. |
| var url = $( ele ).closest( ".ui-page" ).jqmData( "url" ), |
| base = documentBase.hrefNoHash; |
| |
| if ( !url || !path.isPath( url ) ) { |
| url = base; |
| } |
| |
| return path.makeUrlAbsolute( url, base); |
| } |
| |
| //The following event bindings should be bound after mobileinit has been triggered |
| //the following deferred is resolved in the init file |
| $.mobile.navreadyDeferred = $.Deferred(); |
| $.mobile.navreadyDeferred.done(function() { |
| //bind to form submit events, handle with Ajax |
| $.mobile.$document.delegate( "form", "submit", function( event ) { |
| var $this = $( this ); |
| |
| if ( !$.mobile.ajaxEnabled || |
| // test that the form is, itself, ajax false |
| $this.is( ":jqmData(ajax='false')" ) || |
| // test that $.mobile.ignoreContentEnabled is set and |
| // the form or one of it's parents is ajax=false |
| !$this.jqmHijackable().length ) { |
| return; |
| } |
| |
| var type = $this.attr( "method" ), |
| target = $this.attr( "target" ), |
| url = $this.attr( "action" ); |
| |
| // If no action is specified, browsers default to using the |
| // URL of the document containing the form. Since we dynamically |
| // pull in pages from external documents, the form should submit |
| // to the URL for the source document of the page containing |
| // the form. |
| if ( !url ) { |
| // Get the @data-url for the page containing the form. |
| url = getClosestBaseUrl( $this ); |
| if ( url === documentBase.hrefNoHash ) { |
| // The url we got back matches the document base, |
| // which means the page must be an internal/embedded page, |
| // so default to using the actual document url as a browser |
| // would. |
| url = documentUrl.hrefNoSearch; |
| } |
| } |
| |
| url = path.makeUrlAbsolute( url, getClosestBaseUrl( $this ) ); |
| |
| if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) { |
| return; |
| } |
| |
| $.mobile.changePage( |
| url, |
| { |
| type: type && type.length && type.toLowerCase() || "get", |
| data: $this.serialize(), |
| transition: $.mobile.getAttrFixed( $this [0], "data-" + $.mobile.ns + "transition" ), |
| reverse: $.mobile.getAttrFixed( $this [0], "data-" + $.mobile.ns + "direction" ) === "reverse", |
| reloadPage: true |
| } |
| ); |
| event.preventDefault(); |
| }); |
| |
| //add active state on vclick |
| $.mobile.$document.bind( "vclick", function( event ) { |
| // if this isn't a left click we don't care. Its important to note |
| // that when the virtual event is generated it will create the which attr |
| if ( event.which > 1 || !$.mobile.linkBindingEnabled ) { |
| return; |
| } |
| |
| var link = findClosestLink( event.target ); |
| |
| // split from the previous return logic to avoid find closest where possible |
| // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping |
| // can be avoided |
| if ( !$( link ).jqmHijackable().length ) { |
| return; |
| } |
| |
| if ( link ) { |
| if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) { |
| removeActiveLinkClass( true ); |
| $activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" ); |
| $activeClickedLink.addClass( $.mobile.activeBtnClass ); |
| } |
| } |
| }); |
| |
| // click routing - direct to HTTP or Ajax, accordingly |
| $.mobile.$document.bind( "click", function( event ) { |
| if ( !$.mobile.linkBindingEnabled ) { |
| return; |
| } |
| |
| var link = findClosestLink( event.target ), $link = $( link ), httpCleanup; |
| |
| // If there is no link associated with the click or its not a left |
| // click we want to ignore the click |
| // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping |
| // can be avoided |
| if ( !link || event.which > 1 || !$link.jqmHijackable().length ) { |
| return; |
| } |
| |
| //remove active link class if external (then it won't be there if you come back) |
| httpCleanup = function() { |
| window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 ); |
| }; |
| |
| //if there's a data-rel=back attr, go back in history |
| if ( $link.is( ":jqmData(rel='back')" ) ) { |
| $.mobile.back(); |
| return false; |
| } |
| |
| var baseUrl = getClosestBaseUrl( $link ), |
| |
| //get href, if defined, otherwise default to empty hash |
| href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl ); |
| |
| //if ajax is disabled, exit early |
| if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) { |
| httpCleanup(); |
| //use default click handling |
| return; |
| } |
| |
| // XXX_jblas: Ideally links to application pages should be specified as |
| // an url to the application document with a hash that is either |
| // the site relative path or id to the page. But some of the |
| // internal code that dynamically generates sub-pages for nested |
| // lists and select dialogs, just write a hash in the link they |
| // create. This means the actual URL path is based on whatever |
| // the current value of the base tag is at the time this code |
| // is called. For now we are just assuming that any url with a |
| // hash in it is an application page reference. |
| if ( href.search( "#" ) !== -1 ) { |
| href = href.replace( /[^#]*#/, "" ); |
| if ( !href ) { |
| //link was an empty hash meant purely |
| //for interaction, so we ignore it. |
| event.preventDefault(); |
| return; |
| } else if ( path.isPath( href ) ) { |
| //we have apath so make it the href we want to load. |
| href = path.makeUrlAbsolute( href, baseUrl ); |
| } else { |
| //we have a simple id so use the documentUrl as its base. |
| href = path.makeUrlAbsolute( "#" + href, documentUrl.hrefNoHash ); |
| } |
| } |
| |
| // Should we handle this link, or let the browser deal with it? |
| var useDefaultUrlHandling = $link.is( "[rel='external']" ) || $link.is( ":jqmData(ajax='false')" ) || $link.is( "[target]" ), |
| |
| // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR |
| // requests if the document doing the request was loaded via the file:// protocol. |
| // This is usually to allow the application to "phone home" and fetch app specific |
| // data. We normally let the browser handle external/cross-domain urls, but if the |
| // allowCrossDomainPages option is true, we will allow cross-domain http/https |
| // requests to go through our page loading logic. |
| |
| //check for protocol or rel and its not an embedded page |
| //TODO overlap in logic from isExternal, rel=external check should be |
| // moved into more comprehensive isExternalLink |
| isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) ); |
| |
| if ( isExternal ) { |
| httpCleanup(); |
| //use default click handling |
| return; |
| } |
| |
| //use ajax |
| var transition = $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "transition" ), |
| reverse = $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "direction" ) === "reverse" || |
| // deprecated - remove by 1.0 |
| $.mobile.getAttrFixed( $link [0], "data-" + $.mobile.ns + "back" ), |
| |
| //this may need to be more specific as we use data-rel more |
| role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined; |
| |
| $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } ); |
| event.preventDefault(); |
| }); |
| |
| //prefetch pages when anchors with data-prefetch are encountered |
| $.mobile.$document.delegate( ".ui-page", "pageshow.prefetch", function() { |
| var urls = []; |
| $( this ).find( "a:jqmData(prefetch)" ).each(function() { |
| var $link = $( this ), |
| url = $link.attr( "href" ); |
| |
| if ( url && $.inArray( url, urls ) === -1 ) { |
| urls.push( url ); |
| |
| $.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ),prefetch: true } ); |
| } |
| }); |
| }); |
| |
| $.mobile._handleHashChange = function( hash ) { |
| //find first page via hash |
| var to = path.stripHash( hash ), |
| //transition is false if it's the first page, undefined otherwise (and may be overridden by default) |
| transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined, |
| |
| // "navigate" event fired to allow others to take advantage of the more robust hashchange handling |
| navEvent = new $.Event( "navigate" ), |
| |
| // default options for the changPage calls made after examining the current state |
| // of the page and the hash |
| changePageOptions = { |
| transition: transition, |
| changeHash: false, |
| fromHashChange: true |
| }; |
| |
| if ( 0 === urlHistory.stack.length ) { |
| urlHistory.initialDst = to; |
| } |
| |
| // We should probably fire the "navigate" event from those places that make calls to _handleHashChange, |
| // and have _handleHashChange hook into the "navigate" event instead of triggering it here |
| $.mobile.pageContainer.trigger( navEvent ); |
| if ( navEvent.isDefaultPrevented() ) { |
| return; |
| } |
| |
| //if listening is disabled (either globally or temporarily), or it's a dialog hash |
| if ( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) { |
| urlHistory.ignoreNextHashChange = false; |
| return; |
| } |
| |
| // special case for dialogs |
| if ( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) { |
| |
| // If current active page is not a dialog skip the dialog and continue |
| // in the same direction |
| if ( !$.mobile.activePage.is( ".ui-dialog" ) ) { |
| //determine if we're heading forward or backward and continue accordingly past |
| //the current dialog |
| urlHistory.directHashChange({ |
| currentUrl: to, |
| isBack: function() { $.mobile.back(); }, |
| isForward: function() { window.history.forward(); } |
| }); |
| |
| // prevent changePage() |
| return; |
| } else { |
| // if the current active page is a dialog and we're navigating |
| // to a dialog use the dialog objected saved in the stack |
| urlHistory.directHashChange({ |
| currentUrl: to, |
| |
| // regardless of the direction of the history change |
| // do the following |
| either: function( isBack ) { |
| var active = $.mobile.urlHistory.getActive(); |
| |
| to = active.pageUrl; |
| |
| // make sure to set the role, transition and reversal |
| // as most of this is lost by the domCache cleaning |
| $.extend( changePageOptions, { |
| role: active.role, |
| transition: active.transition, |
| reverse: isBack |
| }); |
| } |
| }); |
| } |
| } |
| |
| //if to is defined, load it |
| if ( to ) { |
| // At this point, 'to' can be one of 3 things, a cached page element from |
| // a history stack entry, an id, or site-relative/absolute URL. If 'to' is |
| // an id, we need to resolve it against the documentBase, not the location.href, |
| // since the hashchange could've been the result of a forward/backward navigation |
| // that crosses from an external page/dialog to an internal page/dialog. |
| to = ( typeof to === "string" && !path.isPath( to ) ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to; |
| |
| // If we're about to go to an initial URL that contains a reference to a non-existent |
| // internal page, go to the first page instead. We know that the initial hash refers to a |
| // non-existent page, because the initial hash did not end up in the initial urlHistory entry |
| if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) && |
| urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) { |
| to = $.mobile.firstPage; |
| } |
| $.mobile.changePage( to, changePageOptions ); |
| } else { |
| //there's no hash, go to the first page in the dom |
| $.mobile.changePage( $.mobile.firstPage, changePageOptions ); |
| } |
| }; |
| |
| //hashchange event handler |
| $window.bind( "hashchange", function( e, triggered ) { |
| // Firefox auto-escapes the location.hash as for v13 but |
| // leaves the href untouched |
| $.mobile._handleHashChange( path.parseLocation().hash ); |
| }); |
| |
| });//navreadyDeferred done callback |
| |
| })( jQuery ); |
| |
| (function( $, window ) { |
| // For now, let's Monkeypatch this onto the end of $.mobile._registerInternalEvents |
| // Scope self to pushStateHandler so we can reference it sanely within the |
| // methods handed off as event handlers |
| var pushStateHandler = {}, |
| self = pushStateHandler, |
| $win = $.mobile.$window, |
| url = $.mobile.path.parseLocation(), |
| mobileinitDeferred = $.Deferred(), |
| domreadyDeferred = $.Deferred(); |
| |
| $.mobile.$document.ready( $.proxy( domreadyDeferred, "resolve" ) ); |
| |
| $.mobile.$document.one( "mobileinit", $.proxy( mobileinitDeferred, "resolve" ) ); |
| |
| $.extend( pushStateHandler, { |
| // TODO move to a path helper, this is rather common functionality |
| initialFilePath: (function() { |
| return url.pathname + url.search; |
| })(), |
| |
| hashChangeTimeout: 200, |
| |
| hashChangeEnableTimer: undefined, |
| |
| initialHref: url.hrefNoHash, |
| |
| state: function() { |
| return { |
| // firefox auto decodes the url when using location.hash but not href |
| hash: $.mobile.path.parseLocation().hash || "#" + self.initialFilePath, |
| title: document.title, |
| |
| // persist across refresh |
| initialHref: self.initialHref |
| }; |
| }, |
| |
| resetUIKeys: function( url ) { |
| var dialog = $.mobile.dialogHashKey, |
| subkey = "&" + $.mobile.subPageUrlKey, |
| dialogIndex = url.indexOf( dialog ); |
| |
| if ( dialogIndex > -1 ) { |
| url = url.slice( 0, dialogIndex ) + "#" + url.slice( dialogIndex ); |
| } else if ( url.indexOf( subkey ) > -1 ) { |
| url = url.split( subkey ).join( "#" + subkey ); |
| } |
| |
| return url; |
| }, |
| |
| // TODO sort out a single barrier to hashchange functionality |
| nextHashChangePrevented: function( value ) { |
| $.mobile.urlHistory.ignoreNextHashChange = value; |
| self.onHashChangeDisabled = value; |
| }, |
| |
| // on hash change we want to clean up the url |
| // NOTE this takes place *after* the vanilla navigation hash change |
| // handling has taken place and set the state of the DOM |
| onHashChange: function( e ) { |
| // disable this hash change |
| if ( self.onHashChangeDisabled ) { |
| return; |
| } |
| |
| var href, state, |
| // firefox auto decodes the url when using location.hash but not href |
| hash = $.mobile.path.parseLocation().hash, |
| isPath = $.mobile.path.isPath( hash ), |
| resolutionUrl = isPath ? $.mobile.path.getLocation() : $.mobile.getDocumentUrl(); |
| |
| hash = isPath ? hash.replace( "#", "" ) : hash; |
| |
| |
| // propulate the hash when its not available |
| state = self.state(); |
| |
| // make the hash abolute with the current href |
| href = $.mobile.path.makeUrlAbsolute( hash, resolutionUrl ); |
| |
| if ( isPath ) { |
| href = self.resetUIKeys( href ); |
| } |
| |
| // replace the current url with the new href and store the state |
| // Note that in some cases we might be replacing an url with the |
| // same url. We do this anyways because we need to make sure that |
| // all of our history entries have a state object associated with |
| // them. This allows us to work around the case where $.mobile.back() |
| // is called to transition from an external page to an embedded page. |
| // In that particular case, a hashchange event is *NOT* generated by the browser. |
| // Ensuring each history entry has a state object means that onPopState() |
| // will always trigger our hashchange callback even when a hashchange event |
| // is not fired. |
| history.replaceState( state, document.title, href ); |
| }, |
| |
| // on popstate (ie back or forward) we need to replace the hash that was there previously |
| // cleaned up by the additional hash handling |
| onPopState: function( e ) { |
| var poppedState = e.originalEvent.state, |
| fromHash, toHash, hashChanged; |
| |
| // if there's no state its not a popstate we care about, eg chrome's initial popstate |
| if ( poppedState ) { |
| // if we get two pop states in under this.hashChangeTimeout |
| // make sure to clear any timer set for the previous change |
| clearTimeout( self.hashChangeEnableTimer ); |
| |
| // make sure to enable hash handling for the the _handleHashChange call |
| self.nextHashChangePrevented( false ); |
| |
| // change the page based on the hash in the popped state |
| $.mobile._handleHashChange( poppedState.hash ); |
| |
| // prevent any hashchange in the next self.hashChangeTimeout |
| self.nextHashChangePrevented( true ); |
| |
| // re-enable hash change handling after swallowing a possible hash |
| // change event that comes on all popstates courtesy of browsers like Android |
| self.hashChangeEnableTimer = setTimeout( function() { |
| self.nextHashChangePrevented( false ); |
| }, self.hashChangeTimeout ); |
| } |
| }, |
| |
| init: function() { |
| $win.bind( "hashchange", self.onHashChange ); |
| |
| // Handle popstate events the occur through history changes |
| $win.bind( "popstate", self.onPopState ); |
| |
| // if there's no hash, we need to replacestate for returning to home |
| if ( location.hash === "" ) { |
| history.replaceState( self.state(), document.title, $.mobile.path.getLocation() ); |
| } |
| } |
| }); |
| |
| // We need to init when "mobileinit", "domready", and "navready" have all happened |
| $.when( domreadyDeferred, mobileinitDeferred, $.mobile.navreadyDeferred ).done(function() { |
| if ( $.mobile.pushStateEnabled && $.support.pushState ) { |
| pushStateHandler.init(); |
| } |
| }); |
| })( jQuery, this ); |
| |
| /* |
| * fallback transition for flip in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.flip = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for flow in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.flow = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for pop in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.pop = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slide in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| // Use the simultaneous transitions handler for slide transitions |
| $.mobile.transitionHandlers.slide = $.mobile.transitionHandlers.simultaneous; |
| |
| // Set the slide transitions's fallback to "fade" |
| $.mobile.transitionFallbacks.slide = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slidedown in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.slidedown = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slidefade in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| // Set the slide transitions's fallback to "fade" |
| $.mobile.transitionFallbacks.slidefade = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for slideup in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.slideup = "fade"; |
| |
| })( jQuery, this ); |
| /* |
| * fallback transition for turn in non-3D supporting browsers (which tend to handle complex transitions poorly in general |
| */ |
| |
| (function( $, window, undefined ) { |
| |
| $.mobile.transitionFallbacks.turn = "fade"; |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.page.prototype.options.degradeInputs = { |
| color: false, |
| date: false, |
| datetime: false, |
| "datetime-local": false, |
| email: false, |
| month: false, |
| number: false, |
| range: "number", |
| search: "text", |
| tel: false, |
| time: false, |
| url: false, |
| week: false |
| }; |
| |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| var page = $.mobile.closestPageData( $( e.target ) ), options; |
| |
| if ( !page ) { |
| return; |
| } |
| |
| options = page.options; |
| |
| // degrade inputs to avoid poorly implemented native functionality |
| $( e.target ).find( "input" ).not( page.keepNativeSelector() ).each(function() { |
| var $this = $( this ), |
| type = this.getAttribute( "type" ), |
| optType = options.degradeInputs[ type ] || "text"; |
| |
| if ( options.degradeInputs[ type ] ) { |
| var html = $( "<div>" ).html( $this.clone() ).html(), |
| // In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead |
| hasType = html.indexOf( " type=" ) > -1, |
| findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/, |
| repstr = " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\"" + ( hasType ? "" : ">" ); |
| |
| $this.replaceWith( html.replace( findstr, repstr ) ); |
| } |
| }); |
| |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, window, undefined ) { |
| |
| $.widget( "mobile.dialog", $.mobile.widget, { |
| options: { |
| closeBtnText: "Close", |
| overlayTheme: "a", |
| initSelector: ":jqmData(role='dialog')" |
| }, |
| _create: function() { |
| var self = this, |
| $el = this.element, |
| headerCloseButton = $( "<a href='#' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" ), |
| dialogWrap = $( "<div/>", { |
| "role" : "dialog", |
| "class" : "ui-dialog-contain ui-corner-all ui-overlay-shadow" |
| }); |
| |
| $el.addClass( "ui-dialog ui-overlay-" + this.options.overlayTheme ); |
| |
| // Class the markup for dialog styling |
| // Set aria role |
| $el |
| .wrapInner( dialogWrap ) |
| .children() |
| .find( ":jqmData(role='header')" ) |
| .prepend( headerCloseButton ) |
| .end() |
| .children( ':first-child') |
| .addClass( "ui-corner-top" ) |
| .end() |
| .children( ":last-child" ) |
| .addClass( "ui-corner-bottom" ); |
| |
| // this must be an anonymous function so that select menu dialogs can replace |
| // the close method. This is a change from previously just defining data-rel=back |
| // on the button and letting nav handle it |
| // |
| // Use click rather than vclick in order to prevent the possibility of unintentionally |
| // reopening the dialog if the dialog opening item was directly under the close button. |
| headerCloseButton.bind( "click", function() { |
| self.close(); |
| }); |
| |
| /* bind events |
| - clicks and submits should use the closing transition that the dialog opened with |
| unless a data-transition is specified on the link/form |
| - if the click was on the close button, or the link has a data-rel="back" it'll go back in history naturally |
| */ |
| $el.bind( "vclick submit", function( event ) { |
| var $target = $( event.target ).closest( event.type === "vclick" ? "a" : "form" ), |
| active; |
| |
| if ( $target.length && !$.mobile.getAttrFixed( $target[0], "data-" + $.mobile.ns + "transition" ) ) { |
| |
| active = $.mobile.urlHistory.getActive() || {}; |
| |
| $target.attr( "data-" + $.mobile.ns + "transition", ( active.transition || $.mobile.defaultDialogTransition ) ) |
| .attr( "data-" + $.mobile.ns + "direction", "reverse" ); |
| } |
| }) |
| .bind( "pagehide", function( e, ui ) { |
| $( this ).find( "." + $.mobile.activeBtnClass ).not( ".ui-slider-bg" ).removeClass( $.mobile.activeBtnClass ); |
| }) |
| // Override the theme set by the page plugin on pageshow |
| .bind( "pagebeforeshow", function() { |
| self._isCloseable = true; |
| if ( self.options.overlayTheme ) { |
| self.element |
| .page( "removeContainerBackground" ) |
| .page( "setContainerBackground", self.options.overlayTheme ); |
| } |
| }); |
| }, |
| |
| // Close method goes back in history |
| close: function() { |
| var dst; |
| |
| if ( this._isCloseable ) { |
| this._isCloseable = false; |
| if ( $.mobile.hashListeningEnabled ) { |
| $.mobile.back(); |
| } else { |
| dst = $.mobile.urlHistory.getPrev().url; |
| if ( !$.mobile.path.isPath( dst ) ) { |
| dst = $.mobile.path.makeUrlAbsolute( "#" + dst ); |
| } |
| |
| $.mobile.changePage( dst, { changeHash: false, fromHashChange: true } ); |
| } |
| } |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.delegate( $.mobile.dialog.prototype.options.initSelector, "pagecreate", function() { |
| $.mobile.dialog.prototype.enhance( this ); |
| }); |
| |
| })( jQuery, this ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.page.prototype.options.backBtnText = "Back"; |
| $.mobile.page.prototype.options.addBackBtn = false; |
| $.mobile.page.prototype.options.backBtnTheme = null; |
| $.mobile.page.prototype.options.headerTheme = "a"; |
| $.mobile.page.prototype.options.footerTheme = "a"; |
| $.mobile.page.prototype.options.contentTheme = null; |
| |
| // NOTE bind used to force this binding to run before the buttonMarkup binding |
| // which expects .ui-footer top be applied in its gigantic selector |
| // TODO remove the buttonMarkup giant selector and move it to the various modules |
| // on which it depends |
| $.mobile.$document.bind( "pagecreate", function( e ) { |
| var $page = $( e.target ), |
| o = $page.data( "page" ).options, |
| prefix = "data-"+$.mobile.ns, |
| pageRole = $page[0].getAttribute( prefix + "role" ) || undefined, |
| pageTheme = o.theme; |
| |
| $( ":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')", $page ) |
| .jqmEnhanceable() |
| .each(function() { |
| |
| var $this = $( this ), |
| role = $this[0].getAttribute( prefix + "role" ) || undefined, |
| theme = $this[0].getAttribute( prefix + "theme" ) || undefined, |
| contentTheme = theme || o.contentTheme || ( pageRole === "dialog" && pageTheme ), |
| $headeranchors, |
| leftbtn, |
| rightbtn, |
| $dest = $page.find( ".ui-footer" ), |
| backBtn; |
| |
| $this.addClass( "ui-" + role ); |
| |
| //apply theming and markup modifications to page,header,content,footer |
| if ( role === "header" || role === "footer" ) { |
| |
| var thisTheme = theme || ( role === "header" ? o.headerTheme : o.footerTheme ) || pageTheme; |
| |
| $this |
| //add theme class |
| .addClass( "ui-bar-" + thisTheme ) |
| // Add ARIA role |
| .attr( "role", role === "header" ? "banner" : "contentinfo" ); |
| |
| if ( role === "header") { |
| // Right,left buttons |
| $headeranchors = $this.children( "a, div.naviframe-button, a.naviframe-button, button" ); |
| leftbtn = $headeranchors.hasClass( "ui-btn-left" ); |
| rightbtn = $headeranchors.hasClass( "ui-btn-right" ); |
| |
| leftbtn = leftbtn || $headeranchors.eq( 0 ).not( ".ui-btn-right" ).addClass( "ui-btn-left" ).length; |
| |
| rightbtn = rightbtn || $headeranchors.eq( 1 ).addClass( "ui-btn-right" ).length; |
| |
| $( $headeranchors.get().reverse() ).each( function ( i ) { |
| $( this ).addClass( "ui-btn-right-" + i ); |
| }); |
| } |
| |
| // Auto-add back btn on pages beyond first view |
| if ( o.addBackBtn && |
| ( role === "footer" || role === "header" ) && |
| $page[0].getAttribute( prefix + "url" ) !== $.mobile.path.stripHash( location.hash ) && |
| !leftbtn ) { |
| |
| if ( o.addBackBtn == "header" ) { |
| $dest = $page.find( ".ui-header" ); |
| } else { |
| $dest = $page.find( ".ui-footer" ); |
| } |
| |
| if ( !$dest.find( ".ui-btn-back" ).length ) { |
| backBtn = $( "<a href='javascript:void(0);' class='ui-btn-back' data-" + $.mobile.ns + "rel='back'></a>" ) |
| // // If theme is provided, override default inheritance |
| .buttonMarkup( { icon: "header-back-btn", theme: o.backBtnTheme || thisTheme } ); |
| |
| backBtn.find( ".ui-btn-text" ).text( o.backBtnText ); |
| backBtn.appendTo( $dest ); |
| } |
| } |
| |
| // Page title |
| $this.children( "h1, h2, h3, h4, h5, h6" ) |
| .addClass( "ui-title" ) |
| // Regardless of h element number in src, it becomes h1 for the enhanced page |
| .attr({ |
| "role": "heading", |
| "aria-level": "1", |
| "aria-label": "title", |
| "tabindex": "0" |
| }); |
| |
| $( ".ui-title-text-sub" ).attr( { "tabindex": "0", "aria-label": "subtitle" } ); |
| |
| } else if ( role === "content" ) { |
| if ( contentTheme ) { |
| $this.addClass( "ui-body-" + ( contentTheme ) ); |
| } |
| |
| // Add ARIA role |
| $this.attr( "role", "main" ); |
| } |
| }); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| // filter function removes whitespace between label and form element so we can use inline-block (nodeType 3 = text) |
| $.fn.fieldcontain = function( options ) { |
| return this |
| .addClass( "ui-field-contain ui-body ui-br" ) |
| .contents().filter( function() { |
| return ( this.nodeType === 3 && !/\S/.test( this.nodeValue ) ); |
| }).remove(); |
| }; |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='fieldcontain')", e.target ).jqmEnhanceable().fieldcontain(); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.grid = function( options ) { |
| return this.each(function() { |
| |
| var $this = $( this ), |
| o = $.extend({ |
| grid: null |
| }, options ), |
| $kids = $this.children(), |
| gridCols = { solo:1, a:2, b:3, c:4, d:5 }, |
| grid = o.grid, |
| iterator; |
| |
| if ( !grid ) { |
| if ( $kids.length <= 5 ) { |
| for ( var letter in gridCols ) { |
| if ( gridCols[ letter ] === $kids.length ) { |
| grid = letter; |
| } |
| } |
| } else { |
| grid = "a"; |
| $this.addClass( "ui-grid-duo" ); |
| } |
| } |
| iterator = gridCols[grid]; |
| |
| $this.addClass( "ui-grid-" + grid ); |
| |
| $kids.filter( ":nth-child(" + iterator + "n+1)" ).addClass( "ui-block-a" ); |
| |
| if ( iterator > 1 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+2)" ).addClass( "ui-block-b" ); |
| } |
| if ( iterator > 2 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+3)" ).addClass( "ui-block-c" ); |
| } |
| if ( iterator > 3 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+4)" ).addClass( "ui-block-d" ); |
| } |
| if ( iterator > 4 ) { |
| $kids.filter( ":nth-child(" + iterator + "n+5)" ).addClass( "ui-block-e" ); |
| } |
| }); |
| }; |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='nojs')", e.target ).addClass( "ui-nojs" ); |
| |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.buttonMarkup = function( options ) { |
| var $workingSet = this, |
| prefix = "data-" + $.mobile.ns, |
| mapToDataAttr = function( key, value ) { |
| e.setAttribute( "data-" + $.mobile.ns + key, value ); |
| el.jqmData( key, value ); |
| }; |
| |
| // Enforce options to be of type string |
| options = ( options && ( $.type( options ) === "object" ) )? options : {}; |
| for ( var i = 0; i < $workingSet.length; i++ ) { |
| var el = $workingSet.eq( i ), |
| e = el[ 0 ], |
| o = $.extend( {}, $.fn.buttonMarkup.defaults, { |
| icon: options.icon !== undefined ? options.icon : $.mobile.getAttrFixed( e, prefix + "icon" ), |
| iconpos: options.iconpos !== undefined ? options.iconpos : $.mobile.getAttrFixed( e, prefix + "iconpos" ), |
| theme: options.theme !== undefined ? options.theme : $.mobile.getAttrFixed( e, prefix + "theme" ) || $.mobile.getInheritedTheme( el, $.fn.buttonMarkup.defaults["theme"] ), |
| inline: options.inline !== undefined ? options.inline : $.mobile.getAttrFixed( e, prefix + "inline" ), |
| shadow: options.shadow !== undefined ? options.shadow : $.mobile.getAttrFixed( e, prefix + "shadow" ), |
| corners: options.corners !== undefined ? options.corners : $.mobile.getAttrFixed( e, prefix + "corners" ), |
| iconshadow: options.iconshadow !== undefined ? options.iconshadow : $.mobile.getAttrFixed( e, prefix + "iconshadow" ), |
| mini: options.mini !== undefined ? options.mini : $.mobile.getAttrFixed( e, prefix + "mini" ) |
| }, options ), |
| |
| // Classes Defined |
| innerClass = "ui-btn-inner", |
| textClass = "ui-btn-text", |
| buttonClass, iconClass, |
| // Button inner markup |
| buttonInner, |
| buttonText, |
| buttonIcon, |
| buttonElements; |
| |
| for ( key in o ) { |
| e.setAttribute ( prefix + key, o[ key ]) |
| } |
| |
| if ( $.mobile.getAttrFixed( e, prefix + "rel" ) === "popup" && el.attr( "href" ) ) { |
| e.setAttribute( "aria-haspopup", true ); |
| e.setAttribute( "aria-owns", e.getAttribute( "href" ) ); |
| } |
| |
| if ( e.tagName !== "LI" && e.tagName !== "LABEL" ) { |
| e.setAttribute( "role", "button" ); |
| e.setAttribute( "tabindex", "0" ); |
| } |
| |
| // Check if this element is already enhanced |
| buttonElements = $.data( ( ( e.tagName === "INPUT" || e.tagName === "BUTTON" ) ? e.parentNode : e ), "buttonElements" ); |
| |
| if ( buttonElements ) { |
| e = buttonElements.outer; |
| el = $( e ); |
| buttonInner = buttonElements.inner; |
| buttonText = buttonElements.text; |
| // We will recreate this icon below |
| $( buttonElements.icon ).remove(); |
| buttonElements.icon = null; |
| } |
| else { |
| buttonInner = document.createElement( o.wrapperEls ); |
| buttonText = document.createElement( o.wrapperEls ); |
| } |
| buttonIcon = o.icon ? document.createElement( "span" ) : null; |
| |
| if ( attachEvents && !buttonElements ) { |
| attachEvents(); |
| } |
| |
| // if not, try to find closest theme container |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( el, "c" ); |
| } |
| |
| buttonClass = "ui-btn ui-btn-up-" + o.theme; |
| buttonClass += o.shadow ? " ui-shadow" : ""; |
| buttonClass += o.corners ? " ui-btn-corner-all" : ""; |
| |
| // To distinguish real buttons |
| if( $.mobile.getAttrFixed( e, prefix + "role" ) == "button" || e.tagName == "BUTTON" || e.tagName == "DIV" ){ |
| buttonClass += " ui-btn-box-" + o.theme; |
| } |
| |
| /* TIZEN style markup */ |
| var buttonStyle = $.mobile.getAttrFixed( e, prefix + "style" ); |
| |
| if ( buttonStyle == "circle" && !($(el).text().length > 0) ) { |
| /* style : no text, Icon only */ |
| buttonClass += " ui-btn-corner-circle"; |
| buttonClass += " ui-btn-icon_only"; |
| } else if ( buttonStyle == "nobg" ) { |
| /* style : no text, Icon only, no bg */ |
| buttonClass += " ui-btn-icon-nobg"; |
| buttonClass += " ui-btn-icon_only"; |
| } else if ( buttonStyle == "edit" ) { |
| buttonClass += " ui-btn-edit"; |
| } else if ( buttonStyle == "round" || ( buttonStyle == "circle" && $(el).text().length > 0 ) ) { |
| buttonClass += " ui-btn-round"; |
| } |
| if ( o.icon ) { |
| if ( $(el).text().length > 0 ) { |
| |
| switch ( o.iconpos ) { |
| case "right" : |
| case "left" : |
| case "top" : |
| case "bottom" : |
| textClass += " ui-btn-text-padding-" + o.iconpos; |
| break; |
| default: |
| textClass += " ui-btn-text-padding-left"; |
| break; |
| } |
| |
| innerClass += " ui-btn-hastxt"; |
| } else { |
| if ( buttonStyle == "circle" ) { |
| /* style : no text, Icon only */ |
| innerClass += " ui-btn-corner-circle"; |
| } else if ( buttonStyle == "nobg" ) { |
| /* style : no text, Icon only, no bg */ |
| innerClass += " ui-btn-icon-nobg"; |
| } |
| |
| buttonClass += " ui-btn-icon_only"; |
| innerClass += " ui-btn-icon-only"; |
| |
| if ( e.tagName !== "LABEL" ) { |
| $( el ).text( o.icon.replace( "naviframe-", "" ) ); |
| } |
| } |
| } else { |
| if ( $(el).text().length > 0 ) { |
| innerClass += " ui-btn-hastxt"; |
| } else if ( buttonStyle == "circle" ){ |
| buttonClass += " ui-btn-round"; |
| } |
| } |
| if ( o.mini !== undefined ) { |
| // Used to control styling in headers/footers, where buttons default to `mini` style. |
| buttonClass += o.mini === true ? " ui-mini" : " ui-fullsize"; |
| } |
| |
| if ( o.inline !== undefined ) { |
| // Used to control styling in headers/footers, where buttons default to `inline` style. |
| buttonClass += o.inline === true ? " ui-btn-inline" : " ui-btn-block"; |
| } |
| |
| if ( o.icon ) { |
| o.icon = "ui-icon-" + o.icon; |
| o.iconpos = o.iconpos || "left"; |
| |
| iconClass = "ui-icon " + o.icon; |
| |
| if ( o.iconshadow ) { |
| iconClass += " ui-icon-shadow"; |
| } |
| } |
| |
| if ( o.iconpos ) { |
| buttonClass += " ui-btn-icon-" + o.iconpos; |
| |
| if ( o.iconpos === "notext" && !el.attr( "title" ) ) { |
| el.attr( "title", el.getEncodedText() ); |
| } |
| } |
| |
| innerClass += o.corners ? " ui-btn-corner-all" : ""; |
| |
| if ( o.iconpos && o.iconpos === "notext" && !el.attr( "title" ) ) { |
| el.attr( "title", el.getEncodedText() ); |
| } |
| |
| if ( buttonElements ) { |
| el.removeClass( buttonElements.bcls || "" ); |
| } |
| el.removeClass( "ui-link" ).addClass( buttonClass ); |
| |
| buttonInner.className = innerClass; |
| |
| buttonText.className = textClass; |
| if ( !buttonElements ) { |
| buttonInner.appendChild( buttonText ); |
| } |
| if ( buttonIcon ) { |
| buttonIcon.className = iconClass; |
| if ( !( buttonElements && buttonElements.icon ) ) { |
| buttonIcon.innerHTML = " "; |
| buttonInner.appendChild( buttonIcon ); |
| } |
| } |
| |
| while ( e.firstChild && !buttonElements ) { |
| buttonText.appendChild( e.firstChild ); |
| } |
| |
| if ( !buttonElements ) { |
| e.appendChild( buttonInner ); |
| } |
| |
| // Assign a structure containing the elements of this button to the elements of this button. This |
| // will allow us to recognize this as an already-enhanced button in future calls to buttonMarkup(). |
| buttonElements = { |
| bcls : buttonClass, |
| outer : e, |
| inner : buttonInner, |
| text : buttonText, |
| icon : buttonIcon |
| }; |
| |
| $.data( e, 'buttonElements', buttonElements ); |
| $.data( buttonInner, 'buttonElements', buttonElements ); |
| $.data( buttonText, 'buttonElements', buttonElements ); |
| if ( buttonIcon ) { |
| $.data( buttonIcon, 'buttonElements', buttonElements ); |
| } |
| } |
| |
| return this; |
| }; |
| |
| $.fn.buttonMarkup.defaults = { |
| theme: "c", |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| wrapperEls: "span" |
| }; |
| |
| function closestEnabledButton( element ) { |
| var cname; |
| |
| while ( element ) { |
| // Note that we check for typeof className below because the element we |
| // handed could be in an SVG DOM where className on SVG elements is defined to |
| // be of a different type (SVGAnimatedString). We only operate on HTML DOM |
| // elements, so we look for plain "string". |
| cname = ( typeof element.className === 'string' ) && ( element.className + ' ' ); |
| if ( cname && cname.indexOf( "ui-btn " ) > -1 && cname.indexOf( "ui-disabled " ) < 0 ) { |
| break; |
| } |
| |
| element = element.parentNode; |
| } |
| |
| return element; |
| } |
| |
| var attachEvents = function() { |
| var hoverDelay = $.mobile.buttonMarkup.hoverDelay, hov, foc; |
| |
| $.mobile.$document.bind( { |
| "vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart touchend touchcancel": function( event ) { |
| var theme, |
| $btn = $( closestEnabledButton( event.target ) ), |
| isTouchEvent = event.originalEvent && /^touch/.test( event.originalEvent.type ), |
| evt = event.type; |
| |
| if ( $btn.length ) { |
| theme = $btn.attr( "data-" + $.mobile.ns + "theme" ); |
| |
| if ( evt === "vmousedown" ) { |
| if ( isTouchEvent ) { |
| // Use a short delay to determine if the user is scrolling before highlighting |
| hov = setTimeout( function() { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); |
| }, hoverDelay ); |
| } else { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-down-" + theme ); |
| } |
| } else if ( evt === "vmousecancel" || evt === "vmouseup" || evt === "touchend" || evt === "touchcancel" ) { |
| $btn.removeClass( "ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); |
| } else if ( evt === "vmouseover" || evt === "focus" ) { |
| if ( isTouchEvent ) { |
| // Use a short delay to determine if the user is scrolling before highlighting |
| foc = setTimeout( function() { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); |
| }, hoverDelay ); |
| } else { |
| $btn.removeClass( "ui-btn-up-" + theme ).addClass( "ui-btn-hover-" + theme ); |
| } |
| } else if ( evt === "vmouseout" || evt === "blur" || evt === "scrollstart" ) { |
| $btn.removeClass( "ui-btn-hover-" + theme + " ui-btn-down-" + theme ).addClass( "ui-btn-up-" + theme ); |
| if ( hov ) { |
| clearTimeout( hov ); |
| } |
| if ( foc ) { |
| clearTimeout( foc ); |
| } |
| } |
| } |
| }, |
| "focusin focus": function( event ) { |
| $( closestEnabledButton( event.target ) ).addClass( $.mobile.focusClass ); |
| }, |
| "focusout blur": function( event ) { |
| $( closestEnabledButton( event.target ) ).removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| attachEvents = null; |
| }; |
| |
| //links in bars, or those with data-role become buttons |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| $( ":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a", e.target ) |
| .jqmEnhanceable() |
| .not( "button, input, .ui-btn, :jqmData(role='none'), :jqmData(role='nojs')" ) |
| .buttonMarkup(); |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.collapsible", $.mobile.widget, { |
| options: { |
| expandCueText: " Expandable list, tap to open list", |
| collapseCueText: " Expandable list, tap to close list", |
| collapsed: true, |
| heading: "h1,h2,h3,h4,h5,h6,legend", |
| theme: null, |
| contentTheme: null, |
| inset: true, |
| mini: false, |
| initSelector: ":jqmData(role='collapsible')" |
| }, |
| _create: function() { |
| |
| var $el = this.element, |
| o = this.options, |
| collapsible = $el.addClass( "ui-collapsible" ), |
| collapsibleHeading = $el.children( o.heading ).first(), |
| collapsedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "collapsed-icon" ) || o.collapsedIcon, |
| expandedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "expanded-icon" ) || o.expandedIcon, |
| collapsibleContent = collapsible.wrapInner( "<div class='ui-collapsible-content'></div>" ).children( ".ui-collapsible-content" ), |
| collapsibleSet = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ); |
| |
| // Replace collapsibleHeading if it's a legend |
| if ( collapsibleHeading.is( "legend" ) ) { |
| collapsibleHeading = $( "<div role='heading'>"+ collapsibleHeading.html() +"</div>" ).insertBefore( collapsibleHeading ); |
| collapsibleHeading.next().remove(); |
| } |
| |
| // If we are in a collapsible set |
| if ( collapsibleSet.length ) { |
| // Inherit the theme from collapsible-set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "theme" ) || $.mobile.getInheritedTheme( collapsibleSet, "c" ); |
| } |
| // Inherit the content-theme from collapsible-set |
| if ( !o.contentTheme ) { |
| o.contentTheme = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "content-theme" ); |
| } |
| |
| // Get the preference for collapsed icon in the set |
| if ( !o.collapsedIcon ) { |
| o.collapsedIcon = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "collapsed-icon" ); |
| } |
| // Get the preference for expanded icon in the set |
| if ( !o.expandedIcon ) { |
| o.expandedIcon = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "expanded-icon" ); |
| } |
| // Gets the preference icon position in the set |
| if ( !o.iconPos ) { |
| o.iconPos = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "iconpos" ); |
| } |
| // Inherit the preference for inset from collapsible-set or set the default value to ensure equalty within a set |
| if ( $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "inset" ) !== undefined ) { |
| o.inset = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "inset" ); |
| } else { |
| o.inset = true; |
| } |
| // Gets the preference for mini in the set |
| if ( !o.mini ) { |
| o.mini = $.mobile.getAttrFixed( collapsibleSet[0], "data-" + $.mobile.ns + "mini" ); |
| } |
| } else { |
| // get inherited theme if not a set and no theme has been set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( $el, "c" ); |
| } |
| } |
| |
| if ( !!o.inset ) { |
| collapsible.addClass( "ui-collapsible-inset" ); |
| } |
| |
| collapsibleContent.addClass( ( o.contentTheme ) ? ( "ui-body-" + o.contentTheme ) : ""); |
| |
| collapsedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "collapsed-icon" ) || o.collapsedIcon || "plus"; |
| expandedIcon = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "expanded-icon" ) || o.expandedIcon || "minus"; |
| |
| collapsibleHeading |
| //drop heading in before content |
| .insertBefore( collapsibleContent ) |
| //modify markup & attributes |
| .addClass( "ui-collapsible-heading" ) |
| .append( "<span class='ui-collapsible-heading-status'></span>" ) |
| .wrapInner( "<a href='#' class='ui-collapsible-heading-toggle'></a>" ) |
| .find( "a" ) |
| .first() |
| .buttonMarkup({ |
| shadow: false, |
| corners: false, |
| iconpos: $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "iconpos" ) || o.iconPos || "left", |
| icon: collapsedIcon, |
| mini: o.mini, |
| theme: o.theme |
| }) |
| .attr( "role", ""); |
| |
| if ( !!o.inset ) { |
| collapsibleHeading |
| .find( "a" ).first().add( ".ui-btn-inner", $el ) |
| .addClass( "ui-corner-top ui-corner-bottom" ); |
| } |
| |
| //events |
| collapsible |
| .bind( "expand collapse", function( event ) { |
| if ( !event.isDefaultPrevented() ) { |
| var $this = $( this ), |
| isCollapse = ( event.type === "collapse" ), |
| contentTheme = o.contentTheme; |
| |
| event.preventDefault(); |
| |
| // Custom event callback |
| if ( o.customEventHandler ) { o.customEventHandler.call( this, isCollapse ) }; |
| |
| collapsibleHeading |
| .toggleClass( "ui-collapsible-heading-collapsed", isCollapse ) |
| .find( ".ui-collapsible-heading-status" ) |
| .text( isCollapse ? o.expandCueText : o.collapseCueText ) |
| .end() |
| .find( ".ui-icon" ) |
| .toggleClass( "ui-icon-" + expandedIcon, !isCollapse ) |
| // logic or cause same icon for expanded/collapsed state would remove the ui-icon-class |
| .toggleClass( "ui-icon-" + collapsedIcon, ( isCollapse || expandedIcon === collapsedIcon ) ) |
| .end() |
| .find( "a" ).first().removeClass( $.mobile.activeBtnClass ); |
| |
| $this.toggleClass( "ui-collapsible-collapsed", isCollapse ); |
| collapsibleContent.toggleClass( "ui-collapsible-content-collapsed", isCollapse ).attr( "aria-hidden", isCollapse ); |
| collapsibleContent.children( "li" ).not( "ui-collapsible-content" ).attr( "tabindex", isCollapse ? "" : "0" ); |
| |
| if ( contentTheme && !!o.inset && ( !collapsibleSet.length || collapsible.jqmData( "collapsible-last" ) ) ) { |
| collapsibleHeading |
| .find( "a" ).first().add( collapsibleHeading.find( ".ui-btn-inner" ) ) |
| .toggleClass( "ui-corner-bottom", isCollapse ); |
| collapsibleContent.toggleClass( "ui-corner-bottom", !isCollapse ); |
| } |
| collapsibleContent.trigger( "updatelayout" ); |
| } |
| }) |
| .trigger( o.collapsed ? "collapse" : "expand" ); |
| |
| collapsibleHeading |
| .bind( "tap", function() { |
| collapsibleHeading.find( "a" ).first().addClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "vmousecancel", function() { |
| collapsibleHeading.find( "a" ).first().removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "click", function( event ) { |
| |
| var type = collapsibleHeading.is( ".ui-collapsible-heading-collapsed" ) ? "expand" : "collapse"; |
| |
| collapsible.trigger( type ); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| }); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.collapsible ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.collapsibleset", $.mobile.widget, { |
| options: { |
| initSelector: ":jqmData(role='collapsible-set')" |
| }, |
| _create: function() { |
| var $el = this.element.addClass( "ui-collapsible-set" ), |
| o = this.options; |
| |
| // Inherit the theme from collapsible-set |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( $el, "c" ); |
| } |
| // Inherit the content-theme from collapsible-set |
| if ( !o.contentTheme ) { |
| o.contentTheme = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "content-theme" ); |
| } |
| |
| if ( $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inset" ) !== undefined ) { |
| o.inset = $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inset" ); |
| } |
| o.inset = o.inset !== undefined ? o.inset : true; |
| |
| // Initialize the collapsible set if it's not already initialized |
| if ( !$el.jqmData( "collapsiblebound" ) ) { |
| $el |
| .jqmData( "collapsiblebound", true ) |
| .bind( "expand collapse", function( event ) { |
| var isCollapse = ( event.type === "collapse" ), |
| collapsible = $( event.target ).closest( ".ui-collapsible" ), |
| widget = collapsible.data( "collapsible" ); |
| if ( collapsible.jqmData( "collapsible-last" ) && !!o.inset ) { |
| collapsible.find( ".ui-collapsible-heading" ).first() |
| .find( "a" ).first() |
| .toggleClass( "ui-corner-bottom", isCollapse ) |
| .find( ".ui-btn-inner" ) |
| .toggleClass( "ui-corner-bottom", isCollapse ); |
| collapsible.find( ".ui-collapsible-content" ).toggleClass( "ui-corner-bottom", !isCollapse ); |
| } |
| }) |
| .bind( "expand", function( event ) { |
| var closestCollapsible = $( event.target ) |
| .closest( ".ui-collapsible" ); |
| if ( closestCollapsible.parent().is( ":jqmData(role='collapsible-set')" ) ) { |
| closestCollapsible |
| .siblings( ".ui-collapsible" ) |
| .trigger( "collapse" ); |
| } |
| }); |
| } |
| }, |
| |
| _init: function() { |
| var $el = this.element, |
| collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ), |
| expanded = collapsiblesInSet.filter( ":jqmData(collapsed='false')" ); |
| this.refresh(); |
| |
| // Because the corners are handled by the collapsible itself and the default state is collapsed |
| // That was causing https://github.com/jquery/jquery-mobile/issues/4116 |
| expanded.trigger( "expand" ); |
| }, |
| |
| refresh: function() { |
| var $el = this.element, |
| o = this.options, |
| collapsiblesInSet = $el.children( ":jqmData(role='collapsible')" ); |
| |
| $.mobile.collapsible.prototype.enhance( collapsiblesInSet.not( ".ui-collapsible" ) ); |
| |
| // clean up borders |
| if ( !!o.inset ) { |
| collapsiblesInSet.each(function() { |
| $( this ).jqmRemoveData( "collapsible-last" ) |
| .find( ".ui-collapsible-heading" ) |
| .find( "a" ).first() |
| .removeClass( "ui-corner-top ui-corner-bottom" ) |
| .find( ".ui-btn-inner" ) |
| .removeClass( "ui-corner-top ui-corner-bottom" ); |
| }); |
| |
| collapsiblesInSet.first() |
| .find( "a" ) |
| .first() |
| .addClass( "ui-corner-top" ) |
| .find( ".ui-btn-inner" ) |
| .addClass( "ui-corner-top" ); |
| |
| collapsiblesInSet.last() |
| .jqmData( "collapsible-last", true ) |
| .find( "a" ) |
| .first() |
| .addClass( "ui-corner-bottom" ) |
| .find( ".ui-btn-inner" ) |
| .addClass( "ui-corner-bottom" ); |
| } |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.collapsibleset ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.navbar", $.mobile.widget, { |
| options: { |
| iconpos: "top", |
| grid: null, |
| initSelector: ":jqmData(role='navbar')" |
| }, |
| |
| _create: function() { |
| |
| var $navbar = this.element, |
| $navbtns = $navbar.find( "a" ), |
| iconpos = $navbtns.filter( ":jqmData(icon)" ).length ? |
| this.options.iconpos : undefined; |
| |
| $navbar.addClass( "ui-navbar ui-mini" ) |
| .attr( "role", "navigation" ) |
| .find( "ul" ) |
| .jqmEnhanceable() |
| .grid({ grid: this.options.grid }); |
| |
| $navbtns.buttonMarkup({ |
| corners: false, |
| shadow: false, |
| inline: true, |
| iconpos: iconpos |
| }); |
| |
| $navbar.delegate( "a", "vclick", function( event ) { |
| if ( !$(event.target).hasClass( "ui-disabled" ) ) { |
| $navbtns.removeClass( $.mobile.activeBtnClass ); |
| $( this ).addClass( $.mobile.activeBtnClass ); |
| } |
| }); |
| |
| // Buttons in the navbar with ui-state-persist class should regain their active state before page show |
| $navbar.closest( ".ui-page" ).bind( "pagebeforeshow", function() { |
| $navbtns.filter( ".ui-state-persist" ).addClass( $.mobile.activeBtnClass ); |
| }); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.navbar ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| //Keeps track of the number of lists per page UID |
| //This allows support for multiple nested list in the same page |
| //https://github.com/jquery/jquery-mobile/issues/1617 |
| var listCountPerPage = {}; |
| |
| $.widget( "mobile.listview", $.mobile.widget, { |
| |
| options: { |
| theme: null, |
| countTheme: "c", |
| headerTheme: "b", |
| dividerTheme: "b", |
| splitIcon: "arrow-r", |
| splitTheme: "b", |
| inset: false, |
| initSelector: ":jqmData(role='listview')" |
| }, |
| |
| _create: function() { |
| var t = this, |
| listviewClasses = ""; |
| |
| listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : ""; |
| |
| // create listview markup |
| t.element.addClass(function( i, orig ) { |
| return orig + " ui-listview " + listviewClasses; |
| }); |
| |
| t.refresh( true ); |
| }, |
| |
| _removeCorners: function( li, which ) { |
| var top = "ui-corner-top ui-corner-tr ui-corner-tl", |
| bot = "ui-corner-bottom ui-corner-br ui-corner-bl"; |
| |
| li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) ); |
| |
| if ( which === "top" ) { |
| li.removeClass( top ); |
| } else if ( which === "bottom" ) { |
| li.removeClass( bot ); |
| } else { |
| li.removeClass( top + " " + bot ); |
| } |
| }, |
| |
| _refreshCorners: function( create ) { |
| var $li, |
| $visibleli, |
| $topli, |
| $bottomli; |
| |
| $li = this.element.children( "li" ); |
| // At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden |
| $visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" ); |
| |
| // ui-li-last is used for setting border-bottom on the last li |
| $li.filter( ".ui-li-last" ).removeClass( "ui-li-last" ); |
| |
| if ( this.options.inset ) { |
| this._removeCorners( $li ); |
| |
| // Select the first visible li element |
| $topli = $visibleli.first() |
| .addClass( "ui-corner-top" ); |
| |
| $topli.add( $topli.find( ".ui-btn-inner" ) |
| .not( ".ui-li-link-alt span:first-child" ) ) |
| .addClass( "ui-corner-top" ) |
| .end() |
| .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" ) |
| .addClass( "ui-corner-tr" ) |
| .end() |
| .find( ".ui-li-thumb" ) |
| .not( ".ui-li-icon" ) |
| .addClass( "ui-corner-tl" ); |
| |
| // Select the last visible li element |
| $bottomli = $visibleli.last() |
| .addClass( "ui-corner-bottom ui-li-last" ); |
| |
| $bottomli.add( $bottomli.find( ".ui-btn-inner" ) ) |
| .find( ".ui-li-link-alt" ) |
| .addClass( "ui-corner-br" ) |
| .end() |
| .find( ".ui-li-thumb" ) |
| .not( ".ui-li-icon" ) |
| .addClass( "ui-corner-bl" ); |
| } else { |
| $visibleli.last().addClass( "ui-li-last" ); |
| } |
| if ( !create ) { |
| this.element.trigger( "updatelayout" ); |
| } |
| }, |
| |
| // This is a generic utility method for finding the first |
| // node with a given nodeName. It uses basic DOM traversal |
| // to be fast and is meant to be a substitute for simple |
| // $.fn.closest() and $.fn.children() calls on a single |
| // element. Note that callers must pass both the lowerCase |
| // and upperCase version of the nodeName they are looking for. |
| // The main reason for this is that this function will be |
| // called many times and we want to avoid having to lowercase |
| // the nodeName from the element every time to ensure we have |
| // a match. Note that this function lives here for now, but may |
| // be moved into $.mobile if other components need a similar method. |
| _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) { |
| var dict = {}; |
| dict[ lcName ] = dict[ ucName ] = true; |
| while ( ele ) { |
| if ( dict[ ele.nodeName ] ) { |
| return ele; |
| } |
| ele = ele[ nextProp ]; |
| } |
| return null; |
| }, |
| _getChildrenByTagName: function( ele, lcName, ucName ) { |
| var results = [], |
| dict = {}; |
| dict[ lcName ] = dict[ ucName ] = true; |
| ele = ele.firstChild; |
| while ( ele ) { |
| if ( dict[ ele.nodeName ] ) { |
| results.push( ele ); |
| } |
| ele = ele.nextSibling; |
| } |
| return $( results ); |
| }, |
| |
| _addThumbClasses: function( containers ) { |
| var i, img, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) ); |
| if ( img.length ) { |
| img.addClass( "ui-li-thumb" ).attr( { |
| "role" : "", |
| "aria-label" : "icon" |
| }); |
| $( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" ); |
| } |
| } |
| }, |
| |
| _addCheckboxRadioClasses: function( containers ) |
| { |
| var i, inputAttr, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| inputAttr = $( containers[ i ] ).find( "input" ); |
| if ( inputAttr.attr( "type" ) == "checkbox" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-checkbox" ); |
| } else if ( inputAttr.attr( "type" ) == "radio" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-radio" ); |
| } |
| } |
| }, |
| |
| _addRightBtnClasses: function( containers ) |
| { |
| var i, btnAttr, len = containers.length; |
| for ( i = 0; i < len; i++ ) { |
| btnAttr = $( containers[ i ] ).find( ":jqmData(role='button'),input[type='button'],select:jqmData(role='slider')" ); |
| if ( btnAttr.length ) { |
| if ( btnAttr.jqmData( "style" ) == "circle" ) { |
| $( containers[ i ] ).addClass( "ui-li-has-right-circle-btn" ); |
| } else { |
| $( containers[ i ] ).addClass( "ui-li-has-right-btn" ); |
| } |
| } |
| } |
| }, |
| |
| refresh: function( create ) { |
| this.parentPage = this.element.closest( ".ui-page" ); |
| this._createSubPages(); |
| |
| var o = this.options, |
| $list = this.element, |
| self = this, |
| dividertheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "dividertheme" ) || o.dividerTheme, |
| listsplittheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "splittheme" ), |
| listspliticon = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "spliticon" ), |
| li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ), |
| ol = !!$.nodeName( $list[ 0 ], "ol" ), |
| jsCount = !$.support.cssPseudoElement, |
| start = $list.attr( "start" ), |
| itemClassDict = {}, |
| item, itemClass, itemTheme, |
| a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon; |
| |
| if ( ol && jsCount ) { |
| $list.find( ".ui-li-dec" ).remove(); |
| } |
| |
| if ( ol ) { |
| // Check if a start attribute has been set while taking a value of 0 into account |
| if ( start || start === 0 ) { |
| if ( !jsCount ) { |
| startCount = parseFloat( start ) - 1; |
| $list.css( "counter-reset", "listnumbering " + startCount ); |
| } else { |
| counter = parseFloat( start ); |
| } |
| } else if ( jsCount ) { |
| counter = 1; |
| } |
| } |
| |
| if ( !o.theme ) { |
| o.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| for ( var pos = 0, numli = li.length; pos < numli; pos++ ) { |
| item = li.eq( pos ); |
| itemClass = "ui-li"; |
| |
| // If we're creating the element, we update it regardless |
| if ( create || !item.hasClass( "ui-li" ) ) { |
| itemTheme = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "theme" ) || o.theme; |
| a = this._getChildrenByTagName( item[ 0 ], "a", "A" ).attr( { |
| "role": "", |
| "tabindex": "0" |
| }); |
| var isDivider = ( $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "role" ) === "list-divider" ); |
| |
| if ( item.hasClass( "ui-li-has-checkbox" ) || item.hasClass( "ui-li-has-radio" ) ) { |
| item.on( "vclick", function ( e ) { |
| var targetItem = $( e.target ); |
| var checkboxradio = targetItem.find( ".ui-checkbox" ); |
| if ( !checkboxradio.length ) { |
| checkboxradio = targetItem.find( ".ui-radio" ); |
| } |
| |
| if ( checkboxradio.length ) { |
| checkboxradio.children( "label" ).trigger( "vclick" ); |
| } |
| }); |
| } |
| |
| if ( a.length && !isDivider ) { |
| icon = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "icon" ); |
| |
| /* Remove auto populated right-arrow button. */ |
| if ( icon === undefined ) { |
| icon = false; |
| } |
| |
| item.buttonMarkup({ |
| wrapperEls: "div", |
| shadow: false, |
| corners: false, |
| iconpos: "right", |
| icon: a.length > 1 || icon === false ? false : icon || "arrow-r", |
| theme: itemTheme |
| }); |
| |
| if ( ( icon !== false ) && ( a.length === 1 ) ) { |
| item.addClass( "ui-li-has-arrow" ); |
| } |
| |
| a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" ); |
| |
| if ( a.length > 1 ) { |
| itemClass += " ui-li-has-alt"; |
| |
| last = a.last(); |
| splittheme = listsplittheme || $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "theme" ) || o.splitTheme; |
| linkIcon = $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "icon" ); |
| |
| last.appendTo( item ) |
| .attr( "title", last.getEncodedText() ) |
| .addClass( "ui-li-link-alt" ) |
| .empty() |
| .buttonMarkup({ |
| shadow: false, |
| corners: false, |
| theme: itemTheme, |
| icon: false, |
| iconpos: "notext" |
| }) |
| .find( ".ui-btn-inner" ) |
| .append( |
| $( document.createElement( "span" ) ).buttonMarkup({ |
| shadow: true, |
| corners: true, |
| theme: splittheme, |
| iconpos: "notext", |
| // link icon overrides list item icon overrides ul element overrides options |
| icon: linkIcon || icon || listspliticon || o.splitIcon |
| }) |
| ); |
| } |
| } else if ( isDivider ) { |
| |
| itemClass += " ui-li-divider ui-bar-" + dividertheme; |
| item.attr( { "role": "heading", "tabindex": "0" } ); |
| |
| if ( ol ) { |
| //reset counter when a divider heading is encountered |
| if ( start || start === 0 ) { |
| if ( !jsCount ) { |
| newStartCount = parseFloat( start ) - 1; |
| item.css( "counter-reset", "listnumbering " + newStartCount ); |
| } else { |
| counter = parseFloat( start ); |
| } |
| } else if ( jsCount ) { |
| counter = 1; |
| } |
| } |
| |
| } else { |
| itemClass += " ui-li-static ui-btn-up-" + itemTheme; |
| item.attr( "tabindex", "0" ); |
| } |
| } |
| |
| if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) { |
| countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" ); |
| |
| countParent.addClass( "ui-li-jsnumbering" ) |
| .prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" ); |
| } |
| |
| // Instead of setting item class directly on the list item and its |
| // btn-inner at this point in time, push the item into a dictionary |
| // that tells us what class to set on it so we can do this after this |
| // processing loop is finished. |
| |
| if ( !itemClassDict[ itemClass ] ) { |
| itemClassDict[ itemClass ] = []; |
| } |
| |
| itemClassDict[ itemClass ].push( item[ 0 ] ); |
| } |
| |
| // Set the appropriate listview item classes on each list item |
| // and their btn-inner elements. The main reason we didn't do this |
| // in the for-loop above is because we can eliminate per-item function overhead |
| // by calling addClass() and children() once or twice afterwards. This |
| // can give us a significant boost on platforms like WP7.5. |
| |
| for ( itemClass in itemClassDict ) { |
| $( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass ); |
| } |
| |
| $list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ) |
| .end() |
| |
| .find( "p, dl" ).addClass( "ui-li-desc" ) |
| .end() |
| |
| .find( ".ui-li-aside" ).each(function() { |
| var $this = $( this ); |
| $this.prependTo( $this.parent() ); //shift aside to front for css float |
| }) |
| .end() |
| |
| .find( ".ui-li-count" ).each(function() { |
| $( this ).closest( "li" ).addClass( "ui-li-has-count" ); |
| }).addClass( "ui-btn-up-" + ( $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" ); |
| |
| // The idea here is to look at the first image in the list item |
| // itself, and any .ui-link-inherit element it may contain, so we |
| // can place the appropriate classes on the image and list item. |
| // Note that we used to use something like: |
| // |
| // li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... ); |
| // |
| // But executing a find() like that on Windows Phone 7.5 took a |
| // really long time. Walking things manually with the code below |
| // allows the 400 listview item page to load in about 3 seconds as |
| // opposed to 30 seconds. |
| |
| this._addThumbClasses( li ); |
| this._addThumbClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._addCheckboxRadioClasses( li ); |
| this._addCheckboxRadioClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._addRightBtnClasses( li ); |
| this._addRightBtnClasses( $list.find( ".ui-link-inherit" ) ); |
| |
| this._refreshCorners( create ); |
| |
| // autodividers binds to this to redraw dividers after the listview refresh |
| this._trigger( "afterrefresh" ); |
| }, |
| |
| //create a string for ID/subpage url creation |
| _idStringEscape: function( str ) { |
| return str.replace(/[^a-zA-Z0-9]/g, '-'); |
| }, |
| |
| _createSubPages: function() { |
| var parentList = this.element, |
| parentPage = parentList.closest( ".ui-page" ), |
| parentUrl = parentPage.jqmData( "url" ), |
| parentId = parentUrl || parentPage[ 0 ][ $.expando ], |
| parentListId = parentList.attr( "id" ), |
| o = this.options, |
| dns = "data-" + $.mobile.ns, |
| self = this, |
| persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ), |
| hasSubPages; |
| |
| if ( typeof listCountPerPage[ parentId ] === "undefined" ) { |
| listCountPerPage[ parentId ] = -1; |
| } |
| |
| parentListId = parentListId || ++listCountPerPage[ parentId ]; |
| |
| $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) { |
| var self = this, |
| list = $( this ), |
| listId = list.attr( "id" ) || parentListId + "-" + i, |
| parent = list.parent(), |
| nodeElsFull = $( list.prevAll().toArray().reverse() ), |
| nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ), |
| title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text |
| id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId, |
| theme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "theme" ) || o.theme, |
| countTheme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "counttheme" ) || $.mobile.getAttrFixed( parentList[0], "data-" + $.mobile.ns + "counttheme" ) || o.countTheme, |
| newPage, anchor; |
| |
| //define hasSubPages for use in later removal |
| hasSubPages = true; |
| |
| newPage = list.detach() |
| .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" ) |
| .parent() |
| .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" ) |
| .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" ) |
| .parent() |
| .appendTo( $.mobile.pageContainer ); |
| |
| newPage.page(); |
| |
| anchor = parent.find( 'a:first' ); |
| |
| if ( !anchor.length ) { |
| anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() ); |
| } |
| |
| anchor.attr( "href", "#" + id ); |
| |
| }).listview(); |
| |
| // on pagehide, remove any nested pages along with the parent page, as long as they aren't active |
| // and aren't embedded |
| if ( hasSubPages && |
| parentPage.is( ":jqmData(external-page='true')" ) && |
| parentPage.data( "page" ).options.domCache === false ) { |
| |
| var newRemove = function( e, ui ) { |
| var nextPage = ui.nextPage, npURL, |
| prEvent = new $.Event( "pageremove" ); |
| |
| if ( ui.nextPage ) { |
| npURL = nextPage.jqmData( "url" ); |
| if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) { |
| self.childPages().remove(); |
| parentPage.trigger( prEvent ); |
| if ( !prEvent.isDefaultPrevented() ) { |
| parentPage.removeWithDependents(); |
| } |
| } |
| } |
| }; |
| |
| // unbind the original page remove and replace with our specialized version |
| parentPage |
| .unbind( "pagehide.remove" ) |
| .bind( "pagehide.remove", newRemove); |
| } |
| }, |
| |
| addItem : function( listitem , idx ) { |
| var $item = $(listitem), |
| $li, |
| _self = this; |
| |
| $li = _self.element.children( 'li' ); |
| $item.css( { 'opacity' : 0, |
| 'display' : 'none' } ); |
| if( $li.length == 0 |
| || $li.length <= idx) |
| { |
| $( _self.element ).append( $item ); |
| } else { |
| $( $li.get( idx ) ).before( $item ); |
| } |
| $(_self.element).trigger("create") |
| .listview( 'refresh' ); |
| |
| $item.css( 'min-height' , '0px' ); |
| |
| $item.slideDown( 'fast' , function( ){ |
| $item.addClass("addli"); |
| $item.css( { 'opacity' : 1 } ); |
| } ); |
| }, |
| |
| removeItem : function( idx ) { |
| var $item, |
| $li, |
| _self = this; |
| |
| $li = _self.element.children( 'li' ); |
| if( $li.length <= 0 || |
| $li.length < idx ) { |
| return ; |
| } |
| $item = $( $li.get( idx ) ); |
| $item.addClass("removeli"); |
| $item.slideUp('normal', |
| function( ) { |
| $(this).remove(); |
| }); |
| }, |
| |
| // TODO sort out a better way to track sub pages of the listview this is brittle |
| childPages: function() { |
| var parentUrl = this.parentPage.jqmData( "url" ); |
| |
| return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" ); |
| } |
| }); |
| |
| //delegate auto self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.listview ); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.listview.prototype.options.autodividers = false; |
| $.mobile.listview.prototype.options.autodividersSelector = function( elt ) { |
| // look for the text in the given element |
| var text = elt.text() || null; |
| |
| if ( !text ) { |
| return null; |
| } |
| |
| // create the text for the divider (first uppercased letter) |
| text = text.trim().slice( 0, 1 ).toUpperCase(); |
| |
| return text; |
| }; |
| |
| $.mobile.$document.delegate( "ul,ol", "listviewcreate", function() { |
| |
| var list = $( this ), |
| listview = list.data( "listview" ); |
| |
| if ( !listview || !listview.options.autodividers ) { |
| return; |
| } |
| |
| var replaceDividers = function () { |
| list.find( "li:jqmData(role='list-divider')" ).remove(); |
| |
| var lis = list.find( 'li' ), |
| lastDividerText = null, li, dividerText; |
| |
| for ( var i = 0; i < lis.length ; i++ ) { |
| li = lis[i]; |
| dividerText = listview.options.autodividersSelector( $( li ) ); |
| |
| if ( dividerText && lastDividerText !== dividerText ) { |
| var divider = document.createElement( 'li' ); |
| divider.appendChild( document.createTextNode( dividerText ) ); |
| divider.setAttribute( 'data-' + $.mobile.ns + 'role', 'list-divider' ); |
| li.parentNode.insertBefore( divider, li ); |
| } |
| |
| lastDividerText = dividerText; |
| } |
| }; |
| |
| var afterListviewRefresh = function () { |
| list.unbind( 'listviewafterrefresh', afterListviewRefresh ); |
| replaceDividers(); |
| listview.refresh(); |
| list.bind( 'listviewafterrefresh', afterListviewRefresh ); |
| }; |
| |
| afterListviewRefresh(); |
| }); |
| |
| })( jQuery ); |
| |
| /* |
| * "checkboxradio" plugin |
| */ |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.checkboxradio", $.mobile.widget, { |
| options: { |
| theme: null, |
| initSelector: "input[type='checkbox'],input[type='radio']" |
| }, |
| _create: function() { |
| var self = this, |
| input = this.element, |
| inheritAttr = function( input, dataAttr ) { |
| return input.jqmData( dataAttr ) || input.closest( "form, fieldset" ).jqmData( dataAttr ); |
| }, |
| // NOTE: Windows Phone could not find the label through a selector |
| // filter works though. |
| parentLabel = $( input ).closest( "label" ), |
| label = parentLabel.length ? parentLabel : ( input[0].id ? $( input ).closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ).find( "label" ).filter( "[for='" + input[0].id + "']" ) : [ ] ), |
| inputtype = input[0].type, |
| mini = inheritAttr( input, "mini" ), |
| checkedState = inputtype + "-on", |
| uncheckedState = inputtype + "-off", |
| icon = input.parents( ":jqmData(type='horizontal')" ).length ? undefined : uncheckedState, |
| iconpos = inheritAttr( input, "iconpos" ), |
| activeBtn = icon ? "" : " " + $.mobile.activeBtnClass, |
| checkedClass = "ui-" + checkedState + activeBtn, |
| uncheckedClass = "ui-" + uncheckedState, |
| checkedicon = "ui-icon-" + checkedState, |
| uncheckedicon = "ui-icon-" + uncheckedState, |
| ariaCheckedAttr = ""; |
| |
| if ( inputtype !== "checkbox" && inputtype !== "radio" ) { |
| return; |
| } |
| |
| ariaCheckedAttr = ( inputtype === "checkbox" ) ? "aria-checked" : "aria-selected"; |
| |
| // Support fake label |
| if ( label.length == 0 ) { |
| label = $( "<label for='" + input[ 0 ].id + |
| "'></label>" ); |
| } |
| |
| // Expose for other methods |
| $.extend( this, { |
| label: label, |
| inputtype: inputtype, |
| checkedClass: checkedClass, |
| uncheckedClass: uncheckedClass, |
| checkedicon: checkedicon, |
| uncheckedicon: uncheckedicon, |
| ariaCheckedAttr : ariaCheckedAttr |
| }); |
| |
| // If there's no selected theme check the data attr |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| label.buttonMarkup({ |
| theme: this.options.theme, |
| icon: icon, |
| shadow: false, |
| mini: mini, |
| iconpos: iconpos |
| }); |
| |
| // Wrap the input + label in a div |
| var wrapper = document.createElement('div'); |
| wrapper.className = 'ui-' + inputtype; |
| wrapper.setAttribute( "role", inputtype ); |
| |
| if ( input.hasClass( "favorite" ) ) { |
| wrapper.className += ' favorite'; |
| } |
| |
| input.add( label ).wrapAll( wrapper ); |
| |
| label.bind({ |
| vmouseover: function( event ) { |
| if ( $( this ).parent().is( ".ui-disabled" ) ) { |
| event.stopPropagation(); |
| } |
| }, |
| |
| vclick: function( event ) { |
| if ( input.is( ":disabled" ) ) { |
| event.preventDefault(); |
| return; |
| } |
| |
| self._cacheVals(); |
| |
| input.prop( "checked", inputtype === "radio" && true || !input.prop( "checked" ) ); |
| |
| // trigger click handler's bound directly to the input as a substitute for |
| // how label clicks behave normally in the browsers |
| // TODO: it would be nice to let the browser's handle the clicks and pass them |
| // through to the associate input. we can swallow that click at the parent |
| // wrapper element level |
| input.triggerHandler( 'click' ); |
| |
| // Input set for common radio buttons will contain all the radio |
| // buttons, but will not for checkboxes. clearing the checked status |
| // of other radios ensures the active button state is applied properly |
| self._getInputSet().not( input ).prop( "checked", false ); |
| |
| self._updateAll(); |
| return false; |
| } |
| }); |
| |
| input |
| .bind({ |
| vmousedown: function() { |
| self._cacheVals(); |
| }, |
| |
| vclick: function() { |
| var $this = $( this ); |
| |
| // Adds checked attribute to checked input when keyboard is used |
| if ( $this.is( ":checked" ) ) { |
| |
| $this.prop( "checked", true); |
| self._getInputSet().not( $this ).prop( "checked", false ); |
| } else { |
| |
| $this.prop( "checked", false ); |
| } |
| |
| self._updateAll(); |
| }, |
| |
| focus: function() { |
| label.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| label.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| this.refresh(); |
| }, |
| |
| _cacheVals: function() { |
| this._getInputSet().each(function() { |
| $( this ).jqmData( "cacheVal", this.checked ); |
| }); |
| }, |
| |
| //returns either a set of radios with the same name attribute, or a single checkbox |
| _getInputSet: function() { |
| if ( this.inputtype === "checkbox" ) { |
| return this.element; |
| } |
| |
| return this.element.closest( "form, fieldset, :jqmData(role='page'), :jqmData(role='dialog')" ) |
| .find( "input[name='" + this.element[0].name + "'][type='" + this.inputtype + "']" ); |
| }, |
| |
| _updateAll: function() { |
| var self = this; |
| |
| this._getInputSet().each(function() { |
| var $this = $( this ); |
| |
| if ( this.checked || self.inputtype === "checkbox" ) { |
| $this.trigger( "change" ); |
| } |
| $this.focus(); |
| }) |
| .checkboxradio( "refresh" ); |
| }, |
| |
| refresh: function() { |
| var input = this.element[0], |
| label = this.label, |
| wrapper = input.parentNode, |
| icon = label.find( ".ui-icon" ); |
| |
| if ( input.checked ) { |
| label.addClass( this.checkedClass ).removeClass( this.uncheckedClass ); |
| icon.addClass( this.checkedicon ).removeClass( this.uncheckedicon ); |
| wrapper.setAttribute( this.ariaCheckedAttr, true ); |
| } else { |
| label.removeClass( this.checkedClass ).addClass( this.uncheckedClass ); |
| icon.removeClass( this.checkedicon ).addClass( this.uncheckedicon ); |
| wrapper.setAttribute( this.ariaCheckedAttr, false ); |
| } |
| |
| if ( input.disabled ) { |
| this.disable(); |
| } else { |
| this.enable(); |
| } |
| }, |
| |
| disable: function() { |
| this.element.prop( "disabled", true ).parent().addClass( "ui-disabled" ); |
| }, |
| |
| enable: function() { |
| this.element.prop( "disabled", false ).parent().removeClass( "ui-disabled" ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.checkboxradio.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.button", $.mobile.widget, { |
| options: { |
| theme: null, |
| icon: null, |
| iconpos: null, |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| initSelector: "button, [type='button'], [type='submit'], [type='reset']" |
| }, |
| _create: function() { |
| var $el = this.element, |
| $button, |
| o = this.options, |
| type, |
| name, |
| inline = o.inline || $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "inline" ), |
| mini = o.mini || $.mobile.getAttrFixed( $el[0], "data-" + $.mobile.ns + "mini" ), |
| classes = "", |
| $buttonPlaceholder; |
| |
| // if this is a link, check if it's been enhanced and, if not, use the right function |
| if ( $el[ 0 ].tagName === "A" ) { |
| if ( !$el.hasClass( "ui-btn" ) ) { |
| $el.buttonMarkup(); |
| } |
| |
| return; |
| } |
| |
| // get the inherited theme |
| // TODO centralize for all widgets |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.element, "c" ); |
| } |
| |
| // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 |
| /* if ( $el[0].className.length ) { |
| classes = $el[0].className; |
| } */ |
| if ( !!~$el[0].className.indexOf( "ui-btn-left" ) ) { |
| classes = "ui-btn-left"; |
| } |
| |
| if ( !!~$el[0].className.indexOf( "ui-btn-right" ) ) { |
| classes = "ui-btn-right"; |
| } |
| |
| if ( $el.attr( "type" ) === "submit" || $el.attr( "type" ) === "reset" ) { |
| classes ? classes += " ui-submit" : classes = "ui-submit"; |
| } |
| $( "label[for='" + $el.attr( "id" ) + "']" ).addClass( "ui-submit" ); |
| |
| // Add ARIA role |
| this.button = $( "<div></div>" ) |
| [ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ) |
| .insertBefore( $el ) |
| .buttonMarkup({ |
| theme: o.theme, |
| icon: o.icon, |
| iconpos: o.iconpos, |
| inline: inline, |
| corners: o.corners, |
| shadow: o.shadow, |
| iconshadow: o.iconshadow, |
| mini: mini |
| }) |
| .addClass( classes ) |
| .append( $el.addClass( "ui-btn-hidden" ) ); |
| |
| $button = this.button; |
| type = $el.attr( "type" ); |
| name = $el.attr( "name" ); |
| |
| // Add hidden input during submit if input type="submit" has a name. |
| if ( type !== "button" && type !== "reset" && name ) { |
| $el.bind( "vclick", function() { |
| // Add hidden input if it doesn't already exist. |
| if ( $buttonPlaceholder === undefined ) { |
| $buttonPlaceholder = $( "<input>", { |
| type: "hidden", |
| name: $el.attr( "name" ), |
| value: $el.attr( "value" ) |
| }).insertBefore( $el ); |
| |
| // Bind to doc to remove after submit handling |
| $.mobile.$document.one( "submit", function() { |
| $buttonPlaceholder.remove(); |
| |
| // reset the local var so that the hidden input |
| // will be re-added on subsequent clicks |
| $buttonPlaceholder = undefined; |
| }); |
| } |
| }); |
| } |
| |
| $el.bind({ |
| focus: function() { |
| $button.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| $button.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| |
| this.refresh(); |
| }, |
| |
| enable: function() { |
| this.element.attr( "disabled", false ); |
| this.button.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); |
| return this._setOption( "disabled", false ); |
| }, |
| |
| disable: function() { |
| this.element.attr( "disabled", true ); |
| this.button.addClass( "ui-disabled" ).attr( "aria-disabled", true ); |
| return this._setOption( "disabled", true ); |
| }, |
| |
| refresh: function() { |
| var $el = this.element; |
| |
| if ( $el.prop("disabled") ) { |
| this.disable(); |
| } else { |
| this.enable(); |
| } |
| |
| // Grab the button's text element from its implementation-independent data item |
| $( this.button.data( 'buttonElements' ).text )[ $el.html() ? "html" : "text" ]( $el.html() || $el.val() ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.button.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.fn.controlgroup = function( options ) { |
| function flipClasses( els, flCorners ) { |
| els.removeClass( "ui-btn-corner-all ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-controlgroup-last ui-shadow" ) |
| .eq( 0 ).addClass( flCorners[ 0 ] ) |
| .end() |
| .last().addClass( flCorners[ 1 ] ).addClass( "ui-controlgroup-last" ); |
| } |
| |
| return this.each(function() { |
| var $el = $( this ), |
| o = $.extend({ |
| direction: $.mobile.getAttrFixed( $el[0], "data-"+ $.mobile.ns + "type" ) || "vertical", |
| shadow: false, |
| excludeInvisible: true, |
| mini: $.mobile.getAttrFixed( $el[0], "data-"+ $.mobile.ns + "mini" ) |
| }, options ), |
| grouplegend = $el.children( "legend" ), |
| groupheading = $el.children( ".ui-controlgroup-label" ), |
| groupcontrols = $el.children( ".ui-controlgroup-controls" ), |
| flCorners = o.direction === "horizontal" ? [ "ui-corner-left", "ui-corner-right" ] : [ "ui-corner-top", "ui-corner-bottom" ], |
| type = $el.find( "input" ).first().attr( "type" ); |
| |
| // First unwrap the controls if the controlgroup was already enhanced |
| if ( groupcontrols.length ) { |
| groupcontrols.contents().unwrap(); |
| } |
| $el.wrapInner( "<div class='ui-controlgroup-controls'></div>" ); |
| |
| if ( grouplegend.length ) { |
| // Replace legend with more stylable replacement div |
| $( "<div role='heading' class='ui-controlgroup-label'>" + grouplegend.html() + "</div>" ).insertBefore( $el.children( 0 ) ); |
| grouplegend.remove(); |
| } else if ( groupheading.length ) { |
| // Just move the heading if the controlgroup was already enhanced |
| $el.prepend( groupheading ); |
| } |
| |
| $el.addClass( "ui-corner-all ui-controlgroup ui-controlgroup-" + o.direction ); |
| |
| flipClasses( $el.find( ".ui-btn" + ( o.excludeInvisible ? ":visible" : "" ) ).not( '.ui-slider-handle' ), flCorners ); |
| flipClasses( $el.find( ".ui-btn-inner" ), flCorners ); |
| |
| if ( o.shadow ) { |
| $el.addClass( "ui-shadow" ); |
| } |
| |
| if ( o.mini ) { |
| $el.addClass( "ui-mini" ); |
| } |
| |
| }); |
| }; |
| |
| // The pagecreate handler for controlgroup is in jquery.mobile.init because of the soft-dependency on the wrapped widgets |
| |
| })(jQuery); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| |
| //links within content areas, tests included with page |
| $( e.target ) |
| .find( "a" ) |
| .jqmEnhanceable() |
| .not( ".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')" ) |
| .addClass( "ui-link" ); |
| |
| }); |
| |
| })( jQuery ); |
| |
| |
| (function( $, undefined ) { |
| |
| function fitSegmentInsideSegment( winSize, segSize, offset, desired ) { |
| var ret = desired; |
| |
| if ( winSize < segSize ) { |
| // Center segment if it's bigger than the window |
| ret = offset + ( winSize - segSize ) / 2; |
| } else { |
| // Otherwise center it at the desired coordinate while keeping it completely inside the window |
| ret = Math.min( Math.max( offset, desired - segSize / 2 ), offset + winSize - segSize ); |
| } |
| |
| return ret; |
| } |
| |
| function windowCoords() { |
| var $win = $.mobile.$window; |
| |
| return { |
| x: $win.scrollLeft(), |
| y: $win.scrollTop(), |
| cx: ( window.innerWidth || $win.width() ), |
| cy: ( window.innerHeight || $win.height() ) |
| }; |
| } |
| |
| $.widget( "mobile.popup", $.mobile.widget, { |
| options: { |
| theme: null, |
| overlayTheme: null, |
| shadow: true, |
| corners: true, |
| transition: "pop", |
| positionTo: "origin", |
| tolerance: null, |
| initSelector: ":jqmData(role='popup')", |
| closeLinkSelector: "a:jqmData(rel='back')", |
| closeLinkEvents: "click.popup", |
| navigateEvents: "navigate.popup", |
| closeEvents: "navigate.popup pagebeforechange.popup", |
| isHardwarePopup: false, |
| // NOTE Windows Phone 7 has a scroll position caching issue that |
| // requires us to disable popup history management by default |
| // https://github.com/jquery/jquery-mobile/issues/4784 |
| // |
| // NOTE this option is modified in _create! |
| history: false |
| }, |
| |
| _eatEventAndClose: function( e ) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| this.close(); |
| return false; |
| }, |
| |
| // Make sure the screen size is increased beyond the page height if the popup's causes the document to increase in height |
| _resizeScreen: function() { |
| var popupHeight = this._ui.container.outerHeight( true ); |
| |
| this._ui.screen.removeAttr( "style" ); |
| if ( popupHeight > this._ui.screen.height() ) { |
| this._ui.screen.height( popupHeight ); |
| } |
| }, |
| |
| _handleWindowKeyUp: function( e ) { |
| if ( this._isOpen && e.keyCode === $.mobile.keyCode.ESCAPE ) { |
| return this._eatEventAndClose( e ); |
| } |
| }, |
| |
| _maybeRefreshTimeout: function() { |
| var winCoords = windowCoords(); |
| |
| if ( this._resizeData ) { |
| if ( winCoords.x === this._resizeData.winCoords.x && |
| winCoords.y === this._resizeData.winCoords.y && |
| winCoords.cx === this._resizeData.winCoords.cx && |
| winCoords.cy === this._resizeData.winCoords.cy ) { |
| // timeout not refreshed |
| return false; |
| } else { |
| // clear existing timeout - it will be refreshed below |
| clearTimeout( this._resizeData.timeoutId ); |
| } |
| } |
| |
| this._resizeData = { |
| timeoutId: setTimeout( $.proxy( this, "_resizeTimeout" ), 200 ), |
| winCoords: winCoords |
| }; |
| |
| return true; |
| }, |
| |
| _resizeTimeout: function() { |
| if ( !this._maybeRefreshTimeout() && this.positionTo === "window" && this._isOpen ) { |
| // effectively rapid-open the popup while leaving the screen intact |
| this._trigger( "beforeposition" ); |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( this._placementCoords( this._desiredCoords( undefined, undefined, "window" ) ) ); |
| |
| this._resizeScreen(); |
| this._resizeData = null; |
| this._orientationchangeInProgress = false; |
| } |
| }, |
| |
| _handleWindowResize: function( e ) { |
| if ( this._isOpen ) { |
| // Context popup close when Window resize event |
| if( this.positionTo !== "window" ) { |
| this.close(); |
| return false; |
| } |
| this._maybeRefreshTimeout(); |
| } |
| }, |
| |
| _handleWindowOrientationchange: function( e ) { |
| |
| if ( !this._orientationchangeInProgress ) { |
| // effectively rapid-close the popup while leaving the screen intact |
| this._ui.container |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| |
| this._orientationchangeInProgress = true; |
| } |
| }, |
| |
| _create: function() { |
| var ui = { |
| screen: $( "<div class='ui-screen-hidden ui-popup-screen'></div>" ), |
| placeholder: $( "<div style='display: none;'><!-- placeholder --></div>" ), |
| container: $( "<div class='ui-popup-container ui-selectmenu-hidden'></div>" ), |
| arrow : $("<div class='ui-arrow'></div>") |
| }, |
| thisPage = this.element.closest( ".ui-page" ), |
| myId = this.element.attr( "id" ), |
| self = this; |
| |
| // We need to adjust the history option to be false if there's no AJAX nav. |
| // We can't do it in the option declarations because those are run before |
| // it is determined whether there shall be AJAX nav. |
| this.options.history = this.options.history && $.mobile.ajaxEnabled && $.mobile.hashListeningEnabled; |
| |
| if ( thisPage.length === 0 ) { |
| thisPage = $( "body" ); |
| } |
| |
| // define the container for navigation event bindings |
| // TODO this would be nice at the the mobile widget level |
| this.options.container = this.options.container || $.mobile.pageContainer; |
| |
| // Apply the proto |
| thisPage.append( ui.screen ); |
| ui.container.insertAfter( ui.screen ); |
| // Leave a placeholder where the element used to be |
| ui.placeholder.insertAfter( this.element ); |
| if ( myId ) { |
| ui.screen.attr( "id", myId + "-screen" ); |
| ui.container.attr( "id", myId + "-popup" ); |
| ui.placeholder.html( "<!-- placeholder for " + myId + " -->" ); |
| } |
| ui.container.append( this.element ); |
| ui.container.append( ui.arrow ); |
| // Add class to popup element |
| this.element.addClass( "ui-popup" ); |
| |
| // Define instance variables |
| $.extend( this, { |
| _page: thisPage, |
| _ui: ui, |
| _fallbackTransition: "", |
| _currentTransition: false, |
| _prereqs: null, |
| _isOpen: false, |
| _tolerance: null, |
| _resizeData: null, |
| _orientationchangeInProgress: false, |
| _globalHandlers: [ |
| { |
| src: $.mobile.$window, |
| handler: { |
| orientationchange: $.proxy( this, "_handleWindowOrientationchange" ), |
| resize: $.proxy( this, "_handleWindowResize" ), |
| keyup: $.proxy( this, "_handleWindowKeyUp" ) |
| } |
| } |
| ] |
| }); |
| |
| $.each( this.options, function( key, value ) { |
| // Cause initial options to be applied by their handler by temporarily setting the option to undefined |
| // - the handler then sets it to the initial value |
| self.options[ key ] = undefined; |
| self._setOption( key, value, true ); |
| }); |
| |
| ui.screen.bind( "vclick", $.proxy( this, "_eatEventAndClose" ) ); |
| |
| $.each( this._globalHandlers, function( idx, value ) { |
| value.src.bind( value.handler ); |
| }); |
| }, |
| |
| _applyTheme: function( dst, theme, prefix ) { |
| var classes = ( dst.attr( "class" ) || "").split( " " ), |
| alreadyAdded = true, |
| currentTheme = null, |
| matches, |
| themeStr = String( theme ); |
| |
| while ( classes.length > 0 ) { |
| currentTheme = classes.pop(); |
| matches = ( new RegExp( "^ui-" + prefix + "-([a-z])$" ) ).exec( currentTheme ); |
| if ( matches && matches.length > 1 ) { |
| currentTheme = matches[ 1 ]; |
| break; |
| } else { |
| currentTheme = null; |
| } |
| } |
| |
| if ( theme !== currentTheme ) { |
| dst.removeClass( "ui-" + prefix + "-" + currentTheme ); |
| if ( ! ( theme === null || theme === "none" ) ) { |
| dst.addClass( "ui-" + prefix + "-" + themeStr ); |
| } |
| } |
| }, |
| |
| _setTheme: function( value ) { |
| this._applyTheme( this.element, value, "body" ); |
| }, |
| |
| _setOverlayTheme: function( value ) { |
| this._applyTheme( this._ui.screen, value, "overlay" ); |
| |
| if ( this._isOpen ) { |
| this._ui.screen.addClass( "in" ); |
| } |
| }, |
| |
| _setShadow: function( value ) { |
| this.element.toggleClass( "ui-overlay-shadow", value ); |
| }, |
| |
| _setCorners: function( value ) { |
| this.element.toggleClass( "ui-corner-all", value ); |
| }, |
| |
| _applyTransition: function( value ) { |
| this._ui.container.removeClass( this._fallbackTransition ); |
| if ( value && value !== "none" ) { |
| this._fallbackTransition = $.mobile._maybeDegradeTransition( value ); |
| this._ui.container.addClass( this._fallbackTransition ); |
| } |
| }, |
| |
| _setTransition: function( value ) { |
| if ( !this._currentTransition ) { |
| this._applyTransition( value ); |
| } |
| }, |
| |
| _setTolerance: function( value ) { |
| var tol = { t: 5, r: 5, b: 5, l: 5 }; |
| |
| if ( value ) { |
| var ar = String( value ).split( "," ); |
| |
| $.each( ar, function( idx, val ) { ar[ idx ] = parseInt( val, 10 ); } ); |
| |
| switch( ar.length ) { |
| // All values are to be the same |
| case 1: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.r = tol.b = tol.l = ar[ 0 ]; |
| } |
| break; |
| |
| // The first value denotes top/bottom tolerance, and the second value denotes left/right tolerance |
| case 2: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = tol.b = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.l = tol.r = ar[ 1 ]; |
| } |
| break; |
| |
| // The array contains values in the order top, right, bottom, left |
| case 4: |
| if ( !isNaN( ar[ 0 ] ) ) { |
| tol.t = ar[ 0 ]; |
| } |
| if ( !isNaN( ar[ 1 ] ) ) { |
| tol.r = ar[ 1 ]; |
| } |
| if ( !isNaN( ar[ 2 ] ) ) { |
| tol.b = ar[ 2 ]; |
| } |
| if ( !isNaN( ar[ 3 ] ) ) { |
| tol.l = ar[ 3 ]; |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| this._tolerance = tol; |
| }, |
| |
| _setOption: function( key, value ) { |
| var exclusions, setter = "_set" + key.charAt( 0 ).toUpperCase() + key.slice( 1 ); |
| |
| if ( this[ setter ] !== undefined ) { |
| this[ setter ]( value ); |
| } |
| |
| // TODO REMOVE FOR 1.2.1 by moving them out to a default options object |
| exclusions = [ |
| "initSelector", |
| "closeLinkSelector", |
| "closeLinkEvents", |
| "navigateEvents", |
| "closeEvents", |
| "history", |
| "container" |
| ]; |
| |
| $.mobile.widget.prototype._setOption.apply( this, arguments ); |
| if ( $.inArray( key, exclusions ) === -1 ) { |
| // Record the option change in the options and in the DOM data-* attributes |
| this.element.attr( "data-" + ( $.mobile.ns || "" ) + ( key.replace( /([A-Z])/, "-$1" ).toLowerCase() ), value ); |
| } |
| }, |
| |
| // Try and center the overlay over the given coordinates |
| _placementCoords: function( desired ) { |
| // rectangle within which the popup must fit |
| var |
| winCoords = windowCoords(), |
| rc = { |
| x: this._tolerance.l, |
| y: winCoords.y + this._tolerance.t, |
| cx: winCoords.cx - this._tolerance.l - this._tolerance.r, |
| cy: winCoords.cy - this._tolerance.t - this._tolerance.b |
| }, |
| menuSize, ret, |
| linkOffset = $(this.link).offset(), |
| positionOffsets = [], |
| correctionValue = [0,0], |
| arrayIdx; |
| |
| // Clamp the width of the menu before grabbing its size |
| this._ui.container.css( "max-width", rc.cx ); |
| menuSize = { |
| cx: this._ui.container.outerWidth( true ), |
| cy: this._ui.container.outerHeight( true ) |
| }; |
| |
| // Center the menu over the desired coordinates, while not going outside |
| // the window tolerances. This will center wrt. the window if the popup is too large. |
| ret = { |
| x: fitSegmentInsideSegment( rc.cx, menuSize.cx, rc.x, desired.x ), |
| y: fitSegmentInsideSegment( rc.cy, menuSize.cy, rc.y, desired.y ) |
| }; |
| |
| // Make sure the top of the menu is visible |
| ret.y = Math.max( 0, ret.y ); |
| |
| // If the height of the menu is smaller than the height of the document |
| // align the bottom with the bottom of the document |
| |
| // fix for $( document ).height() bug in core 1.7.2. |
| var docEl = document.documentElement, docBody = document.body, |
| docHeight = Math.max( docEl.clientHeight, docBody.scrollHeight, docBody.offsetHeight, docEl.scrollHeight, docEl.offsetHeight ); |
| |
| ret.y -= Math.min( ret.y, Math.max( 0, ret.y + menuSize.cy - docHeight ) ); |
| |
| if ( this.positionTo !== "origin" ) { |
| return { left: ret.x, top: ret.y , arrowleft: 0 , arrowtop: 0}; |
| } else if( this.options.isHardwarePopup ) { |
| return { left: this._tolerance.l, top: $(window).height() - menuSize.cy - this._tolerance.b, arrowleft: 0 , arrowtop: 0 }; |
| } |
| |
| positionOffsets = [ linkOffset.left, |
| linkOffset.top, |
| docEl.clientHeight - ( linkOffset.top + $(this.link).height() ), |
| docEl.clientWidth - ( linkOffset.left + $(this.link).width() )]; |
| arrayIdx = positionOffsets.indexOf(Math.max.apply(window,positionOffsets)); |
| |
| switch( arrayIdx ) |
| { |
| case 0: |
| correctionValue = [ -$(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = menuSize.cx; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow left" ) |
| break; |
| case 1: |
| correctionValue = [ 0 , -(ret.y + menuSize.cy - linkOffset.top)]; |
| arrowtop = menuSize.cy - 2; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass( "ui-arrow bottom" ); |
| break; |
| case 2: |
| correctionValue = [ 0 , ( linkOffset.top + $(this.link).height() - ret.y ) ]; |
| arrowtop = - parseInt( $(this._ui.arrow).css("border-width") ) * 2 + 1; |
| arrowleft = (linkOffset.left - ret.x + correctionValue[0]) + ( $(this.link).width() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) / 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow top"); |
| break; |
| case 3: |
| correctionValue = [ ( menuSize.cx < $(this.link).width() ) ? ( $(this.link).width() / 2 ) + ( menuSize.cx / 2) : $(this.link).width() , 0]; |
| arrowtop = ( linkOffset.top - ret.y ) + ( $(this.link).height() / 2 ) - parseInt( $(this._ui.arrow).css("border-width") ) ; |
| arrowleft = - parseInt( $(this._ui.arrow).css("border-width") ) * 2; |
| $(this._ui.arrow).attr( "class", "" ) |
| .addClass("ui-arrow right"); |
| break; |
| } |
| |
| return { left: ret.x + correctionValue[0], top: ret.y + correctionValue[1] , arrowleft: arrowleft , arrowtop: arrowtop }; |
| }, |
| |
| _createPrereqs: function( screenPrereq, containerPrereq, whenDone ) { |
| var self = this, prereqs; |
| |
| // It is important to maintain both the local variable prereqs and self._prereqs. The local variable remains in |
| // the closure of the functions which call the callbacks passed in. The comparison between the local variable and |
| // self._prereqs is necessary, because once a function has been passed to .animationComplete() it will be called |
| // next time an animation completes, even if that's not the animation whose end the function was supposed to catch |
| // (for example, if an abort happens during the opening animation, the .animationComplete handler is not called for |
| // that animation anymore, but the handler remains attached, so it is called the next time the popup is opened |
| // - making it stale. Comparing the local variable prereqs to the widget-level variable self._prereqs ensures that |
| // callbacks triggered by a stale .animationComplete will be ignored. |
| |
| prereqs = { |
| screen: $.Deferred(), |
| container: $.Deferred() |
| }; |
| |
| prereqs.screen.then( function() { |
| if ( prereqs === self._prereqs ) { |
| screenPrereq(); |
| } |
| }); |
| |
| prereqs.container.then( function() { |
| if ( prereqs === self._prereqs ) { |
| containerPrereq(); |
| } |
| }); |
| |
| $.when( prereqs.screen, prereqs.container ).done( function() { |
| if ( prereqs === self._prereqs ) { |
| self._prereqs = null; |
| whenDone(); |
| } |
| }); |
| |
| self._prereqs = prereqs; |
| }, |
| |
| _animate: function( args ) { |
| // NOTE before removing the default animation of the screen |
| // this had an animate callback that would relove the deferred |
| // now the deferred is resolved immediately |
| // TODO remove the dependency on the screen deferred |
| this._ui.screen |
| .removeClass( args.classToRemove ) |
| .addClass( args.screenClassToAdd ); |
| |
| args.prereqs.screen.resolve(); |
| |
| if ( args.transition && args.transition !== "none" ) { |
| if ( args.applyTransition ) { |
| this._applyTransition( args.transition ); |
| } |
| this._ui.container |
| .animationComplete( $.proxy( args.prereqs.container, "resolve" ) ) |
| .addClass( args.containerClassToAdd ) |
| .removeClass( args.classToRemove ); |
| } else { |
| this._ui.container.removeClass( args.classToRemove ); |
| args.prereqs.container.resolve(); |
| } |
| }, |
| |
| // The desired coordinates passed in will be returned untouched if no reference element can be identified via |
| // desiredPosition.positionTo. Nevertheless, this function ensures that its return value always contains valid |
| // x and y coordinates by specifying the center middle of the window if the coordinates are absent. |
| _desiredCoords: function( x, y, positionTo ) { |
| var dst = null, offset, winCoords = windowCoords(); |
| |
| self.positionTo = positionTo; |
| |
| // Establish which element will serve as the reference |
| if ( positionTo && positionTo !== "origin" ) { |
| if ( positionTo === "window" ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| y = winCoords.cy / 2 + winCoords.y; |
| } else { |
| try { |
| dst = $( positionTo ); |
| } catch( e ) { |
| dst = null; |
| } |
| if ( dst ) { |
| dst.filter( ":visible" ); |
| if ( dst.length === 0 ) { |
| dst = null; |
| } |
| } |
| } |
| } |
| |
| // If an element was found, center over it |
| if ( dst ) { |
| offset = dst.offset(); |
| x = offset.left + dst.outerWidth() / 2; |
| y = offset.top + dst.outerHeight() / 2; |
| } |
| |
| // Make sure x and y are valid numbers - center over the window |
| if ( $.type( x ) !== "number" || isNaN( x ) ) { |
| x = winCoords.cx / 2 + winCoords.x; |
| } |
| if ( $.type( y ) !== "number" || isNaN( y ) ) { |
| y = winCoords.cy / 2 + winCoords.y; |
| } |
| |
| return { x: x, y: y }; |
| }, |
| |
| _reposition: function() { |
| var self = this, |
| coords; |
| |
| if( self._isOpen |
| && self.link |
| && self.positionTo !== "window") { |
| coords = self._placementCoords( self._desiredCoords( $(self.link).offset().left + $(self.link).outerWidth() /2 , $(self.link).offset().top + $(self.link).outerHeight() /2 , self.positionTo || self.options.positionTo || "origin" ) ); |
| self._ui.container |
| .offset( { top : coords.top } ); |
| } |
| }, |
| |
| _openPrereqsComplete: function() { |
| var self = this; |
| |
| self._ui.container.addClass( "ui-popup-active" ); |
| self._isOpen = true; |
| self._resizeScreen(); |
| |
| // Android appears to trigger the animation complete before the popup |
| // is visible. Allowing the stack to unwind before applying focus prevents |
| // the "blue flash" of element focus in android 4.0 |
| setTimeout(function(){ |
| self._ui.container.attr( "tabindex", "0" ).focus(); |
| self._trigger( "afteropen" ); |
| self._reposition(); |
| }); |
| }, |
| |
| _open: function( options ) { |
| var coords, transition, |
| androidBlacklist = ( function() { |
| var w = window, |
| ua = navigator.userAgent, |
| // Rendering engine is Webkit, and capture major version |
| wkmatch = ua.match( /AppleWebKit\/([0-9\.]+)/ ), |
| wkversion = !!wkmatch && wkmatch[ 1 ], |
| androidmatch = ua.match( /Android (\d+(?:\.\d+))/ ), |
| andversion = !!androidmatch && androidmatch[ 1 ], |
| chromematch = ua.indexOf( "Chrome" ) > -1; |
| |
| // Platform is Android, WebKit version is greater than 534.13 ( Android 3.2.1 ) and not Chrome. |
| if( androidmatch !== null && andversion === "4.0" && wkversion && wkversion > 534.13 && !chromematch ) { |
| return true; |
| } |
| return false; |
| }()); |
| |
| // Make sure options is defined |
| options = ( options || {} ); |
| |
| // Copy out the transition, because we may be overwriting it later and we don't want to pass that change back to the caller |
| transition = options.transition || this.options.transition; |
| |
| // Give applications a chance to modify the contents of the container before it appears |
| this._trigger( "beforeposition" ); |
| |
| coords = this._placementCoords( this._desiredCoords( options.x, options.y, options.positionTo || this.options.positionTo || "origin" ) ); |
| |
| // Count down to triggering "popupafteropen" - we have two prerequisites: |
| // 1. The popup window animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.noop, |
| $.noop, |
| $.proxy( this, "_openPrereqsComplete" ) ); |
| |
| if ( transition ) { |
| this._currentTransition = transition; |
| this._applyTransition( transition ); |
| } else { |
| transition = this.options.transition; |
| } |
| |
| if ( !this.options.theme ) { |
| this._setTheme( this._page.jqmData( "theme" ) || $.mobile.getInheritedTheme( this._page, "c" ) ); |
| } |
| |
| this._ui.screen.removeClass( "ui-screen-hidden" ); |
| |
| this._ui.container |
| .removeClass( "ui-selectmenu-hidden" ) |
| .offset( coords ); |
| this._ui.arrow.css( { top : coords.arrowtop, left : coords.arrowleft } ); |
| if ( this.options.overlayTheme && androidBlacklist ) { |
| /* TODO: |
| The native browser on Android 4.0.X ("Ice Cream Sandwich") suffers from an issue where the popup overlay appears to be z-indexed |
| above the popup itself when certain other styles exist on the same page -- namely, any element set to `position: fixed` and certain |
| types of input. These issues are reminiscent of previously uncovered bugs in older versions of Android's native browser: |
| https://github.com/scottjehl/Device-Bugs/issues/3 |
| |
| This fix closes the following bugs ( I use "closes" with reluctance, and stress that this issue should be revisited as soon as possible ): |
| |
| https://github.com/jquery/jquery-mobile/issues/4816 |
| https://github.com/jquery/jquery-mobile/issues/4844 |
| https://github.com/jquery/jquery-mobile/issues/4874 |
| */ |
| |
| // TODO sort out why this._page isn't working |
| this.element.closest( ".ui-page" ).addClass( "ui-popup-open" ); |
| } |
| this._animate({ |
| additionalCondition: true, |
| transition: transition, |
| classToRemove: "", |
| screenClassToAdd: "in", |
| containerClassToAdd: "in", |
| applyTransition: false, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _closePrereqScreen: function() { |
| this._ui.screen |
| .removeClass( "out" ) |
| .addClass( "ui-screen-hidden" ); |
| }, |
| |
| _closePrereqContainer: function() { |
| this._ui.container |
| .removeClass( "reverse out" ) |
| .addClass( "ui-selectmenu-hidden" ) |
| .removeAttr( "style" ); |
| }, |
| |
| _closePrereqsDone: function() { |
| var self = this, opts = self.options; |
| |
| self._ui.container.removeAttr( "tabindex" ); |
| |
| // remove nav bindings if they are still present |
| opts.container.unbind( opts.closeEvents ); |
| |
| // unbind click handlers added when history is disabled |
| self.element.undelegate( opts.closeLinkSelector, opts.closeLinkEvents ); |
| |
| // remove the global mutex for popups |
| $.mobile.popup.active = undefined; |
| |
| // alert users that the popup is closed |
| self._trigger( "afterclose" ); |
| }, |
| |
| _close: function( immediate ) { |
| this._ui.container.removeClass( "ui-popup-active" ); |
| this._page.removeClass( "ui-popup-open" ); |
| |
| this._isOpen = false; |
| |
| // IME hide when popup is closed |
| this.element.find("input").blur(); |
| |
| // Count down to triggering "popupafterclose" - we have two prerequisites: |
| // 1. The popup window reverse animation completes (container()) |
| // 2. The screen opacity animation completes (screen()) |
| this._createPrereqs( |
| $.proxy( this, "_closePrereqScreen" ), |
| $.proxy( this, "_closePrereqContainer" ), |
| $.proxy( this, "_closePrereqsDone" ) ); |
| |
| this._animate( { |
| additionalCondition: this._ui.screen.hasClass( "in" ), |
| transition: ( immediate ? "none" : ( this._currentTransition || this.options.transition ) ), |
| classToRemove: "in", |
| screenClassToAdd: "out", |
| containerClassToAdd: "reverse out", |
| applyTransition: true, |
| prereqs: this._prereqs |
| }); |
| }, |
| |
| _destroy: function() { |
| var self = this; |
| |
| // hide and remove bindings |
| self._close(); |
| |
| // Put the element back to where the placeholder was and remove the "ui-popup" class |
| self._setTheme( "none" ); |
| self.element |
| .insertAfter( self._ui.placeholder ) |
| .removeClass( "ui-popup ui-overlay-shadow ui-corner-all" ); |
| self._ui.screen.remove(); |
| self._ui.container.remove(); |
| self._ui.placeholder.remove(); |
| |
| // Unbind handlers that were bound to elements outside self.element (the window, in self case) |
| // window history back is call "_destroy method" |
| // if this method is called, all kind of window resize event has been unbind |
| /* |
| $.each( self._globalHandlers, function( idx, oneSrc ) { |
| $.each( oneSrc.handler, function( eventType, handler ) { |
| oneSrc.src.unbind( eventType, handler ); |
| }); |
| }); |
| */ |
| }, |
| |
| _closePopup: function( e, data ) { |
| var parsedDst, toUrl; |
| |
| if ( e.type === "pagebeforechange" && data ) { |
| if ( typeof data.toPage === "string" ) { |
| parsedDst = data.toPage; |
| } else { |
| parsedDst = data.toPage.jqmData( "url" ); |
| } |
| parsedDst = $.mobile.path.parseUrl( parsedDst ); |
| toUrl = parsedDst.pathname + parsedDst.search + parsedDst.hash; |
| |
| if ( this._myUrl !== toUrl ) { |
| this.options.container.unbind( this.options.closeEvents ); |
| this._close( true ); |
| } else { |
| this._close(); |
| } |
| return; // skip normal close |
| } |
| |
| this._close(); |
| }, |
| |
| // any navigation event after a popup is opened should close the popup |
| // NOTE the pagebeforechange is bound to catch navigation events that don't |
| // alter the url (eg, dialogs from popups) |
| _bindContainerClose: function() { |
| var self = this; |
| |
| self.options.container |
| .one( self.options.closeEvents, $.proxy( self, "_closePopup" ) ); |
| }, |
| |
| // TODO no clear deliniation of what should be here and |
| // what should be in _open. Seems to be "visual" vs "history" for now |
| open: function( options ) { |
| var self = this, opts = this.options, url, hashkey, activePage, currentIsDialog, hasHash, urlHistory; |
| // self.link = ( $(event.target).attr('data-role') === 'button') ? event.target : $(event.target).closest('[data-role="button"]')[0]; |
| // make sure open is idempotent |
| if( $.mobile.popup.active ) { |
| return; |
| } |
| // set the global popup mutex |
| $.mobile.popup.active = this; |
| if( !options ) { |
| options = []; |
| } |
| |
| if ( !options.link ) { |
| if ( !event ) { |
| self.positionTo = "window"; |
| } else { |
| self.link = ( $(event.target).closest('a')[0] || $(event.target).closest('div')[0] ); |
| } |
| } else { |
| self.link = options.link; |
| } |
| if ( event ) { |
| self.positionTo = ( options != null && options.positionTo != null ) ? options.positionTo : "origin"; |
| } |
| |
| if ( $(self.link).jqmData("position-to") !== "window" |
| && self.positionTo !== "window" ) { |
| |
| $(self.element).addClass("ui-ctxpopup"); |
| $(self._ui.container).removeClass("ui-popup-container") |
| .addClass("ui-ctxpopup-container"); |
| |
| if( self.positionTo !== "origin" ) { |
| $(self._ui.arrow).hide(); |
| } else { |
| $(self._ui.arrow).show(); |
| } |
| } else { |
| $(self._ui.arrow).hide(); |
| // apply opacity back screen |
| this._setOverlayTheme( "dim" ); |
| } |
| if( !options.x |
| && self.positionTo === "origin" |
| && self.link ) { |
| options.x = $(self.link).offset().left + $(self.link).outerWidth() / 2; |
| } |
| if( !options.y |
| && self.positionTo === "origin" |
| && self.link ) { |
| options.y = $(self.link).offset().top + $(self.link).outerHeight() / 2; |
| } |
| |
| // Hadeware key style popup |
| if( $(self.element).hasClass( "ui-ctxpopup-optionmenu" ) ){ |
| self.options.isHardwarePopup = true; |
| $( self._ui.arrow).hide(); |
| } |
| |
| // if history alteration is disabled close on navigate events |
| // and leave the url as is |
| if( !( opts.history ) ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| |
| // When histoy is disabled we have to grab the data-rel |
| // back link clicks so we can close the popup instead of |
| // relying on history to do it for us |
| self.element |
| .delegate( opts.closeLinkSelector, opts.closeLinkEvents, function( e ) { |
| self._close(); |
| |
| // NOTE prevent the browser and navigation handlers from |
| // working with the link's rel=back. This may cause |
| // issues for developers expecting the event to bubble |
| return false; |
| }); |
| |
| return; |
| } |
| |
| // cache some values for min/readability |
| hashkey = $.mobile.dialogHashKey; |
| activePage = $.mobile.activePage; |
| currentIsDialog = activePage.is( ".ui-dialog" ); |
| // Set active page url |
| this._myUrl = url = urlHistory.getActive().url; |
| // |
| url = $.mobile.urlHistory.getActive().url; |
| hasHash = ( url.indexOf( hashkey ) > -1 ) && !currentIsDialog; |
| urlHistory = $.mobile.urlHistory; |
| |
| if ( hasHash ) { |
| self._open( options ); |
| self._bindContainerClose(); |
| return; |
| } |
| |
| // if the current url has no dialog hash key proceed as normal |
| // otherwise, if the page is a dialog simply tack on the hash key |
| if ( url.indexOf( hashkey ) === -1 && !currentIsDialog ){ |
| url = url + hashkey; |
| } else { |
| url = $.mobile.path.parseLocation().hash + hashkey; |
| } |
| |
| // Tack on an extra hashkey if this is the first page and we've just reconstructed the initial hash |
| if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) { |
| url += hashkey; |
| } |
| |
| // swallow the the initial navigation event, and bind for the next |
| opts.container.one( opts.navigateEvents, function( e ) { |
| e.preventDefault(); |
| self._open( options ); |
| self._bindContainerClose(); |
| }); |
| |
| urlHistory.ignoreNextHashChange = currentIsDialog; |
| |
| // Gotta love methods with 1mm args :( |
| urlHistory.addNew( url, undefined, undefined, undefined, "dialog" ); |
| |
| // set the new url with (or without) the new dialog hash key |
| $.mobile.path.set( url ); |
| }, |
| |
| close: function() { |
| // make sure close is idempotent |
| if( !$.mobile.popup.active ){ |
| return; |
| } |
| |
| if( this.options.history ) { |
| $.mobile.back(); |
| } else { |
| this._close(); |
| } |
| } |
| }); |
| |
| |
| // TODO this can be moved inside the widget |
| $.mobile.popup.handleLink = function( $link ) { |
| var closestPage = $link.closest( ":jqmData(role='page')" ), |
| scope = ( ( closestPage.length === 0 ) ? $( "body" ) : closestPage ), |
| // NOTE make sure to get only the hash, ie7 (wp7) return the absolute href |
| // in this case ruining the element selection |
| popup = $( $.mobile.path.parseUrl($link.attr( "href" )).hash, scope[0] ), |
| offset; |
| |
| if ( popup.data( "popup" ) ) { |
| offset = $link.offset(); |
| popup.popup( "open", { |
| x: offset.left + $link.outerWidth() / 2, |
| y: offset.top + $link.outerHeight() / 2, |
| transition: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "transition" ), |
| positionTo: $.mobile.getAttrFixed( $link[0], "data-" + $.mobile.ns + "position-to" ), |
| link: $link |
| }); |
| } |
| |
| //remove after delay |
| setTimeout( function() { |
| $link.removeClass( $.mobile.activeBtnClass ); |
| }, 300 ); |
| }; |
| |
| // TODO move inside _create |
| $.mobile.$document.bind( "pagebeforechange", function( e, data ) { |
| if ( data.options.role === "popup" ) { |
| $.mobile.popup.handleLink( data.options.link ); |
| e.preventDefault(); |
| } |
| }); |
| |
| //delegate self-init widgets |
| $.delegateSelfInitWithSingleSelector( $.mobile.popup, true ); |
| |
| })( jQuery ); |
| |
| (function( $ ) { |
| var meta = $( "meta[name=viewport]" ), |
| initialContent = meta.attr( "content" ), |
| disabledZoom = initialContent + ",maximum-scale=1, user-scalable=no", |
| enabledZoom = initialContent + ",maximum-scale=10, user-scalable=yes", |
| disabledInitially = /(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test( initialContent ); |
| |
| $.mobile.zoom = $.extend( {}, { |
| enabled: !disabledInitially, |
| locked: false, |
| disable: function( lock ) { |
| if ( !disabledInitially && !$.mobile.zoom.locked ) { |
| meta.attr( "content", disabledZoom ); |
| $.mobile.zoom.enabled = false; |
| $.mobile.zoom.locked = lock || false; |
| } |
| }, |
| enable: function( unlock ) { |
| if ( !disabledInitially && ( !$.mobile.zoom.locked || unlock === true ) ) { |
| meta.attr( "content", enabledZoom ); |
| $.mobile.zoom.enabled = true; |
| $.mobile.zoom.locked = false; |
| } |
| }, |
| restore: function() { |
| if ( !disabledInitially ) { |
| meta.attr( "content", initialContent ); |
| $.mobile.zoom.enabled = true; |
| } |
| } |
| }); |
| |
| }( jQuery )); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.textinput", $.mobile.widget, { |
| options: { |
| theme: null, |
| // This option defaults to true on iOS devices. |
| preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, |
| initSelector: "input[type='text'], input[type='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])", |
| disabled: false |
| }, |
| |
| _create: function() { |
| |
| var self = this, |
| input = this.element, |
| o = this.options, |
| theme = o.theme || $.mobile.getInheritedTheme( this.element, "c" ), |
| themeclass = " ui-body-" + theme, |
| mini = $.mobile.getAttrFixed( input[0], "data-" + $.mobile.ns + "mini" ) === true, |
| miniclass = mini ? " ui-mini" : "", |
| focusedEl, clearbtn; |
| |
| function toggleClear() { |
| setTimeout( function() { |
| clearbtn.toggleClass( "ui-input-clear-hidden", !input.val() ); |
| }, 0 ); |
| } |
| |
| $( "label[for='" + input.attr( "id" ) + "']" ).addClass( "ui-input-text" ); |
| |
| focusedEl = input.addClass("ui-input-text ui-body-"+ theme ); |
| |
| switch ( input.attr( "type" ) ) { |
| case "text": |
| case "password": |
| case "number": |
| case "email": |
| case "url": |
| case "tel": |
| input.attr( { "role" : "textbox", "aria-label" : "Keyboard opened" } ); |
| break; |
| default: |
| if ( input.prop( "tagName" ).toLowerCase() === "textarea" ) { |
| input.attr( { "role" : "textbox", "aria-label" : "Keyboard opened" } ); |
| } |
| } |
| |
| // XXX: Temporary workaround for issue 785 (Apple bug 8910589). |
| // Turn off autocorrect and autocomplete on non-iOS 5 devices |
| // since the popup they use can't be dismissed by the user. Note |
| // that we test for the presence of the feature by looking for |
| // the autocorrect property on the input element. We currently |
| // have no test for iOS 5 or newer so we're temporarily using |
| // the touchOverflow support flag for jQM 1.0. Yes, I feel dirty. - jblas |
| if ( typeof input[0].autocorrect !== "undefined" && !$.support.touchOverflow ) { |
| // Set the attribute instead of the property just in case there |
| // is code that attempts to make modifications via HTML. |
| input[0].setAttribute( "autocorrect", "off" ); |
| input[0].setAttribute( "autocomplete", "off" ); |
| } |
| |
| input.focus(function() { |
| focusedEl.addClass( $.mobile.focusClass ); |
| }) |
| .blur(function() { |
| focusedEl.removeClass( $.mobile.focusClass ); |
| }) |
| // In many situations, iOS will zoom into the select upon tap, this prevents that from happening |
| .bind( "focus", function() { |
| if ( o.preventFocusZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| }) |
| .bind( "blur", function() { |
| if ( o.preventFocusZoom ) { |
| $.mobile.zoom.enable( true ); |
| } |
| }); |
| |
| // Autogrow |
| if ( input.is( "textarea" ) ) { |
| var extraLineHeight = 15, |
| keyupTimeoutBuffer = 100, |
| keyupTimeout; |
| |
| this._keyup = function() { |
| var scrollHeight = input[ 0 ].scrollHeight, |
| clientHeight = input[ 0 ].clientHeight; |
| |
| if ( clientHeight < scrollHeight && window.innerHeight / 2 > scrollHeight ) { |
| input.height(scrollHeight + extraLineHeight); |
| } |
| }; |
| |
| input.keyup(function() { |
| clearTimeout( keyupTimeout ); |
| keyupTimeout = setTimeout( self._keyup, keyupTimeoutBuffer ); |
| }); |
| |
| // binding to pagechange here ensures that for pages loaded via |
| // ajax the height is recalculated without user input |
| this._on( $.mobile.$document, {"pagechange": "_keyup" }); |
| |
| // Issue 509: the browser is not providing scrollHeight properly until the styles load |
| if ( $.trim( input.val() ) ) { |
| // bind to the window load to make sure the height is calculated based on BOTH |
| // the DOM and CSS |
| this._on( $.mobile.$window, {"load": "_keyup"}); |
| } |
| } |
| if ( input.attr( "disabled" ) ) { |
| this.disable(); |
| } |
| }, |
| |
| disable: function() { |
| var $el; |
| if ( this.element.attr( "disabled", true ) ) { |
| $el = this.element; |
| } else { |
| return; |
| } |
| $el.addClass( "ui-disabled" ); |
| return this._setOption( "disabled", true ); |
| }, |
| |
| enable: function() { |
| var $el; |
| |
| // TODO using more than one line of code is acceptable ;) |
| if ( this.element.attr( "disabled", false ) ) { |
| $el = this.element; |
| } else { |
| return; |
| } |
| $el.removeClass( "ui-disabled" ); |
| return this._setOption( "disabled", false ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.textinput.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.mobile.listview.prototype.options.filter = false; |
| $.mobile.listview.prototype.options.filterPlaceholder = ""; |
| $.mobile.listview.prototype.options.filterTheme = "c"; |
| // TODO rename callback/deprecate and default to the item itself as the first argument |
| var defaultFilterCallback = function( text, searchValue, item ) { |
| return text.toString().toLowerCase().indexOf( searchValue ) === -1; |
| }; |
| |
| $.mobile.listview.prototype.options.filterCallback = defaultFilterCallback; |
| |
| $.mobile.$document.delegate( ":jqmData(role='listview')", "listviewcreate", function() { |
| |
| var list = $( this ), |
| listview = list.data( "listview" ); |
| |
| if ( !listview.options.filter ) { |
| return; |
| } |
| |
| var wrapper = $( "<form>", { |
| "class": "ui-listview-filter ui-bar-" + listview.options.filterTheme, |
| "role": "search" |
| }), |
| search = $( "<input>", { |
| placeholder: listview.options.filterPlaceholder |
| }) |
| .attr( "data-" + $.mobile.ns + "type", "search" ) |
| .jqmData( "lastval", "" ) |
| .bind( "keyup change", function() { |
| |
| var $this = $( this ), |
| val = this.value.toLowerCase(), |
| listItems = null, |
| lastval = $this.jqmData( "lastval" ) + "", |
| childItems = false, |
| itemtext = "", |
| item, |
| // Check if a custom filter callback applies |
| isCustomFilterCallback = listview.options.filterCallback !== defaultFilterCallback; |
| |
| listview._trigger( "beforefilter", "beforefilter", { input: this } ); |
| |
| // Change val as lastval for next execution |
| $this.jqmData( "lastval" , val ); |
| if ( isCustomFilterCallback || val.length < lastval.length || val.indexOf( lastval ) !== 0 ) { |
| |
| // Custom filter callback applies or removed chars or pasted something totally different, check all items |
| listItems = list.children(); |
| } else { |
| |
| // Only chars added, not removed, only use visible subset |
| listItems = list.children( ":not(.ui-screen-hidden)" ); |
| } |
| |
| if ( val ) { |
| |
| // This handles hiding regular rows without the text we search for |
| // and any list dividers without regular rows shown under it |
| |
| for ( var i = listItems.length - 1; i >= 0; i-- ) { |
| item = $( listItems[ i ] ); |
| itemtext = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "filtertext" ) || item.text(); |
| |
| if ( item.is( "li:jqmData(role=list-divider)" ) ) { |
| |
| item.toggleClass( "ui-filter-hidequeue" , !childItems ); |
| |
| // New bucket! |
| childItems = false; |
| |
| } else if ( listview.options.filterCallback( itemtext, val, item ) ) { |
| |
| //mark to be hidden |
| item.toggleClass( "ui-filter-hidequeue" , true ); |
| } else { |
| |
| // There's a shown item in the bucket |
| childItems = true; |
| } |
| } |
| |
| // Show items, not marked to be hidden |
| listItems |
| .filter( ":not(.ui-filter-hidequeue)" ) |
| .toggleClass( "ui-screen-hidden", false ); |
| |
| // Hide items, marked to be hidden |
| listItems |
| .filter( ".ui-filter-hidequeue" ) |
| .toggleClass( "ui-screen-hidden", true ) |
| .toggleClass( "ui-filter-hidequeue", false ); |
| |
| } else { |
| |
| //filtervalue is empty => show all |
| listItems.toggleClass( "ui-screen-hidden", false ); |
| } |
| listview._refreshCorners(); |
| }) |
| .appendTo( wrapper ) |
| .textinput(); |
| |
| if ( listview.options.inset ) { |
| wrapper.addClass( "ui-listview-filter-inset" ); |
| } |
| |
| wrapper.bind( "submit", function() { |
| return false; |
| }) |
| .insertBefore( list ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.slider", $.mobile.widget, { |
| widgetEventPrefix: "slide", |
| |
| options: { |
| theme: null, |
| trackTheme: null, |
| disabled: false, |
| initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')", |
| mini: false |
| }, |
| |
| _create: function() { |
| |
| // TODO: Each of these should have comments explain what they're for |
| var self = this, |
| |
| control = this.element, |
| |
| parentTheme = $.mobile.getInheritedTheme( control, "c" ), |
| |
| theme = this.options.theme || parentTheme, |
| |
| trackTheme = this.options.trackTheme || parentTheme, |
| |
| cType = control[ 0 ].nodeName.toLowerCase(), |
| |
| selectClass = ( cType === "select" ) ? "ui-slider-switch" : "", |
| |
| controlID = control.attr( "id" ), |
| |
| $label = $( "[for='" + controlID + "']" ), |
| |
| labelID = $label.attr( "id" ) || controlID + "-label", |
| |
| label = $label.attr( "id", labelID ), |
| |
| val = function() { |
| return cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex; |
| }, |
| |
| min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0, |
| |
| max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1, |
| |
| step = window.parseFloat( control.attr( "step" ) || 1 ), |
| |
| inlineClass = ( this.options.inline || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "inline" ) === true ) ? " ui-slider-inline" : "", |
| |
| miniClass = ( this.options.mini || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "min" ) ) ? " ui-slider-mini" : "", |
| |
| |
| domHandle = document.createElement( 'a' ), |
| handle = $( domHandle ), |
| domSlider = document.createElement( 'div' ), |
| slider = $( domSlider ), |
| |
| valuebg = $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "highlight" ) !== false && cType !== "select" ? (function() { |
| var bg = document.createElement('div'); |
| bg.className = 'ui-slider-bg ' + $.mobile.activeBtnClass + ' ui-btn-corner-all'; |
| return $( bg ).prependTo( slider ); |
| })() : false, |
| |
| options; |
| |
| this._type = cType; |
| |
| domHandle.setAttribute( 'href', "#" ); |
| domSlider.setAttribute('role','application'); |
| domSlider.className = ['ui-slider ',selectClass," ui-btn-down-",trackTheme,' ui-btn-corner-all', inlineClass, miniClass].join( "" ); |
| domHandle.className = 'ui-slider-handle'; |
| domSlider.appendChild( domHandle ); |
| if ( $( control ).find( "option" ).length && $( control ).find( "option" ).text() === "" ) { |
| $( domSlider ).addClass( "ui-toggle-switch" ); |
| } |
| if( val() === "1" ) { |
| $( domHandle ).addClass( "ui-toggle-on" ); |
| } else { |
| $( domHandle ).addClass( "ui-toggle-off" ); |
| } |
| handle.buttonMarkup({ corners: true, theme: theme, shadow: true }) |
| .attr({ |
| "role": "slider", |
| "aria-valuemin": min, |
| "aria-valuemax": max, |
| "aria-valuenow": val(), |
| "aria-valuetext": val(), |
| "title": val(), |
| "aria-labelledby": labelID |
| }); |
| |
| $.extend( this, { |
| slider: slider, |
| handle: handle, |
| valuebg: valuebg, |
| dragging: false, |
| beforeStart: null, |
| userModified: false, |
| mouseMoved: false |
| }); |
| |
| if ( cType === "select" ) { |
| var wrapper = document.createElement('div'); |
| wrapper.className = 'ui-slider-inneroffset'; |
| |
| for ( var j = 0,length = domSlider.childNodes.length;j < length;j++ ) { |
| wrapper.appendChild( domSlider.childNodes[j] ); |
| } |
| |
| domSlider.appendChild( wrapper ); |
| |
| // slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" ); |
| |
| // make the handle move with a smooth transition |
| handle.addClass( "ui-slider-handle-snapping" ); |
| |
| options = control.find( "option" ); |
| |
| for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) { |
| var side = !i ? "b" : "a", |
| sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ), |
| sliderLabel = document.createElement( 'div' ), |
| sliderImg = document.createElement( 'span' ); |
| |
| sliderImg.className = ['ui-slider-label ui-slider-label-',side,sliderTheme," ui-btn-corner-all"].join( "" ); |
| sliderImg.setAttribute('role','img'); |
| sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) ); |
| $( sliderImg ).html() ? $( sliderImg ).html( $( sliderImg ).text() ) : $( sliderImg ).html(); |
| $(sliderImg).prependTo( slider ); |
| } |
| |
| self._labels = $( ".ui-slider-label", slider ); |
| |
| } |
| |
| label.addClass( "ui-slider" ); |
| |
| // monitor the input for updated values |
| control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" ) |
| .change(function() { |
| // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again |
| if ( !self.mouseMoved ) { |
| self.refresh( val(), true ); |
| } |
| }) |
| .keyup(function() { // necessary? |
| self.refresh( val(), true, true ); |
| }) |
| .blur(function() { |
| self.refresh( val(), true ); |
| }); |
| |
| this._preventDocumentDrag = function( event ) { |
| // NOTE: we don't do this in refresh because we still want to |
| // support programmatic alteration of disabled inputs |
| var et = $(event.target); |
| if ( self.dragging && !self.options.disabled && ( ( et.parents( ".ui-slider" ).is( ".ui-toggle-switch" ) && et.parents( ".ui-slider-handle" ).is( ".ui-btn-hover-s" ) ) || ( !$( self.element ).siblings( ".ui-slider" ).is( ".ui-toggle-switch"))) ) { |
| |
| // self.mouseMoved must be updated before refresh() because it will be used in the control "change" event |
| self.mouseMoved = true; |
| |
| if ( cType === "select" ) { |
| // make the handle move in sync with the mouse |
| handle.removeClass( "ui-slider-handle-snapping" ); |
| } |
| |
| self.refresh( event ); |
| |
| // only after refresh() you can calculate self.userModified |
| self.userModified = self.beforeStart !== control[0].selectedIndex; |
| return false; |
| } |
| } |
| |
| this._on( $.mobile.$document, { "vmousemove": this._preventDocumentDrag }); |
| |
| // it appears the clicking the up and down buttons in chrome on |
| // range/number inputs doesn't trigger a change until the field is |
| // blurred. Here we check thif the value has changed and refresh |
| control.bind( "vmouseup", $.proxy( self._checkedRefresh, self)); |
| |
| slider.bind( "vmousedown", function( event ) { |
| // NOTE: we don't do this in refresh because we still want to |
| // support programmatic alteration of disabled inputs |
| if ( self.options.disabled ) { |
| return false; |
| } |
| |
| self.dragging = true; |
| self.userModified = false; |
| self.mouseMoved = false; |
| |
| if ( cType === "select" ) { |
| self.beforeStart = control[0].selectedIndex; |
| } |
| |
| self.refresh( event ); |
| self._trigger( "start" ); |
| return false; |
| }) |
| .bind( "vclick", false ); |
| |
| this._sliderMouseUp = function() { |
| if ( self.dragging ) { |
| self.dragging = false; |
| |
| if ( cType === "select") { |
| // make the handle move with a smooth transition |
| handle.addClass( "ui-slider-handle-snapping" ); |
| |
| if ( self.mouseMoved ) { |
| // this is a drag, change the value only if user dragged enough |
| if ( self.userModified ) { |
| self.refresh( self.beforeStart === 0 ? 1 : 0 ); |
| } |
| else { |
| self.refresh( self.beforeStart ); |
| } |
| } |
| else { |
| // this is just a click, change the value |
| self.refresh( self.beforeStart === 0 ? 1 : 0 ); |
| } |
| } |
| |
| self.mouseMoved = false; |
| self._trigger( "stop" ); |
| return false; |
| } |
| }; |
| |
| this._on( slider.add( document ), { "vmouseup": this._sliderMouseUp }); |
| slider.insertAfter( control ); |
| |
| // Only add focus class to toggle switch, sliders get it automatically from ui-btn |
| if ( cType === 'select' ) { |
| this.handle.bind({ |
| focus: function() { |
| slider.addClass( $.mobile.focusClass ); |
| }, |
| |
| blur: function() { |
| slider.removeClass( $.mobile.focusClass ); |
| } |
| }); |
| } |
| |
| this.handle.bind({ |
| // NOTE force focus on handle |
| vmousedown: function() { |
| $( this ).focus(); |
| }, |
| |
| vclick: false, |
| |
| keydown: function( event ) { |
| var index = val(); |
| |
| if ( self.options.disabled ) { |
| return; |
| } |
| |
| // In all cases prevent the default and mark the handle as active |
| switch ( event.keyCode ) { |
| case $.mobile.keyCode.HOME: |
| case $.mobile.keyCode.END: |
| case $.mobile.keyCode.PAGE_UP: |
| case $.mobile.keyCode.PAGE_DOWN: |
| case $.mobile.keyCode.UP: |
| case $.mobile.keyCode.RIGHT: |
| case $.mobile.keyCode.DOWN: |
| case $.mobile.keyCode.LEFT: |
| event.preventDefault(); |
| |
| if ( !self._keySliding ) { |
| self._keySliding = true; |
| $( this ).addClass( "ui-state-active" ); |
| } |
| break; |
| } |
| |
| // move the slider according to the keypress |
| switch ( event.keyCode ) { |
| case $.mobile.keyCode.HOME: |
| self.refresh( min ); |
| break; |
| case $.mobile.keyCode.END: |
| self.refresh( max ); |
| break; |
| case $.mobile.keyCode.PAGE_UP: |
| case $.mobile.keyCode.UP: |
| case $.mobile.keyCode.RIGHT: |
| self.refresh( index + step ); |
| break; |
| case $.mobile.keyCode.PAGE_DOWN: |
| case $.mobile.keyCode.DOWN: |
| case $.mobile.keyCode.LEFT: |
| self.refresh( index - step ); |
| break; |
| } |
| }, // remove active mark |
| |
| keyup: function( event ) { |
| if ( self._keySliding ) { |
| self._keySliding = false; |
| $( this ).removeClass( "ui-state-active" ); |
| } |
| } |
| }); |
| |
| this.refresh( undefined, undefined, true ); |
| }, |
| |
| _checkedRefresh: function() { |
| if( this.value != this._value() ){ |
| this.refresh( this._value() ); |
| } |
| }, |
| |
| _value: function() { |
| return this._type === "input" ? |
| parseFloat( this.element.val() ) : this.element[0].selectedIndex; |
| }, |
| |
| refresh: function( val, isfromControl, preventInputUpdate ) { |
| |
| // NOTE: we don't return here because we want to support programmatic |
| // alteration of the input value, which should still update the slider |
| if ( this.options.disabled || this.element.attr('disabled')) { |
| this.disable(); |
| } |
| |
| // set the stored value for comparison later |
| this.value = this._value(); |
| |
| var control = this.element, percent, |
| cType = control[0].nodeName.toLowerCase(), |
| min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0, |
| max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1, |
| step = ( cType === "input" && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1; |
| |
| if ( typeof val === "object" ) { |
| var data = val, |
| // a slight tolerance helped get to the ends of the slider |
| tol = 8; |
| if ( !this.dragging || |
| data.pageX < this.slider.offset().left - tol || |
| data.pageX > this.slider.offset().left + this.slider.width() + tol ) { |
| return; |
| } |
| percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 ); |
| } else { |
| if ( val == null ) { |
| val = cType === "input" ? parseFloat( control.val() || 0 ) : control[0].selectedIndex; |
| } |
| percent = ( parseFloat( val ) - min ) / ( max - min ) * 100; |
| } |
| |
| if ( isNaN( percent ) ) { |
| return; |
| } |
| |
| if ( percent < 0 ) { |
| percent = 0; |
| } |
| |
| if ( percent > 100 ) { |
| percent = 100; |
| } |
| |
| var newval = ( percent / 100 ) * ( max - min ) + min; |
| |
| //from jQuery UI slider, the following source will round to the nearest step |
| var valModStep = ( newval - min ) % step; |
| var alignValue = newval - valModStep; |
| |
| if ( Math.abs( valModStep ) * 2 >= step ) { |
| alignValue += ( valModStep > 0 ) ? step : ( -step ); |
| } |
| // Since JavaScript has problems with large floats, round |
| // the final value to 5 digits after the decimal point (see jQueryUI: #4124) |
| newval = parseFloat( alignValue.toFixed(5) ); |
| |
| if ( newval < min ) { |
| newval = min; |
| } |
| |
| if ( newval > max ) { |
| newval = max; |
| } |
| |
| this.handle.css( "left", percent + "%" ); |
| this.handle.attr( { |
| "aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ), |
| "aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(), |
| title: cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText() |
| }); |
| |
| if( percent === 100 && this.handle.hasClass( "ui-slider-handle-snapping" ) ) { |
| this.handle.removeClass( "ui-toggle-off" ); |
| this.handle.addClass( "ui-toggle-on" ); |
| } else if ( percent === 0 && this.handle.hasClass( "ui-slider-handle-snapping" ) ) { |
| this.handle.removeClass( "ui-toggle-on" ); |
| this.handle.addClass( "ui-toggle-off" ); |
| } |
| |
| if ( this.valuebg ) { |
| this.valuebg.css( "width", percent + "%" ); |
| } |
| |
| // drag the label widths |
| if ( this._labels ) { |
| var handlePercent = this.handle.width() / this.slider.width() * 100, |
| aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100, |
| bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 ); |
| |
| this._labels.each(function() { |
| var ab = $( this ).is( ".ui-slider-label-a" ); |
| $( this ).width( ( ab ? aPercent : bPercent ) + "%" ); |
| }); |
| } |
| |
| if ( !preventInputUpdate ) { |
| var valueChanged = false; |
| |
| // update control"s value |
| if ( cType === "input" ) { |
| valueChanged = control.val() !== newval; |
| control.val( newval ); |
| } else { |
| valueChanged = control[ 0 ].selectedIndex !== newval; |
| control[ 0 ].selectedIndex = newval; |
| } |
| if ( !isfromControl && valueChanged ) { |
| control.trigger( "change" ); |
| } |
| } |
| }, |
| |
| enable: function() { |
| this.element.attr( "disabled", false ); |
| this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false ); |
| return this._setOption( "disabled", false ); |
| }, |
| |
| disable: function() { |
| this.element.attr( "disabled", true ); |
| this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true ); |
| return this._setOption( "disabled", true ); |
| } |
| |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.slider.prototype.enhanceWithin( e.target, true ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| $.widget( "mobile.selectmenu", $.mobile.widget, { |
| options: { |
| theme: null, |
| disabled: false, |
| icon: "arrow-d", |
| iconpos: "right", |
| inline: false, |
| corners: true, |
| shadow: true, |
| iconshadow: true, |
| overlayTheme: "a", |
| hidePlaceholderMenuItems: true, |
| closeText: "Close", |
| nativeMenu: true, |
| // This option defaults to true on iOS devices. |
| preventFocusZoom: /iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1, |
| initSelector: "select:not( :jqmData(role='slider') )", |
| mini: false |
| }, |
| |
| _button: function() { |
| return $( "<div/>" ); |
| }, |
| |
| _setDisabled: function( value ) { |
| this.element.attr( "disabled", value ); |
| this.button.attr( "aria-disabled", value ); |
| return this._setOption( "disabled", value ); |
| }, |
| |
| _focusButton : function() { |
| var self = this; |
| |
| setTimeout( function() { |
| self.button.focus(); |
| }, 40); |
| }, |
| |
| _selectOptions: function() { |
| return this.select.find( "option" ); |
| }, |
| |
| // setup items that are generally necessary for select menu extension |
| _preExtension: function() { |
| var classes = ""; |
| // TODO: Post 1.1--once we have time to test thoroughly--any classes manually applied to the original element should be carried over to the enhanced element, with an `-enhanced` suffix. See https://github.com/jquery/jquery-mobile/issues/3577 |
| /* if ( $el[0].className.length ) { |
| classes = $el[0].className; |
| } */ |
| if ( !!~this.element[0].className.indexOf( "ui-btn-left" ) ) { |
| classes = " ui-btn-left"; |
| } |
| |
| if ( !!~this.element[0].className.indexOf( "ui-btn-right" ) ) { |
| classes = " ui-btn-right"; |
| } |
| |
| this.select = this.element.wrap( "<div class='ui-select" + classes + "'>" ); |
| this.selectID = this.select.attr( "id" ); |
| this.label = $( "label[for='"+ this.selectID +"']" ).addClass( "ui-select" ); |
| this.isMultiple = this.select[ 0 ].multiple; |
| if ( !this.options.theme ) { |
| this.options.theme = $.mobile.getInheritedTheme( this.select, "c" ); |
| } |
| }, |
| |
| _create: function() { |
| this._preExtension(); |
| |
| // Allows for extension of the native select for custom selects and other plugins |
| // see select.custom for example extension |
| // TODO explore plugin registration |
| this._trigger( "beforeCreate" ); |
| |
| this.button = this._button(); |
| |
| var self = this, |
| |
| options = this.options, |
| |
| inline = options.inline || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "inline" ), |
| mini = options.mini || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "mini" ), |
| iconpos = options.icon ? ( options.iconpos || $.mobile.getAttrFixed( this.select[0], "data-" + $.mobile.ns + "iconpos" ) ) : false, |
| |
| // IE throws an exception at options.item() function when |
| // there is no selected item |
| // select first in this case |
| selectedIndex = this.select[ 0 ].selectedIndex === -1 ? 0 : this.select[ 0 ].selectedIndex, |
| |
| // TODO values buttonId and menuId are undefined here |
| button = this.button |
| .insertBefore( this.select ) |
| .buttonMarkup( { |
| theme: options.theme, |
| icon: options.icon, |
| iconpos: iconpos, |
| inline: inline, |
| corners: options.corners, |
| shadow: options.shadow, |
| iconshadow: options.iconshadow, |
| mini: mini |
| }); |
| |
| this.setButtonText(); |
| |
| // TIZEN fix: The TIZEN buttonMarkup patch adds 'ui-btn-icon-only' class to the ui-btn-inner. |
| // It makes the text not to be shown, so the class must be removed. (Like JQM) |
| button.children('.ui-btn-inner').removeClass('ui-btn-icon-only'); |
| |
| // Opera does not properly support opacity on select elements |
| // In Mini, it hides the element, but not its text |
| // On the desktop,it seems to do the opposite |
| // for these reasons, using the nativeMenu option results in a full native select in Opera |
| if ( options.nativeMenu && window.opera && window.opera.version ) { |
| button.addClass( "ui-select-nativeonly" ); |
| } |
| |
| // Add counter for multi selects |
| if ( this.isMultiple ) { |
| this.buttonCount = $( "<span>" ) |
| .addClass( "ui-li-count ui-btn-up-c ui-btn-corner-all" ) |
| .hide() |
| .appendTo( button.addClass('ui-li-has-count') ); |
| } |
| |
| // Disable if specified |
| if ( options.disabled || this.element.attr('disabled')) { |
| this.disable(); |
| } |
| |
| // Events on native select |
| this.select.change(function() { |
| self.refresh(); |
| }); |
| |
| this.build(); |
| }, |
| |
| build: function() { |
| var self = this; |
| |
| this.select |
| .appendTo( self.button ) |
| .bind( "vmousedown", function() { |
| // Add active class to button |
| self.button.addClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "focus", function() { |
| self.button.addClass( $.mobile.focusClass ); |
| }) |
| .bind( "blur", function() { |
| self.button.removeClass( $.mobile.focusClass ); |
| }) |
| .bind( "focus vmouseover", function() { |
| self.button.trigger( "vmouseover" ); |
| }) |
| .bind( "vmousemove", function() { |
| // Remove active class on scroll/touchmove |
| self.button.removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "change blur vmouseout", function() { |
| self.button.trigger( "vmouseout" ) |
| .removeClass( $.mobile.activeBtnClass ); |
| }) |
| .bind( "change blur", function() { |
| self.button.removeClass( "ui-btn-down-" + self.options.theme ); |
| }); |
| |
| // In many situations, iOS will zoom into the select upon tap, this prevents that from happening |
| self.button.bind( "vmousedown", function() { |
| if ( self.options.preventFocusZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| }).bind( "mouseup", function() { |
| if ( self.options.preventFocusZoom ) { |
| setTimeout(function() { |
| $.mobile.zoom.enable( true ); |
| }, 0); |
| } |
| }); |
| }, |
| |
| selected: function() { |
| return this._selectOptions().filter( ":selected" ); |
| }, |
| |
| selectedIndices: function() { |
| var self = this; |
| |
| return this.selected().map(function() { |
| return self._selectOptions().index( this ); |
| }).get(); |
| }, |
| |
| setButtonText: function() { |
| var self = this, |
| selected = this.selected(), |
| text = this.placeholder, |
| span = $( document.createElement( "span" ) ); |
| |
| this.button.find( ".ui-btn-text" ).html(function() { |
| if ( selected.length ) { |
| text = selected.map(function() { |
| return $( this ).text(); |
| }).get().join( ", " ); |
| } else { |
| text = self.placeholder; |
| } |
| |
| // TODO possibly aggregate multiple select option classes |
| return span.text( text ) |
| .addClass( self.select.attr( "class" ) ) |
| .addClass( selected.attr( "class" ) ); |
| }); |
| }, |
| |
| setButtonCount: function() { |
| var selected = this.selected(); |
| |
| // multiple count inside button |
| if ( this.isMultiple ) { |
| this.buttonCount[ selected.length > 1 ? "show" : "hide" ]().text( selected.length ); |
| } |
| }, |
| |
| refresh: function() { |
| this.setButtonText(); |
| this.setButtonCount(); |
| }, |
| |
| // open and close preserved in native selects |
| // to simplify users code when looping over selects |
| open: $.noop, |
| close: $.noop, |
| |
| disable: function() { |
| this._setDisabled( true ); |
| this.button.addClass( "ui-disabled" ); |
| }, |
| |
| enable: function() { |
| this._setDisabled( false ); |
| this.button.removeClass( "ui-disabled" ); |
| } |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $.mobile.selectmenu.prototype.enhanceWithin( e.target, true ); |
| }); |
| })( jQuery ); |
| |
| /* |
| * custom "selectmenu" plugin |
| */ |
| |
| (function( $, undefined ) { |
| var extendSelect = function( widget ) { |
| |
| var select = widget.select, |
| selectID = widget.selectID, |
| label = widget.label, |
| thisPage = widget.select.closest( ".ui-page" ), |
| selectOptions = widget._selectOptions(), |
| isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, |
| buttonId = selectID + "-button", |
| menuId = selectID + "-menu", |
| menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" + |
| "<div data-" + $.mobile.ns + "role='header'>" + |
| "<div class='ui-title'>" + label.getEncodedText() + "</div>"+ |
| "</div>"+ |
| "<div data-" + $.mobile.ns + "role='content'></div>"+ |
| "</div>" ), |
| |
| listbox = $( "<div>", { "class": "ui-selectmenu" } ).insertAfter( widget.select ).popup( { theme: "a" } ), |
| |
| list = $( "<ul>", { |
| "class": "ui-selectmenu-list", |
| "id": menuId, |
| "role": "listbox", |
| "aria-labelledby": buttonId |
| }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ), |
| |
| header = $( "<div>", { |
| "class": "ui-header ui-bar-" + widget.options.theme |
| }).prependTo( listbox ), |
| |
| headerTitle = $( "<h1>", { |
| "class": "ui-title" |
| }).appendTo( header ), |
| |
| menuPageContent, |
| menuPageClose, |
| headerClose; |
| |
| if ( widget.isMultiple ) { |
| headerClose = $( "<a>", { |
| "text": widget.options.closeText, |
| "href": "#", |
| "class": "ui-btn-left" |
| }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(); |
| } |
| |
| $.extend( widget, { |
| select: widget.select, |
| selectID: selectID, |
| buttonId: buttonId, |
| menuId: menuId, |
| thisPage: thisPage, |
| menuPage: menuPage, |
| label: label, |
| selectOptions: selectOptions, |
| isMultiple: isMultiple, |
| theme: widget.options.theme, |
| listbox: listbox, |
| list: list, |
| header: header, |
| headerTitle: headerTitle, |
| headerClose: headerClose, |
| menuPageContent: menuPageContent, |
| menuPageClose: menuPageClose, |
| placeholder: "", |
| |
| build: function() { |
| var self = this; |
| |
| // Create list from select, update state |
| self.refresh(); |
| |
| self.select.attr( "tabindex", "-1" ).focus(function() { |
| $( this ).blur(); |
| self.button.focus(); |
| }); |
| |
| // Button events |
| self.button.bind( "vclick keydown" , function( event ) { |
| if (event.type === "vclick" || |
| event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER || |
| event.keyCode === $.mobile.keyCode.SPACE)) { |
| |
| self.open(); |
| event.preventDefault(); |
| } |
| }); |
| |
| // Events for list items |
| self.list.attr( "role", "listbox" ) |
| .bind( "focusin", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "0" ) |
| .trigger( "vmouseover" ); |
| |
| }) |
| .bind( "focusout", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "-1" ) |
| .trigger( "vmouseout" ); |
| }) |
| .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) { |
| |
| // index of option tag to be selected |
| var oldIndex = self.select[ 0 ].selectedIndex, |
| newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ), |
| option = self._selectOptions().eq( newIndex )[ 0 ]; |
| |
| // toggle selected status on the tag for multi selects |
| option.selected = self.isMultiple ? !option.selected : true; |
| |
| // toggle checkbox class for multiple selects |
| if ( self.isMultiple ) { |
| $( this ).find( ".ui-icon" ) |
| .toggleClass( "ui-icon-checkbox-on", option.selected ) |
| .toggleClass( "ui-icon-checkbox-off", !option.selected ); |
| } |
| |
| // trigger change if value changed |
| if ( self.isMultiple || oldIndex !== newIndex ) { |
| self.select.trigger( "change" ); |
| } |
| |
| // hide custom select for single selects only - otherwise focus clicked item |
| // We need to grab the clicked item the hard way, because the list may have been rebuilt |
| if ( self.isMultiple ) { |
| self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex ) |
| .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| else { |
| self.close(); |
| } |
| |
| event.preventDefault(); |
| }) |
| .keydown(function( event ) { //keyboard events for menu items |
| var target = $( event.target ), |
| li = target.closest( "li" ), |
| prev, next; |
| |
| // switch logic based on which key was pressed |
| switch ( event.keyCode ) { |
| // up or left arrow keys |
| case 38: |
| prev = li.prev().not( ".ui-selectmenu-placeholder" ); |
| |
| if ( prev.is( ".ui-li-divider" ) ) { |
| prev = prev.prev(); |
| } |
| |
| // if there's a previous option, focus it |
| if ( prev.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // down or right arrow keys |
| case 40: |
| next = li.next(); |
| |
| if ( next.is( ".ui-li-divider" ) ) { |
| next = next.next(); |
| } |
| |
| // if there's a next option, focus it |
| if ( next.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // If enter or space is pressed, trigger click |
| case 13: |
| case 32: |
| target.trigger( "click" ); |
| |
| return false; |
| } |
| }); |
| |
| // button refocus ensures proper height calculation |
| // by removing the inline style and ensuring page inclusion |
| self.menuPage.bind( "pagehide", function() { |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| |
| // TODO centralize page removal binding / handling in the page plugin. |
| // Suggestion from @jblas to do refcounting |
| // |
| // TODO extremely confusing dependency on the open method where the pagehide.remove |
| // bindings are stripped to prevent the parent page from disappearing. The way |
| // we're keeping pages in the DOM right now sucks |
| // |
| // rebind the page remove that was unbound in the open function |
| // to allow for the parent page removal from actions other than the use |
| // of a dialog sized custom select |
| // |
| // doing this here provides for the back button on the custom select dialog |
| $.mobile._bindPageRemove.call( self.thisPage ); |
| }); |
| |
| // Events on the popup |
| self.listbox.bind( "popupafterclose", function( event ) { |
| self.close(); |
| }); |
| |
| // Close button on small overlays |
| if ( self.isMultiple ) { |
| self.headerClose.click(function() { |
| if ( self.menuType === "overlay" ) { |
| self.close(); |
| return false; |
| } |
| }); |
| } |
| |
| // track this dependency so that when the parent page |
| // is removed on pagehide it will also remove the menupage |
| self.thisPage.addDependents( this.menuPage ); |
| }, |
| |
| _isRebuildRequired: function() { |
| var list = this.list.find( "li" ), |
| options = this._selectOptions(); |
| |
| // TODO exceedingly naive method to determine difference |
| // ignores value changes etc in favor of a forcedRebuild |
| // from the user in the refresh method |
| return options.text() !== list.text(); |
| }, |
| |
| selected: function() { |
| return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" ); |
| }, |
| |
| refresh: function( forceRebuild , foo ) { |
| var self = this, |
| select = this.element, |
| isMultiple = this.isMultiple, |
| indicies; |
| |
| if ( forceRebuild || this._isRebuildRequired() ) { |
| self._buildList(); |
| } |
| |
| indicies = this.selectedIndices(); |
| |
| self.setButtonText(); |
| self.setButtonCount(); |
| |
| self.list.find( "li:not(.ui-li-divider)" ) |
| .removeClass( $.mobile.activeBtnClass ) |
| .attr( "aria-selected", false ) |
| .each(function( i ) { |
| |
| if ( $.inArray( i, indicies ) > -1 ) { |
| var item = $( this ); |
| |
| // Aria selected attr |
| item.attr( "aria-selected", true ); |
| |
| // Multiple selects: add the "on" checkbox state to the icon |
| if ( self.isMultiple ) { |
| item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" ); |
| } else { |
| if ( item.is( ".ui-selectmenu-placeholder" ) ) { |
| item.next().addClass( $.mobile.activeBtnClass ); |
| } else { |
| item.addClass( $.mobile.activeBtnClass ); |
| } |
| } |
| } |
| }); |
| }, |
| |
| close: function() { |
| if ( this.options.disabled || !this.isOpen ) { |
| return; |
| } |
| |
| var self = this; |
| |
| if ( self.menuType === "page" ) { |
| // doesn't solve the possible issue with calling change page |
| // where the objects don't define data urls which prevents dialog key |
| // stripping - changePage has incoming refactor |
| $.mobile.back(); |
| } else { |
| self.listbox.popup( "close" ); |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| } |
| |
| // allow the dialog to be closed again |
| self.isOpen = false; |
| }, |
| |
| open: function() { |
| if ( this.options.disabled ) { |
| return; |
| } |
| |
| var self = this, |
| $window = $.mobile.$window, |
| selfListParent = self.list.parent(), |
| menuHeight = selfListParent.outerHeight(), |
| menuWidth = selfListParent.outerWidth(), |
| activePage = $( "." + $.mobile.activePageClass ), |
| scrollTop = $window.scrollTop(), |
| btnOffset = self.button.offset().top, |
| screenHeight = $window.height(), |
| screenWidth = $window.width(); |
| |
| //add active class to button |
| self.button.addClass( $.mobile.activeBtnClass ); |
| |
| //remove after delay |
| setTimeout( function() { |
| self.button.removeClass( $.mobile.activeBtnClass ); |
| }, 300); |
| |
| function focusMenuItem() { |
| var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" ); |
| if ( selector.length === 0 ) { |
| selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" ); |
| } |
| selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme ); |
| } |
| |
| if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { |
| |
| self.menuPage.appendTo( $.mobile.pageContainer ).page(); |
| self.menuPageContent = menuPage.find( ".ui-content" ); |
| self.menuPageClose = menuPage.find( ".ui-header a" ); |
| |
| // prevent the parent page from being removed from the DOM, |
| // otherwise the results of selecting a list item in the dialog |
| // fall into a black hole |
| self.thisPage.unbind( "pagehide.remove" ); |
| |
| //for WebOS/Opera Mini (set lastscroll using button offset) |
| if ( scrollTop === 0 && btnOffset > screenHeight ) { |
| self.thisPage.one( "pagehide", function() { |
| $( this ).jqmData( "lastScroll", btnOffset ); |
| }); |
| } |
| |
| self.menuPage.one( "pageshow", function() { |
| focusMenuItem(); |
| self.isOpen = true; |
| }); |
| |
| self.menuType = "page"; |
| self.menuPageContent.append( self.list ); |
| self.menuPage.find("div .ui-title").text(self.label.text()); |
| $.mobile.changePage( self.menuPage, { |
| transition: $.mobile.defaultDialogTransition |
| }); |
| } else { |
| self.menuType = "overlay"; |
| |
| self.listbox |
| .one( "popupafteropen", focusMenuItem ) |
| .popup( "open", { |
| x: self.button.offset().left + self.button.outerWidth() / 2, |
| y: self.button.offset().top + self.button.outerHeight() / 2 |
| }); |
| |
| // duplicate with value set in page show for dialog sized selects |
| self.isOpen = true; |
| } |
| }, |
| |
| _buildList: function() { |
| var self = this, |
| o = this.options, |
| placeholder = this.placeholder, |
| needPlaceholder = true, |
| optgroups = [], |
| lis = [], |
| dataIcon = self.isMultiple ? "checkbox-off" : "false"; |
| |
| self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); |
| |
| var $options = self.select.find( "option" ), |
| numOptions = $options.length, |
| select = this.select[ 0 ], |
| dataPrefix = 'data-' + $.mobile.ns, |
| dataIndexAttr = dataPrefix + 'option-index', |
| dataIconAttr = dataPrefix + 'icon', |
| dataRoleAttr = dataPrefix + 'role', |
| dataPlaceholderAttr = dataPrefix + 'placeholder', |
| fragment = document.createDocumentFragment(), |
| isPlaceholderItem = false, |
| optGroup; |
| |
| for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) { |
| var option = $options[i], |
| $option = $( option ), |
| parent = option.parentNode, |
| text = $option.text(), |
| anchor = document.createElement( 'a' ), |
| classes = []; |
| |
| anchor.setAttribute( 'href', '#' ); |
| anchor.appendChild( document.createTextNode( text ) ); |
| |
| // Are we inside an optgroup? |
| if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) { |
| var optLabel = parent.getAttribute( 'label' ); |
| if ( optLabel !== optGroup ) { |
| var divider = document.createElement( 'li' ); |
| divider.setAttribute( dataRoleAttr, 'list-divider' ); |
| divider.setAttribute( 'role', 'option' ); |
| divider.setAttribute( 'tabindex', '-1' ); |
| divider.appendChild( document.createTextNode( optLabel ) ); |
| fragment.appendChild( divider ); |
| optGroup = optLabel; |
| } |
| } |
| |
| if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) { |
| needPlaceholder = false; |
| isPlaceholderItem = true; |
| |
| // If we have identified a placeholder, mark it retroactively in the select as well |
| option.setAttribute( dataPlaceholderAttr, true ); |
| if ( o.hidePlaceholderMenuItems ) { |
| classes.push( "ui-selectmenu-placeholder" ); |
| } |
| if (!placeholder) { |
| placeholder = self.placeholder = text; |
| } |
| } |
| |
| var item = document.createElement('li'); |
| if ( option.disabled ) { |
| classes.push( "ui-disabled" ); |
| item.setAttribute('aria-disabled',true); |
| } |
| item.setAttribute( dataIndexAttr,i ); |
| item.setAttribute( dataIconAttr, dataIcon ); |
| if ( isPlaceholderItem ) { |
| item.setAttribute( dataPlaceholderAttr, true ); |
| } |
| item.className = classes.join( " " ); |
| item.setAttribute( 'role', 'option' ); |
| anchor.setAttribute( 'tabindex', '-1' ); |
| item.appendChild( anchor ); |
| fragment.appendChild( item ); |
| } |
| |
| self.list[0].appendChild( fragment ); |
| |
| // Hide header if it's not a multiselect and there's no placeholder |
| if ( !this.isMultiple && !placeholder.length ) { |
| this.header.hide(); |
| } else { |
| this.headerTitle.text( this.placeholder ); |
| } |
| |
| // Now populated, create listview |
| self.list.listview(); |
| }, |
| |
| _button: function() { |
| return $( "<a>", { |
| "href": "#", |
| "role": "button", |
| // TODO value is undefined at creation |
| "id": this.buttonId, |
| "aria-haspopup": "true", |
| |
| // TODO value is undefined at creation |
| "aria-owns": this.menuId |
| }); |
| } |
| }); |
| }; |
| |
| // issue #3894 - core doesn't trigger events on disabled delegates |
| $.mobile.$document.bind( "selectmenubeforecreate", function( event ) { |
| var selectmenuWidget = $( event.target ).data( "selectmenu" ); |
| |
| if ( !selectmenuWidget.options.nativeMenu && |
| selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) { |
| extendSelect( selectmenuWidget ); |
| } |
| }); |
| })( jQuery ); |
| |
| (function( $, undefined ) { |
| |
| |
| $.widget( "mobile.fixedtoolbar", $.mobile.widget, { |
| options: { |
| visibleOnPageShow: true, |
| disablePageZoom: true, |
| transition: "slide", //can be none, fade, slide (slide maps to slideup or slidedown) |
| fullscreen: false, |
| tapToggle: true, |
| tapToggleBlacklist: "a, button, input, select, textarea, .ui-header-fixed, .ui-footer-fixed, .ui-popup", |
| hideDuringFocus: "input, textarea, select", |
| updatePagePadding: true, |
| trackPersistentToolbars: true, |
| |
| // Browser detection! Weeee, here we go... |
| // Unfortunately, position:fixed is costly, not to mention probably impossible, to feature-detect accurately. |
| // Some tests exist, but they currently return false results in critical devices and browsers, which could lead to a broken experience. |
| // Testing fixed positioning is also pretty obtrusive to page load, requiring injected elements and scrolling the window |
| // The following function serves to rule out some popular browsers with known fixed-positioning issues |
| // This is a plugin option like any other, so feel free to improve or overwrite it |
| supportBlacklist: function() { |
| var w = window, |
| ua = navigator.userAgent, |
| platform = navigator.platform, |
| // Rendering engine is Webkit, and capture major version |
| wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ), |
| wkversion = !!wkmatch && wkmatch[ 1 ], |
| ffmatch = ua.match( /Fennec\/([0-9]+)/ ), |
| ffversion = !!ffmatch && ffmatch[ 1 ], |
| operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ), |
| omversion = !!operammobilematch && operammobilematch[ 1 ]; |
| |
| if( |
| // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) |
| ( ( platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534 ) || |
| // Opera Mini |
| ( w.operamini && ({}).toString.call( w.operamini ) === "[object OperaMini]" ) || |
| ( operammobilematch && omversion < 7458 ) || |
| //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) |
| ( ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533 ) || |
| // Firefox Mobile before 6.0 - |
| ( ffversion && ffversion < 6 ) || |
| // WebOS less than 3 |
| ( "palmGetResource" in window && wkversion && wkversion < 534 ) || |
| // MeeGo |
| ( ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1 ) ) { |
| return true; |
| } |
| |
| return false; |
| }, |
| initSelector: ":jqmData(position='dummy')" |
| }, |
| |
| _create: function() { |
| |
| var self = this, |
| o = self.options, |
| $el = self.element, |
| tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer", |
| $page = $el.closest( ".ui-page" ); |
| |
| // Feature detecting support for |
| if ( o.supportBlacklist() ) { |
| self.destroy(); |
| return; |
| } |
| |
| $el.addClass( "ui-"+ tbtype +"-fixed" ); |
| |
| // "fullscreen" overlay positioning |
| if ( o.fullscreen ) { |
| $el.addClass( "ui-"+ tbtype +"-fullscreen" ); |
| $page.addClass( "ui-page-" + tbtype + "-fullscreen" ); |
| } |
| // If not fullscreen, add class to page to set top or bottom padding |
| else{ |
| $page.addClass( "ui-page-" + tbtype + "-fixed" ); |
| } |
| |
| self._addTransitionClass(); |
| self._bindPageEvents(); |
| self._bindToggleHandlers(); |
| }, |
| |
| _addTransitionClass: function() { |
| var tclass = this.options.transition; |
| |
| if ( tclass && tclass !== "none" ) { |
| // use appropriate slide for header or footer |
| if ( tclass === "slide" ) { |
| tclass = this.element.is( ".ui-header" ) ? "slidedown" : "slideup"; |
| } |
| |
| this.element.addClass( tclass ); |
| } |
| }, |
| |
| _bindPageEvents: function() { |
| var self = this, |
| o = self.options, |
| $el = self.element; |
| |
| //page event bindings |
| // Fixed toolbars require page zoom to be disabled, otherwise usability issues crop up |
| // This method is meant to disable zoom while a fixed-positioned toolbar page is visible |
| $el.closest( ".ui-page" ) |
| .bind( "pagebeforeshow", function() { |
| if ( o.disablePageZoom ) { |
| $.mobile.zoom.disable( true ); |
| } |
| if ( !o.visibleOnPageShow ) { |
| self.hide( true ); |
| } |
| } ) |
| .bind( "webkitAnimationStart animationstart updatelayout", function() { |
| var thisPage = this; |
| if ( o.updatePagePadding ) { |
| self.updatePagePadding( thisPage ); |
| } |
| }) |
| .bind( "pageshow", function() { |
| var thisPage = this; |
| self.updatePagePadding( thisPage ); |
| if ( o.updatePagePadding ) { |
| $.mobile.$window.bind( "throttledresize." + self.widgetName, function() { |
| self.updatePagePadding( thisPage ); |
| }); |
| } |
| }) |
| .bind( "pagebeforehide", function( e, ui ) { |
| if ( o.disablePageZoom ) { |
| $.mobile.zoom.enable( true ); |
| } |
| if ( o.updatePagePadding ) { |
| $.mobile.$window.unbind( "throttledresize." + self.widgetName ); |
| } |
| |
| if ( o.trackPersistentToolbars ) { |
| var thisFooter = $( ".ui-footer-fixed:jqmData(id)", this ), |
| thisHeader = $( ".ui-header-fixed:jqmData(id)", this ), |
| nextFooter = thisFooter.length && ui.nextPage && $( ".ui-footer-fixed:jqmData(id='" + thisFooter.jqmData( "id" ) + "')", ui.nextPage ) || $(), |
| nextHeader = thisHeader.length && ui.nextPage && $( ".ui-header-fixed:jqmData(id='" + thisHeader.jqmData( "id" ) + "')", ui.nextPage ) || $(); |
| |
| if ( nextFooter.length || nextHeader.length ) { |
| |
| nextFooter.add( nextHeader ).appendTo( $.mobile.pageContainer ); |
| |
| ui.nextPage.one( "pageshow", function() { |
| nextFooter.add( nextHeader ).appendTo( this ); |
| }); |
| } |
| } |
| }); |
| }, |
| |
| _visible: true, |
| |
| // This will set the content element's top or bottom padding equal to the toolbar's height |
| updatePagePadding: function( tbPage ) { |
| var $el = this.element, |
| header = $el.is( ".ui-header" ); |
| |
| // This behavior only applies to "fixed", not "fullscreen" |
| if ( this.options.fullscreen ) { return; } |
| |
| tbPage = tbPage || $el.closest( ".ui-page" ); |
| $( tbPage ).css( "padding-" + ( header ? "top" : "bottom" ), $el.outerHeight() ); |
| }, |
| |
| _useTransition: function( notransition ) { |
| var $win = $.mobile.$window, |
| $el = this.element, |
| scroll = $win.scrollTop(), |
| elHeight = $el.height(), |
| pHeight = $el.closest( ".ui-page" ).height(), |
| viewportHeight = $.mobile.getScreenHeight(), |
| tbtype = $el.is( ":jqmData(role='header')" ) ? "header" : "footer"; |
| |
| return !notransition && |
| ( this.options.transition && this.options.transition !== "none" && |
| ( |
| ( tbtype === "header" && !this.options.fullscreen && scroll > elHeight ) || |
| ( tbtype === "footer" && !this.options.fullscreen && scroll + viewportHeight < pHeight - elHeight ) |
| ) || this.options.fullscreen |
| ); |
| }, |
| |
| show: function( notransition ) { |
| var hideClass = "ui-fixed-hidden", |
| $el = this.element; |
| |
| if ( this._useTransition( notransition ) ) { |
| $el |
| .removeClass( "out " + hideClass ) |
| .addClass( "in" ); |
| } |
| else { |
| $el.removeClass( hideClass ); |
| } |
| this._visible = true; |
| }, |
| |
| hide: function( notransition ) { |
| var hideClass = "ui-fixed-hidden", |
| $el = this.element, |
| // if it's a slide transition, our new transitions need the reverse class as well to slide outward |
| outclass = "out" + ( this.options.transition === "slide" ? " reverse" : "" ); |
| |
| if( this._useTransition( notransition ) ) { |
| $el |
| .addClass( outclass ) |
| .removeClass( "in" ) |
| .animationComplete(function() { |
| $el.addClass( hideClass ).removeClass( outclass ); |
| }); |
| } |
| else { |
| $el.addClass( hideClass ).removeClass( outclass ); |
| } |
| this._visible = false; |
| }, |
| |
| toggle: function() { |
| this[ this._visible ? "hide" : "show" ](); |
| }, |
| |
| _bindToggleHandlers: function() { |
| var self = this, |
| o = self.options, |
| $el = self.element; |
| |
| // tap toggle |
| $el.closest( ".ui-page" ) |
| .bind( "vclick", function( e ) { |
| if ( o.tapToggle && !$( e.target ).closest( o.tapToggleBlacklist ).length ) { |
| self.toggle(); |
| } |
| }) |
| .bind( "focusin focusout", function( e ) { |
| if ( screen.width < 500 && $( e.target ).is( o.hideDuringFocus ) && !$( e.target ).closest( ".ui-header-fixed, .ui-footer-fixed" ).length ) { |
| self[ ( e.type === "focusin" && self._visible ) ? "hide" : "show" ](); |
| } |
| }); |
| }, |
| |
| destroy: function() { |
| this.element.removeClass( "ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden" ); |
| this.element.closest( ".ui-page" ).removeClass( "ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen" ); |
| } |
| |
| }); |
| |
| //auto self-init widgets |
| $.mobile.$document |
| .bind( "pagecreate create", function( e ) { |
| |
| // DEPRECATED in 1.1: support for data-fullscreen=true|false on the page element. |
| // This line ensures it still works, but we recommend moving the attribute to the toolbars themselves. |
| if ( $( e.target ).jqmData( "fullscreen" ) ) { |
| $( $.mobile.fixedtoolbar.prototype.options.initSelector, e.target ).not( ":jqmData(fullscreen)" ).jqmData( "fullscreen", true ); |
| } |
| |
| $.mobile.fixedtoolbar.prototype.enhanceWithin( e.target ); |
| }); |
| |
| })( jQuery ); |
| |
| (function( $, window ) { |
| |
| // This fix addresses an iOS bug, so return early if the UA claims it's something else. |
| if ( !(/iPhone|iPad|iPod/.test( navigator.platform ) && navigator.userAgent.indexOf( "AppleWebKit" ) > -1 ) ) { |
| return; |
| } |
| |
| var zoom = $.mobile.zoom, |
| evt, x, y, z, aig; |
| |
| function checkTilt( e ) { |
| evt = e.originalEvent; |
| aig = evt.accelerationIncludingGravity; |
| |
| x = Math.abs( aig.x ); |
| y = Math.abs( aig.y ); |
| z = Math.abs( aig.z ); |
| |
| // If portrait orientation and in one of the danger zones |
| if ( !window.orientation && ( x > 7 || ( ( z > 6 && y < 8 || z < 8 && y > 6 ) && x > 5 ) ) ) { |
| if ( zoom.enabled ) { |
| zoom.disable(); |
| } |
| } else if ( !zoom.enabled ) { |
| zoom.enable(); |
| } |
| } |
| |
| $.mobile.$window |
| .bind( "orientationchange.iosorientationfix", zoom.enable ) |
| .bind( "devicemotion.iosorientationfix", checkTilt ); |
| |
| }( jQuery, this )); |
| |
| (function( $, window, undefined ) { |
| var $html = $( "html" ), |
| $head = $( "head" ), |
| $window = $.mobile.$window; |
| |
| //remove initial build class (only present on first pageshow) |
| function hideRenderingClass() { |
| $html.removeClass( "ui-mobile-rendering" ); |
| } |
| |
| // trigger mobileinit event - useful hook for configuring $.mobile settings before they're used |
| $( window.document ).trigger( "mobileinit" ); |
| |
| // support conditions |
| // if device support condition(s) aren't met, leave things as they are -> a basic, usable experience, |
| // otherwise, proceed with the enhancements |
| if ( !$.mobile.gradeA() ) { |
| return; |
| } |
| |
| // override ajaxEnabled on platforms that have known conflicts with hash history updates |
| // or generally work better browsing in regular http for full page refreshes (BB5, Opera Mini) |
| if ( $.mobile.ajaxBlacklist ) { |
| $.mobile.ajaxEnabled = false; |
| } |
| |
| // Add mobile, initial load "rendering" classes to docEl |
| $html.addClass( "ui-mobile ui-mobile-rendering" ); |
| |
| // This is a fallback. If anything goes wrong (JS errors, etc), or events don't fire, |
| // this ensures the rendering class is removed after 5 seconds, so content is visible and accessible |
| setTimeout( hideRenderingClass, 5000 ); |
| |
| $.extend( $.mobile, { |
| addEventBlocker: function () { |
| $html.addClass( "ui-blocker" ); |
| $html.bind( "touchstart touchend vclick mousedown mouseup click", function () { |
| return false; |
| } ); |
| }, |
| |
| removeEventBlocker: function () { |
| $html.removeClass( "ui-blocker" ); |
| $html.unbind( "touchstart touchend vclick mousedown mouseup click" ); |
| }, |
| |
| // find and enhance the pages in the dom and transition to the first page. |
| initializePage: function() { |
| // find present pages |
| var $pages = $( ":jqmData(role='page'), :jqmData(role='dialog')" ), |
| hash = $.mobile.path.parseLocation().hash.replace("#", ""), |
| hashPage = document.getElementById( hash ); |
| |
| // if no pages are found, create one with body's inner html |
| if ( !$pages.length ) { |
| $pages = $( "body" ).wrapInner( "<div data-" + $.mobile.ns + "role='page'></div>" ).children( 0 ); |
| } |
| |
| // add dialogs, set data-url attrs |
| $pages.each(function() { |
| var $this = $( this ); |
| |
| // unless the data url is already set set it to the pathname |
| if ( !$this[0].getAttribute( "data-" + $.mobile.ns + "url" ) ) { |
| $this.attr( "data-" + $.mobile.ns + "url", $this.attr( "id" ) || location.pathname + location.search ); |
| } |
| }); |
| |
| // define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback) |
| $.mobile.firstPage = $pages.first(); |
| |
| // define page container |
| $.mobile.pageContainer = $.mobile.firstPage.parent().addClass( "ui-mobile-viewport" ); |
| |
| // alert listeners that the pagecontainer has been determined for binding |
| // to events triggered on it |
| $window.trigger( "pagecontainercreate" ); |
| |
| // cue page loading message |
| $.mobile.showPageLoadingMsg(); |
| $.mobile.addEventBlocker(); |
| |
| //remove initial build class (only present on first pageshow) |
| hideRenderingClass(); |
| |
| // if hashchange listening is disabled, there's no hash deeplink, |
| // the hash is not valid (contains more than one # or does not start with #) |
| // or there is no page with that hash, change to the first page in the DOM |
| // Remember, however, that the hash can also be a path! |
| if ( ! ( $.mobile.hashListeningEnabled && |
| $.mobile.path.isHashValid( location.hash ) && |
| ( $( hashPage ).is( ':jqmData(role="page")' ) || |
| $.mobile.path.isPath( hash ) || |
| hash === $.mobile.dialogHashKey ) ) ) { |
| |
| // Store the initial destination |
| if ( $.mobile.path.isHashValid( location.hash ) ) { |
| $.mobile.urlHistory.initialDst = hash.replace( "#", "" ); |
| } |
| $.mobile.changePage( $.mobile.firstPage, { transition: "none", reverse: true, changeHash: false, fromHashChange: true } ); |
| } |
| // otherwise, trigger a hashchange to load a deeplink |
| else { |
| $window.trigger( "hashchange", [ true ] ); |
| } |
| } |
| }); |
| |
| // initialize events now, after mobileinit has occurred |
| $.mobile.navreadyDeferred.resolve(); |
| |
| // check which scrollTop value should be used by scrolling to 1 immediately at domready |
| // then check what the scroll top is. Android will report 0... others 1 |
| // note that this initial scroll won't hide the address bar. It's just for the check. |
| $(function() { |
| // window.scrollTo( 0, 1 ); |
| |
| // if defaultHomeScroll hasn't been set yet, see if scrollTop is 1 |
| // it should be 1 in most browsers, but android treats 1 as 0 (for hiding addr bar) |
| // so if it's 1, use 0 from now on |
| $.mobile.defaultHomeScroll = ( !$.support.scrollTop || $.mobile.$window.scrollTop() === 1 ) ? 0 : 1; |
| |
| |
| // TODO: Implement a proper registration mechanism with dependency handling in order to not have exceptions like the one below |
| //auto self-init widgets for those widgets that have a soft dependency on others |
| if ( $.fn.controlgroup ) { |
| $.mobile.$document.bind( "pagecreate create", function( e ) { |
| $( ":jqmData(role='controlgroup')", e.target ) |
| .jqmEnhanceable() |
| .controlgroup({ excludeInvisible: false }); |
| }); |
| } |
| |
| //dom-ready inits |
| if ( $.mobile.autoInitializePage ) { |
| $.mobile.initializePage(); |
| } |
| |
| // window load event |
| // hide iOS browser chrome on load |
| $window.load( $.mobile.silentScroll ); |
| |
| if ( !$.support.cssPointerEvents ) { |
| // IE and Opera don't support CSS pointer-events: none that we use to disable link-based buttons |
| // by adding the 'ui-disabled' class to them. Using a JavaScript workaround for those browser. |
| // https://github.com/jquery/jquery-mobile/issues/3558 |
| |
| $.mobile.$document.delegate( ".ui-disabled", "vclick", |
| function( e ) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| } |
| ); |
| } |
| }); |
| }( jQuery, this )); |
| |
| })); |