jquery-mobile/js/jquery.mobile.fixHeaderFooter.js
2011-05-18 18:32:59 -04:00

262 lines
8.7 KiB
JavaScript

/*
* jQuery Mobile Framework : "fixHeaderFooter" plugin - on-demand positioning for headers,footers
* Copyright (c) jQuery Project
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
(function($, undefined ) {
$.fn.fixHeaderFooter = function(options){
if( !$.support.scrollTop ){ return this; }
return this.each(function(){
var $this = $(this);
if( $this.jqmData('fullscreen') ){ $this.addClass('ui-page-fullscreen'); }
$this.find( ".ui-header:jqmData(position='fixed')" ).addClass('ui-header-fixed ui-fixed-inline fade'); //should be slidedown
$this.find( ".ui-footer:jqmData(position='fixed')" ).addClass('ui-footer-fixed ui-fixed-inline fade'); //should be slideup
});
};
//single controller for all showing,hiding,toggling
$.fixedToolbars = (function(){
if( !$.support.scrollTop ){ return; }
var currentstate = 'inline',
autoHideMode = false,
showDelay = 100,
delayTimer,
ignoreTargets = 'a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed',
toolbarSelector = '.ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last',
stickyFooter, //for storing quick references to duplicate footers
supportTouch = $.support.touch,
touchStartEvent = supportTouch ? "touchstart" : "mousedown",
touchStopEvent = supportTouch ? "touchend" : "mouseup",
stateBefore = null,
scrollTriggered = false,
touchToggleEnabled = true;
function showEventCallback(event)
{
// An event that affects the dimensions of the visual viewport has
// been triggered. If the header and/or footer for the current page are in overlay
// mode, we want to hide them, and then fire off a timer to show them at a later
// point. Events like a resize can be triggered continuously during a scroll, on
// some platforms, so the timer is used to delay the actual positioning until the
// flood of events have subsided.
//
// If we are in autoHideMode, we don't do anything because we know the scroll
// callbacks for the plugin will fire off a show when the scrolling has stopped.
if (!autoHideMode && currentstate == 'overlay') {
if (!delayTimer)
$.fixedToolbars.hide(true);
$.fixedToolbars.startShowTimer();
}
}
$(function() {
$(document)
.bind( "vmousedown",function(event){
if( touchToggleEnabled ) {
stateBefore = currentstate;
}
})
.bind( "vclick",function(event){
if( touchToggleEnabled ) {
if( $(event.target).closest(ignoreTargets).length ){ return; }
if( !scrollTriggered ){
$.fixedToolbars.toggle(stateBefore);
stateBefore = null;
}
}
})
.bind('scrollstart',function(event){
scrollTriggered = true;
if(stateBefore == null){ stateBefore = currentstate; }
// We only enter autoHideMode if the headers/footers are in
// an overlay state or the show timer was started. If the
// show timer is set, clear it so the headers/footers don't
// show up until after we're done scrolling.
var isOverlayState = stateBefore == 'overlay';
autoHideMode = isOverlayState || !!delayTimer;
if (autoHideMode){
$.fixedToolbars.clearShowTimer();
if (isOverlayState) {
$.fixedToolbars.hide(true);
}
}
})
.bind('scrollstop',function(event){
if( $(event.target).closest(ignoreTargets).length ){ return; }
scrollTriggered = false;
if (autoHideMode) {
autoHideMode = false;
$.fixedToolbars.startShowTimer();
}
stateBefore = null;
})
.bind('silentscroll', showEventCallback);
$(window).bind('resize', showEventCallback);
});
//before page is shown, check for duplicate footer
$('.ui-page').live('pagebeforeshow', function(event, ui){
var page = $(event.target),
footer = page.find( ":jqmData(role='footer')" ),
id = footer.data('id'),
prevPage = ui.prevPage,
prevFooter = prevPage && prevPage.find( ":jqmData(role='footer')" ),
prevFooterMatches = prevFooter.length && prevFooter.jqmData( "id" ) === id;
if( id && prevFooterMatches ){
stickyFooter = footer;
setTop( stickyFooter.removeClass( "fade in out" ).appendTo( $.mobile.pageContainer ) );
}
});
//after page is shown, append footer to new page
$('.ui-page').live('pageshow', function(event, ui){
var $this = $(this);
if( stickyFooter && stickyFooter.length ){
setTimeout(function(){
setTop( stickyFooter.appendTo( $this ).addClass("fade") );
stickyFooter = null;
}, 500);
}
$.fixedToolbars.show(true, this);
});
//When a collapsiable is hidden or shown we need to trigger the fixed toolbar to reposition itself (#1635)
$( ".ui-collapsible-contain" ).live( "collapse expand", showEventCallback );
// element.getBoundingClientRect() is broken in iOS 3.2.1 on the iPad. The
// coordinates inside of the rect it returns don't have the page scroll position
// factored out of it like the other platforms do. To get around this,
// we'll just calculate the top offset the old fashioned way until core has
// a chance to figure out how to handle this situation.
//
// TODO: We'll need to get rid of getOffsetTop() once a fix gets folded into core.
function getOffsetTop(ele)
{
var top = 0;
if (ele)
{
var op = ele.offsetParent, body = document.body;
top = ele.offsetTop;
while (ele && ele != body)
{
top += ele.scrollTop || 0;
if (ele == op)
{
top += op.offsetTop;
op = ele.offsetParent;
}
ele = ele.parentNode;
}
}
return top;
}
function setTop(el){
var fromTop = $(window).scrollTop(),
thisTop = getOffsetTop(el[0]), // el.offset().top returns the wrong value on iPad iOS 3.2.1, call our workaround instead.
thisCSStop = el.css('top') == 'auto' ? 0 : parseFloat(el.css('top')),
screenHeight = window.innerHeight,
thisHeight = el.outerHeight(),
useRelative = el.parents('.ui-page:not(.ui-page-fullscreen)').length,
relval;
if( el.is('.ui-header-fixed') ){
relval = fromTop - thisTop + thisCSStop;
if( relval < thisTop){ relval = 0; }
return el.css('top', ( useRelative ) ? relval : fromTop);
}
else{
//relval = -1 * (thisTop - (fromTop + screenHeight) + thisCSStop + thisHeight);
//if( relval > thisTop ){ relval = 0; }
relval = fromTop + screenHeight - thisHeight - (thisTop - thisCSStop);
return el.css('top', ( useRelative ) ? relval : fromTop + screenHeight - thisHeight );
}
}
//exposed methods
return {
show: function(immediately, page){
$.fixedToolbars.clearShowTimer();
currentstate = 'overlay';
var $ap = page ? $(page) : ($.mobile.activePage ? $.mobile.activePage : $(".ui-page-active"));
return $ap.children( toolbarSelector ).each(function(){
var el = $(this),
fromTop = $(window).scrollTop(),
thisTop = getOffsetTop(el[0]), // el.offset().top returns the wrong value on iPad iOS 3.2.1, call our workaround instead.
screenHeight = window.innerHeight,
thisHeight = el.outerHeight(),
alreadyVisible = (el.is('.ui-header-fixed') && fromTop <= thisTop + thisHeight) || (el.is('.ui-footer-fixed') && thisTop <= fromTop + screenHeight);
//add state class
el.addClass('ui-fixed-overlay').removeClass('ui-fixed-inline');
if( !alreadyVisible && !immediately ){
el.animationComplete(function(){
el.removeClass('in');
}).addClass('in');
}
setTop(el);
});
},
hide: function(immediately){
currentstate = 'inline';
var $ap = $.mobile.activePage ? $.mobile.activePage : $(".ui-page-active");
return $ap.children( toolbarSelector ).each(function(){
var el = $(this);
var thisCSStop = el.css('top'); thisCSStop = thisCSStop == 'auto' ? 0 : parseFloat(thisCSStop);
//add state class
el.addClass('ui-fixed-inline').removeClass('ui-fixed-overlay');
if (thisCSStop < 0 || (el.is('.ui-header-fixed') && thisCSStop != 0))
{
if(immediately){
el.css('top',0);
}
else{
if( el.css('top') !== 'auto' && parseFloat(el.css('top')) !== 0 ){
var classes = 'out reverse';
el.animationComplete(function(){
el.removeClass(classes);
el.css('top',0);
}).addClass(classes);
}
}
}
});
},
startShowTimer: function(){
$.fixedToolbars.clearShowTimer();
var args = $.makeArray(arguments);
delayTimer = setTimeout(function(){
delayTimer = undefined;
$.fixedToolbars.show.apply(null, args);
}, showDelay);
},
clearShowTimer: function() {
if (delayTimer) {
clearTimeout(delayTimer);
}
delayTimer = undefined;
},
toggle: function(from){
if(from){ currentstate = from; }
return (currentstate == 'overlay') ? $.fixedToolbars.hide() : $.fixedToolbars.show();
},
setTouchToggleEnabled: function(enabled) {
touchToggleEnabled = enabled;
}
};
})();
})(jQuery);