Skip to content

Instantly share code, notes, and snippets.

@malko
Created November 26, 2012 18:34
Show Gist options
  • Save malko/4149808 to your computer and use it in GitHub Desktop.
Save malko/4149808 to your computer and use it in GitHub Desktop.
touch events support
/**
* minimal jquery/zepto compatibility layer
* be aware that won't mimic jquery/zepto at all but offer a similar api for basic stuffs as querySelectorAll and addEventListener ...
* @author jgotti at modedemploi dot fr for agence-modedemploi.com
* @licence Dual licence LGPL / MIT
* @changelog
* - 2012-11-28 - more jquery like syntax and some events related stuffs
*/
(function($){
"use strict";
/*jshint expr:true*/
if(! $){
/**
* can be used as querySelectorAll (selector as first parameter and optionaly domElement used as context passed as second parameter )
* or if first parameter is a function just a shorthand of $.ready
*/
$ = function(selector,context){ // not supporting IE
if( selector instanceof Function ){
return $.ready(selector);
}
var c;
if( selector === window || selector===document || (selector instanceof Element) ){
c = [selector];
}else if( selector instanceof Array ){
c = selector;
}else if(! (context instanceof Array) ){
context === window && (context = document);
c = Array.prototype.slice.call((context||document).querySelectorAll(selector),0) || [];
}else{
c = [];
$.each(context,function(k,v){
$(selector,v).each(function(k,v){
c.push(v);
});
});
}
$.each($.fn,function(k,v){
c[k] = v;
});
return c;
};
/**
* take a callback to execute when dom is ready
*/
$.ready = function(cb){ // not supporting IE
if(document.readyState.match(/complete|loaded|interactive/)){
cb();
}else{
document.addEventListener('DOMContentLoaded', function(){ cb();}, false);
}
return this;
};
/**
* first parameter is destination object to extend or boolean forcing deep extension.
* all others parameters are object to copy property from to destination object
*/
$.extend = function(){
var A=arguments
, deepPassed = typeof A[0] === 'boolean' ? true : false
, deep = deepPassed ? A[0] : false
, dest = A[deepPassed?1:0]
, a = deepPassed?2:1
, al = A.length
, p
;
if( a ===2 ){
dest = A[1];
}
for(;a<al;a++){
for(p in A[a]){
if( A[a].hasOwnProperty(p) ){
if( deep && typeof(A[a][p]) === 'object' ){
dest[p] = dest[p] || {};
$.extend(deep,dest[p],A[a][p]);
}else{
dest[p] = A[a][p];
}
}
}
}
return dest;
};
/**
* bind callback on given event to given element
* don't support namespaced events
*/
$.on = (window.document.addEventListener ?
function(type, e, cb){ $.each(type.split(/\s+/),function(){e.addEventListener(this, cb, false);}); }
: function(type, e, cb){ $.each(type.split(/\s+/),function(){ e.attachEvent('on' + this, cb);}); }
);
/**
* unbind callback on given event to given element
* don't support namespaced events
*/
$.off = (window.document.removeEventListener ?
function(type, e, cb){ $.each(type.split(/\s+/),function(){e.removeEventListener(this, cb, false);}); }
: function(type, e, cb){$.each(type.split(/\s+/),function(){e.detachEvent('on' + this, cb);}); }
);
/**
* iterate over collection using a given callback (cb)
*/
$.each = function(collection, cb){
var i,l,key;
if((collection instanceof Array) || (collection instanceof NodeList) ) {
for(i=0,l=collection.length; i<l;i++){
if(cb.call(collection[i], i, collection[i]) === false){
return collection;
}
}
}else{
for(key in collection){
if(collection.hasOwnProperty(key) && cb.call(collection[key],key,collection[key]) === false){
return collection;
}
}
}
return collection;
};
$.Event=function(type,props){
var event = document.createEvent(type.match(/^(click|mousedown|mouseup|mousemove)$/) ? 'MouseEvents':'Events');
props && $.extend(event,props);
event.initEvent(type, (props && props.bubbles===false)?false:true, true, null, null, null, null, null, null, null, null, null, null, null, null);
return event;
};
$.fn = {
each:function(cb){ $.each(this,cb); return this; }
,on:function(type,cb){ return $.each(this,function(k,v){ $.on(type,v,cb); }); }
,off:function(type,cb){ return $.each(this,function(k,v){ $.off(type,v,cb); }); }
,trigger:function(event, data){
if( typeof event === 'string' ){
event = $.Event(event);
}
data && (event.data = data);
return $.each(this,function(k,v){
if( 'dispatchEvent' in v){ v.dispatchEvent(event);}
});
}
};
window.$ = $;
}})(typeof $ !== 'undefined'? $ : false);
<!DOCTYPE HTML>
<html>
<head>
<!--
<script src="zepto.js"></script>
-->
<script src="basic-compat.js"></script>
<script src="tutechie.js"></script>
<style>
#galleryTest, #galleryTest2{ overflow: hidden;width:400px;height:200px;position:relative;background: #333;}
#galleryTest img, #galleryTest2 img{ position:absolute; left:0;right:0;}
</style>
</head>
<body>
<h2>Testing Swipe right/left</h2>
<div id="galleryTest" >
<img src="http://lorempixel.com/400/200/abstract">
<img src="http://lorempixel.com/400/200/nature">
<img src="http://lorempixel.com/400/200/city">
<img src="http://lorempixel.com/400/200/technics">
<img src="http://lorempixel.com/400/200/people">
</div>
Tap and long Tap me to see a message appear in the console on the right
<br />
<h2>Testing drag </h2>
<div id="galleryTest2" >
<img src="http://lorempixel.com/400/200/abstract">
<img src="http://lorempixel.com/400/200/nature">
<img src="http://lorempixel.com/400/200/city">
<img src="http://lorempixel.com/400/200/technics">
<img src="http://lorempixel.com/400/200/people">
</div>
zoomin/out activable by pinch or mousewheel
<pre style="position:absolute;right:10px;top:10px; width:300px;height:600px;border:solid silver 1px" id="pre"></pre>
<script>
/*global $*/
/*jshint expr:true,browser:true*/
//--initilaise gallery
/**
* code is made abnormally complex to manage compatibility between jQuery/Zepto/basic-compat libraries.
*/
function log(){
var p = $('#pre')[0];
p.innerHTML += Array.prototype.join.call(arguments,', ')+'\n';
}
function gallery(container,draggable){
if( ! (this instanceof gallery) ){
return new gallery(container,draggable);
}
var g = this;
g.draggable = draggable;
g.container = $(container);
g.zoom = 1;
g.offset = {left:this.container[0].left,top:this.container[0].top};
g.currentSlide = 0;
g.width = window.getComputedStyle(g.container[0]).width;
g.slides = [];
$('img',g.container).each(function(k,v){
v.setAttribute('data-slideId',k);
v.setAttribute('draggable',false);
if(k){
v.style.left = g.width;
}
g.slides.push($(v));
});
g.lastSlideId = parseInt($('img:last-child',g.container)[0].getAttribute('data-slideId'),10);
g.container.on('Touchstart',function(e){
e.preventDefault();
});
if( !draggable ){
g.container
.on('Tap',function(){
log('you just tapped me');
})
.on('Heldtap',function(){
log('you just tapped me so long');
})
.on('Swipe',function(e){
if( e.direction ==='left'){
g.goNext();
}else if( e.direction === 'right'){
g.goPrev();
}
log('swiped '+e.direction);
})
;
}else{
g.container.on('Move',function(e){
var w = parseInt(g.width,10)
,prev = g.getSlide(g.currentSlide === 0 ? g.lastSlideId : (g.currentSlide-1))[0]
,next = g.getSlide(g.currentSlide === g.lastSlideId ? 0 : (g.currentSlide+1))[0]
,current = g.getSlide(g.currentSlide)[0]
,distanceX = Math.min(w,Math.max(-w,e.distanceX))
;
$([prev,current,next]).each(function(){
this.setAttribute('style','');
});
current.style.left = distanceX+'px';
prev.style.left = (distanceX-w)+'px';
next.style.left = (distanceX+w)+'px';
});
g.container.on('Moveend',function(e){
var treshold = parseInt(g.width,10)*0.33;
if( Math.abs(e.distanceX) > treshold ){
e.distanceX > 0 ? g.goPrev() : g.goNext();
}else{
g.getSlide(g.currentSlide)[0].setAttribute('style','left:0;-webkit-transition: left '+(500*(Math.abs(e.distanceX)/treshold))+'ms linear;z-index:1');
}
});
}
g.container
.on('Zoomin',function(e){
e.preventDefault();
e.stopPropagation();
var factor = e.isTouchEvent ? 0.02 : 0.1;
(g.zoom+=factor) > 2 && (g.zoom = 2);
g.container[0].style.webkitTransform = 'scale('+g.zoom+')';
g.container[0].style.transform = 'scale('+g.zoom+')';
})
.on('Zoomout',function(e){
e.preventDefault();
e.stopPropagation();
var factor = e.isTouchEvent ? 0.02 : 0.1;
(g.zoom-=factor) < 0.5 && (g.zoom = 0.5);
g.container[0].style.webkitTransform = 'scale('+g.zoom+')';
g.container[0].style.transform = 'scale('+g.zoom+')';
})
;
}
gallery.prototype.getSlide = function(id){
return this.slides[id];
};
gallery.prototype.goNext=function(){
var g=this,next = g.currentSlide+1;
if( next > g.lastSlideId){
next = 0;
}
if( $.fn.animate ){
g.getSlide(g.currentSlide).animate({left:'-'+g.width},500);
(! g.draggable) && g.getSlide(next).css('left',g.width);
g.getSlide(next).animate({left:0},500);
}else{
(function(cur,next){
(! g.draggable) && next.setAttribute('style','left:'+g.width);
setTimeout(function(){
next.setAttribute('style','left:0; -webkit-transition: left 500ms linear');
cur.setAttribute('style','left:-'+g.width+'; -webkit-transition: left 500ms linear');
},0);
})(g.getSlide(g.currentSlide)[0],g.getSlide(next)[0]);
}
g.currentSlide=next;
};
gallery.prototype.goPrev=function(){
var g=this, next = g.currentSlide-1;
if( next < 0 ){
next = g.lastSlideId;
}
if( $.fn.animate ){
g.getSlide(g.currentSlide).animate({left:g.width},500);
(! g.draggable) && g.getSlide(next).css('left','-'+g.width);
g.getSlide(next).animate({left:0},500);
}else{
(function(cur,next){
(! g.draggable) && next.setAttribute('style','left:-'+g.width);
setTimeout(function(){
next.setAttribute('style','left:0; -webkit-transition: left 500ms linear');
cur.setAttribute('style','left:'+g.width+'; -webkit-transition: left 500ms linear');
},0);
})(g.getSlide(g.currentSlide)[0],g.getSlide(next)[0]);
}
g.currentSlide=next;
};
gallery('#galleryTest',false);
gallery('#galleryTest2',true);
</script>
</body>
</html>
/*global $*/
/*jshint expr:true*/
/**
* Tutechie.js is a library made to support some basic touch/mouse events cross browser (depending on the underlying library u use).
* it can be used with basic-compat.js or zepto or even jquery
* it add following custom events :
* - Touchstart: triggered at touch start or mousedown if no touchStart event is enabled on android you should preventDefault the event emitted to track down further events
* - Tap: triggered when no movement occured and touch event is released before a Heldtap is triggered
* - Heldtap: this is a tap which duration was longer than longDelay it doesn't require to be ended to get triggered (no need to release the touchEvent)
* - Move: triggered after a Touchstart when moving arround more than treshold (extra event properties: pageX,pageY,distanceX,distanceY)
* - Moveend: triggered after a Move event occured when the user release the touch event (same extra properties as Move events)
* - Swipe: triggered when a user Move more than treshold px in a given direction (extra event properties: direction)
* - Zoomin/Zoomout: this is a pinch open/close or a mousewheel event (extra event properties: isTouchEvent)
* @param {Object} $ Description
* @returns {Object} Description
* @author jgotti at modedemploi dot fr for agence-modedemploi.com
* @licence Dual licence LGPL / MIT
*/
(function($){
"user strict";
var tcEvent= {}
, startPosition={}
, currentPosition = {}
, distance = {}
, treshold = 20
, longDelay = 500
, longDelayCb = null
, b = $(document)
, scale = 1
, emulateGesture = 'ongesturestart' in window ? false : {}
, touchCapable = 'ontouchstart' in window ? true : false
, scrollDetection = (emulateGesture && touchCapable) ? {interval:null,lastPos:[]} : false
;
function extendEvent(e,props){
var type,event;
if( typeof props === 'string' ){
type = props;
props = {};
}else{
type = props.type || e.type;
delete props.type;
}
props.originalEvent = e;
event = $.Event(type,props);
event.preventDefault = function(){
if( e.preventDefault ){
e.preventDefault();
}else{
e.returnValue=false;
}
};
event.stopPropagation = function(){
if( e.stopPropagation ){
e.stopPropagation();
}
e.cancelBubble = true;
};
return event;
}
function resetEvent(e){
tcEvent = {target:null,type:null,touchEvent:false,running:false};
startPosition = {x:0,y:0};
currentPosition = {x:0,y:0};
distance = {x:0,y:0};
if( longDelayCb ){
clearTimeout(longDelayCb);
longDelayCb = null;
}
if( scrollDetection ){
scrollDetection.interval && clearInterval(scrollDetection.interval);
scrollDetection.lastPos=[];
scrollDetection.interval = null;
}
if( e && e.target ){
tcEvent.target = e.target;
tcEvent.running = true;
var p = getPositionHolder(e),p2;
startPosition.x = p.pageX;
startPosition.y = p.pageY;
currentPosition = {x:startPosition.x,y:startPosition.y};
if( emulateGesture && (p2=getPositionHolder(e,1)) && p2.pageX){
startPosition.x2 = p2.pageX;
startPosition.y2 = p2.pageY;
startPosition.distance = Math.sqrt(Math.pow(startPosition.x2 - startPosition.x,2) + Math.pow(startPosition.y2 - startPosition.y,2));
}
if( scrollDetection ){
scrollDetection.lastPos = [window.pageXOffset,window.pageYOffset];
scrollDetection.interval=setInterval(function(){
if( window.pageXOffset !== scrollDetection.lastPos[0] || window.pageYOffset!==scrollDetection.lastPos[1]){
b.off('touchmove',moveEvent);
resetEvent();
}
},50);
}
}
}
function getTouches(e){
return e.changedTouches || (e.originalEvent && e.originalEvent.changedTouches) || null;
}
function getPositionHolder(e,touchPos){
touchPos !== undefined || (touchPos = 0);
var holder=e, touches = getTouches(e);
if( touches && touches.length ){
tcEvent.touchEvent = true;
holder = touches[touchPos];
}else{
tcEvent.touchEvent = false;
}
return holder;
}
function setXYFromEvent(e){
var p = getPositionHolder(e);
currentPosition.x=p.pageX;
currentPosition.y=p.pageY;
distance.x = currentPosition.x - startPosition.x;
distance.y = currentPosition.y - startPosition.y;
return currentPosition;
}
function fillEmulateGesture(e){
var pos=getPositionHolder(e,1),lastDistance=emulateGesture.distance||startPosition.distance;
emulateGesture = {
x:pos.pageX
,y:pos.pageY
,distance: Math.sqrt(Math.pow(pos.pageX - currentPosition.x,2) + Math.pow(pos.pageY - currentPosition.y,2))
};
if( Math.abs(emulateGesture.distance - lastDistance) < treshold/2 ){
emulateGesture.distance = lastDistance;
}
emulateGesture.scale = emulateGesture.distance / startPosition.distance;
}
function eventStart(e){
var touches = getTouches(e);
if( touches && touches.length ){
tcEvent.touchEvent = true;
}else{
if( e.button !== 0){ return; } // don't track non left mouse button events
tcEvent.touchEvent = false;
}
if( tcEvent && tcEvent.type ){
return;
}
resetEvent(e);
var event = extendEvent(e,'Touchstart');
$(tcEvent.target).trigger(event);
longDelayCb = setTimeout(function(){ tcEvent.running && (tcEvent.type === null) && longTapEnd(); },longDelay);
}
function eventEnd(e){
if( tcEvent.touchEvent===false && e.button !== 0){ return; } // don't track non left mouse button events
if(! tcEvent.running ){ return; }
setXYFromEvent(e);
if( tcEvent.type === null && Math.abs(distance.x) < treshold && Math.abs(distance.y) < treshold ){
$(tcEvent.target).trigger(extendEvent(e,'Tap'));
//moveEnd(e);
resetEvent();
return;
}
if( tcEvent.type !== 'Move'){
tcEvent.type === 'gesture' && gestureEventEnd(e);
return;
}
var eventData;
if( Math.abs(distance.x) > Math.abs(distance.y)){
eventData={direction:distance.x < 0 ? 'left' : 'right'};
}else{
eventData={direction:distance.y < 0 ? 'up' : 'down'};
}
eventData.type = 'Swipe';
$(tcEvent.target).trigger(extendEvent(e,eventData));
moveEnd(e);
resetEvent();
}
function longTapEnd(){
if(tcEvent.type !== null || ! tcEvent.running ){ return; }
if( treshold > Math.abs(distance.x) && treshold > Math.abs(distance.y) ){ // this is a longtap event
b.off(tcEvent.touchEvent?'touchmove':'mousemove',moveEvent); // stop tracking movement
tcEvent.type = 'Heldtap';
$(tcEvent.target).trigger('Heldtap');
resetEvent();
}
}
function moveEvent(e){
setXYFromEvent(e);
if(! tcEvent.running ){ return; }
var touches = getTouches(e);
if( tcEvent.type !== 'gesture' && tcEvent.touchEvent && emulateGesture && touches && touches.length > 1){ // we consider to be in a gestureEvent
return gestureEventStart(e);
}
if( emulateGesture && tcEvent.type === 'gesture'){
return gestureEvent(e);
}
if( Math.abs(distance.x) < treshold && Math.abs(distance.y) < treshold){
return;
}else if(tcEvent.type && tcEvent.type !== 'Move'){
return;
}
tcEvent.type = 'Move';
$(tcEvent.target).trigger(extendEvent(e,{
type:'Move'
,pageX:currentPosition.x
,pageY:currentPosition.y
,distanceX:distance.x
,distanceY:distance.y
}));
}
function moveEnd(e){
setXYFromEvent(e);
if(! tcEvent.running){ return;}
/*if( tcEvent.type==='gesture'){
return gestureEventEnd(e);
}*/
if( tcEvent.type !== 'Move'){ return; }
$(tcEvent.target).trigger(extendEvent(e,{
type:'Moveend'
,pageX:currentPosition.x
,pageY:currentPosition.y
,distanceX:distance.x
,distanceY:distance.y
}));
}
function gestureEventStart(e){
if( tcEvent.type !== null ){
return;
}
if( 'scale' in e){
scale = e.scale;
}else{
fillEmulateGesture(e);
scale = 1;
}
resetEvent(e);
tcEvent.type = 'gesture';
}
function gestureEvent(e){
var direction,event;
if( e.wheelDelta ){
direction = e.wheelDelta > 0 ?'in':'out';
event = extendEvent(e,{
type: 'Zoom'+direction
,direction: direction
,isTouchEvent:false
});
}else if( e.detail ){
direction = e.detail < 0 ?'in':'out';
event = extendEvent(e,{
type: 'Zoom'+direction
,direction: direction
,isTouchEvent:false
});
}else if( e.scale !== scale ){
if( emulateGesture ){
fillEmulateGesture(e);
e.scale = emulateGesture.scale;
if( scale === e.scale){ return; }
}
direction = e.scale > scale ?'in':'out';
event = extendEvent(e,{
type: 'Zoom'+direction
,direction: direction
,isTouchEvent:true
});
scale = e.scale;
}
if(direction){
$(tcEvent.target).trigger(event);
(event.type = 'Zoom') && $(e.target).trigger(event);
}
}
function gestureEventEnd(e){
scale = 1;
emulateGesture && (emulateGesture={});
resetEvent();
}
$(function(){
if(! emulateGesture){
b.on('gesturestart',gestureEventStart)
.on('gesturechange',gestureEvent)
.on('gestureend',gestureEventEnd)
;
}else if(! touchCapable) {
if('onmousewheel' in window){
b.on('mousewheel',function(e){
!e && (e = window.event);
gestureEventStart(e);
gestureEvent(e);
gestureEventEnd(e);
});
}else{
b.on('DOMMouseScroll',function(e){
gestureEventStart(e);
gestureEvent(e);
gestureEventEnd(e);
});
}
}
if( touchCapable){
b
.on('touchstart',function(e){
b.on('touchmove',moveEvent);
eventStart(e);
})
.on('touchend',function(e){
b.off('touchmove',moveEvent);
eventEnd(e);
})
.on('blur touchcancel',function(){ //stop tracking touch events when loosing focus
b.off('touchmove',moveEvent);
resetEvent();
})
;
}else{
b
.on('mousedown',function(e){
b.on('mousemove',moveEvent);
eventStart(e);
})
.on('mouseup',function(e){
b.off('mousemove',moveEvent);
eventEnd(e);
})
.on('blur',function(){ //stop tracking touch events when loosing focus
b.off('mousemove',moveEvent);
resetEvent();
})
;
}
});
})($);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment