From d56380fc22ab3108ae2096ea1973b74716db4443 Mon Sep 17 00:00:00 2001 From: scottjehl Date: Thu, 25 Nov 2010 06:13:51 -0500 Subject: [PATCH] refactor of jquery.mobile.core.js. Moved all of the page navigation model related scripting into jquery.mobile.navigation.js. This includes event handling for click, submit, and hashchange, all of the base tag management, path management, active link class handling, etc. All existing core tests are passing, and all demos work as expected. More tests are needed in core, and this refactor should make those tests easier to write. --- Makefile | 5 +- build.xml | 22 +- .../jquery.mobile.themeswitcher.js | 2 +- js/index.php | 1 + js/jquery.mobile.core.js | 544 ++---------------- js/jquery.mobile.forms.select.js | 2 +- js/jquery.mobile.listview.js | 5 +- js/jquery.mobile.navigation.js | 466 +++++++++++++++ 8 files changed, 540 insertions(+), 507 deletions(-) create mode 100644 js/jquery.mobile.navigation.js diff --git a/Makefile b/Makefile index 5cbe697b..e97775a6 100755 --- a/Makefile +++ b/Makefile @@ -14,6 +14,8 @@ FILES = js/jquery.ui.widget.js \ js/jquery.mobile.support.js \ js/jquery.mobile.event.js \ js/jquery.mobile.hashchange.js \ + js/jquery.mobile.core.js \ + js/jquery.mobile.navigation.js \ js/jquery.mobile.page.js \ js/jquery.ui.position.js \ js/jquery.mobile.fixHeaderFooter.js \ @@ -30,8 +32,7 @@ FILES = js/jquery.ui.widget.js \ js/jquery.mobile.listview.filter.js \ js/jquery.mobile.dialog.js \ js/jquery.mobile.navbar.js \ - js/jquery.mobile.grid.js \ - js/jquery.mobile.core.js + js/jquery.mobile.grid.js CSSFILES = themes/default/jquery.mobile.theme.css \ themes/default/jquery.mobile.core.css \ diff --git a/build.xml b/build.xml index 236a4bfb..bb03acff 100644 --- a/build.xml +++ b/build.xml @@ -20,11 +20,20 @@ jquery.mobile.theme.css, jquery.mobile.transitions.css"/> + jquery.mobile.listview.filter.js, + jquery.mobile.navbar.js" + /> diff --git a/experiments/themeswitcher/jquery.mobile.themeswitcher.js b/experiments/themeswitcher/jquery.mobile.themeswitcher.js index dc6d847c..34d67712 100644 --- a/experiments/themeswitcher/jquery.mobile.themeswitcher.js +++ b/experiments/themeswitcher/jquery.mobile.themeswitcher.js @@ -10,7 +10,7 @@ ''+ '
    '+ '' ) - .appendTo( $.pageContainer ), + .appendTo( $.mobile.pageContainer ), menu = menuPage.find('ul'); //menu items diff --git a/js/index.php b/js/index.php index 32ba4fed..e8239c7c 100644 --- a/js/index.php +++ b/js/index.php @@ -9,6 +9,7 @@ $elements = array( 'jquery.mobile.event.js', 'jquery.mobile.hashchange.js', 'jquery.mobile.core.js', + 'jquery.mobile.navigation.js', 'jquery.mobile.page.js', 'jquery.ui.position.js', 'jquery.mobile.fixHeaderFooter.js', diff --git a/js/jquery.mobile.core.js b/js/jquery.mobile.core.js index bbb913d1..11709e9f 100644 --- a/js/jquery.mobile.core.js +++ b/js/jquery.mobile.core.js @@ -49,23 +49,24 @@ } }); - //trigger mobileinit event - useful hook for configuring $.mobile settings before they're used - $( window.document ).trigger('mobileinit'); +//trigger mobileinit event - useful hook for configuring $.mobile settings before they're used + $( window.document ).trigger('mobileinit'); + + +//support conditions //if device support condition(s) aren't met, leave things as they are -> a basic, usable experience, //otherwise, proceed with the enhancements if ( !$.mobile.gradeA() ) { return; } - //define vars for interal use + +//define vars for interal use var $window = $(window), $html = $('html'), $head = $('head'), - //to be populated at DOM ready - $body, - //loading div which appears during Ajax requests //will not appear if $.mobile.loadingMessage is false $loader = $.mobile.loadingMessage ? @@ -73,504 +74,60 @@ ''+ '

    '+ $.mobile.loadingMessage +'

    '+ '') - : undefined, - - //define meta viewport tag, if content is defined - $metaViewport = $.mobile.metaViewportContent ? $("", { name: "viewport", content: $.mobile.metaViewportContent}).prependTo( $head ) : undefined, - - //define baseUrl for use in relative url management - baseUrl = getPathDir( location.protocol + '//' + location.host + location.pathname ), - - //define base element, for use in routing asset urls that are referenced in Ajax-requested markup - $base = $.support.dynamicBaseTag ? $("", { href: baseUrl }).prependTo( $head ) : undefined, - - //will be defined as first page element in DOM - $startPage, - - //will be defined as $startPage.parent(), which is usually the body element - //will receive ui-mobile-viewport class - $pageContainer, - - //will be defined when a link is clicked and given an active class - $activeClickedLink = null, - - //array of pages that are visited during a single page load - //length will grow as pages are visited, and shrink as "back" link/button is clicked - //each item has a url (string matches ID), and transition (saved for reuse when "back" link/button is clicked) - urlStack = [ { - url: location.hash.replace( /^#/, "" ), - transition: undefined - } ], - - //define first selector to receive focus when a page is shown - focusable = "[tabindex],a,button:visible,select:visible,input", - - //contains role for next page, if defined on clicked link via data-rel - nextPageRole = null, - - //enable/disable hashchange event listener - //toggled internally when location.hash is updated to match the url of a successful page load - hashListener = true; - - //add mobile, initial load "rendering" classes to docEl - $html.addClass('ui-mobile ui-mobile-rendering'); - - // TODO: don't expose (temporary during code reorg) - $.mobile.urlStack = urlStack; - - //consistent string escaping for urls and IDs - function idStringEscape(str){ - return str.replace(/[^a-zA-Z0-9]/g, '-'); - } - - $.mobile.idStringEscape = idStringEscape; - - // hide address bar - function silentScroll( ypos ) { - // prevent scrollstart and scrollstop events - $.event.special.scrollstart.enabled = false; - setTimeout(function() { - window.scrollTo( 0, ypos || 0 ); - },20); - setTimeout(function() { - $.event.special.scrollstart.enabled = true; - }, 150 ); - } - - function getPathDir( path ){ - var newPath = path.replace(/#/,'').split('/'); - newPath.pop(); - return newPath.join('/') + (newPath.length ? '/' : ''); - } - - function getBaseURL( nonHashPath ){ - return getPathDir( nonHashPath || location.hash ); - } - - var setBaseURL = !$.support.dynamicBaseTag ? $.noop : function( nonHashPath ){ - //set base url for new page assets - $base.attr('href', baseUrl + getBaseURL( nonHashPath )); - } - - var resetBaseURL = !$.support.dynamicBaseTag ? $.noop : function(){ - $base.attr('href', baseUrl); - } - - //set base href to pathname - resetBaseURL(); - - //for form submission - $('form').live('submit', function(event){ - if( !$.mobile.ajaxFormsEnabled ){ return; } - - var type = $(this).attr("method"), - url = $(this).attr( "action" ).replace( location.protocol + "//" + location.host, ""); - - //external submits use regular HTTP - if( /^(:?\w+:)/.test( url ) ){ - return; - } - - //if it's a relative href, prefix href with base url - if( url.indexOf('/') && url.indexOf('#') !== 0 ){ - url = getBaseURL() + url; - } - - changePage({ - url: url, - type: type, - data: $(this).serialize() - }, - undefined, - undefined, - true - ); - event.preventDefault(); - }); - - //click routing - direct to HTTP or Ajax, accordingly - $( "a" ).live( "click", function(event) { - var $this = $(this), - //get href, remove same-domain protocol and host - href = $this.attr( "href" ).replace( location.protocol + "//" + location.host, ""), - //if target attr is specified, it's external, and we mimic _blank... for now - target = $this.is( "[target]" ), - //if it still starts with a protocol, it's external, or could be :mailto, etc - external = target || /^(:?\w+:)/.test( href ) || $this.is( "[rel=external]" ), - target = $this.is( "[target]" ); - - if( href === '#' ){ - //for links created purely for interaction - ignore - return false; - } - - $activeClickedLink = $this.closest( ".ui-btn" ).addClass( $.mobile.activeBtnClass ); - - if( external || !$.mobile.ajaxLinksEnabled ){ - //remove active link class if external - removeActiveLinkClass(true); - - //deliberately redirect, in case click was triggered - if( target ){ - window.open(href); - } - else{ - location.href = href; - } - } - else { - //use ajax - var transition = $this.data( "transition" ), - back = $this.data( "back" ), - changeHashOnSuccess = !$this.is( "[data-rel="+ $.mobile.nonHistorySelectors +"]" ); - - nextPageRole = $this.attr( "data-rel" ); - - //if it's a relative href, prefix href with base url - if( href.indexOf('/') && href.indexOf('#') !== 0 ){ - href = getBaseURL() + href; - } - - href.replace(/^#/,''); - - changePage(href, transition, back, changeHashOnSuccess); - } - event.preventDefault(); - }); - - // turn on/off page loading message. - function pageLoading( done ) { - if ( done ) { - $html.removeClass( "ui-loading" ); - } else { - - if( $.mobile.loadingMessage ){ - $loader.appendTo($pageContainer).css({top: $(window).scrollTop() + 75}); - } - $html.addClass( "ui-loading" ); - } - }; - - //for directing 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.find( focusable ).eq(0).focus(); - } - } - - //function for setting role of next page - function setPageRole( newPage ) { - if ( nextPageRole ) { - newPage.attr( "data-role", nextPageRole ); - nextPageRole = undefined; - } - } - - //update hash, with or without triggering hashchange event - $.mobile.updateHash = function(url, disableListening){ - if(disableListening) { hashListener = false; } - location.hash = url; - } - - //wrap page and transfer data-attrs if it has an ID - function wrapNewPage( newPage ){ - var copyAttrs = ['data-role', 'data-theme', 'data-fullscreen'], //TODO: more page-level attrs? - wrapper = newPage.wrap( "
    " ).parent(); - - $.each(copyAttrs,function(i){ - if( newPage.attr( copyAttrs[ i ] ) ){ - wrapper.attr( copyAttrs[ i ], newPage.attr( copyAttrs[ i ] ) ); - newPage.removeAttr( copyAttrs[ i ] ); - } - }); - return wrapper; - } - - //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; - } + : undefined; - //for getting or creating a new page - function changePage( to, transition, back, changeHash){ - - //from is always the currently viewed page - var toIsArray = $.type(to) === "array", - from = toIsArray ? to[0] : $.mobile.activePage, - to = toIsArray ? to[1] : to, - url = fileUrl = $.type(to) === "string" ? to.replace( /^#/, "" ) : null, - data = undefined, - type = 'get', - isFormRequest = false, - duplicateCachedPage = null, - back = (back !== undefined) ? back : ( urlStack.length > 1 && urlStack[ urlStack.length - 2 ].url === url ), - transition = (transition !== undefined) ? transition : $.mobile.defaultTransition; +//add mobile, initial load "rendering" classes to docEl + $html.addClass('ui-mobile ui-mobile-rendering'); - if( $.type(to) === "object" && to.url ){ - url = to.url, - data = to.data, - type = to.type, - isFormRequest = true; - //make get requests bookmarkable - if( data && type == 'get' ){ - url += "?" + data; - data = undefined; - } - } - //reset base to pathname for new request - resetBaseURL(); - - // if the new href is the same as the previous one - if ( back ) { - var pop = urlStack.pop(); - if( pop ){ - transition = pop.transition; - } - } else { - urlStack.push({ url: url, transition: transition }); - } - - //function for transitioning between two existing pages - function transitionPages() { - - //kill the keyboard - $( window.document.activeElement ).blur(); - - //get current scroll distance - var currScroll = $window.scrollTop(); - - //set as data for returning to that spot - from.data('lastScroll', currScroll); - - //trigger before show/hide events - from.data("page")._trigger("beforehide", {nextPage: to}); - to.data("page")._trigger("beforeshow", {prevPage: from}); - - function loadComplete(){ - pageLoading( true ); - //trigger show/hide events, allow preventing focus change through return false - if( from.data("page")._trigger("hide", null, {nextPage: to}) !== false && to.data("page")._trigger("show", null, {prevPage: from}) !== false ){ - $.mobile.activePage = to; - } - reFocus( to ); - if( changeHash && url ){ - $.mobile.updateHash(url, true); - } - removeActiveLinkClass(); - - //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden - if( duplicateCachedPage != null ){ - duplicateCachedPage.remove(); - } - - //jump to top or prev scroll, if set - silentScroll( to.data( 'lastScroll' ) ); - } - - if(transition && (transition !== 'none')){ - $pageContainer.addClass('ui-mobile-viewport-transitioning'); - // animate in / out - from.addClass( transition + " out " + ( back ? "reverse" : "" ) ); - to.addClass( $.mobile.activePageClass + " " + transition + - " in " + ( back ? "reverse" : "" ) ); - - // callback - remove classes, etc - to.animationComplete(function() { - from.add( to ).removeClass("out in reverse " + transition ); - from.removeClass( $.mobile.activePageClass ); - loadComplete(); - $pageContainer.removeClass('ui-mobile-viewport-transitioning'); - }); - } - else{ - from.removeClass( $.mobile.activePageClass ); - to.addClass( $.mobile.activePageClass ); - loadComplete(); - } - }; - - //shared page enhancements - function enhancePage(){ - setPageRole( to ); - to.page(); - } - - //get the actual file in a jq-mobile nested url - function getFileURL( url ){ - return url.match( '&' + $.mobile.subPageUrlKey ) ? url.split( '&' + $.mobile.subPageUrlKey )[0] : url; - } - - //if url is a string - if( url ){ - to = $( "[id='" + url + "']" ), - fileUrl = getFileURL(url); - } - else{ //find base url of element, if avail - var toID = to.attr('id'), - toIDfileurl = getFileURL(toID); - - if(toID != toIDfileurl){ - fileUrl = toIDfileurl; - } - } - - // find the "to" page, either locally existing in the dom or by creating it through ajax - if ( to.length && !isFormRequest ) { - if( fileUrl ){ - setBaseURL(fileUrl); - } - enhancePage(); - transitionPages(); - } else { - - //if to exists in DOM, save a reference to it in duplicateCachedPage for removal after page change - if( to.length ){ - duplicateCachedPage = to; - } - - pageLoading(); - - $.ajax({ - url: fileUrl, - type: type, - data: data, - success: function( html ) { - setBaseURL(fileUrl); - var all = $("
    "); - //workaround to allow scripts to execute when included in page divs - all.get(0).innerHTML = html; - to = all.find('[data-role="page"]'); - - //rewrite src and href attrs to use a base url - if( !$.support.dynamicBaseTag ){ - var baseUrl = getBaseURL(fileUrl); - to.find('[src],link[href]').each(function(){ - var thisAttr = $(this).is('[href]') ? 'href' : 'src', - thisUrl = $(this).attr(thisAttr); - - //if full path exists and is same, chop it - helps IE out - thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' ); - - if( !/^(\w+:|#|\/)/.test(thisUrl) ){ - $(this).attr(thisAttr, baseUrl + thisUrl); - } - }); - } - - //preserve ID on a retrieved page - if ( to.attr('id') ) { - to = wrapNewPage( to ); - } - - to - .attr( "id", fileUrl ) - .appendTo( $pageContainer ); - - enhancePage(); - transitionPages(); - }, - error: function() { - pageLoading( true ); - removeActiveLinkClass(true); - $("

    Error Loading Page

    ") - .css({ "display": "block", "opacity": 0.96, "top": $(window).scrollTop() + 100 }) - .appendTo( $pageContainer ) - .delay( 800 ) - .fadeOut( 400, function(){ - $(this).remove(); - }); - } - }); - } - - }; +//define & prepend meta viewport tag, if content is defined + $.mobile.metaViewportContent ? $("", { name: "viewport", content: $.mobile.metaViewportContent}).prependTo( $head ) : undefined; - $(function() { - - $body = $( "body" ); - pageLoading(); - - // needs to be bound at domready (for IE6) - // find or load content, make it active - $window.bind( "hashchange", function(e, triggered) { - if( !hashListener ){ - hashListener = true; - return; - } - - if( $(".ui-page-active").is("[data-role=" + $.mobile.nonHistorySelectors + "]") ){ - return; - } - - var to = location.hash, - transition = triggered ? false : undefined; - - // either we've backed up to the root page url - // or it's the first page load with no hash present - //there's a hash and it wasn't manually triggered - // > probably a new page, "back" will be figured out by changePage - if ( to ){ - changePage( to, transition); - } - //there's no hash, the active page is not the start page, and it's not manually triggered hashchange - // > probably backed out to the first page visited - else if( $.mobile.activePage.length && $startPage[0] !== $.mobile.activePage[0] && !triggered ) { - changePage( $startPage, transition, true ); - } - else{ - $startPage.trigger("pagebeforeshow", {prevPage: $('')}); - $startPage.addClass( $.mobile.activePageClass ); - pageLoading( true ); - - if( $startPage.trigger("pageshow", {prevPage: $('')}) !== false ){ - reFocus($startPage); - } - } - - }); - }); - - //animation complete callback - //TODO - update support test and create special event for transitions - //check out transitionEnd (opera per Paul's request) - $.fn.animationComplete = function(callback){ - if($.support.cssTransitions){ - return $(this).one('webkitAnimationEnd', callback); - } - else{ - callback(); - } - }; - - //TODO - add to jQuery.mobile, not $ +//expose some core utilities $.extend($.mobile, { - pageLoading: pageLoading, - changePage: changePage, - silentScroll: silentScroll - }); - - //dom-ready + + // turn on/off page loading message. + pageLoading: function ( done ) { + if ( done ) { + $html.removeClass( "ui-loading" ); + } else { + if( $.mobile.loadingMessage ){ + $loader.appendTo($.mobile.pageContainer).css({top: $(window).scrollTop() + 75}); + } + $html.addClass( "ui-loading" ); + } + }, + + //scroll page vertically: scroll to 0 to hide iOS address bar, or pass a Y value + silentScroll: function( ypos ) { + // prevent scrollstart and scrollstop events + $.event.special.scrollstart.enabled = false; + setTimeout(function() { + window.scrollTo( 0, ypos || 0 ); + },20); + setTimeout(function() { + $.event.special.scrollstart.enabled = true; + }, 150 ); + } + }); + + +//dom-ready inits $(function(){ + + //find present pages var $pages = $("[data-role='page']"); + //set up active page - $startPage = $.mobile.activePage = $pages.first(); + $.mobile.startPage = $.mobile.activePage = $pages.first(); //set page container - $pageContainer = $startPage.parent().addClass('ui-mobile-viewport'); + $.mobile.pageContainer = $.mobile.startPage.parent().addClass('ui-mobile-viewport'); - $.extend({ - pageContainer: $pageContainer - }); + //cue page loading message + $.mobile.pageLoading(); //initialize all pages present $pages.page(); @@ -582,6 +139,9 @@ $html.removeClass('ui-mobile-rendering'); }); - $window.load(silentScroll); + +//window load event + //hide iOS browser chrome on load + $window.load( $.mobile.silentScroll ); })( jQuery, this ); diff --git a/js/jquery.mobile.forms.select.js b/js/jquery.mobile.forms.select.js index 22e8c265..cfb2106d 100644 --- a/js/jquery.mobile.forms.select.js +++ b/js/jquery.mobile.forms.select.js @@ -65,7 +65,7 @@ $.widget( "mobile.selectmenu", $.mobile.widget, { "
    "+ "
    "+ "" ) - .appendTo( $.pageContainer ) + .appendTo( $.mobile.pageContainer ) .page(), menuPageContent = menuPage.find( ".ui-content" ), diff --git a/js/jquery.mobile.listview.js b/js/jquery.mobile.listview.js index 6ed95f3c..ff2a42da 100644 --- a/js/jquery.mobile.listview.js +++ b/js/jquery.mobile.listview.js @@ -289,13 +289,14 @@ $.widget( "mobile.listview", $.mobile.widget, { parentPage = parentList.closest( ".ui-page" ), parentId = parentPage.attr( "id" ), o = this.options, + self = this, persistentFooterID = parentPage.find( "[data-role='footer']" ).data( "id" ); $( parentList.find( "ul, ol" ).toArray().reverse() ).each(function( i ) { var list = $( this ), parent = list.parent(), title = parent.contents()[ 0 ].nodeValue.split("\n")[0], - id = parentId + "&" + $.mobile.subPageUrlKey + "=" + self.idStringEscape(title + " " + i), + id = parentId + "&" + $.mobile.subPageUrlKey + "=" + self._idStringEscape(title + " " + i), theme = list.data( "theme" ) || o.theme, countTheme = list.data( "counttheme" ) || parentList.data( "counttheme" ) || o.countTheme, newPage = list.wrap( "
    " ) @@ -308,7 +309,7 @@ $.widget( "mobile.listview", $.mobile.widget, { "data-theme": theme, "data-count-theme": countTheme }) - .appendTo( $.pageContainer ); + .appendTo( $.mobile.pageContainer ); diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js new file mode 100644 index 00000000..b45ad18c --- /dev/null +++ b/js/jquery.mobile.navigation.js @@ -0,0 +1,466 @@ +/* +* jQuery Mobile Framework : core utilities for auto ajax navigation, base tag mgmt, +* Copyright (c) jQuery Project +* Dual licensed under the MIT or GPL Version 2 licenses. +* http://jquery.org/license +*/ +(function($, undefined ) { + + //define vars for interal use + var $window = $(window), + $html = $('html'), + $head = $('head'), + + //url path helpers for use in relative url management + path = { + + //get path from current hash, or from a file path + get: function( newPath ){ + if( newPath == undefined ){ + newPath = location.hash; + } + newPath = newPath.replace(/#/,'').split('/'); + newPath.pop(); + return newPath.join('/') + (newPath.length ? '/' : ''); + }, + + //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.indexOf( splitkey ) > -1 ? path.split( splitkey )[0] : path; + }, + + set: function( path, disableListening){ + if(disableListening) { hashListener = false; } + location.hash = path; + }, + + //location pathname from intial directory request + origin: null, + + setOrigin: function(){ + path.origin = path.get( location.protocol + '//' + location.host + location.pathname ); + } + }, + + //base element management, defined depending on dynamic base tag support + base = $.support.dynamicBaseTag ? { + + //define base element, for use in routing asset urls that are referenced in Ajax-requested markup + element: $("", { href: path.origin }).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.origin + path.get( href )); + }, + + //set the generated BASE element's href attribute to a new page's base path + reset: function(){ + base.element.attr('href', path.origin ); + } + + } : undefined, + + + //will be defined when a link is clicked and given an active class + $activeClickedLink = null, + + //array of pages that are visited during a single page load + //length will grow as pages are visited, and shrink as "back" link/button is clicked + //each item has a url (string matches ID), and transition (saved for reuse when "back" link/button is clicked) + urlStack = [ { + url: location.hash.replace( /^#/, "" ), + transition: undefined + } ], + + //define first selector to receive focus when a page is shown + focusable = "[tabindex],a,button:visible,select:visible,input", + + //contains role for next page, if defined on clicked link via data-rel + nextPageRole = null, + + //enable/disable hashchange event listener + //toggled internally when location.hash is updated to match the url of a successful page load + hashListener = true; + + //set location pathname from intial directory request + path.setOrigin(); + + +/* + 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.find( focusable ).eq(0).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; + }; + + + //animation complete callback + $.fn.animationComplete = function( callback ){ + if($.support.cssTransitions){ + return $(this).one('webkitAnimationEnd', callback); + } + else{ + callback(); + } + }; + + + +/* exposed $.mobile methods */ + + //update location.hash, with or without triggering hashchange event + $.mobile.updateHash = path.set; + + //url stack, useful when plugins need to be aware of previous pages viewed + $.mobile.urlStack = urlStack; + + // changepage function + $.mobile.changePage = function( to, transition, back, changeHash){ + + //from is always the currently viewed page + var toIsArray = $.type(to) === "array", + from = toIsArray ? to[0] : $.mobile.activePage, + to = toIsArray ? to[1] : to, + url = fileUrl = $.type(to) === "string" ? to.replace( /^#/, "" ) : null, + data = undefined, + type = 'get', + isFormRequest = false, + duplicateCachedPage = null, + back = (back !== undefined) ? back : ( urlStack.length > 1 && urlStack[ urlStack.length - 2 ].url === url ), + transition = (transition !== undefined) ? transition : $.mobile.defaultTransition; + + if( $.type(to) === "object" && to.url ){ + url = to.url, + data = to.data, + type = to.type, + isFormRequest = true; + //make get requests bookmarkable + if( data && type == 'get' ){ + url += "?" + data; + data = undefined; + } + } + + //reset base to pathname for new request + if(base){ base.reset(); } + + // if the new href is the same as the previous one + if ( back ) { + var pop = urlStack.pop(); + if( pop ){ + transition = pop.transition; + } + } else { + urlStack.push({ url: url, transition: transition }); + } + + //function for transitioning between two existing pages + function transitionPages() { + + //kill the keyboard + $( window.document.activeElement ).blur(); + + //get current scroll distance + var currScroll = $window.scrollTop(); + + //set as data for returning to that spot + from.data('lastScroll', currScroll); + + //trigger before show/hide events + from.data("page")._trigger("beforehide", {nextPage: to}); + to.data("page")._trigger("beforeshow", {prevPage: from}); + + function loadComplete(){ + $.mobile.pageLoading( true ); + //trigger show/hide events, allow preventing focus change through return false + if( from.data("page")._trigger("hide", null, {nextPage: to}) !== false && to.data("page")._trigger("show", null, {prevPage: from}) !== false ){ + $.mobile.activePage = to; + } + reFocus( to ); + if( changeHash !== false && url ){ + path.set(url, true); + } + removeActiveLinkClass(); + + //if there's a duplicateCachedPage, remove it from the DOM now that it's hidden + if( duplicateCachedPage != null ){ + duplicateCachedPage.remove(); + } + + //jump to top or prev scroll, if set + $.mobile.silentScroll( to.data( 'lastScroll' ) ); + }; + + if(transition && (transition !== 'none')){ + $.mobile.pageContainer.addClass('ui-mobile-viewport-transitioning'); + // animate in / out + from.addClass( transition + " out " + ( back ? "reverse" : "" ) ); + to.addClass( $.mobile.activePageClass + " " + transition + + " in " + ( back ? "reverse" : "" ) ); + + // callback - remove classes, etc + to.animationComplete(function() { + from.add( to ).removeClass("out in reverse " + transition ); + from.removeClass( $.mobile.activePageClass ); + loadComplete(); + $.mobile.pageContainer.removeClass('ui-mobile-viewport-transitioning'); + }); + } + else{ + from.removeClass( $.mobile.activePageClass ); + to.addClass( $.mobile.activePageClass ); + loadComplete(); + } + }; + + //shared page enhancements + function enhancePage(){ + + //set next page role, if defined + if ( nextPageRole ) { + to.attr( "data-role", nextPageRole ); + nextPageRole = undefined; + } + + //run page plugin + to.page(); + }; + + //if url is a string + if( url ){ + to = $( "[id='" + url + "']" ), + fileUrl = path.getFilePath(url); + } + else{ //find base url of element, if avail + var toID = to.attr('id'), + toIDfileurl = path.getFilePath(toID); + + if(toID != toIDfileurl){ + fileUrl = toIDfileurl; + } + } + + // find the "to" page, either locally existing in the dom or by creating it through ajax + if ( to.length && !isFormRequest ) { + if( fileUrl && base ){ + base.set( fileUrl ); + } + enhancePage(); + transitionPages(); + } else { + + //if to exists in DOM, save a reference to it in duplicateCachedPage for removal after page change + if( to.length ){ + duplicateCachedPage = to; + } + + $.mobile.pageLoading(); + + $.ajax({ + url: fileUrl, + type: type, + data: data, + success: function( html ) { + + if(base){ base.set(fileUrl); } + + var all = $("
    "); + //workaround to allow scripts to execute when included in page divs + all.get(0).innerHTML = html; + to = all.find('[data-role="page"]'); + + //rewrite src and href attrs to use a base url + if( !$.support.dynamicBaseTag ){ + var newPath = path.get( fileUrl ); + to.find('[src],link[href]').each(function(){ + var thisAttr = $(this).is('[href]') ? 'href' : 'src', + thisUrl = $(this).attr(thisAttr); + + //if full path exists and is same, chop it - helps IE out + thisUrl.replace( location.protocol + '//' + location.host + location.pathname, '' ); + + if( !/^(\w+:|#|\/)/.test(thisUrl) ){ + $(this).attr(thisAttr, newPath + thisUrl); + } + }); + } + + //preserve ID on a retrieved page + if ( to.attr('id') ) { + //wrap page and transfer data-attrs if it has an ID + var copyAttrs = ['data-role', 'data-theme', 'data-fullscreen'], //TODO: more page-level attrs? + wrapper = to.wrap( "
    " ).parent(); + + $.each(copyAttrs,function(i){ + if( to.attr( copyAttrs[ i ] ) ){ + wrapper.attr( copyAttrs[ i ], to.attr( copyAttrs[ i ] ) ); + to.removeAttr( copyAttrs[ i ] ); + } + }); + to = wrapper; + } + + to + .attr( "id", fileUrl ) + .appendTo( $.mobile.pageContainer ); + + enhancePage(); + transitionPages(); + }, + error: function() { + $.mobile.pageLoading( true ); + removeActiveLinkClass(true); + $("

    Error Loading Page

    ") + .css({ "display": "block", "opacity": 0.96, "top": $(window).scrollTop() + 100 }) + .appendTo( $.mobile.pageContainer ) + .delay( 800 ) + .fadeOut( 400, function(){ + $(this).remove(); + }); + } + }); + } + + }; + + + + +/* Event Bindings - hashchange, submit, and click */ + + //bind to form submit events, handle with Ajax + $('form').live('submit', function(event){ + if( !$.mobile.ajaxFormsEnabled ){ return; } + + var type = $(this).attr("method"), + url = $(this).attr( "action" ).replace( location.protocol + "//" + location.host, ""); + + //external submits use regular HTTP + if( /^(:?\w+:)/.test( url ) ){ + return; + } + + //if it's a relative href, prefix href with base url + if( url.indexOf('/') && url.indexOf('#') !== 0 ){ + url = path.get() + url; + } + + $.mobile.changePage({ + url: url, + type: type, + data: $(this).serialize() + }, + undefined, + undefined, + true + ); + event.preventDefault(); + }); + + + //click routing - direct to HTTP or Ajax, accordingly + $( "a" ).live( "click", function(event) { + if( !$.mobile.ajaxLinksEnabled ){ return; } + var $this = $(this), + //get href, remove same-domain protocol and host + href = $this.attr( "href" ).replace( location.protocol + "//" + location.host, ""), + //if target attr is specified, it's external, and we mimic _blank... for now + target = $this.is( "[target]" ), + //if it still starts with a protocol, it's external, or could be :mailto, etc + external = target || /^(:?\w+:)/.test( href ) || $this.is( "[rel=external]" ), + target = $this.is( "[target]" ); + + if( href === '#' ){ + //for links created purely for interaction - ignore + return false; + } + + $activeClickedLink = $this.closest( ".ui-btn" ).addClass( $.mobile.activeBtnClass ); + + if( external || !$.mobile.ajaxLinksEnabled ){ + //remove active link class if external + removeActiveLinkClass(true); + + //deliberately redirect, in case click was triggered + if( target ){ + window.open(href); + } + else{ + location.href = href; + } + } + else { + //use ajax + var transition = $this.data( "transition" ), + back = $this.data( "back" ), + changeHashOnSuccess = !$this.is( "[data-rel="+ $.mobile.nonHistorySelectors +"]" ); + + nextPageRole = $this.attr( "data-rel" ); + + //if it's a relative href, prefix href with base url + if( href.indexOf('/') && href.indexOf('#') !== 0 ){ + href = path.get() + href; + } + + href.replace(/^#/,''); + + $.mobile.changePage(href, transition, back, changeHashOnSuccess); + } + event.preventDefault(); + }); + + + + //hashchange event handler + $window.bind( "hashchange", function(e, triggered) { + if( !hashListener ){ + hashListener = true; + return; + } + + if( $(".ui-page-active").is("[data-role=" + $.mobile.nonHistorySelectors + "]") ){ + return; + } + + var to = location.hash, + transition = triggered ? false : undefined; + + //if to is defined, use it + if ( to ){ + $.mobile.changePage( to, transition); + } + //there's no hash, the active page is not the start page, and it's not manually triggered hashchange + //we probably backed out to the first page visited + else if( $.mobile.activePage.length && $.mobile.startPage[0] !== $.mobile.activePage[0] && !triggered ) { + $.mobile.changePage( $.mobile.startPage, transition, true ); + } + //probably the first page - show it + else{ + $.mobile.startPage.trigger("pagebeforeshow", {prevPage: $('')}); + $.mobile.startPage.addClass( $.mobile.activePageClass ); + $.mobile.pageLoading( true ); + + if( $.mobile.startPage.trigger("pageshow", {prevPage: $('')}) !== false ){ + reFocus($.mobile.startPage); + } + } + }); +})( jQuery ); \ No newline at end of file