2 * "fixHeaderFooter" plugin - on-demand positioning for headers,footers
5 (function( $, undefined ) {
7 var slideDownClass = "ui-header-fixed ui-fixed-inline fade",
8 slideUpClass = "ui-footer-fixed ui-fixed-inline fade",
10 slideDownSelector = ".ui-header:jqmData(position='fixed')",
11 slideUpSelector = ".ui-footer:jqmData(position='fixed')";
13 $.fn.fixHeaderFooter = function( options ) {
15 if ( !$.support.scrollTop || ( $.support.touchOverflow && $.mobile.touchOverflowEnabled ) ) {
19 return this.each(function() {
20 var $this = $( this );
22 if ( $this.jqmData( "fullscreen" ) ) {
23 $this.addClass( "ui-page-fullscreen" );
26 // Should be slidedown
27 $this.find( slideDownSelector ).addClass( slideDownClass );
30 $this.find( slideUpSelector ).addClass( slideUpClass );
34 // single controller for all showing,hiding,toggling
35 $.mobile.fixedToolbars = (function() {
37 if ( !$.support.scrollTop || ( $.support.touchOverflow && $.mobile.touchOverflowEnabled ) ) {
41 var stickyFooter, delayTimer,
42 currentstate = "inline",
45 ignoreTargets = "a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed",
46 toolbarSelector = ".ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last",
47 // for storing quick references to duplicate footers
48 supportTouch = $.support.touch,
49 touchStartEvent = supportTouch ? "touchstart" : "mousedown",
50 touchStopEvent = supportTouch ? "touchend" : "mouseup",
52 scrollTriggered = false,
53 touchToggleEnabled = true;
55 function showEventCallback( event ) {
56 // An event that affects the dimensions of the visual viewport has
57 // been triggered. If the header and/or footer for the current page are in overlay
58 // mode, we want to hide them, and then fire off a timer to show them at a later
59 // point. Events like a resize can be triggered continuously during a scroll, on
60 // some platforms, so the timer is used to delay the actual positioning until the
61 // flood of events have subsided.
63 // If we are in autoHideMode, we don't do anything because we know the scroll
64 // callbacks for the plugin will fire off a show when the scrolling has stopped.
65 if ( !autoHideMode && currentstate === "overlay" ) {
67 $.mobile.fixedToolbars.hide( true );
70 $.mobile.fixedToolbars.startShowTimer();
75 var $document = $( document ),
76 $window = $( window );
79 .bind( "vmousedown", function( event ) {
80 if ( touchToggleEnabled ) {
81 stateBefore = currentstate;
84 .bind( "vclick", function( event ) {
85 if ( touchToggleEnabled ) {
87 if ( $(event.target).closest( ignoreTargets ).length ) {
91 if ( !scrollTriggered ) {
92 $.mobile.fixedToolbars.toggle( stateBefore );
97 .bind( "silentscroll", showEventCallback );
100 // The below checks first for a $(document).scrollTop() value, and if zero, binds scroll events to $(window) instead.
101 // If the scrollTop value is actually zero, both will return zero anyway.
103 // Works with $(document), not $(window) : Opera Mobile (WinMO phone; kinda broken anyway)
104 // Works with $(window), not $(document) : IE 7/8
105 // Works with either $(window) or $(document) : Chrome, FF 3.6/4, Android 1.6/2.1, iOS
106 // Needs work either way : BB5, Opera Mobile (iOS)
108 ( ( $document.scrollTop() === 0 ) ? $window : $document )
109 .bind( "scrollstart", function( event ) {
111 scrollTriggered = true;
113 if ( stateBefore === null ) {
114 stateBefore = currentstate;
117 // We only enter autoHideMode if the headers/footers are in
118 // an overlay state or the show timer was started. If the
119 // show timer is set, clear it so the headers/footers don't
120 // show up until after we're done scrolling.
121 var isOverlayState = stateBefore == "overlay";
123 autoHideMode = isOverlayState || !!delayTimer;
125 if ( autoHideMode ) {
126 $.mobile.fixedToolbars.clearShowTimer();
128 if ( isOverlayState ) {
129 $.mobile.fixedToolbars.hide( true );
133 .bind( "scrollstop", function( event ) {
135 if ( $( event.target ).closest( ignoreTargets ).length ) {
139 scrollTriggered = false;
141 if ( autoHideMode ) {
142 $.mobile.fixedToolbars.startShowTimer();
143 autoHideMode = false;
148 $window.bind( "resize updatelayout", showEventCallback );
151 // 1. Before page is shown, check for duplicate footer
152 // 2. After page is shown, append footer to new page
154 .live( "pagebeforeshow", function( event, ui ) {
156 var page = $( event.target ),
157 footer = page.find( ":jqmData(role='footer')" ),
158 id = footer.data( "id" ),
159 prevPage = ui.prevPage,
160 prevFooter = prevPage && prevPage.find( ":jqmData(role='footer')" ),
161 prevFooterMatches = prevFooter.length && prevFooter.jqmData( "id" ) === id;
163 if ( id && prevFooterMatches ) {
164 stickyFooter = footer;
165 setTop( stickyFooter.removeClass( "fade in out" ).appendTo( $.mobile.pageContainer ) );
168 .live( "pageshow", function( event, ui ) {
170 var $this = $( this );
172 if ( stickyFooter && stickyFooter.length ) {
174 setTimeout(function() {
175 setTop( stickyFooter.appendTo( $this ).addClass( "fade" ) );
180 $.mobile.fixedToolbars.show( true, this );
183 // When a collapsiable is hidden or shown we need to trigger the fixed toolbar to reposition itself (#1635)
184 $( ".ui-collapsible-contain" ).live( "collapse expand", showEventCallback );
186 // element.getBoundingClientRect() is broken in iOS 3.2.1 on the iPad. The
187 // coordinates inside of the rect it returns don't have the page scroll position
188 // factored out of it like the other platforms do. To get around this,
189 // we'll just calculate the top offset the old fashioned way until core has
190 // a chance to figure out how to handle this situation.
192 // TODO: We'll need to get rid of getOffsetTop() once a fix gets folded into core.
194 function getOffsetTop( ele ) {
199 body = document.body;
200 op = ele.offsetParent;
203 while ( ele && ele != body ) {
204 top += ele.scrollTop || 0;
208 op = ele.offsetParent;
211 ele = ele.parentNode;
217 function setTop( el ) {
218 var fromTop = $(window).scrollTop(),
219 thisTop = getOffsetTop( el[ 0 ] ), // el.offset().top returns the wrong value on iPad iOS 3.2.1, call our workaround instead.
220 thisCSStop = el.css( "top" ) == "auto" ? 0 : parseFloat(el.css( "top" )),
221 screenHeight = window.innerHeight,
222 thisHeight = el.outerHeight(),
223 useRelative = el.parents( ".ui-page:not(.ui-page-fullscreen)" ).length,
226 if ( el.is( ".ui-header-fixed" ) ) {
228 relval = fromTop - thisTop + thisCSStop;
230 if ( relval < thisTop ) {
234 return el.css( "top", useRelative ? relval : fromTop );
236 // relval = -1 * (thisTop - (fromTop + screenHeight) + thisCSStop + thisHeight);
237 // if ( relval > thisTop ) { relval = 0; }
238 relval = fromTop + screenHeight - thisHeight - (thisTop - thisCSStop );
240 return el.css( "top", useRelative ? relval : fromTop + screenHeight - thisHeight );
247 show: function( immediately, page ) {
249 $.mobile.fixedToolbars.clearShowTimer();
251 currentstate = "overlay";
253 var $ap = page ? $( page ) :
254 ( $.mobile.activePage ? $.mobile.activePage :
255 $( ".ui-page-active" ) );
257 return $ap.children( toolbarSelector ).each(function() {
260 fromTop = $( window ).scrollTop(),
261 // el.offset().top returns the wrong value on iPad iOS 3.2.1, call our workaround instead.
262 thisTop = getOffsetTop( el[ 0 ] ),
263 screenHeight = window.innerHeight,
264 thisHeight = el.outerHeight(),
265 alreadyVisible = ( el.is( ".ui-header-fixed" ) && fromTop <= thisTop + thisHeight ) ||
266 ( el.is( ".ui-footer-fixed" ) && thisTop <= fromTop + screenHeight );
269 el.addClass( "ui-fixed-overlay" ).removeClass( "ui-fixed-inline" );
271 if ( !alreadyVisible && !immediately ) {
272 el.animationComplete(function() {
273 el.removeClass( "in" );
280 hide: function( immediately ) {
282 currentstate = "inline";
284 var $ap = $.mobile.activePage ? $.mobile.activePage :
285 $( ".ui-page-active" );
287 return $ap.children( toolbarSelector ).each(function() {
290 thisCSStop = el.css( "top" ),
293 thisCSStop = thisCSStop == "auto" ? 0 :
294 parseFloat(thisCSStop);
297 el.addClass( "ui-fixed-inline" ).removeClass( "ui-fixed-overlay" );
299 if ( thisCSStop < 0 || ( el.is( ".ui-header-fixed" ) && thisCSStop !== 0 ) ) {
305 if ( el.css( "top" ) !== "auto" && parseFloat( el.css( "top" ) ) !== 0 ) {
307 classes = "out reverse";
309 el.animationComplete(function() {
310 el.removeClass( classes ).css( "top", 0 );
311 }).addClass( classes );
318 startShowTimer: function() {
320 $.mobile.fixedToolbars.clearShowTimer();
322 var args = [].slice.call( arguments );
324 delayTimer = setTimeout(function() {
325 delayTimer = undefined;
326 $.mobile.fixedToolbars.show.apply( null, args );
330 clearShowTimer: function() {
332 clearTimeout( delayTimer );
334 delayTimer = undefined;
337 toggle: function( from ) {
341 return ( currentstate === "overlay" ) ? $.mobile.fixedToolbars.hide() :
342 $.mobile.fixedToolbars.show();
345 setTouchToggleEnabled: function( enabled ) {
346 touchToggleEnabled = enabled;
351 //auto self-init widgets
352 $( document ).bind( "pagecreate create", function( event ) {
354 if ( $( ":jqmData(position='fixed')", event.target ).length ) {
356 $( event.target ).each(function() {
358 if ( !$.support.scrollTop || ( $.support.touchOverflow && $.mobile.touchOverflowEnabled ) ) {
362 var $this = $( this );
364 if ( $this.jqmData( "fullscreen" ) ) {
365 $this.addClass( "ui-page-fullscreen" );
368 // Should be slidedown
369 $this.find( slideDownSelector ).addClass( slideDownClass );
372 $this.find( slideUpSelector ).addClass( slideUpClass );