1 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
2 //>>description: Applies the AJAX navigation system to links and forms to enable page transitions
3 //>>label: AJAX Navigation System
8 "./jquery.mobile.core",
9 "./jquery.mobile.events",
10 "./jquery.mobile.support",
11 "depend!./jquery.hashchange[jquery]",
13 "./jquery.mobile.transition" ], function( $ ) {
14 //>>excludeEnd("jqmBuildExclude");
15 (function( $, undefined ) {
17 //define vars for interal use
18 var $window = $( window ),
22 //url path helpers for use in relative url management
25 // This scary looking regular expression parses an absolute URL or its relative
26 // variants (protocol, site, document, query, and hash), into the various
27 // components (protocol, host, path, query, fragment, etc that make up the
28 // URL as well as some other commonly used sub-parts. When used with RegExp.exec()
29 // or String.match, it parses the URL into a results array that looks like this:
31 // [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
32 // [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
33 // [2]: http://jblas:password@mycompany.com:8080/mail/inbox
34 // [3]: http://jblas:password@mycompany.com:8080
37 // [6]: jblas:password@mycompany.com:8080
38 // [7]: jblas:password
41 // [10]: mycompany.com:8080
42 // [11]: mycompany.com
47 // [16]: ?msg=1234&type=unread
50 urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
52 // Abstraction to address xss (Issue #4787) by removing the authority in
53 // browsers that auto decode it. All references to location.href should be
54 // replaced with a call to this method so that it can be dealt with properly here
55 getLocation: function( url ) {
56 var uri = url ? this.parseUrl( url ) : location,
57 hash = this.parseUrl( url || location.href ).hash;
59 // mimic the browser with an empty string when the hash is empty
60 hash = hash === "#" ? "" : hash;
62 // Make sure to parse the url or the location object for the hash because using location.hash
63 // is autodecoded in firefox, the rest of the url should be from the object (location unless
64 // we're testing) to avoid the inclusion of the authority
65 return uri.protocol + "//" + uri.host + uri.pathname + uri.search + hash;
68 parseLocation: function() {
69 return this.parseUrl( this.getLocation() );
72 //Parse a URL into a structure that allows easy access to
73 //all of the URL components by name.
74 parseUrl: function( url ) {
75 // If we're passed an object, we'll assume that it is
76 // a parsed url object and just return it back to the caller.
77 if ( $.type( url ) === "object" ) {
81 var matches = path.urlParseRE.exec( url || "" ) || [];
83 // Create an object that allows the caller to access the sub-matches
84 // by name. Note that IE returns an empty string instead of undefined,
85 // like all other browsers do, so we normalize everything so its consistent
86 // no matter what browser we're running on.
88 href: matches[ 0 ] || "",
89 hrefNoHash: matches[ 1 ] || "",
90 hrefNoSearch: matches[ 2 ] || "",
91 domain: matches[ 3 ] || "",
92 protocol: matches[ 4 ] || "",
93 doubleSlash: matches[ 5 ] || "",
94 authority: matches[ 6 ] || "",
95 username: matches[ 8 ] || "",
96 password: matches[ 9 ] || "",
97 host: matches[ 10 ] || "",
98 hostname: matches[ 11 ] || "",
99 port: matches[ 12 ] || "",
100 pathname: matches[ 13 ] || "",
101 directory: matches[ 14 ] || "",
102 filename: matches[ 15 ] || "",
103 search: matches[ 16 ] || "",
104 hash: matches[ 17 ] || ""
108 //Turn relPath into an asbolute path. absPath is
109 //an optional absolute path which describes what
110 //relPath is relative to.
111 makePathAbsolute: function( relPath, absPath ) {
112 if ( relPath && relPath.charAt( 0 ) === "/" ) {
116 relPath = relPath || "";
117 absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : "";
119 var absStack = absPath ? absPath.split( "/" ) : [],
120 relStack = relPath.split( "/" );
121 for ( var i = 0; i < relStack.length; i++ ) {
122 var d = relStack[ i ];
127 if ( absStack.length ) {
136 return "/" + absStack.join( "/" );
139 //Returns true if both urls have the same domain.
140 isSameDomain: function( absUrl1, absUrl2 ) {
141 return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain;
144 //Returns true for any relative variant.
145 isRelativeUrl: function( url ) {
146 // All relative Url variants have one thing in common, no protocol.
147 return path.parseUrl( url ).protocol === "";
150 //Returns true for an absolute url.
151 isAbsoluteUrl: function( url ) {
152 return path.parseUrl( url ).protocol !== "";
155 //Turn the specified realtive URL into an absolute one. This function
156 //can handle all relative variants (protocol, site, document, query, fragment).
157 makeUrlAbsolute: function( relUrl, absUrl ) {
158 if ( !path.isRelativeUrl( relUrl ) ) {
162 if ( absUrl === undefined ) {
163 absUrl = documentBase;
166 var relObj = path.parseUrl( relUrl ),
167 absObj = path.parseUrl( absUrl ),
168 protocol = relObj.protocol || absObj.protocol,
169 doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ),
170 authority = relObj.authority || absObj.authority,
171 hasPath = relObj.pathname !== "",
172 pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ),
173 search = relObj.search || ( !hasPath && absObj.search ) || "",
176 return protocol + doubleSlash + authority + pathname + search + hash;
179 //Add search (aka query) params to the specified url.
180 addSearchParams: function( url, params ) {
181 var u = path.parseUrl( url ),
182 p = ( typeof params === "object" ) ? $.param( params ) : params,
184 return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" );
187 convertUrlToDataUrl: function( absUrl ) {
188 var u = path.parseUrl( absUrl );
189 if ( path.isEmbeddedPage( u ) ) {
190 // For embedded pages, remove the dialog hash key as in getFilePath(),
191 // otherwise the Data Url won't match the id of the embedded Page.
192 return u.hash.split( dialogHashKey )[0].replace( /^#/, "" );
193 } else if ( path.isSameDomain( u, documentBase ) ) {
194 return u.hrefNoHash.replace( documentBase.domain, "" ).split( dialogHashKey )[0];
197 return window.decodeURIComponent(absUrl);
200 //get path from current hash, or from a file path
201 get: function( newPath ) {
202 if ( newPath === undefined ) {
203 newPath = path.parseLocation().hash;
205 return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' );
208 //return the substring of a filepath before the sub-page key, for making a server request
209 getFilePath: function( path ) {
210 var splitkey = '&' + $.mobile.subPageUrlKey;
211 return path && path.split( splitkey )[0].split( dialogHashKey )[0];
214 //set location hash to path
215 set: function( path ) {
216 location.hash = path;
219 //test if a given url (string) is a path
220 //NOTE might be exceptionally naive
221 isPath: function( url ) {
222 return ( /\// ).test( url );
225 //return a url path with the window's location protocol/hostname/pathname removed
226 clean: function( url ) {
227 return url.replace( documentBase.domain, "" );
230 //just return the url without an initial #
231 stripHash: function( url ) {
232 return url.replace( /^#/, "" );
235 //remove the preceding hash, any query params, and dialog notations
236 cleanHash: function( hash ) {
237 return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
240 isHashValid: function( hash ) {
241 return ( /^#[^#]+$/ ).test( hash );
244 //check whether a url is referencing the same domain, or an external domain or different protocol
245 //could be mailto, etc
246 isExternal: function( url ) {
247 var u = path.parseUrl( url );
248 return u.protocol && u.domain !== documentUrl.domain ? true : false;
251 hasProtocol: function( url ) {
252 return ( /^(:?\w+:)/ ).test( url );
255 //check if the specified url refers to the first page in the main application document.
256 isFirstPageUrl: function( url ) {
257 // We only deal with absolute paths.
258 var u = path.parseUrl( path.makeUrlAbsolute( url, documentBase ) ),
260 // Does the url have the same path as the document?
261 samePath = u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ),
263 // Get the first page element.
264 fp = $.mobile.firstPage,
266 // Get the id of the first page element if it has one.
267 fpId = fp && fp[0] ? fp[0].id : undefined;
269 // The url refers to the first page if the path matches the document and
270 // it either has no hash value, or the hash is exactly equal to the id of the
271 // first page element.
272 return samePath && ( !u.hash || u.hash === "#" || ( fpId && u.hash.replace( /^#/, "" ) === fpId ) );
275 isEmbeddedPage: function( url ) {
276 var u = path.parseUrl( url );
278 //if the path is absolute, then we need to compare the url against
279 //both the documentUrl and the documentBase. The main reason for this
280 //is that links embedded within external documents will refer to the
281 //application document, whereas links embedded within the application
282 //document will be resolved against the document base.
283 if ( u.protocol !== "" ) {
284 return ( u.hash && ( u.hrefNoHash === documentUrl.hrefNoHash || ( documentBaseDiffers && u.hrefNoHash === documentBase.hrefNoHash ) ) );
286 return ( /^#/ ).test( u.href );
290 // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
291 // requests if the document doing the request was loaded via the file:// protocol.
292 // This is usually to allow the application to "phone home" and fetch app specific
293 // data. We normally let the browser handle external/cross-domain urls, but if the
294 // allowCrossDomainPages option is true, we will allow cross-domain http/https
295 // requests to go through our page loading logic.
296 isPermittedCrossDomainRequest: function( docUrl, reqUrl ) {
297 return $.mobile.allowCrossDomainPages &&
298 docUrl.protocol === "file:" &&
299 reqUrl.search( /^https?:/ ) !== -1;
303 //will be defined when a link is clicked and given an active class
304 $activeClickedLink = null,
306 //urlHistory is purely here to make guesses at whether the back or forward button was clicked
307 //and provide an appropriate transition
309 // Array of pages that are visited during a single page load.
310 // Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs)
313 //maintain an index number for the active page in the stack
317 getActive: function() {
318 return urlHistory.stack[ urlHistory.activeIndex ];
321 getPrev: function() {
322 return urlHistory.stack[ urlHistory.activeIndex - 1 ];
325 getNext: function() {
326 return urlHistory.stack[ urlHistory.activeIndex + 1 ];
329 // addNew is used whenever a new page is added
330 addNew: function( url, transition, title, pageUrl, role ) {
331 //if there's forward history, wipe it
332 if ( urlHistory.getNext() ) {
333 urlHistory.clearForward();
336 urlHistory.stack.push( {url : url, transition: transition, title: title, pageUrl: pageUrl, role: role } );
338 urlHistory.activeIndex = urlHistory.stack.length - 1;
341 //wipe urls ahead of active index
342 clearForward: function() {
343 urlHistory.stack = urlHistory.stack.slice( 0, urlHistory.activeIndex + 1 );
346 directHashChange: function( opts ) {
347 var back , forward, newActiveIndex, prev = this.getActive();
349 // check if url is in history and if it's ahead or behind current page
350 $.each( urlHistory.stack, function( i, historyEntry ) {
352 //if the url is in the stack, it's a forward or a back
353 if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) {
354 //define back and forward by whether url is older or newer than current page
355 back = i < urlHistory.activeIndex;
361 // save new page index, null check to prevent falsey 0 result
362 this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex;
365 ( opts.either || opts.isBack )( true );
366 } else if ( forward ) {
367 ( opts.either || opts.isForward )( false );
371 //disable hashchange event listener internally to ignore one change
372 //toggled internally when location.hash is updated to match the url of a successful page load
373 ignoreNextHashChange: false
376 //define first selector to receive focus when a page is shown
377 focusable = "[tabindex],a,button:visible,select:visible,input",
379 //queue to hold simultanious page transitions
380 pageTransitionQueue = [],
382 //indicates whether or not page is in process of transitioning
383 isPageTransitioning = false,
385 //nonsense hash change key for dialogs, so they create a history entry
386 dialogHashKey = "&ui-state=dialog",
389 $base = $head.children( "base" ),
391 //tuck away the original document URL minus any fragment.
392 documentUrl = path.parseLocation(),
394 //if the document has an embedded base tag, documentBase is set to its
395 //initial value. If a base tag does not exist, then we default to the documentUrl.
396 documentBase = $base.length ? path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), documentUrl.href ) ) : documentUrl,
398 //cache the comparison once.
399 documentBaseDiffers = ( documentUrl.hrefNoHash !== documentBase.hrefNoHash ),
401 getScreenHeight = $.mobile.getScreenHeight;
403 //base element management, defined depending on dynamic base tag support
404 var base = $.support.dynamicBaseTag ? {
406 //define base element, for use in routing asset urls that are referenced in Ajax-requested markup
407 element: ( $base.length ? $base : $( "<base>", { href: documentBase.hrefNoHash } ).prependTo( $head ) ),
409 //set the generated BASE element's href attribute to a new page's base path
410 set: function( href ) {
411 base.element.attr( "href", path.makeUrlAbsolute( href, documentBase ) );
414 //set the generated BASE element's href attribute to a new page's base path
416 base.element.attr( "href", documentBase.hrefNoHash );
421 /* internal utility functions */
423 // NOTE Issue #4950 Android phonegap doesn't navigate back properly
424 // when a full page refresh has taken place. It appears that hashchange
425 // and replacestate history alterations work fine but we need to support
426 // both forms of history traversal in our code that uses backward history
428 $.mobile.back = function() {
429 var nav = window.navigator;
431 // if the setting is on and the navigator object is
432 // available use the phonegap navigation capability
433 if( this.phonegapNavigationEnabled &&
436 nav.app.backHistory ){
437 nav.app.backHistory();
439 window.history.back();
443 //direct focus to the page title, or otherwise first focusable element
444 $.mobile.focusPage = function ( page ) {
445 var autofocus = page.find( "[autofocus]" ),
446 pageTitle = page.find( ".ui-title:eq(0)" );
448 if ( autofocus.length ) {
453 if ( pageTitle.length ) {
460 //remove active classes after page transition or error
461 function removeActiveLinkClass( forceRemoval ) {
462 if ( !!$activeClickedLink && ( !$activeClickedLink.closest( "." + $.mobile.activePageClass ).length || forceRemoval ) ) {
463 $activeClickedLink.removeClass( $.mobile.activeBtnClass );
465 $activeClickedLink = null;
468 function releasePageTransitionLock() {
469 isPageTransitioning = false;
470 if ( pageTransitionQueue.length > 0 ) {
471 $.mobile.changePage.apply( null, pageTransitionQueue.pop() );
475 // Save the last scroll distance per page, before it is hidden
476 var setLastScrollEnabled = true,
477 setLastScroll, delayedSetLastScroll;
479 setLastScroll = function() {
480 // this barrier prevents setting the scroll value based on the browser
481 // scrolling the window based on a hashchange
482 if ( !setLastScrollEnabled ) {
486 var active = $.mobile.urlHistory.getActive();
489 var lastScroll = $window.scrollTop();
491 // Set active page's lastScroll prop.
492 // If the location we're scrolling to is less than minScrollBack, let it go.
493 active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll;
497 // bind to scrollstop to gather scroll position. The delay allows for the hashchange
498 // event to fire and disable scroll recording in the case where the browser scrolls
499 // to the hash targets location (sometimes the top of the page). once pagechange fires
500 // getLastScroll is again permitted to operate
501 delayedSetLastScroll = function() {
502 setTimeout( setLastScroll, 100 );
505 // disable an scroll setting when a hashchange has been fired, this only works
506 // because the recording of the scroll position is delayed for 100ms after
507 // the browser might have changed the position because of the hashchange
508 $window.bind( $.support.pushState ? "popstate" : "hashchange", function() {
509 setLastScrollEnabled = false;
512 // handle initial hashchange from chrome :(
513 $window.one( $.support.pushState ? "popstate" : "hashchange", function() {
514 setLastScrollEnabled = true;
517 // wait until the mobile page container has been determined to bind to pagechange
518 $window.one( "pagecontainercreate", function() {
519 // once the page has changed, re-enable the scroll recording
520 $.mobile.pageContainer.bind( "pagechange", function() {
522 setLastScrollEnabled = true;
524 // remove any binding that previously existed on the get scroll
525 // which may or may not be different than the scroll element determined for
526 // this page previously
527 $window.unbind( "scrollstop", delayedSetLastScroll );
529 // determine and bind to the current scoll element which may be the window
530 // or in the case of touch overflow the element with touch overflow
531 $window.bind( "scrollstop", delayedSetLastScroll );
535 // bind to scrollstop for the first page as "pagechange" won't be fired in that case
536 $window.bind( "scrollstop", delayedSetLastScroll );
538 // No-op implementation of transition degradation
539 $.mobile._maybeDegradeTransition = $.mobile._maybeDegradeTransition || function( transition ) {
543 //function for transitioning between two existing pages
544 function transitionPages( toPage, fromPage, transition, reverse ) {
547 //trigger before show/hide events
548 fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } );
551 toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } );
554 $.mobile.hidePageLoadingMsg();
556 transition = $.mobile._maybeDegradeTransition( transition );
558 //find the transition handler for the specified transition. If there
559 //isn't one in our transitionHandlers dictionary, use the default one.
560 //call the handler immediately to kick-off the transition.
561 var th = $.mobile.transitionHandlers[ transition || "default" ] || $.mobile.defaultTransitionHandler,
562 promise = th( transition, reverse, toPage, fromPage );
564 promise.done(function() {
566 //trigger show/hide events
568 fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } );
571 //trigger pageshow, define prevPage as either fromPage or empty jQuery obj
572 toPage.data( "page" )._trigger( "show", null, { prevPage: fromPage || $( "" ) } );
578 //simply set the active page's minimum height to screen height, depending on orientation
579 function resetActivePageHeight() {
580 var aPage = $( "." + $.mobile.activePageClass ),
581 aPagePadT = parseFloat( aPage.css( "padding-top" ) ),
582 aPagePadB = parseFloat( aPage.css( "padding-bottom" ) ),
583 aPageBorderT = parseFloat( aPage.css( "border-top-width" ) ),
584 aPageBorderB = parseFloat( aPage.css( "border-bottom-width" ) );
586 aPage.css( "min-height", getScreenHeight() - aPagePadT - aPagePadB - aPageBorderT - aPageBorderB );
589 //shared page enhancements
590 function enhancePage( $page, role ) {
591 // If a role was specified, make sure the data-role attribute
592 // on the page element is in sync.
594 $page.attr( "data-" + $.mobile.ns + "role", role );
601 /* exposed $.mobile methods */
603 //animation complete callback
604 $.fn.animationComplete = function( callback ) {
605 if ( $.support.cssTransitions ) {
606 return $( this ).one( 'webkitAnimationEnd animationend', callback );
609 // defer execution for consistency between webkit/non webkit
610 setTimeout( callback, 0 );
615 //expose path object on $.mobile
616 $.mobile.path = path;
618 //expose base object on $.mobile
619 $.mobile.base = base;
622 $.mobile.urlHistory = urlHistory;
624 $.mobile.dialogHashKey = dialogHashKey;
628 //enable cross-domain page support
629 $.mobile.allowCrossDomainPages = false;
631 //return the original document url
632 $.mobile.getDocumentUrl = function( asParsedObject ) {
633 return asParsedObject ? $.extend( {}, documentUrl ) : documentUrl.href;
636 //return the original document base url
637 $.mobile.getDocumentBase = function( asParsedObject ) {
638 return asParsedObject ? $.extend( {}, documentBase ) : documentBase.href;
641 $.mobile._bindPageRemove = function() {
642 var page = $( this );
644 // when dom caching is not enabled or the page is embedded bind to remove the page on hide
645 if ( !page.data( "page" ).options.domCache &&
646 page.is( ":jqmData(external-page='true')" ) ) {
648 page.bind( 'pagehide.remove', function() {
649 var $this = $( this ),
650 prEvent = new $.Event( "pageremove" );
652 $this.trigger( prEvent );
654 if ( !prEvent.isDefaultPrevented() ) {
655 $this.removeWithDependents();
661 // Load a page into the DOM.
662 $.mobile.loadPage = function( url, options ) {
663 // This function uses deferred notifications to let callers
664 // know when the page is done loading, or if an error has occurred.
665 var deferred = $.Deferred(),
667 // The default loadPage options with overrides specified by
669 settings = $.extend( {}, $.mobile.loadPage.defaults, options ),
671 // The DOM element for the page after it has been loaded.
674 // If the reloadPage option is true, and the page is already
675 // in the DOM, dupCachedPage will be set to the page element
676 // so that it can be removed after the new version of the
677 // page is loaded off the network.
678 dupCachedPage = null,
680 // determine the current base url
681 findBaseWithDefault = function() {
682 var closestBase = ( $.mobile.activePage && getClosestBaseUrl( $.mobile.activePage ) );
683 return closestBase || documentBase.hrefNoHash;
686 // The absolute version of the URL passed into the function. This
687 // version of the URL may contain dialog/subpage params in it.
688 absUrl = path.makeUrlAbsolute( url, findBaseWithDefault() );
691 // If the caller provided data, and we're using "get" request,
692 // append the data to the URL.
693 if ( settings.data && settings.type === "get" ) {
694 absUrl = path.addSearchParams( absUrl, settings.data );
695 settings.data = undefined;
698 // If the caller is using a "post" request, reloadPage must be true
699 if ( settings.data && settings.type === "post" ) {
700 settings.reloadPage = true;
703 // The absolute version of the URL minus any dialog/subpage params.
704 // In otherwords the real URL of the page to be loaded.
705 var fileUrl = path.getFilePath( absUrl ),
707 // The version of the Url actually stored in the data-url attribute of
708 // the page. For embedded pages, it is just the id of the page. For pages
709 // within the same domain as the document base, it is the site relative
710 // path. For cross-domain pages (Phone Gap only) the entire absolute Url
711 // used to load the page.
712 dataUrl = path.convertUrlToDataUrl( absUrl );
714 // Make sure we have a pageContainer to work with.
715 settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
717 // Check to see if the page already exists in the DOM.
718 // NOTE do _not_ use the :jqmData psuedo selector because parenthesis
719 // are a valid url char and it breaks on the first occurence
720 page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
722 // If we failed to find the page, check to see if the url is a
723 // reference to an embedded page. If so, it may have been dynamically
724 // injected by a developer, in which case it would be lacking a data-url
725 // attribute and in need of enhancement.
726 if ( page.length === 0 && dataUrl && !path.isPath( dataUrl ) ) {
727 page = settings.pageContainer.children( "#" + dataUrl )
728 .attr( "data-" + $.mobile.ns + "url", dataUrl )
729 .jqmData( "url", dataUrl );
732 // If we failed to find a page in the DOM, check the URL to see if it
733 // refers to the first page in the application. If it isn't a reference
734 // to the first page and refers to non-existent embedded page, error out.
735 if ( page.length === 0 ) {
736 if ( $.mobile.firstPage && path.isFirstPageUrl( fileUrl ) ) {
737 // Check to make sure our cached-first-page is actually
738 // in the DOM. Some user deployed apps are pruning the first
739 // page from the DOM for various reasons, we check for this
740 // case here because we don't want a first-page with an id
741 // falling through to the non-existent embedded page error
742 // case. If the first-page is not in the DOM, then we let
743 // things fall through to the ajax loading code below so
744 // that it gets reloaded.
745 if ( $.mobile.firstPage.parent().length ) {
746 page = $( $.mobile.firstPage );
748 } else if ( path.isEmbeddedPage( fileUrl ) ) {
749 deferred.reject( absUrl, options );
750 return deferred.promise();
754 // If the page we are interested in is already in the DOM,
755 // and the caller did not indicate that we should force a
756 // reload of the file, we are done. Otherwise, track the
757 // existing page as a duplicated.
759 if ( !settings.reloadPage ) {
760 enhancePage( page, settings.role );
761 deferred.resolve( absUrl, options, page );
762 return deferred.promise();
764 dupCachedPage = page;
767 var mpc = settings.pageContainer,
768 pblEvent = new $.Event( "pagebeforeload" ),
769 triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings };
771 // Let listeners know we're about to load a page.
772 mpc.trigger( pblEvent, triggerData );
774 // If the default behavior is prevented, stop here!
775 if ( pblEvent.isDefaultPrevented() ) {
776 return deferred.promise();
779 if ( settings.showLoadMsg ) {
781 // This configurable timeout allows cached pages a brief delay to load without showing a message
782 var loadMsgDelay = setTimeout(function() {
783 $.mobile.showPageLoadingMsg();
784 }, settings.loadMsgDelay ),
786 // Shared logic for clearing timeout and removing message.
787 hideMsg = function() {
789 // Stop message show timer
790 clearTimeout( loadMsgDelay );
792 // Hide loading message
793 $.mobile.hidePageLoadingMsg();
797 // Reset base to the default document base.
802 if ( !( $.mobile.allowCrossDomainPages || path.isSameDomain( documentUrl, absUrl ) ) ) {
803 deferred.reject( absUrl, options );
805 // Load the new page.
811 success: function( html, textStatus, xhr ) {
812 //pre-parse html to check for a data-url,
813 //use it as the new fileUrl, base path, etc
814 var all = $( "<div></div>" ),
817 newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1,
819 // TODO handle dialogs again
820 pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ),
821 dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" );
824 // data-url must be provided for the base tag so resource requests can be directed to the
825 // correct url. loading into a temprorary element makes these requests immediately
826 if ( pageElemRegex.test( html ) &&
828 dataUrlRegex.test( RegExp.$1 ) &&
830 url = fileUrl = path.getFilePath( $( "<div>" + RegExp.$1 + "</div>" ).text() );
837 //workaround to allow scripts to execute when included in page divs
838 all.get( 0 ).innerHTML = html;
839 page = all.find( ":jqmData(role='page'), :jqmData(role='dialog')" ).first();
841 //if page elem couldn't be found, create one and insert the body element's contents
842 if ( !page.length ) {
843 page = $( "<div data-" + $.mobile.ns + "role='page'>" + html.split( /<\/?body[^>]*>/gmi )[1] + "</div>" );
846 if ( newPageTitle && !page.jqmData( "title" ) ) {
847 if ( ~newPageTitle.indexOf( "&" ) ) {
848 newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text();
850 page.jqmData( "title", newPageTitle );
853 //rewrite src and href attrs to use a base url
854 if ( !$.support.dynamicBaseTag ) {
855 var newPath = path.get( fileUrl );
856 page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() {
857 var thisAttr = $( this ).is( '[href]' ) ? 'href' :
858 $( this ).is( '[src]' ) ? 'src' : 'action',
859 thisUrl = $( this ).attr( thisAttr );
861 // XXX_jblas: We need to fix this so that it removes the document
862 // base URL, and then prepends with the new page URL.
863 //if full path exists and is same, chop it - helps IE out
864 thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' );
866 if ( !/^(\w+:|#|\/)/.test( thisUrl ) ) {
867 $( this ).attr( thisAttr, newPath + thisUrl );
872 //append to page and enhance
873 // TODO taging a page with external to make sure that embedded pages aren't removed
874 // by the various page handling code is bad. Having page handling code in many
875 // places is bad. Solutions post 1.0
877 .attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) )
878 .attr( "data-" + $.mobile.ns + "external-page", true )
879 .appendTo( settings.pageContainer );
881 // wait for page creation to leverage options defined on widget
882 page.one( 'pagecreate', $.mobile._bindPageRemove );
884 enhancePage( page, settings.role );
886 // Enhancing the page may result in new dialogs/sub pages being inserted
887 // into the DOM. If the original absUrl refers to a sub-page, that is the
888 // real page we are interested in.
889 if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) {
890 page = settings.pageContainer.children( "[data-" + $.mobile.ns +"url='" + dataUrl + "']" );
893 //bind pageHide to removePage after it's hidden, if the page options specify to do so
895 // Remove loading message.
896 if ( settings.showLoadMsg ) {
900 // Add the page reference and xhr to our triggerData.
901 triggerData.xhr = xhr;
902 triggerData.textStatus = textStatus;
903 triggerData.page = page;
905 // Let listeners know the page loaded successfully.
906 settings.pageContainer.trigger( "pageload", triggerData );
908 deferred.resolve( absUrl, options, page, dupCachedPage );
910 error: function( xhr, textStatus, errorThrown ) {
911 //set base back to current path
913 base.set( path.get() );
916 // Add error info to our triggerData.
917 triggerData.xhr = xhr;
918 triggerData.textStatus = textStatus;
919 triggerData.errorThrown = errorThrown;
921 var plfEvent = new $.Event( "pageloadfailed" );
923 // Let listeners know the page load failed.
924 settings.pageContainer.trigger( plfEvent, triggerData );
926 // If the default behavior is prevented, stop here!
927 // Note that it is the responsibility of the listener/handler
928 // that called preventDefault(), to resolve/reject the
929 // deferred object within the triggerData.
930 if ( plfEvent.isDefaultPrevented() ) {
934 // Remove loading message.
935 if ( settings.showLoadMsg ) {
937 // Remove loading message.
940 // show error message
941 $.mobile.showPageLoadingMsg( $.mobile.pageLoadErrorMessageTheme, $.mobile.pageLoadErrorMessage, true );
944 setTimeout( $.mobile.hidePageLoadingMsg, 1500 );
947 deferred.reject( absUrl, options );
952 return deferred.promise();
955 $.mobile.loadPage.defaults = {
959 role: undefined, // By default we rely on the role defined by the @data-role attribute.
961 pageContainer: undefined,
962 loadMsgDelay: 50 // This delay allows loads that pull from browser cache to occur without showing the loading message.
965 // Show a specific page in the page container.
966 $.mobile.changePage = function( toPage, options ) {
967 // If we are in the midst of a transition, queue the current request.
968 // We'll call changePage() once we're done with the current transition to
969 // service the request.
970 if ( isPageTransitioning ) {
971 pageTransitionQueue.unshift( arguments );
975 var settings = $.extend( {}, $.mobile.changePage.defaults, options );
977 // Make sure we have a pageContainer to work with.
978 settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
980 // Make sure we have a fromPage.
981 settings.fromPage = settings.fromPage || $.mobile.activePage;
983 var mpc = settings.pageContainer,
984 pbcEvent = new $.Event( "pagebeforechange" ),
985 triggerData = { toPage: toPage, options: settings };
987 // Let listeners know we're about to change the current page.
988 mpc.trigger( pbcEvent, triggerData );
990 // If the default behavior is prevented, stop here!
991 if ( pbcEvent.isDefaultPrevented() ) {
995 // We allow "pagebeforechange" observers to modify the toPage in the trigger
996 // data to allow for redirects. Make sure our toPage is updated.
998 toPage = triggerData.toPage;
1000 // Set the isPageTransitioning flag to prevent any requests from
1001 // entering this method while we are in the midst of loading a page
1002 // or transitioning.
1004 isPageTransitioning = true;
1006 // If the caller passed us a url, call loadPage()
1007 // to make sure it is loaded into the DOM. We'll listen
1008 // to the promise object it returns so we know when
1009 // it is done loading or if an error ocurred.
1010 if ( typeof toPage === "string" ) {
1011 $.mobile.loadPage( toPage, settings )
1012 .done(function( url, options, newPage, dupCachedPage ) {
1013 isPageTransitioning = false;
1014 options.duplicateCachedPage = dupCachedPage;
1015 $.mobile.changePage( newPage, options );
1017 .fail(function( url, options ) {
1018 isPageTransitioning = false;
1020 //clear out the active button state
1021 removeActiveLinkClass( true );
1023 //release transition lock so navigation is free again
1024 releasePageTransitionLock();
1025 settings.pageContainer.trigger( "pagechangefailed", triggerData );
1030 // If we are going to the first-page of the application, we need to make
1031 // sure settings.dataUrl is set to the application document url. This allows
1032 // us to avoid generating a document url with an id hash in the case where the
1033 // first-page of the document has an id attribute specified.
1034 if ( toPage[ 0 ] === $.mobile.firstPage[ 0 ] && !settings.dataUrl ) {
1035 settings.dataUrl = documentUrl.hrefNoHash;
1038 // The caller passed us a real page DOM element. Update our
1039 // internal state and then trigger a transition to the page.
1040 var fromPage = settings.fromPage,
1041 url = ( settings.dataUrl && path.convertUrlToDataUrl( settings.dataUrl ) ) || toPage.jqmData( "url" ),
1042 // The pageUrl var is usually the same as url, except when url is obscured as a dialog url. pageUrl always contains the file path
1044 fileUrl = path.getFilePath( url ),
1045 active = urlHistory.getActive(),
1046 activeIsInitialPage = urlHistory.activeIndex === 0,
1048 pageTitle = document.title,
1049 isDialog = settings.role === "dialog" || toPage.jqmData( "role" ) === "dialog";
1051 // By default, we prevent changePage requests when the fromPage and toPage
1052 // are the same element, but folks that generate content manually/dynamically
1053 // and reuse pages want to be able to transition to the same page. To allow
1054 // this, they will need to change the default value of allowSamePageTransition
1055 // to true, *OR*, pass it in as an option when they manually call changePage().
1056 // It should be noted that our default transition animations assume that the
1057 // formPage and toPage are different elements, so they may behave unexpectedly.
1058 // It is up to the developer that turns on the allowSamePageTransitiona option
1059 // to either turn off transition animations, or make sure that an appropriate
1060 // animation transition is used.
1061 if ( fromPage && fromPage[0] === toPage[0] && !settings.allowSamePageTransition ) {
1062 isPageTransitioning = false;
1063 mpc.trigger( "pagechange", triggerData );
1065 // Even if there is no page change to be done, we should keep the urlHistory in sync with the hash changes
1066 if ( settings.fromHashChange ) {
1067 urlHistory.directHashChange({
1069 isBack: function() {},
1070 isForward: function() {}
1077 // We need to make sure the page we are given has already been enhanced.
1078 enhancePage( toPage, settings.role );
1080 // If the changePage request was sent from a hashChange event, check to see if the
1081 // page is already within the urlHistory stack. If so, we'll assume the user hit
1082 // the forward/back button and will try to match the transition accordingly.
1083 if ( settings.fromHashChange ) {
1084 urlHistory.directHashChange({
1086 isBack: function() { historyDir = -1; },
1087 isForward: function() { historyDir = 1; }
1091 // Kill the keyboard.
1092 // XXX_jblas: We need to stop crawling the entire document to kill focus. Instead,
1093 // we should be tracking focus with a delegate() handler so we already have
1094 // the element in hand at this point.
1095 // Wrap this in a try/catch block since IE9 throw "Unspecified error" if document.activeElement
1096 // is undefined when we are in an IFrame.
1098 if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== 'body' ) {
1099 $( document.activeElement ).blur();
1101 $( "input:focus, textarea:focus, select:focus" ).blur();
1105 // 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
1106 var alreadyThere = false;
1108 // If we're displaying the page as a dialog, we don't want the url
1109 // for the dialog content to be used in the hash. Instead, we want
1110 // to append the dialogHashKey to the url of the current page.
1111 if ( isDialog && active ) {
1112 // on the initial page load active.url is undefined and in that case should
1113 // be an empty string. Moving the undefined -> empty string back into
1114 // urlHistory.addNew seemed imprudent given undefined better represents
1117 // If we are at a place in history that once belonged to a dialog, reuse
1118 // this state without adding to urlHistory and without modifying the hash.
1119 // However, if a dialog is already displayed at this point, and we're
1120 // about to display another dialog, then we must add another hash and
1121 // history entry on top so that one may navigate back to the original dialog
1122 if ( active.url.indexOf( dialogHashKey ) > -1 && !$.mobile.activePage.is( ".ui-dialog" ) ) {
1123 settings.changeHash = false;
1124 alreadyThere = true;
1127 // Normally, we tack on a dialog hash key, but if this is the location of a stale dialog,
1128 // we reuse the URL from the entry
1129 url = ( active.url || "" ) + ( alreadyThere ? "" : dialogHashKey );
1131 // tack on another dialogHashKey if this is the same as the initial hash
1132 // this makes sure that a history entry is created for this dialog
1133 if ( urlHistory.activeIndex === 0 && url === urlHistory.initialDst ) {
1134 url += dialogHashKey;
1138 // Set the location hash.
1139 if ( settings.changeHash !== false && url ) {
1140 //disable hash listening temporarily
1141 urlHistory.ignoreNextHashChange = true;
1142 //update hash and history
1146 // if title element wasn't found, try the page div data attr too
1147 // If this is a deep-link or a reload ( active === undefined ) then just use pageTitle
1148 var newPageTitle = ( !active )? pageTitle : toPage.jqmData( "title" ) || toPage.children( ":jqmData(role='header')" ).find( ".ui-title" ).getEncodedText();
1149 if ( !!newPageTitle && pageTitle === document.title ) {
1150 pageTitle = newPageTitle;
1152 if ( !toPage.jqmData( "title" ) ) {
1153 toPage.jqmData( "title", pageTitle );
1156 // Make sure we have a transition defined.
1157 settings.transition = settings.transition ||
1158 ( ( historyDir && !activeIsInitialPage ) ? active.transition : undefined ) ||
1159 ( isDialog ? $.mobile.defaultDialogTransition : $.mobile.defaultPageTransition );
1161 //add page to history stack if it's not back or forward
1162 if ( !historyDir ) {
1163 // Overwrite the current entry if it's a leftover from a dialog
1164 if ( alreadyThere ) {
1165 urlHistory.activeIndex = Math.max( 0, urlHistory.activeIndex - 1 );
1167 urlHistory.addNew( url, settings.transition, pageTitle, pageUrl, settings.role );
1171 document.title = urlHistory.getActive().title;
1173 //set "toPage" as activePage
1174 $.mobile.activePage = toPage;
1176 // If we're navigating back in the URL history, set reverse accordingly.
1177 settings.reverse = settings.reverse || historyDir < 0;
1179 transitionPages( toPage, fromPage, settings.transition, settings.reverse )
1180 .done(function( name, reverse, $to, $from, alreadyFocused ) {
1181 removeActiveLinkClass();
1183 //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden
1184 if ( settings.duplicateCachedPage ) {
1185 settings.duplicateCachedPage.remove();
1188 // Send focus to the newly shown page. Moved from promise .done binding in transitionPages
1189 // itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility)
1190 // despite visibility: hidden addresses issue #2965
1191 // https://github.com/jquery/jquery-mobile/issues/2965
1192 if ( !alreadyFocused ) {
1193 $.mobile.focusPage( toPage );
1196 releasePageTransitionLock();
1198 // Let listeners know we're all done changing the current page.
1199 mpc.trigger( "pagechange", triggerData );
1203 $.mobile.changePage.defaults = {
1204 transition: undefined,
1207 fromHashChange: false,
1208 role: undefined, // By default we rely on the role defined by the @data-role attribute.
1209 duplicateCachedPage: undefined,
1210 pageContainer: undefined,
1211 showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage
1213 fromPage: undefined,
1214 allowSamePageTransition: false
1217 /* Event Bindings - hashchange, submit, and click */
1218 function findClosestLink( ele )
1221 // Look for the closest element with a nodeName of "a".
1222 // Note that we are checking if we have a valid nodeName
1223 // before attempting to access it. This is because the
1224 // node we get called with could have originated from within
1225 // an embedded SVG document where some symbol instance elements
1226 // don't have nodeName defined on them, or strings are of type
1227 // SVGAnimatedString.
1228 if ( ( typeof ele.nodeName === "string" ) && ele.nodeName.toLowerCase() === "a" ) {
1231 ele = ele.parentNode;
1236 // The base URL for any given element depends on the page it resides in.
1237 function getClosestBaseUrl( ele )
1239 // Find the closest page and extract out its url.
1240 var url = $( ele ).closest( ".ui-page" ).jqmData( "url" ),
1241 base = documentBase.hrefNoHash;
1243 if ( !url || !path.isPath( url ) ) {
1247 return path.makeUrlAbsolute( url, base);
1250 //The following event bindings should be bound after mobileinit has been triggered
1251 //the following deferred is resolved in the init file
1252 $.mobile.navreadyDeferred = $.Deferred();
1253 $.mobile.navreadyDeferred.done(function() {
1254 //bind to form submit events, handle with Ajax
1255 $( document ).delegate( "form", "submit", function( event ) {
1256 var $this = $( this );
1258 if ( !$.mobile.ajaxEnabled ||
1259 // test that the form is, itself, ajax false
1260 $this.is( ":jqmData(ajax='false')" ) ||
1261 // test that $.mobile.ignoreContentEnabled is set and
1262 // the form or one of it's parents is ajax=false
1263 !$this.jqmHijackable().length ) {
1267 var type = $this.attr( "method" ),
1268 target = $this.attr( "target" ),
1269 url = $this.attr( "action" );
1271 // If no action is specified, browsers default to using the
1272 // URL of the document containing the form. Since we dynamically
1273 // pull in pages from external documents, the form should submit
1274 // to the URL for the source document of the page containing
1277 // Get the @data-url for the page containing the form.
1278 url = getClosestBaseUrl( $this );
1279 if ( url === documentBase.hrefNoHash ) {
1280 // The url we got back matches the document base,
1281 // which means the page must be an internal/embedded page,
1282 // so default to using the actual document url as a browser
1284 url = documentUrl.hrefNoSearch;
1288 url = path.makeUrlAbsolute( url, getClosestBaseUrl( $this ) );
1290 if ( ( path.isExternal( url ) && !path.isPermittedCrossDomainRequest( documentUrl, url ) ) || target ) {
1294 $.mobile.changePage(
1297 type: type && type.length && type.toLowerCase() || "get",
1298 data: $this.serialize(),
1299 transition: $this.jqmData( "transition" ),
1300 reverse: $this.jqmData( "direction" ) === "reverse",
1304 event.preventDefault();
1307 //add active state on vclick
1308 $( document ).bind( "vclick", function( event ) {
1309 // if this isn't a left click we don't care. Its important to note
1310 // that when the virtual event is generated it will create the which attr
1311 if ( event.which > 1 || !$.mobile.linkBindingEnabled ) {
1315 var link = findClosestLink( event.target );
1317 // split from the previous return logic to avoid find closest where possible
1318 // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
1320 if ( !$( link ).jqmHijackable().length ) {
1325 if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) {
1326 removeActiveLinkClass( true );
1327 $activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" );
1328 $activeClickedLink.addClass( $.mobile.activeBtnClass );
1333 // click routing - direct to HTTP or Ajax, accordingly
1334 $( document ).bind( "click", function( event ) {
1335 if ( !$.mobile.linkBindingEnabled ) {
1339 var link = findClosestLink( event.target ), $link = $( link ), httpCleanup;
1341 // If there is no link associated with the click or its not a left
1342 // click we want to ignore the click
1343 // TODO teach $.mobile.hijackable to operate on raw dom elements so the link wrapping
1345 if ( !link || event.which > 1 || !$link.jqmHijackable().length ) {
1349 //remove active link class if external (then it won't be there if you come back)
1350 httpCleanup = function() {
1351 window.setTimeout(function() { removeActiveLinkClass( true ); }, 200 );
1354 //if there's a data-rel=back attr, go back in history
1355 if ( $link.is( ":jqmData(rel='back')" ) ) {
1360 var baseUrl = getClosestBaseUrl( $link ),
1362 //get href, if defined, otherwise default to empty hash
1363 href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl );
1365 //if ajax is disabled, exit early
1366 if ( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ) {
1368 //use default click handling
1372 // XXX_jblas: Ideally links to application pages should be specified as
1373 // an url to the application document with a hash that is either
1374 // the site relative path or id to the page. But some of the
1375 // internal code that dynamically generates sub-pages for nested
1376 // lists and select dialogs, just write a hash in the link they
1377 // create. This means the actual URL path is based on whatever
1378 // the current value of the base tag is at the time this code
1379 // is called. For now we are just assuming that any url with a
1380 // hash in it is an application page reference.
1381 if ( href.search( "#" ) !== -1 ) {
1382 href = href.replace( /[^#]*#/, "" );
1384 //link was an empty hash meant purely
1385 //for interaction, so we ignore it.
1386 event.preventDefault();
1388 } else if ( path.isPath( href ) ) {
1389 //we have apath so make it the href we want to load.
1390 href = path.makeUrlAbsolute( href, baseUrl );
1392 //we have a simple id so use the documentUrl as its base.
1393 href = path.makeUrlAbsolute( "#" + href, documentUrl.hrefNoHash );
1397 // Should we handle this link, or let the browser deal with it?
1398 var useDefaultUrlHandling = $link.is( "[rel='external']" ) || $link.is( ":jqmData(ajax='false')" ) || $link.is( "[target]" ),
1400 // Some embedded browsers, like the web view in Phone Gap, allow cross-domain XHR
1401 // requests if the document doing the request was loaded via the file:// protocol.
1402 // This is usually to allow the application to "phone home" and fetch app specific
1403 // data. We normally let the browser handle external/cross-domain urls, but if the
1404 // allowCrossDomainPages option is true, we will allow cross-domain http/https
1405 // requests to go through our page loading logic.
1407 //check for protocol or rel and its not an embedded page
1408 //TODO overlap in logic from isExternal, rel=external check should be
1409 // moved into more comprehensive isExternalLink
1410 isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !path.isPermittedCrossDomainRequest( documentUrl, href ) );
1414 //use default click handling
1419 var transition = $link.jqmData( "transition" ),
1420 reverse = $link.jqmData( "direction" ) === "reverse" ||
1421 // deprecated - remove by 1.0
1422 $link.jqmData( "back" ),
1424 //this may need to be more specific as we use data-rel more
1425 role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined;
1427 $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role, link: $link } );
1428 event.preventDefault();
1431 //prefetch pages when anchors with data-prefetch are encountered
1432 $( document ).delegate( ".ui-page", "pageshow.prefetch", function() {
1434 $( this ).find( "a:jqmData(prefetch)" ).each(function() {
1435 var $link = $( this ),
1436 url = $link.attr( "href" );
1438 if ( url && $.inArray( url, urls ) === -1 ) {
1441 $.mobile.loadPage( url, { role: $link.attr( "data-" + $.mobile.ns + "rel" ) } );
1446 $.mobile._handleHashChange = function( hash ) {
1447 //find first page via hash
1448 var to = path.stripHash( hash ),
1449 //transition is false if it's the first page, undefined otherwise (and may be overridden by default)
1450 transition = $.mobile.urlHistory.stack.length === 0 ? "none" : undefined,
1452 // "navigate" event fired to allow others to take advantage of the more robust hashchange handling
1453 navEvent = new $.Event( "navigate" ),
1455 // default options for the changPage calls made after examining the current state
1456 // of the page and the hash
1457 changePageOptions = {
1458 transition: transition,
1460 fromHashChange: true
1463 if ( 0 === urlHistory.stack.length ) {
1464 urlHistory.initialDst = to;
1467 // We should probably fire the "navigate" event from those places that make calls to _handleHashChange,
1468 // and have _handleHashChange hook into the "navigate" event instead of triggering it here
1469 $.mobile.pageContainer.trigger( navEvent );
1470 if ( navEvent.isDefaultPrevented() ) {
1474 //if listening is disabled (either globally or temporarily), or it's a dialog hash
1475 if ( !$.mobile.hashListeningEnabled || urlHistory.ignoreNextHashChange ) {
1476 urlHistory.ignoreNextHashChange = false;
1480 // special case for dialogs
1481 if ( urlHistory.stack.length > 1 && to.indexOf( dialogHashKey ) > -1 && urlHistory.initialDst !== to ) {
1483 // If current active page is not a dialog skip the dialog and continue
1484 // in the same direction
1485 if ( !$.mobile.activePage.is( ".ui-dialog" ) ) {
1486 //determine if we're heading forward or backward and continue accordingly past
1487 //the current dialog
1488 urlHistory.directHashChange({
1490 isBack: function() { $.mobile.back(); },
1491 isForward: function() { window.history.forward(); }
1494 // prevent changePage()
1497 // if the current active page is a dialog and we're navigating
1498 // to a dialog use the dialog objected saved in the stack
1499 urlHistory.directHashChange({
1502 // regardless of the direction of the history change
1504 either: function( isBack ) {
1505 var active = $.mobile.urlHistory.getActive();
1507 to = active.pageUrl;
1509 // make sure to set the role, transition and reversal
1510 // as most of this is lost by the domCache cleaning
1511 $.extend( changePageOptions, {
1513 transition: active.transition,
1521 //if to is defined, load it
1523 // At this point, 'to' can be one of 3 things, a cached page element from
1524 // a history stack entry, an id, or site-relative/absolute URL. If 'to' is
1525 // an id, we need to resolve it against the documentBase, not the location.href,
1526 // since the hashchange could've been the result of a forward/backward navigation
1527 // that crosses from an external page/dialog to an internal page/dialog.
1528 to = ( typeof to === "string" && !path.isPath( to ) ) ? ( path.makeUrlAbsolute( '#' + to, documentBase ) ) : to;
1530 // If we're about to go to an initial URL that contains a reference to a non-existent
1531 // internal page, go to the first page instead. We know that the initial hash refers to a
1532 // non-existent page, because the initial hash did not end up in the initial urlHistory entry
1533 if ( to === path.makeUrlAbsolute( '#' + urlHistory.initialDst, documentBase ) &&
1534 urlHistory.stack.length && urlHistory.stack[0].url !== urlHistory.initialDst.replace( dialogHashKey, "" ) ) {
1535 to = $.mobile.firstPage;
1537 $.mobile.changePage( to, changePageOptions );
1539 //there's no hash, go to the first page in the dom
1540 $.mobile.changePage( $.mobile.firstPage, changePageOptions );
1544 //hashchange event handler
1545 $window.bind( "hashchange", function( e, triggered ) {
1546 // Firefox auto-escapes the location.hash as for v13 but
1547 // leaves the href untouched
1548 $.mobile._handleHashChange( path.parseLocation().hash );
1551 //set page min-heights to be device specific
1552 $( document ).bind( "pageshow", resetActivePageHeight );
1553 $( window ).bind( "throttledresize", resetActivePageHeight );
1555 });//navreadyDeferred done callback
1558 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
1560 //>>excludeEnd("jqmBuildExclude");