I am trying to do something similar to what embedded Google maps do. My component should ignore single touch (allowing user to scroll page) and pinch outside of itself (allowing user to zoom page), but should react to double touch (allowing user to navigate inside the component) and disallow any default action in this case.
How do I prevent default handling of touch events, but only in the case when user is interacting with my component with two fingers?
What I have tried:
I tried capturing onTouchStart, onTouchMove and onTouchEnd. It turns out that on FF Android the first event that fires when doing pinch on component is onTouchStart with a single touch, then onTouchStart with two touches, then onTouchMove. But calling event.preventDefault() or event.stopPropagation() in onTouchMove handler doesn't (always) stop page zoom/scroll. Preventing event escalation in the first call to onTouchStart does help - unfortunately at that time I don't know yet if it's going to be multitouch or not, so I can't use this.
Second approach was setting touch-action: none on document.body. This works with Chrome Android, but I could only make it work with Firefox Android if I set this on all elements (except for my component). So while this is doable, it seems like it could have unwanted side effects and performance issues. EDIT: Further testing revealed that this works for Chrome only if the CSS is set before the touch has started. In other words, if I inject CSS styles when I detect 2 fingers then touch-action is ignored. So this is not useful on Chrome.
I have also tried adding a new event listener on component mount:
document.body.addEventListener("touchmove", ev => {
ev.preventDefault();
ev.stopImmediatePropagation();
}, true);
(and the same for touchstart). Doing so works in Firefox Android, but does nothing on Chrome Android.
I am running out of ideas. Is there a reliable cross-browser way to achieve what Google apparently did, or did they use multiple hacks and lots of testing on every browser to make it work? I would appreciate if someone pointed out an error in my approach(es) or propose a new way.
TL;DR: I was missing { passive: false } when registering event handlers.
The issue I had with preventDefault() with Chrome was due to their scrolling "intervention" (read: breaking the web IE-style). In short, because the handlers that don't call preventDefault() can be handled faster, a new option was added to addEventListener named passive. If set to true then event handler promises not to call preventDefault (if it does, the call will be ignored). Chrome however decided to go a step further and make {passive: true} default (since version 56).
Solution is calling the event listener with passive explicitly set to false:
window.addEventListener('touchmove', ev => {
if (weShouldStopDefaultScrollAndZoom) {
ev.preventDefault();
ev.stopImmediatePropagation();
};
}, { passive: false });
Note that this negatively impacts performance.
As a side note, it seems I misunderstood touch-action CSS, however I still can't use it because it needs to be set before touch sequence starts. But if this is not the case, it is probably more performant and seems to be supported on all applicable platforms (Safari on Mac does not support it, but on iOS it does). This post says it best:
For your case you probably want to mark your text area (or whatever)
'touch-action: none' to disable scrolling/zooming without disabling
all the other behaviors.
The CSS property should be set on the component and not on document as I did it:
<div style="touch-action: none;">
... my component ...
</div>
In my case I will still need to use passive event handlers, but it's good to know the options... Hope it helps someone.
Try using an if statement to see if there is more than one touch:
document.body.addEventListener("touchmove", ev => {
if (ev.touches.length > 1) {
ev.preventDefault();
ev.stopImmediatePropagation();
}
}, true);
This is my idea:
I used one div with opacity: 0 to cover the map with z-index > map z-index
And I will detect in the covered div. If I detect 2 fingers touched in this covered div, I will display: none this div to allow user can use 2 finger in this map.
Otherwise, in document touchEnd I will recover this covered div using display: block to make sure we can scroll.
In my case I have solved it with
#HostListener('touchmove', ['$event'])
public onTouch(event: any): void {
event.stopPropagation();
console.log('onTouch', event);
}
Related
I'm looking for the best practices with regard to Chromium that would allow me to convert either a "drag" event or a "hold" event into a "click" event.
I'm building a touch screen application that runs on a TS-7990 touchscreen that is giving me fits mainly because the users have a tendency to hit the buttons on an angle which registers as a "drag" event as opposed to a "click" event. I'm using ExtJS 6.2 for this, but I would LOVE to have a solution that involves a few Chromium switches, perhaps in order to make the changes global and sweeping for this device. Any ideas?
So, my discovery is that I could fix this with just a few lines of code in ExtJS.
Ext.define('override.event.gesture.Tap', {
override: 'Ext.event.gesture.Tap'
}, function (Tap) {
Tap.instance.setMoveDistance(4000);
});
By setting the move distance to 4000, I am basically saying, "If your drag is less than 4000, treat it as a "click". Previously the value was set to 8 pixels. Which any ham fisted engineer would be able to provide with a glancing blow to the touch screen.
For a solution that 'involves a few Chromium switches' as you mentioned, you need to enable Chromium's touch events, then convert the touchmove event into a click event.
turn on chromium's touch events when it starts
start chromium with chromium-browser --touch-events=enabled
This gives you touchstart, touchmove, touchstop, and touchcancel events. See MDN Touch Events for a great explanation.
set a flag in your JavaScript code (maybe in index.html) when these touch events exist
var isTouchDevice = 'ontouchstart' in document.documentElement;
when this flag is true, intercept the 'drag' and do a 'click' instead. This is one simple way to accomplish that:
if (isTouchEvent) {
document.ontouchmove = function (e) {
e.target.click();
}
}
I'm making an app to draw inside an HTML5 canvas, but I can not do it from the mobile or tablet.
I can not avoid the browser's native scroll or chrome refresh when pushed down
I have created an example in jsfiddle so you can see it.
To suppress default UA behaviour you need to add the CSS property touch-action: none to the canvas element.
touch-action specifies if and how a HTML element should respond to gestures. With touch-action: none no UA behaviour is triggered (e.g. dragging or zooming). The default property is touch-action: auto, which allows all UA behaviour to be triggered.
This looks like a genuine Chrome bug, which I will report using your sample code. Setting a touch-action in CSS isn't a solution if you want to dynamically decide in JavaScript whether to handle the pointerMove with your own code vs. allow the native browser handling.
If you set touch-action: "none", then the native browser handling will never run. Conversely, as you've reported, having no touch-action setting, which is equivalent to the default of "auto", won't work because Chrome ignores calls to preventDefault() on pointerMove and still calls pointerCancel - a bug.
Note that if you added a listener for touchMove instead, using the passive: false flag, then preventDefault() would work as expected, avoiding touchCancel. However, then you've got to use separate mouse event and touch event listeners, which is what you're trying to avoid by using pointer events.
I've created a web application that works well with touch events and click events with their own respective handlers.
However, issues are encountered when moving to touch IE11. Touch works fine on other platforms. The mouse on IE11 works great. But if you use both a touch screen and IE11, then the touch events are wrapped as pointer events instead of touch events or click events.
Is there any way to just globally map pointer touch events to pre-defined touch events for the entire window/document? It seems that it is already doing this by default for click events in IE11...
I tried following code example as displayed here: JavaScript mapping touch events to mouse events
And ended up with:
if(window.PointerEvent)
{
...
document.onpointerdown = function(e){
var event = document.createEvent("TouchEvent");
...
}
...
}
However, IE errors at document.crateEvent("TouchEvent") claiming that it is "not supported." However, the TouchEvent object is documented in IE as being supported: https://msdn.microsoft.com/en-us/subscriptions/dn792856(v=vs.85)
as specified in http://www.w3.org/TR/touch-events/#list-of-touchevent-types
I've also tried changing it to
var event = document.createEvent("UIEvent");
which IE allows, but then does later not recognize
event.initTouchEvent(someParameters);
I'm starting to believe that IE11 doesn't support creation of touch events.
I've also seen talk about changing the .css files to include combinations of:
touch-action: none
ms-touch-action: none
pointer-events: none
but completely disabling touch tends to break the application in other ways that I was hoping to avoid.
Is there any way to fire touch events instead of Microsoft's pointer events?
I would say you're actually going about this the wrong way. There are several pollyfill libraries to map touch events to pointer events. HandJS and jQuery PEP are two well functioning examples. Given that the only modern browser that doesn't support pointer events at this time is Safari, that seems the better way to go (Who wants pointer events...).
I am making a mobile website that uses JavaScript touch events. Things work fine in iOS Safari and Chrome for Android, but the stock Android Browser (version 4.1.2) is giving me trouble.
During a touch process, the touchstart and touchmove events are called as expected. However, one of the actions performed by the touchmove handler seems to trigger a premature touchcancel event. (I'm not sure whether this is significant, but the action that triggers the touchcancel is the modification of an SVG object's viewBox attribute.) If I comment out this action, the touch process proceeds normally (i.e., completion of touchmove through to touchend).
All of my touch handlers call the preventDefault() function, so the issue isn't the one that's described in this bug: https://code.google.com/p/android/issues/detail?id=19827.
I've read that there is a lot of inconsistency among browsers as to when touchcancel is called. The stock Android browser is the only one that is problematic for me.
Is there a workaround out there? For example, is there away I can completely disable the touchcancel event? Another idea I had was to have the touchcancel handler programmatically trigger another touchstart/touchmove event, but I didn't get very far with that. Any ideas would be appreciated.
I know it's a bit late but if someonee else is facing this issue, here's a small jQuery extension that explains how to deal with pointercancel and touchcancel events.
Basically, if you switch to pointer events you will be able to prevent pointercancel by simply using the touch-action CSS property with a value of none, as long as the browser has both features correctly implemented. (Android 5+, Chrome 55+, IE11, Edge, etc)
If you really must use the legacy touch events instead, you'll have to implement event.preventDefault() in your touchmove events and this will prevent touchcancel from firing, but it will also disable entirely the browser handling of any default action like a pan or click.
As a final note, I wouldn't use touch events + touch-action CSS rules because touch-action was only recently added, at the same that that Pointer Events. So while that combination may work in newer browsers, it will most certainly fail in older ones (by triggering touchcancel event unexpectedly).
Check the README from the jQuery extension I posted because it explains the implications of using either TouchEvent and PointerEvent interfaces.
If you are useing hammer.js, you can disable pointer-events (which cause problems like this), by adding:
delete window.PointerEvent;
in your index.html PRIOR to loading hammer.js, and it will ignore those events.
You can also set SUPPORT_POINTER_EVENTS = false; (line 384 of v2.0.8) to do the same thing.
Ideally the devs would add the ability to turn this off, but so goes the open source dilemma...
I think touchcansel is special event that triggers when user touched button in specific behavior.
Ex. User touch button, but didn't removed his finger from display on exactly that button, instead of this he could move it a bit to the left, and end his action there. In that case touchcancel would appear.
So if you don't want that action to fire, i think you have to remove all handlers that connected with that action in android browser.
$('selector').off('touchcancel'), of if it's delefated $('parent').undelegate('touchcancel','selector')
I don't think that there is problem with e.preventDefault(). Only may be if you click on a link, and page starts to reload. May be page is reloaded when you click on button? and you wait for some actions after page reloaded, you actually may not understand, that it happend.
I've read that mobile Safari has a 300ms delay on click events from the time the link/button is clicked to the time the event fires. The reason for the delay is to wait to see if the user intends to double-click, but from a UX perspective waiting 300ms is often undesirable.
One solution to eliminate this 300ms delay is to use jQuery Mobile "tap" handling. Unfortunately I'm not familiar with this framework and don't want to load some big framework if all I need is a line or two of code applying touchend in the right way.
Like many sites, my site has many click events like this:
$("button.submitBtn").on('click', function (e) {
$.ajaxSubmit({... //ajax form submisssion
});
$("a.ajax").on('click', function (e) {
$.ajax({... //ajax page loading
});
$("button.modal").on('click', function (e) {
//show/hide modal dialog
});
and what I'd like to do is to get rid of the 300ms delay on ALL those click events using a single code snippet like this:
$("a, button").on('tap', function (e) {
$(this).trigger('click');
e.preventDefault();
});
Is that a bad/good idea?
Now some mobile browsers eliminate 300 ms click delay if you set the viewport. You don't need to use workarounds anymore.
<meta name="viewport" content="width=device-width, user-scalable=no">
This is currently supported Chrome for Android, Firefox for Android and Safari for iOS
However on iOS Safari, double-tap is a scroll gesture on unzoomable pages. For that reason they can't remove the 300ms delay. If they can't remove the delay on unzoomable pages, they're unlikely to remove it on zoomable pages.
Windows Phones also retain the 300ms delay on unzoomable pages, but they don't have an alternative gesture like iOS so it's possible for them to remove this delay as Chrome has. You can remove the delay on Windows Phone using:
html {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
Source: http://updates.html5rocks.com/2013/12/300ms-tap-delay-gone-away
UPDATE 2015 December
Until now, WebKit and Safari on iOS had a 350ms delay before single taps activate links or buttons to allow people to zoom into pages with a double tap. Chrome changed this a couple of months ago already by using a smarter algorithm to detect that and WebKit will follow with a similar approach. The article gives some great insights how browsers work with touch gestures and how browsers can still get so much smarter than they are today.
UPDATE 2016 March
On Safari for iOS, the 350 ms wait time to detect a second tap has been removed to create a “fast-tap” response. This is enabled for pages that declare a viewport with either width=device-width or user-scalable=no. Authors can also opt in to fast-tap behavior on specific elements by using the CSS touch-action: manipulation as documented here (scroll down to the 'Styling Fast-Tap Behavior' heading) and here.
This plugin -FastClick developed by Financial Times does it perfectly for you!
Make sure though to add event.stopPropagation(); and/or event.preventDefault(); directly after the click function, otherwise it might run twice as it did for me, i.e.:
$("#buttonId").on('click',function(event){
event.stopPropagation(); event.preventDefault();
//do your magic
});
i know this is old but can't you just test to see if "touch" is supported in the browser? Then create a variable that's either "touchend" or "click" and use that variable as the event that gets bound to your element?
var clickOrTouch = (('ontouchend' in window)) ? 'touchend' : 'click';
$('#element').on(clickOrTouch, function() {
// do something
});
So that code sample checks to see if the "touchend" event is supported in the browser and if not then we use the "click" event.
(Edit: changed "touchend" to "ontouchend")
I've come across a hugely popular alternative called Hammer.js (Github page) which I think is the best approach.
Hammer.js is a more full-featured touch library (has many swipe commands) than Fastclick.js (most upvoted answer).
Beware though: scrolling fast on mobile devices tends to really lock up the UI when you use either Hammer.js or Fastclick.js. This is a major problem if your site has a newsfeed or an interface where users will be scrolling a lot (would seem like most web apps). For this reason, I'm using neither of these plugins at the moment.
Somehow, disabling zoom seems to disable this small delay. Makes sense, as double-tap isn't needed anymore then.
How can I "disable" zoom on a mobile web page?
But please be aware of the usability impact this will have. It may be useful for webpages designed as apps, but shouldn't be used for more general-purpose 'static' pages IMHO. I use it for a pet project that needs low latency.
Unfortunately there is no easy way to do this. So just using touchstart or touchend will leave you with other problems like someone starts scrolling when click on on a button for example. We use zepto for a while, and even with this really good framework there are some issues that came up over the time. A lot of them are closed, but it seems is not a field of simple solution.
We have this solution to globally handle clicks on links:
$(document.body).
on('tap', 'a',function (e) {
var href = this.getAttribute('href');
if (e.defaultPrevented || !href) { return; }
e.preventDefault();
location.href= href;
}).
on('click', 'a', function (e) {
e.preventDefault();
});
I searched for an easy way without jquery and without fastclick library. This works for me:
var keyboard = document.getElementById("keyboard");
var buttons = keyboard.children;
var isTouch = ("ontouchstart" in window);
for (var i=0;i<buttons.length;i++) {
if ( isTouch ) {
buttons[i].addEventListener('touchstart', clickHandler, false);
} else {
buttons[i].addEventListener('click', clickHandler, false);
}
}
In jQuery you can bind "touchend" event, witch trigger code inmediatly after tap (is like a keydown in keyboard). Tested on current Chrome and Firefox tablet versions. Don't forget "click" also, for your touch screen laptops and desktop devices.
jQuery('.yourElements').bind('click touchend',function(event){
event.stopPropagation();
event.preventDefault();
// everything else
});
Just to provide some extra information.
On iOS 10, <button>s on my page could not be triggered continuously. There was always a lag.
I tried fastclick / Hammer / tapjs / replacing click with touchstart, all failed.
UPDATE: the reason seems to be that the button is too close to the edge! move it to near the center and lag gone!
You're supposed to explicitly declare passive mode :
window.addEventListener('touchstart', (e) => {
alert('fast touch');
}, { passive : true});