I build a Slider that handles touchstart, touchend and touchmove events.
It works fine on Android and iOS.
Only when I'm moving my finger on iOS slowly out of the screen the touchend event won't fire.
After I put my finger back on the screen the touchend event fired immediatly but not the touch start.
Does anybody know why the touchend event won't fire?
I searched for hours to find a solution.
I tried touchcancel but it doesn't help.
Javascript:
this.element = document.createElement('div');
this.element.classList.add('slider');
parent.appendChild(this.element);
//other stuff
this.element.addEventListener('touchstart', ()=> {
console.log('start');
});
this.element.addEventListener('touchend', ()=> {
console.log('end');
});
this.element.addEventListener('touchmove', ()=> {
console.log('move');
});
css:
.slider{
display: block;
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
EDIT #1
I tried also gestureend but it doesn't help.
I found out that it only doesn't fire when I slide to the side with the homebutton (I am in landscape mode).
I've encountered the exact same problem while building a slider in a Vue.js app for ios, wrapped in Cordova.
I didn't find the cause of this behaviour where touchend events don't fire at the 'button-end' of the ios device, but I did create a workaround.
I'm assuming this is no longer relevant for you, but it might help someone else who has this problem:
Context of my events (for non-Vue users this.$refs.profileSlider is just a DOM reference to the element wrapping my slider):
this.$refs.profileSlider.addEventListener('mousedown', this.lock, false);
this.$refs.profileSlider.addEventListener('touchstart', this.lock, false);
this.$refs.profileSlider.addEventListener('mousemove', this.drag, false);
this.$refs.profileSlider.addEventListener('touchmove', this.drag, false);
this.$refs.profileSlider.addEventListener('mouseup', this.move, false);
this.$refs.profileSlider.addEventListener('touchend', this.move, false);
To ensure that the move() function is called even when the touchend event doesn't fire (when a user swipes off the end of the screen), I added a conditional check at the end of the touchmove handler, here called drag:
// Rest of touchmove handler here
const unified = this.unify(e);
if (unified.clientX >= window.innerWidth - 3 || unified.clientX <= 3) {
this.$refs.profileSlider.dispatchEvent(new TouchEvent('touchend', e));
}
e in this function is the context of the the touchmove event. This way the drag() handler will run as normal, then check if the touchmove event occurred within 3px (this sensitivity works for my purposes) of either side of the screen. If so, it fires off a new touchend TouchEvent and passes it the same e value as the previous touchmove.
This resolves the problem of touchend not firing when a user's touch continues off the edge of the screen. Instead touchend is fired preemptively, just before the swipe would otherwise exit the screen.
// For reference, the unify function called in the above block simply checks for changedTouches on the event, and returns the first changedTouch if it exists:
unify(e) {
return e.changedTouches ? e.changedTouches[0] : e;
}
Related
Background
I'm trying to achieve something, but it's really driving me crazy. So any help would be appreciated!
I've created a scene in Matter.js that will be placed in a container further down on the page. I want the visitor to be able to interact with the scene, dragging and dropping the bodies. But allowing interaction creates the problem where Matter.js prevents the user from scrolling whenever the mouse is over the canvas.
So to work around this, I'm using the following code:
mouseConstraint.mouse.element.removeEventListener("mousewheel", mouseConstraint.mouse.mousewheel);
mouseConstraint.mouse.element.removeEventListener("DOMMouseScroll", mouseConstraint.mouse.mousewheel);
This makes it possible for the user to scroll through the page and still being able to interact with the scene by clicking and dragging the bodies, as it's only the scroll event listeners that are being removed. At least on desktop.
The problem
However, on mobile, the touch event is the event that makes it possible for the user to scroll on the page, so that would require me to also remove the touchmove, touchstart and touchend event listeners from the mouse constraint in Matter.js. Like this:
mouseConstraint.mouse.element.removeEventListener('touchmove', mouseConstraint.mouse.mousemove);
mouseConstraint.mouse.element.removeEventListener('touchstart', mouseConstraint.mouse.mousedown);
mouseConstraint.mouse.element.removeEventListener('touchend', mouseConstraint.mouse.mouseup);
And here's where the problem occurs. If I remove the touch events, the user can scroll by the canvas, but the user can't interact with the scene as that requires the touch events to be activated.
So I'm wondering if there is any way to have the touch events added but only allow them to work on certain bodies in the scene? I've found that I can use mouseConstraint.body as a boolean in order to know if a body has been clicked/touched or not. Could this be used in some way with this as a base?:
Matter.Events.on(mouseConstraint, "mousedown", function(event) {
if (mouseConstraint.body) {
console.log("Body clicked!");
}
});
This is the solution that I came up with. It's not perfect, but it's better than nothing.
let touchStart;
mouseConstraint.mouse.element.addEventListener('touchstart', (event) => {
if (!mouseConstraint.body) {
touchStart = event;
}
});
mouseConstraint.mouse.element.addEventListener('touchend', (event) => {
if (!mouseConstraint.body) {
const startY = touchStart.changedTouches[0].clientY;
const endY = event.changedTouches[0].clientY;
const delta = Math.abs(startY - endY);
if (delta > 80) {
window.scrollTo(0, 600);
}
}
});
You listen for the touch start event and store that in a variable. Then in the touchend event you check if the swipe is large enough to constitute a scroll, then scroll to the content.
I have animation with ScrollMagic library and I am also using GSAP. This is description of animation on scroll in steps. Every number is one scroll:
Add class overflow-hidden to body, to disable scrolling.
Move credit-cards
Move remove some images
Start doing transform: translate(x,y) rotateZ(zdeg)
Stop scrolling and make image that was translated sticky
So this works good with mouse on mousewheel event. The question is:
What is the best way to implement touch scroll with very same effect when user comes from iOS or Andorid. (When user comes to my website from android, iPhone, iPad etc.)
I know that there is touchmove event.
var image = document.getElementById('image-phone');
if(step == 1){
//do first step
}...
//mousewheel event
window.addEventListener('mousewheel', function (e) {
//this is implemented
});
//touchnmove event
window.addEventListener('touchmove', function (e) {
//should I use this event
});
When you scroll to the middle of the page and then refresh it, the browser will automatically scroll to the position you were on. I need to be able to differentiate between this automatic scroll and user scroll and attach different events to them.
I'm currently using
window.addEventListener('scroll', function(e) {});
to listen to the scroll event, but it gets triggered in both user and automatic scrolls.
Is there a way to tell between the two?
PS the suggested answer to similar question only uses mousewheel as an indication of user scroll, but if the user uses mouse to pull the scroll, it will fail
If you have two flags for both events, will that work?
I don't have a mouse to test this in whole unfortunately.
Try this fiddle
window.addEventListener( 'load', function() {
var mousewheel = 0;
var scroll = 0;
window.addEventListener( 'scroll', function(e) {
mousewheel = 0;
scroll = 1;
alert("mousewheel: " + mousewheel + ", scroll: " + scroll);
}, false);
window.addEventListener("mousewheel", function(e) {
mousewheel = 1;
scroll = 0;
alert("mousewheel: " + mousewheel + ", scroll: " + scroll);
}, false);
}, false);
As Manohar previously mentioned, you can use some combination of an onscroll event and an onwheel event. onscroll runs on all scroll events, whenever the content is interacted with. onwheel will run on mouse or trackpad scrolls, but not arrow scrolling. It's also worth mentioning that onwheel is not guaranteed to trigger an onscroll event, depending on the context. There's more detail provided in its documentation.
There's another similar question here with some additional suggestions on redesigning your app, while setting some boolean flags that can help determine who (computer or real user) is initiating the scroll.
i listen for touchstart and touchend events to make my app more responsive for mobile.
the problem is, if you 'flick scroll' (the page is still scrolling even after finger has left screen), and then stop the scroll with a tap - if there is an event on touchend attached to the element you tapped, it will fire.
I need a way to detect if the touchstart or touchend has stopped a scroll, so i can stop any events firing.
I tried setting a variable on scroll (i noticed scroll event on mobile only fires after scroll has finished, i.e page has stopped even on momentum scrolling):
$(window).scroll(function(){
cancelled_scrolling = true;
setTimeout(function(){
cancelled_scrolling = false;
},200);
});
however, when i tap it seems the touchend fires before the .scroll() event, as this doesn't work:
$('body').on('touchend', function(){
if(cancelled_scrolling){
alert('ahahahah');
return false;
}
//code to trigger events depending on event.target after here
});
how can I achieve this?
EDIT:
found an answer to this -
step1 - save the scrollTop on touchend
step2 - on touchstart, check the saved scrollTop against a new scrollTop
if they don't match, the page was scrolled even after the touchend event occurred
On touchStart, keep track of the scrollTop and scrollLeft for each parent node. On touchMove, check if any of these values have changed. If so, cancel the touch. This is slightly better than just checking on touchEnd because maybe they scrolled the page and then unscrolled.
You can also see this logic implemented here: https://github.com/JedWatson/react-tappable/blob/cf755ea0ba4e90dfa6ac970316ff7c35633062bd/src/TappableMixin.js#L120
I can't seem to capture the scroll event on an iPad.
None of these work, what I am doing wrong?
window.onscroll=myFunction;
document.onscroll=myFunction;
window.attachEvent("scroll",myFunction,false);
document.attachEvent("scroll",myFunction,false);
They all work even on Safari 3 on Windows.
Ironically, EVERY browser on the PC supports window.onload= if you don't mind clobbering existing events. But no go on iPad.
The iPhoneOS does capture onscroll events, except not the way you may expect.
One-finger panning doesn’t generate any events until the user stops panning—an onscroll event is generated when the page stops moving and redraws—as shown in Figure 6-1.
Similarly, scroll with 2 fingers fires onscroll only after you've stopped scrolling.
The usual way of installing the handler works e.g.
window.addEventListener('scroll', function() { alert("Scrolled"); });
// or
$(window).scroll(function() { alert("Scrolled"); });
// or
window.onscroll = function() { alert("Scrolled"); };
// etc
(See also https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html)
For iOS you need to use the touchmove event as well as the scroll event like this:
document.addEventListener("touchmove", ScrollStart, false);
document.addEventListener("scroll", Scroll, false);
function ScrollStart() {
//start of scroll event for iOS
}
function Scroll() {
//end of scroll event for iOS
//and
//start/end of scroll event for other browsers
}
Sorry for adding another answer to an old post but I usually get a scroll event very well by using this code (it works at least on 6.1)
element.addEventListener('scroll', function() {
console.log(this.scrollTop);
});
// This is the magic, this gives me "live" scroll events
element.addEventListener('gesturechange', function() {});
And that works for me. Only thing it doesn't do is give a scroll event for the deceleration of the scroll (Once the deceleration is complete you get a final scroll event, do as you will with it.) but if you disable inertia with css by doing this
-webkit-overflow-scrolling: none;
You don't get inertia on your elements, for the body though you might have to do the classic
document.addEventListener('touchmove', function(e) {e.preventDefault();}, true);
I was able to get a great solution to this problem with iScroll, with the feel of momentum scrolling and everything https://github.com/cubiq/iscroll The github doc is great, and I mostly followed it. Here's the details of my implementation.
HTML:
I wrapped the scrollable area of my content in some divs that iScroll can use:
<div id="wrapper">
<div id="scroller">
... my scrollable content
</div>
</div>
CSS:
I used the Modernizr class for "touch" to target my style changes only to touch devices (because I only instantiated iScroll on touch).
.touch #wrapper {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}
.touch #scroller {
position: absolute;
z-index: 1;
width: 100%;
}
JS:
I included iscroll-probe.js from the iScroll download, and then initialized the scroller as below, where updatePosition is my function that reacts to the new scroll position.
# coffeescript
if Modernizr.touch
myScroller = new IScroll('#wrapper', probeType: 3)
myScroller.on 'scroll', updatePosition
myScroller.on 'scrollEnd', updatePosition
You have to use myScroller to get the current position now, instead of looking at the scroll offset. Here is a function taken from http://markdalgleish.com/presentations/embracingtouch/ (a super helpful article, but a little out of date now)
function getScroll(elem, iscroll) {
var x, y;
if (Modernizr.touch && iscroll) {
x = iscroll.x * -1;
y = iscroll.y * -1;
} else {
x = elem.scrollTop;
y = elem.scrollLeft;
}
return {x: x, y: y};
}
The only other gotcha was occasionally I would lose part of my page that I was trying to scroll to, and it would refuse to scroll. I had to add in some calls to myScroller.refresh() whenever I changed the contents of the #wrapper, and that solved the problem.
EDIT: Another gotcha was that iScroll eats all the "click" events. I turned on the option to have iScroll emit a "tap" event and handled those instead of "click" events. Thankfully I didn't need much clicking in the scroll area, so this wasn't a big deal.
Since iOS 8 came out, this problem does not exist any more. The scroll event is now fired smoothly in iOS Safari as well.
So, if you register the scroll event handler and check window.pageYOffset inside that event handler, everything works just fine.
After some testing on the ios, I found that this is the way to go for ios and desktop, if you are not worried of that delay of 120ms on desktop. Works like a charm.
let isScrolling;
document.addEventListener("scroll", () => {
// Clear our timeout throughout the scroll
window.clearTimeout( isScrolling );
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function() {
// Run the callback
console.log( 'Scrolling has stopped.' );
}, 120);
});