| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| //>>description: Applies the AJAX navigation system to links and forms to enable page transitions |
| //>>label: AJAX Navigation System |
| //>>group: Navigation |
| |
| define( [ |
| "jquery", |
| "./jquery.mobile.core", |
| "./jquery.mobile.events", |
| "./jquery.mobile.support", |
| "./jquery.hashchange", |
| "./widgets/page", |
| "./jquery.mobile.transition" ], function( jQuery ) { |
| //>>excludeEnd("jqmBuildExclude"); |
| (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 ); |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| }); |
| //>>excludeEnd("jqmBuildExclude"); |