//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); //>>description: Formats groups of links as nav bars. //>>label: Navigation Bars define( [ "jquery", "./jquery.mobile.core", "./jquery.mobile.event", "./jquery.mobile.hashchange", "./jquery.mobile.page", "./jquery.mobile.transition" ], function( $ ) { //>>excludeEnd("jqmBuildExclude"); ( function( $, undefined ) { //define vars for interal use var $window = $( 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]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, //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; } 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, "" ); } return absUrl; }, //get path from current hash, or from a file path get: function( newPath ) { if( newPath === undefined ) { newPath = location.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, "" ) ); }, //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 ); } }, //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 isp in history and if it's ahead or behind current page $.each( urlHistory.stack, function( i, historyEntry ) { //if the url is in the stack, it's a forward or a back if( opts.currentUrl === historyEntry.url ) { //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.parseUrl( location.href ), //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 ); //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 : $( "", { 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 --------------------------------------*/ //direct focus to the page title, or otherwise first focusable element $.mobile.focusPage = function ( page ) { var pageTitle = page.find( ".ui-title:eq(0)" ); if( pageTitle.length ) { pageTitle.focus(); } else{ page.focus(); } } //remove active classes after page transition or error function removeActiveLinkClass( forceRemoval ) { if( !!$activeClickedLink && ( !$activeClickedLink.closest( '.ui-page-active' ).length || forceRemoval ) ) { $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 ); //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(); //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 || $( "" ) } ); }); return promise; } //simply set the active page's minimum height to screen height, depending on orientation function getScreenHeight(){ var orientation = $.event.special.orientationchange.orientation(), port = orientation === "portrait", winMin = port ? 480 : 320, screenHeight = port ? screen.availHeight : screen.availWidth, winHeight = Math.max( winMin, $( window ).height() ), pageMin = Math.min( screenHeight, winHeight ); return pageMin; } $.mobile.getScreenHeight = getScreenHeight; //simply set the active page's minimum height to screen height, depending on orientation function resetActivePageHeight(){ $( "." + $.mobile.activePageClass ).css( "min-height", getScreenHeight() ); } //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. page = settings.pageContainer.children( ":jqmData(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 ); } // 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(); } } // Reset base to the default document base. if ( base ) { base.reset(); } // If the page we are interested in is already in the DOM, // and the caller did not indicate that we should force a // reload of the file, we are done. Otherwise, track the // existing page as a duplicated. if ( page.length ) { if ( !settings.reloadPage ) { enhancePage( page, settings.role ); deferred.resolve( absUrl, options, page ); 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(); }; } 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 = $( "
" ), //page title regexp newPageTitle = html.match( /]*>([^<]*)/ ) && 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( RegExp.$1 ); } if ( base ) { 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 = $( "
" + html.split( /<\/?body[^>]*>/gmi )[1] + "
" ); } if ( newPageTitle && !page.jqmData( "title" ) ) { if ( ~newPageTitle.indexOf( "&" ) ) { newPageTitle = $( "
" + newPageTitle + "
" ).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( ":jqmData(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" || toPage.jqmData( "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 ); 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) {} // 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 url = ( active.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 ) { 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(); } //remove initial build class (only present on first pageshow) $html.removeClass( "ui-mobile-rendering" ); // Send focus to the newly shown page. Moved from promise .done binding in transitionPages // itself to avoid ie bug that reports offsetWidth as > 0 (core check for visibility) // despite visibility: hidden addresses issue #2965 // https://github.com/jquery/jquery-mobile/issues/2965 if( !alreadyFocused ){ $.mobile.focusPage( toPage ); } releasePageTransitionLock(); // 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 function is called in the init file $.mobile._registerInternalEvents = function(){ //bind to form submit events, handle with Ajax $( document ).delegate( "form", "submit", function( event ) { var $this = $( this ); if( !$.mobile.ajaxEnabled || $this.is( ":jqmData(ajax='false')" ) ) { 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) ); //external submits use regular HTTP if( path.isExternal( url ) || target ) { return; } $.mobile.changePage( url, { type: type && type.length && type.toLowerCase() || "get", data: $this.serialize(), transition: $this.jqmData( "transition" ), direction: $this.jqmData( "direction" ), reloadPage: true } ); event.preventDefault(); }); //add active state on vclick $( document ).bind( "vclick", function( event ) { // if this isn't a left click we don't care. Its important to note // that when the virtual event is generated it will create if ( event.which > 1 || !$.mobile.linkBindingEnabled ){ return; } var link = findClosestLink( event.target ); if ( link ) { if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) { removeActiveLinkClass( true ); $activeClickedLink = $( link ).closest( ".ui-btn" ).not( ".ui-disabled" ); $activeClickedLink.addClass( $.mobile.activeBtnClass ); $( "." + $.mobile.activePageClass + " .ui-btn" ).not( link ).blur(); // By caching the href value to data and switching the href to a #, we can avoid address bar showing in iOS. The click handler resets the href during its initial steps if this data is present $( link ) .jqmData( "href", $( link ).attr( "href" ) ) .attr( "href", "#" ); } } }); // click routing - direct to HTTP or Ajax, accordingly $( document ).bind( "click", function( event ) { if( !$.mobile.linkBindingEnabled ){ return; } var link = findClosestLink( event.target ); // If there is no link associated with the click or its not a left // click we want to ignore the click if ( !link || event.which > 1) { return; } var $link = $( link ), //remove active link class if external (then it won't be there if you come back) httpCleanup = function(){ window.setTimeout( function() { removeActiveLinkClass( true ); }, 200 ); }; // If there's data cached for the real href value, set the link's href back to it again. This pairs with an address bar workaround from the vclick handler if( $link.jqmData( "href" ) ){ $link.attr( "href", $link.jqmData( "href" ) ); } //if there's a data-rel=back attr, go back in history if( $link.is( ":jqmData(rel='back')" ) ) { window.history.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. isCrossDomainPageLoad = ( $.mobile.allowCrossDomainPages && documentUrl.protocol === "file:" && href.search( /^https?:/ ) != -1 ), //check for protocol or rel and its not an embedded page //TODO overlap in logic from isExternal, rel=external check should be // moved into more comprehensive isExternalLink isExternal = useDefaultUrlHandling || ( path.isExternal( href ) && !isCrossDomainPageLoad ); if( isExternal ) { httpCleanup(); //use default click handling return; } //use ajax var transition = $link.jqmData( "transition" ), direction = $link.jqmData( "direction" ), reverse = ( direction && direction === "reverse" ) || // deprecated - remove by 1.0 $link.jqmData( "back" ), //this may need to be more specific as we use data-rel more role = $link.attr( "data-" + $.mobile.ns + "rel" ) || undefined; $.mobile.changePage( href, { transition: transition, reverse: reverse, role: role } ); event.preventDefault(); }); //prefetch pages when anchors with data-prefetch are encountered $( 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")} ); } }); }); $.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, // 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 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 ) { // If current active page is not a dialog skip the dialog and continue // in the same direction if(!$.mobile.activePage.is( ".ui-dialog-page" )) { //determine if we're heading forward or backward and continue accordingly past //the current dialog urlHistory.directHashChange({ currentUrl: to, isBack: function() { window.history.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; $.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 ) { $.mobile._handleHashChange( location.hash ); }); //set page min-heights to be device specific $( document ).bind( "pageshow", resetActivePageHeight ); $( window ).bind( "throttledresize", resetActivePageHeight ); };//_registerInternalEvents callback })( jQuery ); //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); }); //>>excludeEnd("jqmBuildExclude");