I'm creating an ad for a magazine on iPad that has a long timeline that needs to be controlled by a horizontal scrollbar. The scrollbar had to be custom, and the best plugin for custom scrolling that I could find that worked on iPads was fleXcroll.
My problem is that I'm trying to disable the ability for the user to swipe scroll the timeline. It needs to be exclusively controlled by the scrollbar (since a swipe from the user should bring them to the next page of the magazine.)
I've been looking at this problem for the past two days and the closest I can get to solving it at the moment is by controlling the touch events for the div. When I use event.preventDefault() for touchstart and touchmove on the div it works partially. If the screen has not been moved, a swipe will not move the timeline. However, after using the scrollbar, if the timeline is still moving (iPads have a sort of easing when things are swiped so that whatever is being moved slows down before stopping) you can grab the timeline and move it by swiping it. Also, if you slowly let the scrollbar stop moving with your finger on it until it stops, you can then swipe the timeline again.
So, the problem is preventing the ability for users to swipe a certain div to the side in a magazine ad for iPad. The scrolling should ONLY be controlled by the scrollbar.
Any help is greatly appreciated! Thanks!
You can register JS to observe the swipe event and then just "ignore" them and prevent them from propagating further up the chain. Something like this might help:
(function($) {
$.fn.touchwipe = function(settings) {
var config = {
min_move_x: 20,
wipeLeft: function() { alert("left"); },
wipeRight: function() { alert("right"); },
preventDefaultEvents: true
};
if (settings) {
$.extend(config, settings);
}
this.each(function() {
var startX;
var isMoving = false;
function cancelTouch() {
this.removeEventListener('touchmove', onTouchMove);
startX = null;
isMoving = false;
}
function onTouchMove(e) {
if(config.preventDefaultEvents) {
e.preventDefault();
}
if(isMoving) {
var x = e.touches[0].pageX;
var dx = startX - x;
if(Math.abs(dx) >= config.min_move_x) {
cancelTouch();
if(dx > 0) {
config.wipeLeft();
}
else {
config.wipeRight();
}
}
}
}
function onTouchStart(e)
{
if (e.touches.length == 1) {
startX = e.touches[0].pageX;
isMoving = true;
this.addEventListener('touchmove', onTouchMove, false);
}
}
this.addEventListener('touchstart', onTouchStart, false);
});
return this;
};
})(jQuery);
Related
I have an animation in HTML5 Canvas that simulates scrolling when the user clicks and drags. It moves vertically when the screen is vertical and horizontally when horizontal.
this.scroll = function(e) {
// scroll behavior for canvas
// feed x movements into horizontal (true) comic, y movments into (false) vertical
// this number is zero or less, hence added to displacements
comic.readShift += (comic.orientation ? e.movementX : e.movementY);
// ... other code here
comic.drawPanels();
};
Listeners hook this up:
canvas.addEventListener("mousedown", function(e) {
canvas.addEventListener("mousemove", comic.scroll);
});
canvas.addEventListener("mouseup", function(e) {
canvas.removeEventListener("mousemove", comic.scroll);
});
canvas.addEventListener("mouseleave", function(e) {
canvas.removeEventListener("mousemove", comic.scroll);
});
This works, as far as I can tell, on desktop browsers. But now I'm trying to get it to work on mobile. With no equivalent for movement[X|Y] for touch events, I tried this for the scroll behavior...
this.drag = function(e) {
// scroll behavior on touch screen
var touch = e.changedTouches[0];
var movementX = comic.touchstart.screenX - touch.screenX;
var movementY = comic.touchstart.screenY - touch.screenY;
// decrement in this case
comic.readShift -= (comic.orientation ? movementX : movementY);
// ... other code here
comic.drawPanels();
};
comic.touchstart gets recorded by the touchstart listener:
canvas.addEventListener("touchstart", function(e) {
comic.touchstart = e.changedTouches[0];
canvas.addEventListener("touchmove", comic.drag);
});
canvas.addEventListener("touchend", function(e) {
canvas.removeEventListener("touchmove", comic.drag);
});
canvas.addEventListener("touchcancel", function(e) {
canvas.removeEventListener("touchmove", comic.drag);
});
Which isn't very good. It'll function if the user swipes really slowly, but normal swiping causes weird acceleration of the scrolling effect and finally borks the page. Do Touch events have movementX and movementY equivalents? If not, what's the right way to set this up?
I'd like to set up Hammer.js so that I can respond to horizontal pan events. My first attempt looks like this:
var mc = new Hammer(document.body);
mc.on("panleft panright", runBind(this, 'updatePosition'));
mc.on("panend", runBind(this, 'finalisePosition'));
This almost gets the behaviour that I'm looking for: if I pan left or right, the updatePosition function is called, and when I stop panning the finalisePosition function is called.
But these functions are also triggered if the gesture drifts left or right while scrolling vertically. For example, suppose I touch near the top of the screen then drag my finger down half the screen: that should register as a scroll event. Now suppose that I continue by dragging diagonally: downwards and to the left. In this scenario, I'd like to ignore the horizontal part of the gesture and treat the gesture as a vertical scroll event only, but Hammer.js is triggering the panright and panleft events as before.
My next attempt looks like this:
var mc = new Hammer(document.body, {
recognizers: [
[Hammer.Swipe],
[
Hammer.Pan,
{event: 'panvertical', direction: Hammer.DIRECTION_VERTICAL}
],
[
Hammer.Pan, // RecognizerClass
{direction: Hammer.DIRECTION_HORIZONTAL}, // options
['swipe'], // recognizeWith
['panvertical'] // requireFailure
],
]
});
mc.on("panleft panright", runBind(this, 'updatePosition'));
mc.on("panend", runBind(this, 'finalisePosition'));
This specifies that the horizontal pan events should only be triggered if the panvertical event has failed. Sure enough, this prevents the problem I described above. If I begin a vertical scrolling gesture then start to move horizontally, the panleft and panright events are not triggered. But this version has a more serious probelem: the default scroll behaviour doesn't happen! As a result it's impossible to scroll the app.
Can anyone suggest a better solution?
I'm using Hammer.js version 2.0.4.
I've had the same problem and
I've managed to make it work with this really ugly code
var first = false, lock = false;
var containerHandler = function(event) {
if(event.type == 'panend' || event.type == 'pancancel') {
// iOS bug fix
lock = false;
first = false;
} else if(event.type == 'swipe' && event.direction & Hammer.DIRECTION_VERTICAL) {
// iOS bug fix
lock = true;
first = true;
}
else if(event.type == 'panmove') {
// iOS bug fix ...
if(first === false && event.direction & Hammer.DIRECTION_VERTICAL) {
lock = true;
}
first = true;
if(lock === true)
return;
//your code etc...
};
var focusMC = new Hammer.Manager(mainContainer[0], {domEvents:true});
var pan = new Hammer.Pan({threshold: 5, direction:Hammer.DIRECTION_HORIZONTAL});
focusMC.add( pan );
focusMC.on('panstart panmove panend pancancel swipe', containerHandler);
small threshold is important..
As far as I remember this issue was only on iOS Safari, but on WP it worked correctly even without this code
Edit: Instead of the solution above, try forcing the touch-action property
to pan-y
I'm keeping both answers as they should all work
Hammer.js will automatically infer a touch-action property based on your recognizers. This might make the application more responsive, but it won't prevent vertical page scrolling just because a user is interacting with the element.
I had the same problem you were having, and found a dead simple solution that works pretty well for a workaround.
var isScrolling = false;
var galleryHammer = new Hammer(element, {
recognizers: [
[Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL }]
]
});
// Making the event listener passive means we don't get any delays between
// the user scrolling and the browser having checked if it should prevent
// that event
window.addEventListener('scroll', function() {
isScrolling = true;
}, { passive: true });
window.addEventListener('touchend', function() {
isScrolling = false;
});
galleryHammer.on('pan', function(event) {
if (isScrolling) {
return;
}
// Normal logic...
});
This worked for me (the code comes from a class, and this.hm is simply an Hammer instance):
this.hm.on('panleft', function(e){ // ...and same for panright
if(e.pointerType == 'touch' && (Math.abs(e.deltaY) > Math.abs(e.deltaX))){ return false; }
// do stuff
}
The extra pointerType check is there because I didn't have any issue on desktop computer (mouse events). So the whole is applied ony on touch devices/events.
I have some code which changes the class of a table. On a phone, sometimes the table will be too wide for the screen and the user will drag/scroll about to see the contents. However, when they touch and drag the table around, it triggers touchend on every drag.
How do I test to see whether the touchend came as a result of a touch-drag? I tried tracking dragstart and dragend but I couldn't get that to work and it seems an inelegant approach. Is there something I could add to below which would essentially determine, "Did this touchend come at the end of a drag?"
$("#resultTable").on("touchend","#resultTable td",function(){
$(this).toggleClass('stay');
});
My thanks in advance for your help.
PS - using latest jquery, and while a regular click works, it is very slow in comparison to touchend.
Use two listeners:
First set a variable to false:
var dragging = false;
Then ontouchmove set dragging to true
$("body").on("touchmove", function(){
dragging = true;
});
Then on drag complete, check to see if dragging is true, and if so count it as a dragged touch:
$("body").on("touchend", function(){
if (dragging)
return;
// wasn't a drag, just a tap
// more code here
});
The touch end will still fire, but will terminate itself before your tap script is run.
To ensure the next time you touch it isn't already set as dragged, reset it back to false on touch down.
$("body").on("touchstart", function(){
dragging = false;
});
Looks like one solution to my problem is found here:
http://alxgbsn.co.uk/2011/08/16/event-delegation-for-touch-events-in-javascript/
This bit of code detects any move after touchstart in order to abort tap behavior after tapend.
var tapArea, moved, startX, startY;
tapArea = document.querySelector('#list'); //element to delegate
moved = false; //flags if the finger has moved
startX = 0; //starting x coordinate
startY = 0; //starting y coordinate
//touchstart
tapArea.ontouchstart = function(e) {
moved = false;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
};
//touchmove
tapArea.ontouchmove = function(e) {
//if finger moves more than 10px flag to cancel
//code.google.com/mobile/articles/fast_buttons.html
if (Math.abs(e.touches[0].clientX - startX) > 10 ||
Math.abs(e.touches[0].clientY - startY) > 10) {
moved = true;
}
};
//touchend
tapArea.ontouchend = function(e) {
e.preventDefault();
//get element from touch point
var element = e.changedTouches[0].target;
//if the element is a text node, get its parent.
if (element.nodeType === 3) {
element = element.parentNode;
}
if (!moved) {
//check for the element type you want to capture
if (element.tagName.toLowerCase() === 'label') {
alert('tap');
}
}
};
//don't forget about touchcancel!
tapArea.ontouchcancel = function(e) {
//reset variables
moved = false;
startX = 0;
startY = 0;
};
More here:
https://developers.google.com/mobile/articles/fast_buttons
I would say you can't tell the difference when the user drags to see more content or drag the element arround. I think you should change the approach. You could detect if it's a mobile device and then draw a switch that will enable/disable the movement of the element.
To shorten the solution of #lededge, this might help.
$("body").on("touchmove", function(){
dragging = true;
}).on("touchend", function(){
if (dragging)
return;
}).on("touchstart", function(){
dragging = false;
});
I'd like to mimick iPhone main screen in JavaScript on Safari / Chrome / Firefox.
By mimicking I mean:
- Having a couple of pages
- Switching between the pages by clicking & dragging / swiping with my mouse
- Having those dots from the bottom iPhone main screen displaying which page it is
The closest to what I want is:
http://jquery.hinablue.me/jqiphoneslide/
But the sliding doesn't work nearly as good as in iPhone (i have to slide first, and the animation appears after i release the mouse button), and there are no dots at the bottom.
I solved the problem by using jQuery & jquery.slide-0.4.3.js .
jQuery Slide automatically slides between each page, so I had to add a mouse event (onMouseDrag) that stops automatic slide & reacts to user. It works very well.
This is what I added to jSlide
var jSlide = function(element, options)
{
element = $(element);
$('ul.layers li', element).sliderDisableTextSelect();
// my code here
var dragging = false;
var srcX;
var offsetX;
var diff;
this.manualDown = function(event) {
dragging = true;
srcX = event.pageX;
offsetX = parseFloat($('ul.layers li', element).css('marginLeft'));
obj.settings.easing = "easeOutExpo";
if(obj.settings.loopNr != null) {
obj.toggleLoop(0);
};
return false;
};
this.manualMove = function(event) {
if (dragging) {
diff = event.pageX - srcX;
$('ul.layers li', element).css('marginLeft',(offsetX+diff)+'px');
console.log((offsetX+diff)+'px');
};
return false;
};
this.manualUp = function(event) {
if (dragging) {
dragging = false;
if ((diff<-obj.settings.layerWidth/5) && (obj.settings.slidePos<obj.settings.layersSize-1)) {
obj.slideTo(parseInt(obj.settings.slidePos)+1);
} else if ((diff>obj.settings.layerWidth/5) && (obj.settings.slidePos>0)) {
obj.slideTo(parseInt(obj.settings.slidePos)-1);
} else { // if not slid far enough nor is it the last slide
obj.slideTo(obj.settings.slidePos);
};
};
};
this.manualLeave = function(event) {
if (dragging) {
dragging = false;
if ((diff<0) && (obj.settings.slidePos<obj.settings.layersSize-1)) {
obj.slideTo(parseInt(obj.settings.slidePos)+1);
} else if ((diff>0) && (obj.settings.slidePos>0)) {
obj.slideTo(parseInt(obj.settings.slidePos)-1);
} else { // if it's the last slide
obj.slideTo(obj.settings.slidePos);
};
};
};
element.mousedown(this.manualDown);
element.mousemove(this.manualMove);
element.mouseup(this.manualUp);
element.mouseleave(this.manualLeave);
And also, to prevent text selection when dragging with mouse I added before jSlide class declaration:
$.extend($.fn.disableTextSelect = function() {
return this.each(function(){
if($.browser.mozilla){//Firefox
$(this).css('MozUserSelect','none');
}else if($.browser.msie){//IE
$(this).bind('selectstart',function(){return false;});
}else{//Opera, etc.
$(this).mousedown(function(){return false;}); // this is handled in jSlide
}
});
});
$.extend($.fn.sliderDisableTextSelect = function() {
return this.each(function(){
if($.browser.mozilla){//Firefox
$(this).css('MozUserSelect','none');
}else if($.browser.msie){//IE
$(this).bind('selectstart',function(){return false;});
}else{//Opera, etc.
// $(this).mousedown(function(){return false;}); // this is handled in jSlide
}
});
});
I'm not sure if all the code is necessary... most probably you'll still need to tweak it after pasting into jquery.slide, but it should get you started..
I'm creating a scrolling effect using JQuery and I'm wondering if it's possible to distinguish between the user scrolling vs. programmatically scrolling.
I have something like this:
$('#element').on('scroll',function(e){
$('#element').stop(true); // stop previous scrolling animation
$('#element').animate({ // start new scrolling animation (maybe different speed, different direction, etc)
scrollTop:...
});
});
However, this event is triggered during every step of the animation. How can I tell if this event was triggered by the user or by the animation?
Use a variable to determine when you are scrolling programmatically
Example:
var programScrolling = false;
$('#element').on('scroll',function(e){
if (programScrolling) {
return;
}
$('#element').stop(true); // stop scrolling animation
programScrolling = true;
$('#element').animate({
scrollTop:...
});
programScrolling = false;
});
Not sure if that is exactly what you want, but the concept should work.
I would make functions for different kinds of scrollings to detect them and call a scroll handler for all of them, like so:
JS Fiddle
$(window).bind('mousewheel DOMMouseScroll', function(event){
var direction;
if (event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0) {
direction = 'up';
}
else {
direction = 'down';
}
scrollHandler(direction, 'mouseWheel');
event.preventDefault();
});
var scrollHandler = function(direction, origin) {
var height = $(document).scrollTop();
var movement = (direction == 'up') ? -100 : 100;
console.log(origin);
$('body').stop(true);
$('body').animate({
scrollTop: height + movement
}, 250);
};
Then you can do different stuff according to the origin of the event!
You could also check if the user scrolls to the same direction that the screen is scrolling and do something different, or whatever you want with the info passed by the mousewheel event.
Original mousewheel event function copied from THIS answer
I would suggest possibly using the .originalEvent method. The downside is, this is very browser dependent. See here. Hopefully the following helps:
$('#element').scroll(function(e){
var humanScroll = e.originalEvent === undefined;
if(humanScroll) {
$(this).stop(true);
}
})