--- a/js/jquery.mobile-1.0rc1.js +++ b/js/jquery.mobile-1.0rc1.js @@ -1,5 +1,5 @@ /*! - * jQuery Mobile v1.0b3 + * jQuery Mobile v1.0rc1 * http://jquerymobile.com/ * * Copyright 2010, jQuery Project @@ -278,6 +278,16 @@ (function( $, undefined ) { $.widget( "mobile.widget", { + // decorate the parent _createWidget to trigger `widgetinit` for users + // who wish to do post post `widgetcreate` alterations/additions + // + // TODO create a pull request for jquery ui to trigger this event + // in the original _createWidget + _createWidget: function() { + $.Widget.prototype._createWidget.apply( this, arguments ); + this._trigger( 'init' ); + }, + _getCreateOptions: function() { var elem = this.element, @@ -301,7 +311,7 @@ })( jQuery ); /* -* jQuery Mobile Framework : resolution and CSS media query related helpers and behavior +* jQuery Mobile Framework : a workaround for window.matchMedia * Copyright (c) jQuery Project * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license @@ -421,9 +431,7 @@ touchOverflow: !!propExists( "overflowScrolling" ), boxShadow: !!propExists( "boxShadow" ) && !bb, scrollTop: ( "pageXOffset" in window || "scrollTop" in document.documentElement || "scrollTop" in fakeBody[ 0 ] ) && !webos, - dynamicBaseTag: baseTagTest(), - // TODO: This is a weak test. We may want to beef this up later. - eventCapture: "addEventListener" in document + dynamicBaseTag: baseTagTest() }); fakeBody.remove(); @@ -502,7 +510,7 @@ clickBlockList = [], blockMouseTriggers = false, blockTouchTriggers = false, - eventCaptureSupported = $.support.eventCapture, + eventCaptureSupported = "addEventListener" in document, $document = $( document ), nextTouchID = 1, lastTouchID = 0; @@ -540,6 +548,12 @@ prop = props[ --i ]; event[ prop ] = oe[ prop ]; } + } + + // make sure that if the mouse and click virtual events are generated + // without a .which one is defined + if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ){ + event.which = 1; } if ( t.search(/^touch/) !== -1 ) { @@ -1809,11 +1823,19 @@ // Mobile version of data and removeData and hasData methods // ensures all data is set and retrieved using jQuery Mobile's data namespace $.fn.jqmData = function( prop, value ) { - return this.data( prop ? $.mobile.nsNormalize( prop ) : prop, value ); + var result; + if ( typeof prop != "undefined" ) { + result = this.data( prop ? $.mobile.nsNormalize( prop ) : prop, value ); + } + return result; }; $.jqmData = function( elem, prop, value ) { - return $.data( elem, $.mobile.nsNormalize( prop ), value ); + var result; + if ( typeof prop != "undefined" ) { + result = $.data( elem, prop ? $.mobile.nsNormalize( prop ) : prop, value ); + } + return result; }; $.fn.jqmRemoveData = function( prop ) { @@ -1824,8 +1846,32 @@ return $.removeData( elem, $.mobile.nsNormalize( prop ) ); }; - $.jqmHasData = function( elem, prop ) { - return $.hasData( elem, $.mobile.nsNormalize( prop ) ); + $.fn.removeWithDependents = function() { + $.removeWithDependents( this ); + }; + + $.removeWithDependents = function( elem ) { + var $elem = $( elem ); + + ( $elem.jqmData('dependents') || $() ).remove(); + $elem.remove(); + }; + + $.fn.addDependents = function( newDependents ) { + $.addDependents( $(this), newDependents ); + }; + + $.addDependents = function( elem, newDependents ) { + var dependents = $(elem).jqmData( 'dependents' ) || $(); + + $(elem).jqmData( 'dependents', $.merge(dependents, newDependents) ); + }; + + // note that this helper doesn't attempt to handle the callback + // or setting of an html elements text, its only purpose is + // to return the html encoded version of the text in all cases. (thus the name) + $.fn.getEncodedText = function() { + return $( "<div/>" ).text( $(this).text() ).html(); }; // Monkey-patching Sizzle to filter the :jqmData selector @@ -1875,20 +1921,21 @@ // [2]: http://jblas:password@mycompany.com:8080/mail/inbox // [3]: http://jblas:password@mycompany.com:8080 // [4]: http: - // [5]: jblas:password@mycompany.com:8080 - // [6]: jblas:password - // [7]: jblas - // [8]: password - // [9]: mycompany.com:8080 - // [10]: mycompany.com - // [11]: 8080 - // [12]: /mail/inbox - // [13]: /mail/ - // [14]: inbox - // [15]: ?msg=1234&type=unread - // [16]: #msg-content + // [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]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, + urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, //Parse a URL into a structure that allows easy access to //all of the URL components by name. @@ -1899,34 +1946,31 @@ return url; } - var u = url || "", - matches = path.urlParseRE.exec( url ), - results; - if ( matches ) { + 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. - results = { - href: matches[0] || "", - hrefNoHash: matches[1] || "", - hrefNoSearch: matches[2] || "", - domain: matches[3] || "", - protocol: matches[4] || "", - authority: matches[5] || "", - username: matches[7] || "", - password: matches[8] || "", - host: matches[9] || "", - hostname: matches[10] || "", - port: matches[11] || "", - pathname: matches[12] || "", - directory: matches[13] || "", - filename: matches[14] || "", - search: matches[15] || "", - hash: matches[16] || "" + 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 ] || "" }; - } - return results || {}; }, //Turn relPath into an asbolute path. absPath is @@ -1986,13 +2030,14 @@ 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 + "//" + authority + pathname + search + hash; + return protocol + doubleSlash + authority + pathname + search + hash; }, //Add search (aka query) params to the specified url. @@ -2248,41 +2293,83 @@ $.mobile.changePage.apply( null, pageTransitionQueue.pop() ); } } - + // Save the last scroll distance per page, before it is hidden - var getLastScroll = (function( lastScrollEnabled ){ - return function(){ - if( !lastScrollEnabled ){ - lastScrollEnabled = true; - return; - } - - lastScrollEnabled = false; - - var active = $.mobile.urlHistory.getActive(), - activePage = $( ".ui-page-active" ), - scrollElem = $( window ), - touchOverflow = $.support.touchOverflow && $.mobile.touchOverflowEnabled; - - if( touchOverflow ){ - scrollElem = activePage.is( ".ui-native-fixed" ) ? activePage.find( ".ui-content" ) : activePage; - } - - if( active ){ - var lastScroll = scrollElem.scrollTop(); - - // Set active page's lastScroll prop. - // If the Y location we're scrolling to is less than minScrollBack, let it go. - active.lastScroll = lastScroll < $.mobile.minScrollBack ? $.mobile.defaultHomeScroll : lastScroll; - } - }; - })( true ); - - // to get last scroll, we need to get scrolltop before the page change - // using beforechangepage or popstate/hashchange (whichever comes first) - $( document ).bind( "beforechangepage", getLastScroll ); - $( window ).bind( $.support.pushState ? "popstate" : "hashchange", getLastScroll ); - + 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 ){ @@ -2304,7 +2391,7 @@ 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 ); @@ -2312,22 +2399,22 @@ //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 ); @@ -2350,7 +2437,7 @@ // Send focus to the newly shown page reFocus( toPage ); } - + // Jump to top or prev scroll, sometimes on iOS the page has not rendered yet. if( !touchOverflow ){ $.mobile.silentScroll( toScroll ); @@ -2361,7 +2448,7 @@ if( !touchOverflow ){ fromPage.height( "" ); } - + fromPage.data( "page" )._trigger( "hide", null, { nextPage: toPage } ); } @@ -2383,11 +2470,15 @@ 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() ); } @@ -2417,19 +2508,11 @@ } }; - //update location.hash, with or without triggering hashchange event - //TODO - deprecate this one at 1.0 - $.mobile.updateHash = path.set; - //expose path object on $.mobile $.mobile.path = path; //expose base object on $.mobile $.mobile.base = base; - - //url stack, useful when plugins need to be aware of previous pages viewed - //TODO: deprecate this one at 1.0 - $.mobile.urlstack = urlHistory.stack; //history stack $.mobile.urlHistory = urlHistory; @@ -2465,6 +2548,26 @@ //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. @@ -2502,6 +2605,11 @@ 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. @@ -2520,6 +2628,15 @@ // 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 && !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. @@ -2543,6 +2660,18 @@ 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 ) { @@ -2581,7 +2710,7 @@ newPageTitle = html.match( /<title[^>]*>([^<]*)/ ) && RegExp.$1, // TODO handle dialogs again - pageElemRegex = new RegExp( ".*(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>).*" ), + pageElemRegex = new RegExp( "(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)" ), dataUrlRegex = new RegExp( "\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?" ); @@ -2593,9 +2722,6 @@ && RegExp.$1 ) { url = fileUrl = path.getFilePath( RegExp.$1 ); } - else{ - - } if ( base ) { base.set( fileUrl ); @@ -2634,20 +2760,16 @@ } //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', function(){ - - // when dom caching is not enabled bind to remove the page on hide - if( !page.data("page").options.domCache ){ - page.bind( "pagehide.remove", function(){ - $(this).remove(); - }); - } - }); + page.one( 'pagecreate', $.mobile._bindPageRemove ); enhancePage( page, settings.role ); @@ -2665,12 +2787,31 @@ hideMsg(); } + // Add the page reference to our triggerData. + triggerData.page = page; + + // Let listeners know the page loaded successfully. + settings.pageContainer.trigger( "pageload", triggerData ); + deferred.resolve( absUrl, options, page, dupCachedPage ); }, error: function() { //set base back to current path if( base ) { base.set( path.get() ); + } + + 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. @@ -2709,43 +2850,6 @@ // Show a specific page in the page container. $.mobile.changePage = function( toPage, options ) { - // XXX: REMOVE_BEFORE_SHIPPING_1.0 - // This is temporary code that makes changePage() compatible with previous alpha versions. - if ( typeof options !== "object" ) { - var opts = null; - - // Map old-style call signature for form submit to the new options object format. - if ( typeof toPage === "object" && toPage.url && toPage.type ) { - opts = { - type: toPage.type, - data: toPage.data, - forcePageLoad: true - }; - toPage = toPage.url; - } - - // The arguments passed into the function need to be re-mapped - // to the new options object format. - var len = arguments.length; - if ( len > 1 ) { - var argNames = [ "transition", "reverse", "changeHash", "fromHashChange" ], i; - for ( i = 1; i < len; i++ ) { - var a = arguments[ i ]; - if ( typeof a !== "undefined" ) { - opts = opts || {}; - opts[ argNames[ i - 1 ] ] = a; - } - } - } - - // If an options object was created, then we know changePage() was called - // with an old signature. - if ( opts ) { - return $.mobile.changePage( toPage, opts ); - } - } - // XXX: REMOVE_BEFORE_SHIPPING_1.0 - // 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. @@ -2769,8 +2873,6 @@ // Let listeners know we're about to change the current page. mpc.trigger( pbcEvent, triggerData ); - mpc.trigger( "beforechangepage", triggerData ); // XXX: DEPRECATED for 1.0 - // If the default behavior is prevented, stop here! if( pbcEvent.isDefaultPrevented() ){ return; @@ -2799,7 +2901,6 @@ $.mobile.changePage( newPage, options ); }) .fail(function( url, options ) { - // XXX_jblas: Fire off changepagefailed notificaiton. isPageTransitioning = false; //clear out the active button state @@ -2808,7 +2909,6 @@ //release transition lock so navigation is free again releasePageTransitionLock(); settings.pageContainer.trigger( "pagechangefailed", triggerData ); - settings.pageContainer.trigger( "changepagefailed", triggerData ); // XXX: DEPRECATED for 1.0 }); return; } @@ -2826,16 +2926,19 @@ pageTitle = document.title, isDialog = settings.role === "dialog" || toPage.jqmData( "role" ) === "dialog"; - // If we are trying to transition to the same page that we are currently on ignore the request. - // an illegal same page request is defined by the current page being the same as the url, as long as there's history - // and toPage is not an array or object (those are allowed to be "same") - // - // XXX_jblas: We need to remove this at some point when we allow for transitions - // to the same page. - if( fromPage && fromPage[0] === toPage[0] ) { + // 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 ); - mpc.trigger( "changepage", triggerData ); // XXX: DEPRECATED for 1.0 return; } @@ -2923,8 +3026,6 @@ // Let listeners know we're all done changing the current page. mpc.trigger( "pagechange", triggerData ); - - mpc.trigger( "changepage", triggerData ); // XXX: DEPRECATED for 1.0 }); }; @@ -2938,7 +3039,8 @@ pageContainer: undefined, showLoadMsg: true, //loading message shows by default when pages are being fetched during changePage dataUrl: undefined, - fromPage: undefined + fromPage: undefined, + allowSamePageTransition: false }; /* Event Bindings - hashchange, submit, and click */ @@ -3023,6 +3125,12 @@ //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 ){ + return; + } + var link = findClosestLink( event.target ); if ( link ) { if ( path.parseUrl( link.getAttribute( "href" ) || "#" ).hash !== "#" ) { @@ -3037,7 +3145,10 @@ // click routing - direct to HTTP or Ajax, accordingly $( document ).bind( "click", function( event ) { var link = findClosestLink( event.target ); - if ( !link ) { + + // 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; } @@ -3053,17 +3164,17 @@ 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 ){ + if( !$.mobile.ajaxEnabled && !path.isEmbeddedPage( href ) ){ httpCleanup(); //use default click handling return; } - - var baseUrl = getClosestBaseUrl( $link ), - - //get href, if defined, otherwise default to empty hash - href = path.makeUrlAbsolute( $link.attr( "href" ) || "#", baseUrl ); // XXX_jblas: Ideally links to application pages should be specified as // an url to the application document with a hash that is either @@ -3172,7 +3283,7 @@ isForward: function() { window.history.forward(); } }); - // prevent changepage + // prevent changePage() return; } else { // if the current active page is a dialog and we're navigating @@ -3201,7 +3312,12 @@ //if to is defined, load it if ( to ) { - to = ( typeof to === "string" && !path.isPath( to ) ) ? ( '#' + to ) : 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 @@ -3271,28 +3387,47 @@ return url; }, + // TODO sort out a single barrier to hashchange functionality + nextHashChangePrevented: function( value ) { + $.mobile.urlHistory.ignoreNextHashChange = value; + self.onHashChangeDisabled = value; + }, + // on hash change we want to clean up the url // NOTE this takes place *after* the vanilla navigation hash change // handling has taken place and set the state of the DOM onHashChange: function( e ) { - var href, state; - - self.hashchangeFired = true; - - // only replaceState when the hash doesn't represent an embeded page - if( $.mobile.path.isPath(location.hash) ) { - - // propulate the hash when its not available - state = self.state(); - - // make the hash abolute with the current href - href = $.mobile.path.makeUrlAbsolute( state.hash.replace("#", ""), location.href ); - + // disable this hash change + if( self.onHashChangeDisabled ){ + return; + } + + var href, state, + hash = location.hash, + isPath = $.mobile.path.isPath( hash ); + hash = isPath ? hash.replace( "#", "" ) : hash; + + // propulate the hash when its not available + state = self.state(); + + // make the hash abolute with the current href + href = $.mobile.path.makeUrlAbsolute( hash, location.href ); + + if ( isPath ) { href = self.resetUIKeys( href ); - - // replace the current url with the new href and store the state - history.replaceState( state, document.title, href ); - } + } + + // replace the current url with the new href and store the state + // Note that in some cases we might be replacing an url with the + // same url. We do this anyways because we need to make sure that + // all of our history entries have a state object associated with + // them. This allows us to work around the case where window.history.back() + // is called to transition from an external page to an embedded page. + // In that particular case, a hashchange event is *NOT* generated by the browser. + // Ensuring each history entry has a state object means that onPopState() + // will always trigger our hashchange callback even when a hashchange event + // is not fired. + history.replaceState( state, document.title, href ); }, // on popstate (ie back or forward) we need to replace the hash that was there previously @@ -3304,14 +3439,14 @@ // or forward popstate if( poppedState ) { // disable any hashchange triggered by the browser - $.mobile.urlHistory.ignoreNextHashChange = true; + self.nextHashChangePrevented( true ); // defer our manual hashchange until after the browser fired // version has come and gone setTimeout(function() { // make sure that the manual hash handling takes place - $.mobile.urlHistory.ignoreNextHashChange = false; - + self.nextHashChangePrevented( false ); + // change the page based on the hash $.mobile._handleHashChange( poppedState.hash ); }, 100); @@ -3356,7 +3491,7 @@ $to.add( $from ).removeClass( "out in reverse " + name ); - if ( $from ) { + if ( $from && $from[ 0 ] !== $to[ 0 ] ) { $from.removeClass( $.mobile.activePageClass ); } @@ -3427,10 +3562,13 @@ optType = o.degradeInputs[ type ] || "text"; if ( o.degradeInputs[ type ] ) { - $this.replaceWith( - $( "<div>" ).html( $this.clone() ).html() - .replace( /\s+type=["']?\w+['"]?/, " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\" " ) - ); + var html = $( "<div>" ).html( $this.clone() ).html(), + // In IE browsers, the type sometimes doesn't exist in the cloned markup, so we replace the closing tag instead + hasType = html.indexOf( " type=" ) > -1, + findstr = hasType ? /\s+type=["']?\w+['"]?/ : /\/?>/, + repstr = " type=\"" + optType + "\" data-" + $.mobile.ns + "type=\"" + type + "\"" + ( hasType ? "" : ">" ); + + $this.replaceWith( html.replace( findstr, repstr ) ); } }); @@ -3451,26 +3589,35 @@ initSelector : ":jqmData(role='dialog')" }, _create: function() { - var $el = this.element, - pageTheme = $el.attr( "class" ).match( /ui-body-[a-z]/ ); - + var self = this, + $el = this.element, + pageTheme = $el.attr( "class" ).match( /ui-body-[a-z]/ ), + headerCloseButton = $( "<a href='#' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" ); + if( pageTheme.length ){ $el.removeClass( pageTheme[ 0 ] ); - } - + } + $el.addClass( "ui-body-" + this.options.theme ); - + // Class the markup for dialog styling // Set aria role $el.attr( "role", "dialog" ) .addClass( "ui-dialog" ) .find( ":jqmData(role='header')" ) .addClass( "ui-corner-top ui-overlay-shadow" ) - .prepend( "<a href='#' data-" + $.mobile.ns + "icon='delete' data-" + $.mobile.ns + "rel='back' data-" + $.mobile.ns + "iconpos='notext'>"+ this.options.closeBtnText + "</a>" ) + .prepend( headerCloseButton ) .end() .find( ":jqmData(role='content'),:jqmData(role='footer')" ) .last() .addClass( "ui-corner-bottom ui-overlay-shadow" ); + + // this must be an anonymous function so that select menu dialogs can replace + // the close method. This is a change from previously just defining data-rel=back + // on the button and letting nav handle it + headerCloseButton.bind( "vclick", function() { + self.close(); + }); /* bind events - clicks and submits should use the closing transition that the dialog opened with @@ -3613,6 +3760,7 @@ collapsed: true, heading: ">:header,>legend", theme: null, + contentTheme: null, iconTheme: "d", initSelector: ":jqmData(role='collapsible')" }, @@ -3620,16 +3768,31 @@ var $el = this.element, o = this.options, - collapsibleContain = $el.addClass( "ui-collapsible-contain" ), + collapsible = $el.addClass( "ui-collapsible" ), collapsibleHeading = $el.find( o.heading ).eq( 0 ), - collapsibleContent = collapsibleContain.wrapInner( "<div class='ui-collapsible-content'></div>" ).find( ".ui-collapsible-content" ), - collapsibleParent = $el.closest( ":jqmData(role='collapsible-set')" ).addClass( "ui-collapsible-set" ); + collapsibleContent = collapsible.wrapInner( "<div class='u