PDA

View Full Version : iPhone framework jQTouch for Ext Core = ExtTouch



letssurf
15 Jun 2010, 5:56 AM
I tried to use jQTouch and found it very buggy and difficult to use. I've gotten very used to the way Ext works.
So I decided it was time to take all the ideas that where in jQTouch and convert them over to Ext.

This code requires the themes and css from jQTouch and Ext core to work.



/*global window,Ext*/
Ext.touch = {
icon: null,
addGlossToIcon: true,
startupScreen: null,
fixedViewport: true,
fullscreen: true,
statusBar: 'default', // other options: black-translucent, black
preloadImages: [],
busy: false,

lastAnimationTime: 0,
history: [],
animations: ['slide', 'flip', 'slideup', 'swap', 'cube', 'pop', 'dissolve', 'fade', 'back'],

/**
* Goto new page
*
* @param {String} pageId
* @param {String} anim Optional
* @param {String} callback Optional
* @param {Object} scope Optional
* @return {Boolean}
*/
goTo: function(pageId, anim, callback, scope) {
// check if we're already busy
if (this.busy) {
return false;
}

// if last page in the history matches the new one leave now
var lastPage = this.history[this.history.length-1];
if (lastPage.pageId == pageId) {
return true;
}

// add new page to the history
this.history.push({
pageId: pageId,
anim: anim
});

this.animatePage({
from: lastPage.pageId,
to: pageId,
anim: anim,
reverse: false,
callback: callback,
scope: scope
});
return true;
},

/**
* Go back
*
* @param {Function} callback Optioanl
* @param {Object} scope Optional
* @return {Boolean}
*/
goBack: function(callback, scope) {
// check if we're already busy
if (this.busy) {
return false;
}

var pageFrom = this.history.pop(),
pageTo = this.history[this.history.length-1],
anim = pageFrom.anim;

this.animatePage({
from: pageFrom.pageId,
to: pageTo.pageId,
anim: anim,
reverse: true,
callback: callback,
scope: scope
});
return true;
},

/**
* Animate page
*
* config
* from
* to
* anim
* reverse
* callback
* scope
*
* @param {Object} config
* @return {Boolean}
*/
animatePage: function(config) {
// check if we're already busy
if (this.busy) {
return false;
}

var fromPage = Ext.get(config.from),
toPage = Ext.get(config.to),
reverse = (config.reverse === true) ? 'reverse' : '';

if (!config.anim) {
// no animate just swap the current page
fromPage.removeClass('current');
toPage.addClass('current');
return true;
}

// now we'e busy with the animation
this.busy = true;

// animate the last page out
fromPage.addClass([config.anim, 'out', reverse]);

// animate the new page in, execute call if specified
toPage.addClass([config.anim, 'in', 'current', reverse]).on('webkitAnimationEnd', function() {
this.lastAnimationTime = (new Date()).getTime();
fromPage.removeClass(['current', config.anim, 'out', 'reverse']);
toPage.removeClass([config.anim, 'in', 'reverse']);
// animation finished
this.busy = false;
if (Ext.isFunction(config.callback)) {
config.callback.call(config.scope||toPage, toPage, config.to, config.anim);
}
}, this, {single: true});

return true;
},

/**
* Init
*
* Needs to be called before the touch stuff can be used properly
*
*/
init: function(config) {
// apply the config over the default values
Ext.apply(this, config);

// check for params and add the needed tags
var headValue = [];
if (this.icon) {
var precomposed = this.addGlossToIcon ? '' : '-precomposed';
headValue.push('<link rel="apple-touch-icon' + precomposed + '" href="' + this.icon + '" />');
}
if (this.startupScreen) {
headValue.push('<link rel="apple-touch-startup-image" href="' + this.startupScreen + '" />');
}
if (this.fixedViewport) {
headValue.push('<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"/>');
}
if (this.fullscreen) {
headValue.push('<meta name="apple-mobile-web-app-capable" content="yes" />');
if (this.statusBar) {
headValue.push('<meta name="apple-mobile-web-app-status-bar-style" content="' + this.statusBar + '" />');
}
}
if (headValue.length > 0) {
// we have some new head values to insert
Ext.select('head').insertHtml('beforeEnd', headValue.join(''));
}

// preload some images
Ext.each(this.preloadImages, function(src) {
(new window.Image()).src = src;
});

// some things can only be done after the body is ready
Ext.onReady(function() {
// init the custom events
this.initEvents();

// stop normal link actions from happening on links,
// like open in new window
Ext.select('a').setStyle('-webkit-touch-callout', 'none');

// this fixes forms becoming focused thus showing the keyboard
// when pages are animated in
Ext.getBody().on('mousedown', function(e) {
var timeDiff = (new Date()).getTime() - this.lastAnimationTime;
if (timeDiff < 200) {
// if the last animation was really recently,
// stop the mouse down event
e.preventDefault();
}
}, this);

if (window.navigator.standalone === true) {
// this is when the app is running as an app not in a browser
Ext.getBody().addClass('fullscreen').addClass(this.statusBar);
}

// Start off the history with the first page
var currentPage = Ext.DomQuery.selectNode('.current');
this.history.push({
pageId: currentPage.id,
anim: null
});
}, this);
},

/**
* Add animation
*
* @param {String} anim
* @return void
*/
addAnimation: function(anim) {
if (this.animations.indexOf(anim) == -1) {
this.animations.push(anim);
}
},

// private
initEvents: function() {
var events = {
tap: [],
swipe: [],
turn: []
},
// does the device have touch support
supportTouch = (typeof window.Touch == 'object');

// create an interceptor for the on method
Ext.Element.prototype.on =
Ext.Element.prototype.on.createInterceptor(function(eventName, fn, scope, options) {
// is the event a custom event
if (eventName == 'tap') {
// if we have touch support deal with it
if (supportTouch) {
// store the event details for later use
events.tap.push({
el: this,
eventName: eventName,
fn: fn,
scope: scope||this,
options: options
});
} else {
// no touch support, remap tap to click
this.on('click', fn, scope, options);
}
// return false stopping the original method from being called
// we handled it here
return false;
} else if (eventName == 'swipe' || eventName == 'turn') {
// store the event details for later use
events[eventName].push({
el: this,
eventName: eventName,
fn: fn,
scope: scope||this,
options: options
});
return false;
}
// it wasn't a custom event, leave it to handled as normal
return true;
});

// create an interceptor for the un method
Ext.Element.prototype.un =
Ext.Element.prototype.un.createInterceptor(function(eventName, fn, scope) {
// is the event a custom event
if (eventName == 'tap') {
// if we have touch support deal with it
if (supportTouch) {
// find the stored event details and remove
scope = scope||this;
Ext.each(events.tap, function(event) {
if (event.el == this && event.fn == fn && event.scope == scope) {
events.tap.remove(event);
}
}, this);
} else {
// no touch support, remap tap to click
this.un('click', fn, scope);
}
return false;
} else if (eventName == 'swipe' || eventName == 'turn') {
// find the stored event details and remove
Ext.each(events[eventName], function(event) {
if (event.el == this && event.fn == fn && event.scope == scope) {
events[eventName].remove(event);
}
}, this);
return false;
}
// it wasn't a custom event, leave it to handled as normal
return true;
});

var startTime, startX, startY, deltaX, deltaY, deltaT;

function updateTouch(e) {
// get the current touch pos
var currentX = e.browserEvent.changedTouches[0].clientX,
currentY = e.browserEvent.changedTouches[0].clientY;

// work out the pos diff from the start
deltaX = startX - currentX;
deltaY = startY - currentY;

// work out the time we've been doing the touch
deltaT = (new Date()).getTime() - startTime;
}

Ext.getBody().on('touchstart', function(e) {
// store the starting values
startTime = (new Date()).getTime();
startX = e.browserEvent.changedTouches[0].clientX;
startY = e.browserEvent.changedTouches[0].clientY;

// make the target look active
if (e.target.tagName.toUpperCase() == 'A') {
Ext.fly(e.target).addClass('active');
}
}, this);

Ext.getBody().on('touchmove', function(e) {
// update the touch values
updateTouch(e);

// we moved so the items shouldn't look active
if (e.target.tagName.toUpperCase() == 'A') {
Ext.fly(e.target).removeClass('active');
}
// check if we did a swipe
if (Math.abs(deltaX) >= 30 && deltaY === 0 && deltaT < 1000) {
// trigger the swipe event for all matching elements
Ext.each(events.swipe, function(event) {
if (event.el.dom == e.target || event.el.contains(e.target)) {
event.fn.call(event.scope, e);
}
});
}
}, this);

Ext.getBody().on('touchend', function(e) {
// update the touch values
updateTouch(e);

// we didn't move so treat this as a tap
if (deltaX === 0 && deltaY === 0) {
// trigger the tap event for all matching elements
Ext.each(events.tap, function(event) {
if (event.el.dom == e.target || event.el.contains(e.target)) {
event.fn.call(event.scope, e);
}
});
}
}, this);

// monitor the faked tap event
Ext.getBody().on('tap', function(e) {
var target = Ext.get(e.getTarget());
// find the nearest link to the target
if (!target.is('a')) {
target = target.parent('a');
}
// if we found an A tag
if (target && target.is('a')) {
// does the target have any of the back classes
if (target.hasClass('back') || target.hasClass('goback')) {
// yes it does, go back a page
Ext.touch.goBack();
} else {
// get the hash value from the targets dom
var hash = String(target.dom.hash);
// make sure we have a value, not just a hash
if (hash !== '#') {
// defaut to the default animation
var anim = null;
// loop the animations that are availible
Ext.each(this.animations, function(animClass) {
// check if this animation is definied as a
// class on the element
if (target.hasClass(animClass)) {
// store the anim and exit the loops
anim = animClass;
return false;
}
return true;
});
// make the target look active
target.addClass('active');
// animate to the page specifed in the hash
Ext.touch.goTo(hash.substr(1), anim, function() {
// callback after animating the new page in
// remove the active class
target.removeClass('active');
});
}
}
}
}, this);

// monitor the real orientation event and convert into a turn event
Ext.getBody().on('orientationchange', function(e) {
var orientation = this.getOrientation();
// remove orientation classes and add the new one
Ext.getBody().removeClass(['portrait', 'landscape']).addClass(orientation);
Ext.each(events.turn, function(event) {
// call the stored callback for the stored events
event.fn.call(event.scope, e, orientation);
});
}, this);
},

/**
* Get orientation
*
* @return {String} landscape or portrait
*/
getOrientation: function() {
var orientation = 'portrait';
if (Math.abs(window.orientation) == 90) {
orientation = 'landscape';
}
return orientation;
}
};




<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Ext.touch</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style type="text/css" media="screen">@import "jqtouch/jqtouch.css";</style>
<style type="text/css" media="screen">@import "themes/apple/theme.css";</style>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/ext-core/3.1.0/ext-core.js"></script>
<script type="text/javascript" src="Ext.touch.js"></script>
<script type="text/javascript">
Ext.touch.init({
icon: 'icon.png',
startupScreen: 'startup.png'
});
</script>

<style type="text/css">
ul li a.loading {
background-image: url(themes/apple/img/loading.gif);
background-position: 95% center;
background-repeat: no-repeat;
}

#lnkButton {
margin:0 20px;
}
</style>
</head>
<body>
<div id="home" class="">
<div class="toolbar">
<h1>Home</h1>
<a class="button slideup" href="#about">About</a>
</div>
<div class="info"><p>Welcome to the home screen.</p></div>
<h2>Section</h2>
<ul class="rounded">
<li class="arrow"><a href="#other" id="lnkOther" class="slide">Other</a></li>
</ul>
</div>
<form id="login" class="current">
<div class="toolbar">
<h1>Login</h1>
<a class="button slideup" href="#about">About</a>
</div>
<ul class="rounded">
<li><input type="text" value="" name="username" placeHolder="Username" /></li>
<li><input type="text" value="" name="password" placeHolder="Password" /></li>
</ul>
<a href="#" class="grayButton" id="lnkLogin">Login</a>
</form>
<div id="other">
<div class="toolbar">
<h1>Other</h1>
<a href="#" class="back">Back</a>
</div>
<div class="info"><p>Welcome</p></div>
</div>
<div id="about" class="selectable">
<div class="toolbar">
<h1>About</h1>
</div>
<p>
<strong>About stuff</strong>
<a href="#" class="grayButton goback">Close</a>
</p>
</div>
</body>
</html>


Feel free to use it as it is.

I was going to host it on google code. But with the recent announcement from ExtJS (now known as Sencha) it possibly pointless. But releasing it incase people find useful.

Sesshomurai
24 Jun 2010, 9:09 AM
Does it work well on iPad?

letssurf
25 Jun 2010, 12:20 AM
I don't think it works too well on the iPad (not that I have one to test it on). All the sizes are suited to the iPhone. Sencha Touch is much better suited to iPad stuff.