mirror of
https://github.com/Hopiu/jquery-mobile.git
synced 2026-03-17 06:20:26 +00:00
1501 lines
52 KiB
JavaScript
Executable file
1501 lines
52 KiB
JavaScript
Executable file
/*
|
|
* core utilities for auto ajax navigation, base tag mgmt,
|
|
*/
|
|
|
|
( 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 : $( "<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
|
|
function reFocus( 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,
|
|
firstScrollElem, getScrollElem, setLastScroll, delayedSetLastScroll;
|
|
|
|
getScrollElem = function() {
|
|
var scrollElem = $window, activePage,
|
|
touchOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled;
|
|
|
|
if( touchOverflow ){
|
|
activePage = $( ".ui-page-active" );
|
|
scrollElem = activePage.is( ".ui-native-fixed" ) ? activePage.find( ".ui-content" ) : activePage;
|
|
}
|
|
|
|
return scrollElem;
|
|
};
|
|
|
|
setLastScroll = function( scrollElem ) {
|
|
// 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 = scrollElem && scrollElem.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, $(this) );
|
|
};
|
|
|
|
// 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() {
|
|
var scrollElem = getScrollElem();
|
|
|
|
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
|
|
scrollElem.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
|
|
scrollElem.bind( "scrollstop", delayedSetLastScroll );
|
|
});
|
|
});
|
|
|
|
// bind to scrollstop for the first page as "pagechange" won't be fired in that case
|
|
getScrollElem().bind( "scrollstop", delayedSetLastScroll );
|
|
|
|
// Make the iOS clock quick-scroll work again if we're using native overflow scrolling
|
|
/*
|
|
if( $.support.touchOverflow ){
|
|
if( $.mobile.touchOverflowEnabled ){
|
|
$( window ).bind( "scrollstop", function(){
|
|
if( $( this ).scrollTop() === 0 ){
|
|
$.mobile.activePage.scrollTop( 0 );
|
|
}
|
|
});
|
|
}
|
|
}
|
|
*/
|
|
|
|
//function for transitioning between two existing pages
|
|
function transitionPages( toPage, fromPage, transition, reverse ) {
|
|
|
|
//get current scroll distance
|
|
var active = $.mobile.urlHistory.getActive(),
|
|
touchOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled,
|
|
toScroll = active.lastScroll || ( touchOverflow ? 0 : $.mobile.defaultHomeScroll ),
|
|
screenHeight = getScreenHeight();
|
|
|
|
// Scroll to top, hide addr bar
|
|
window.scrollTo( 0, $.mobile.defaultHomeScroll );
|
|
|
|
if( fromPage ) {
|
|
//trigger before show/hide events
|
|
fromPage.data( "page" )._trigger( "beforehide", null, { nextPage: toPage } );
|
|
}
|
|
|
|
if( !touchOverflow){
|
|
toPage.height( screenHeight + toScroll );
|
|
}
|
|
|
|
toPage.data( "page" )._trigger( "beforeshow", null, { prevPage: fromPage || $( "" ) } );
|
|
|
|
//clear page loader
|
|
$.mobile.hidePageLoadingMsg();
|
|
|
|
if( touchOverflow && toScroll ){
|
|
|
|
toPage.addClass( "ui-mobile-pre-transition" );
|
|
// Send focus to page as it is now display: block
|
|
reFocus( toPage );
|
|
|
|
//set page's scrollTop to remembered distance
|
|
if( toPage.is( ".ui-native-fixed" ) ){
|
|
toPage.find( ".ui-content" ).scrollTop( toScroll );
|
|
}
|
|
else{
|
|
toPage.scrollTop( toScroll );
|
|
}
|
|
}
|
|
|
|
//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 || "none"] || $.mobile.defaultTransitionHandler,
|
|
promise = th( transition, reverse, toPage, fromPage );
|
|
|
|
promise.done(function() {
|
|
//reset toPage height back
|
|
if( !touchOverflow ){
|
|
toPage.height( "" );
|
|
}
|
|
|
|
// Jump to top or prev scroll, sometimes on iOS the page has not rendered yet.
|
|
if( !touchOverflow ){
|
|
$.mobile.silentScroll( toScroll );
|
|
}
|
|
|
|
//trigger show/hide events
|
|
if( fromPage ) {
|
|
if( !touchOverflow ){
|
|
fromPage.height( "" );
|
|
}
|
|
|
|
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(){
|
|
// Don't apply this height in touch overflow enabled mode
|
|
if( $.support.touchOverflow && $.mobile.touchOverflowEnabled ){
|
|
return;
|
|
}
|
|
$( "." + $.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', 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;
|
|
|
|
//default non-animation transition handler
|
|
$.mobile.noneTransitionHandler = function( name, reverse, $toPage, $fromPage ) {
|
|
if ( $fromPage ) {
|
|
$fromPage.removeClass( $.mobile.activePageClass );
|
|
}
|
|
$toPage.addClass( $.mobile.activePageClass );
|
|
|
|
return $.Deferred().resolve( name, reverse, $toPage, $fromPage ).promise();
|
|
};
|
|
|
|
//default handler for unknown transitions
|
|
$.mobile.defaultTransitionHandler = $.mobile.noneTransitionHandler;
|
|
|
|
//transition handler dictionary for 3rd party transitions
|
|
$.mobile.transitionHandlers = {
|
|
none: $.mobile.defaultTransitionHandler
|
|
};
|
|
|
|
//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 = $( "<div></div>" ),
|
|
|
|
//page title regexp
|
|
newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1,
|
|
|
|
// TODO handle dialogs again
|
|
pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ),
|
|
dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" );
|
|
|
|
|
|
// data-url must be provided for the base tag so resource requests can be directed to the
|
|
// correct url. loading into a temprorary element makes these requests immediately
|
|
if( pageElemRegex.test( html )
|
|
&& RegExp.$1
|
|
&& dataUrlRegex.test( RegExp.$1 )
|
|
&& RegExp.$1 ) {
|
|
url = fileUrl = path.getFilePath( 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 = $( "<div data-" + $.mobile.ns + "role='page'>" + html.split( /<\/?body[^>]*>/gmi )[1] + "</div>" );
|
|
}
|
|
|
|
if ( newPageTitle && !page.jqmData( "title" ) ) {
|
|
if ( ~newPageTitle.indexOf( "&" ) ) {
|
|
newPageTitle = $( "<div>" + newPageTitle + "</div>" ).text();
|
|
}
|
|
page.jqmData( "title", newPageTitle );
|
|
}
|
|
|
|
//rewrite src and href attrs to use a base url
|
|
if( !$.support.dynamicBaseTag ) {
|
|
var newPath = path.get( fileUrl );
|
|
page.find( "[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]" ).each(function() {
|
|
var thisAttr = $( this ).is( '[href]' ) ? 'href' :
|
|
$(this).is('[src]') ? 'src' : 'action',
|
|
thisUrl = $( this ).attr( thisAttr );
|
|
|
|
// XXX_jblas: We need to fix this so that it removes the document
|
|
// base URL, and then prepends with the new page URL.
|
|
//if full path exists and is same, chop it - helps IE out
|
|
thisUrl = thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' );
|
|
|
|
if( !/^(\w+:|#|\/)/.test( thisUrl ) ) {
|
|
$( this ).attr( thisAttr, newPath + thisUrl );
|
|
}
|
|
});
|
|
}
|
|
|
|
//append to page and enhance
|
|
// TODO taging a page with external to make sure that embedded pages aren't removed
|
|
// by the various page handling code is bad. Having page handling code in many
|
|
// places is bad. Solutions post 1.0
|
|
page
|
|
.attr( "data-" + $.mobile.ns + "url", path.convertUrlToDataUrl( fileUrl ) )
|
|
.attr( "data-" + $.mobile.ns + "external-page", true )
|
|
.appendTo( settings.pageContainer );
|
|
|
|
// wait for page creation to leverage options defined on widget
|
|
page.one( 'pagecreate', $.mobile._bindPageRemove );
|
|
|
|
enhancePage( page, settings.role );
|
|
|
|
// Enhancing the page may result in new dialogs/sub pages being inserted
|
|
// into the DOM. If the original absUrl refers to a sub-page, that is the
|
|
// real page we are interested in.
|
|
if ( absUrl.indexOf( "&" + $.mobile.subPageUrlKey ) > -1 ) {
|
|
page = settings.pageContainer.children( ":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
|
|
$( "<div class='ui-loader ui-overlay-shadow ui-body-e ui-corner-all'><h1>"+ $.mobile.pageLoadErrorMessage +"</h1></div>" )
|
|
.css({ "display": "block", "opacity": 0.96, "top": $window.scrollTop() + 100 })
|
|
.appendTo( settings.pageContainer )
|
|
.delay( 800 )
|
|
.fadeOut( 400, function() {
|
|
$( this ).remove();
|
|
});
|
|
}
|
|
|
|
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() {
|
|
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
|
|
reFocus( 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();
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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 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" )) {
|
|
//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 );
|