jquery-mobile/js/jQuery.mobile.js
2010-09-10 18:23:13 -04:00

469 lines
No EOL
16 KiB
JavaScript

/*
* jQuery Mobile Framework
* Copyright (c) jQuery Project
* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
* Note: Code is in draft form and is subject to change!!
- This file adds properties to $.support,
*/
(function($,window,undefined){
var $window = $(window),
$html = $('html'),
$head = $('head'),
$body,
$loader = $('<div class="ui-loader ui-body-c ui-corner-all fade in"><span class="ui-icon ui-icon-loading spin"></span><h1>loading.</h1></div>'),
startPage,
startPageId = 'ui-page-start',
activePageClass = 'ui-page-active',
transitions = 'slide slideup pop flip fade dissolve swap',
transitionSpecified = false,
currentTransition = 'slide',
transitionDuration = 350,
orientation,
backBtnText = "Back",
prevUrl = location.hash,
//vars for custom event tracking
scrolling = false,
touching = false,
touchstartdata,
touchstopdata,
tapNotMoveTime = 50,
tapHoldTime = 700,
maxSwipeTime = 1000,
minSwipeXDistance = 180,
maxSwipeYtolerance = 80;
/*
add some properties to $.support
- Notes:
- add $.support.scrollTop ?
- CSS matrix support needed?
*/
$.support.orientation = !!window.orientation;
//ajax support: to use bbq-style navigation with external pages, we will need to first test for ajax support (and fall back to normal urls)
//note: maybe core should be updated with this support property?
$.support.ajax = (function(){
//factory test borrowed from quirksmode.org
var xmlhttp = false, index = -1, factory,
XMLHttpFactories = [
function() { return new XMLHttpRequest() },
function() { return new ActiveXObject("Msxml2.XMLHTTP") },
function() { return new ActiveXObject("Msxml3.XMLHTTP") },
function() { return new ActiveXObject("Microsoft.XMLHTTP") }
];
while ((factory = XMLHttpFactories[++index])) {
try { xmlhttp = factory(); }
catch (e) { continue; }
break;
}
return !!xmlhttp;
})();
/* Some CSS capability tests from EnhanceJS -- in the vein of $.support.boxmodel -- almost certainly needed for widgets to work
* note: assuming they work in Firefox 4, these can run without domready (currently FF4 beta won't report these offset dimensions before body is present)
*/
//test CSS display none
$.support.display = (function(){
var fakeBody = $('<body></body>').prependTo($html),
testDiv = $('<div style="height: 5px; position: absolute; display: none;"></div>').prependTo(fakeBody),
divHeight = testDiv[0].offsetHeight; //note: jQuery .height() returned "5"
fakeBody.remove();
return divHeight === 0;
})();
//test CSS absolute positioning
$.support.position = (function(){
var fakeBody = $('<body></body>').prependTo($html),
testDiv = $('<div style="position: absolute; left: 10px;"></div>').prependTo(fakeBody),
divLeft = testDiv[0].offsetLeft;
fakeBody.remove();
return divLeft === 10;
})();
//test CSS overflow (used in widgets for clearfix, hiding, etc)
$.support.overflow = (function(){
var fakeBody = $('<body></body>').prependTo($html),
testDiv = $('<div style="position: absolute; overflow: hidden; height: 0;"><div style="height: 10px;"></div></div>').prependTo(fakeBody),
divHeight = testDiv[0].offsetHeight;
fakeBody.remove();
return divHeight === 0;
})();
//test CSS float,clear
$.support.floatclear = (function(){
var fakeBody = $('<body></body>').prependTo($html),
pass = false,
innerStyle = 'style="width: 5px; height: 5px; float: left;"',
testDiv = $('<div><div ' + innerStyle + '></div><div ' + innerStyle + '></div></div>').prependTo(fakeBody),
kids = testDiv[0].childNodes,
topA = kids[0].offsetTop,
divB = kids[1],
topB = divB.offsetTop;
if (topA === topB) {
divB.style.clear = 'left';
topB = divB.offsetTop;
if (topA !== topB) {
pass = true;
}
}
fakeBody.remove();
return pass;
})();
//right about here, we *could* make sure all of the above css support props are true, if not, return and leave the page usable fercryin'outloud
if(!$.support.display || !$.support.position || !$.support.overflow || !$.support.floatclear ) { return; }
//support properties from jQtouch
$.support.touch = (typeof Touch == "object");
$.support.WebKitAnimationEvent = (typeof WebKitTransitionEvent == "object");
/*
add some core behavior,events
*/
//test whether a CSS media type or query applies (adapted from work by Scott Jehl & Paul Irish: http://gist.github.com/557891)
$.media = (function(){
if ( (window.media && media.matchMedium) ){
//use native support if available
return media.matchMedium;
}
var cache = {},
testDiv = $('<div id="jq-mediatest"></div>'),
fakeBody = $('<body></body>').append(testDiv);
return function(q){
if (cache[q] === undefined) {
var styleBlock = $('<style type="text/css"></style>');
var cssrule = '@media '+q+' { #jq-mediatest { position: absolute; } }';
if (styleBlock[0].styleSheet){
styleBlock[0].styleSheet.cssText = cssrule;
}
else {
styleBlock.text(cssrule);
}
$html.prepend(fakeBody).prepend(styleBlock);
cache[q] = ((window.getComputedStyle ? window.getComputedStyle(testDiv[0],null) : testDiv[0].currentStyle)['position'] == 'absolute');
fakeBody.add(styleBlock).remove();
}
return cache[q];
};
})();
//hide Address bar
function hideBrowserChrome(){
//kill addr bar
window.scrollTo(0,0);
}
//get vert scroll dist
$.scrollY = function(){
return $(window).scrollTop(); //always returns 0 WebOS!! HELP! http://jsbin.com/unufu3/5/edit
};
//add new event shortcuts
$.each( ("touchstart touchmove touchend orientationchange tap swipe swipeleft swiperight scrollstart scrollstop").split(" "), function( i, name ) {
// Handle event binding
$.fn[ name ] = function( fn ) {
return fn ? this.live( name, fn ) : this.trigger( name );
};
if ( $.attrFn ) {
$.attrFn[ name ] = true;
}
});
//detect and trigger some custom events (scrollstart,scrollstop,tap,taphold,swipe,swipeleft,swiperight)
$(document)
.scroll(function(e){
var prevscroll = $.scrollY();
function checkscrollstop(){
if(prevscroll === $.scrollY()){
$body.trigger('scrollstop');
scrolling = false;
}
}
setTimeout(checkscrollstop,50);
})
.bind( ($.support.touch ? 'touchmove' : 'scroll'), function(e){
//iPhone triggers scroll a tad late - touchmoved preferred
if(!scrolling){
scrolling = true;
$body.trigger('scrollstart'); //good place to trigger?
}
})
.bind( ($.support.touch ? 'touchstart' : 'mousedown'), function(e){
touching = true;
//make sure it's not a touchmove and still touching
function checktap(eType){
if(!scrolling && touching){
$(e.target).trigger(eType);
}
}
//tap / taphold detection timeouts (make sure it's not a touchmove & before firing)
setTimeout(checktap, tapNotMoveTime, ['tap']);
setTimeout(checktap, tapHoldTime, ['taphold']);
//cache data from touch start - for later use in swipe testing
var eScope = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
touchstartdata = {
'time': (new Date).getTime(),
'coords': [eScope.pageX, eScope.pageY],
'origin': $(e.target)
};
})
.bind(($.support.touch ? 'touchmove' : 'mousemove'), function(e){
if(touchstartdata){
var eScope = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
touchstopdata = {
'time': (new Date).getTime(),
'coords': [eScope.pageX, eScope.pageY]
};
//trying not to interfere with scrolling here...
//this may need to be expanded to any non-y movement...
if(Math.abs(touchstartdata.coords[0] - touchstopdata.coords[0]) > 10){
e.preventDefault();
}
}
})
.bind(($.support.touch ? 'touchend' : 'mouseup'), function(e){
touching = false;
if(touchstartdata && touchstopdata){
//detect whether a swipe occurred, trigger it
if( touchstopdata.time - touchstartdata.time < maxSwipeTime &&
Math.abs(touchstartdata.coords[0] - touchstopdata.coords[0]) > minSwipeXDistance &&
Math.abs(touchstartdata.coords[1] - touchstopdata.coords[1]) < maxSwipeYtolerance ){
touchstartdata.origin.trigger('swipe');
touchstartdata.origin.trigger( (touchstartdata.coords[0] > touchstopdata.coords[0] ? 'swipeleft' : 'swiperight'));
}
}
touchstartdata = touchstopdata = null;
});
//add orientation class on flip/resize. This should probably use special events. Also, any drawbacks to just using resize?
$window.bind( ($.support.orientation ? 'orientationchange' : 'resize'), updateOrientation);
//orientation change classname logic - logic borrowed/modified from jQtouch
function updateOrientation() {
var neworientation = window.innerWidth < window.innerHeight ? 'portrait' : 'landscape';
if(orientation !== neworientation){
$body.trigger('turn', {orientation: orientation}); //temp event name
}
orientation = neworientation;
$html.removeClass('portrait landscape').addClass(orientation);
}
//add mobile, loading classes to doc
$html.addClass('ui-mobile');
//insert mobile meta (any other metas needed? webapp? iPhone icon? etc)
$head.append('<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0" />');
//send a link through hash tracking
$.fn.ajaxClick = function(){
var href = $(this).attr( "href" ),
transitionAttr = $(this).attr('data-transition');
if(transitionAttr){
currentTransition = transitionAttr;
transitionSpecified = true;
}
else{
transitionSpecified = false;
}
prevUrl = $.bbq.getState('url');
$.bbq.pushState({ url: href });
//note: if it's a non-local-anchor and Ajax is not supported, go to page
if(href.match(/^[^#]/) && !$.support.ajax){
window.location = href;
}
else {
return false;
}
};
//ajaxify all navigable links
$('a:not([href="#"])').live('click',function(e){
if ($(this).is('[target=_blank]') || $(this).is('[rel=external]')){ return true; }
$(this).ajaxClick();
return false;
});
//turn on/off page loading message.. also hides the ui-content div
function pageLoading(done){
if(done){
//remove loading msg
$html.removeClass('ui-loading');
//fade in page content, remove loading msg
$('.ui-page-active .ui-content')//.addClass('dissolve in');
}
else{
$html.addClass('ui-loading');
$loader.appendTo($body).addClass('dissolve in');
}
};
//transition between pages - based on transitions from jQtouch
function changePage(from,to,back){
hideBrowserChrome();
if(!back && !transitionSpecified){ currentTransition = 'slide'; }
//kill keyboard (thx jQtouch :) )
$(':focus').blur();
//animate in / out
from.addClass(currentTransition + ' out ' + (back ? 'reverse':''));
to.appendTo('body').addClass(activePageClass + ' ' + currentTransition + ' in ' + (back ? 'reverse':''));
//callback - remove classes, etc
to.animationComplete(function(){
from.add(to).removeClass(' out in reverse '+ transitions);
from.removeClass(activePageClass);
pageLoading(true);
});
if(back){ currentTransition = 'slide'; }
};
//potential (probably incomplete) fallback to workaround lack of animation callbacks.
//should this be extended into a full special event?
// note: Expects CSS animations use transitionDuration (350ms)
$.fn.animationComplete = function(callback){
if($.support.WebKitAnimationEvent){
$(this).one('webkitAnimationEnd', callback);
}
else{
setTimeout(callback, transitionDuration);
}
};
//markup-driven enhancements, to be called on any ui-page upon loading
function mobilize($el){
return $el.each(function(){
//checkboxes, radios
$el.find('input[type=radio],input[type=checkbox]').customCheckboxRadio();
//custom buttons
$el.find('button, input[type=submit]').customButton();
//custom text inputs
$el.find('input[type=text],input[type=password],textarea').customTextInput();
//collapsible groupings
$el.find('[data-role="collapsible"]').collapsible();
//single-field separators
$el.find('.field').fieldcontain();
//selects
$el.find('select').customSelect();
//tabs
$('[data-role="tabs"]').tabs({fixedAsFooter:true});
//global nav
$('[data-role="globalnav"]').globalnav();
//fix toolbars
$el.fixHeaderFooter();
//buttons from links in headers,footers,bars, or with data-role
$('.ui-header a, .ui-footer a, .ui-bar a, [data-role="button"]').not('.ui-btn').buttonMarkup();
//vertical controlgroups
$el.find('[data-role="controlgroup"]:not([data-type="horizontal"])').controlgroup();
//horizontal controlgroups
$el.find('[data-role="controlgroup"][data-type="horizontal"]').controlgroup({direction: 'horizontal'});
//tree from data role
$el.find('[data-role="tree"]').tree();
//links within content areas
$el.find('.ui-body a:not(.ui-btn):not(.ui-link-inherit)').addClass('ui-link');
//rewrite "home" links to mimic the back button (pre-js, these links are usually "home" links)
var backBtn = $el.find('.ui-header a.ui-back');
if(!backBtn.length){
backBtn = $('<a href="#" class="ui-back" data-icon="arrow-l"></a>').appendTo($el.find('.ui-header')).buttonMarkup();
}
backBtn
.click(function(){
history.go(-1);
return false;
})
.find('.ui-btn-text').text(backBtnText);
});
};
//swipe left always triggers a back
$(document).swipeleft(function(){
history.go(-1);
return false;
});
//dom-ready
$(function(){
//set the page loader up
pageLoading();
//define body
$body = $('body');
//hashchange for page state tracking - uses bbq. - needs to be bound at domready (for IE6 Ben?)
//When document.location.hash changes, find or load content, make it active
$window.bind( "hashchange", function(e){
var url = e.getState('url'),
back = (url === prevUrl);
if(url){
//see if content is present - NOTE: local urls aren't working right now - need logic to kill #
var localDiv = $('[id="'+url+'"]');
if(localDiv.length){
changePage($('.ui-page-active'), localDiv, back);
pageLoading(true);
}
else { //ajax it in
pageLoading();
var newPage = $('<div class="ui-page" id="'+url+'"></div>')
.appendTo($body)
.load(url + ' .ui-page',function(){
//dumping in HTML() from ui-page div - cleaner way?
$(this).html( $(this).find('.ui-page:eq(0)').html() );
mobilize($(this));
changePage($('.ui-page-active'), $(this), back);
pageLoading(true);
});
}
}
else{
//either...
// we've backed up to the root page url,
// it's a plugin state
//or it's the first page load with no hash present
var currentPage = $('.ui-page-active');
if( currentPage.length && !startPage.is('.ui-page-active')){
changePage(currentPage, startPage, back);
}
else{
startPage.addClass(activePageClass);
}
pageLoading(true);
}
});
//mobilize all pages present
mobilize($('.ui-page'));
//set up active page - mobilize it!
startPage = $('body > .ui-page:first');
//make sure it has an ID - for finding it later
if(!startPage.attr('id')){
startPage.attr('id', startPageId);
}
//trigger a new hashchange, hash or not
$window.trigger( "hashchange" );
//... this shouldn't be needed, but chrome isn't getting it sometimes
setTimeout(function(){ pageLoading(true); }, 3000);
//update orientation
updateOrientation();
//some debug stuff for the events pages
$('body').bind('scrollstart scrollstop swipe swipeleft swiperight tap taphold turn',function(e){
$('#eventlogger').prepend('<div>Event fired: '+ e.type +'</div>');
});
});
})(jQuery, this);