I'm working on an interface to allow the user to drag an object around the screen using hammer.js. In a desktop browser it works, but on mobile when I start dragging it works briefly, then suddenly acts as if the event's client coordinates suddenly went to 0,0.
I've got a simple SVG circle I'm trying to drag:
<svg version="1.1" viewBox="0 0 640 480" id="svg" style="width: 640px; height: 480px; top:0px; left: 0px; position: absolute;">
<circle class="selhan" cx="320" cy="240" r="200" id="circ"
style="fill: none; stroke: rgb(0, 0, 0); pointer-events: all;
touch-action: none; user-select: none; -webkit-user-drag: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">
</circle>
</svg>
I'm setting up a simple pan handler:
var circ = document.getElementById("circ")
var Hn1=new Hammer(circ, {/*domEvents:true*/})
Hn1.add( new Hammer.Pan({ direction: Hammer.DIRECTION_ALL, threshold: 0, pointers:1 }) )
var X0,Y0
Hn1.on("panstart", function(e) {
var ob = e.target
var cx = ob.getAttribute("cx")
var cy = ob.getAttribute("cy")
X0=e.center.x-cx
Y0=e.center.y-cy
})
Hn1.on("pan", function(e) {
var ob = e.target
var X = e.center.x-X0
var Y = e.center.y-Y0
ob.setAttribute("cx",X)
ob.setAttribute("cy",Y)
})
I have created a Fiddle to demonstrate. On mobile, if I put my finger in the center of the circle and move very slowly I see the circle track for a brief period of time, but then suddenly the center jumps to 0,0, whereas on desktop it follows the pointer around all day.
I created an Updated Fiddle which displays when panstart, pancancel, and panend events occur. What I am seeing is that when the circle jumps to the origin I am getting a pancancel event. Why is my pan being cancelled even though I haven't lifted my finger from the screen?
Even stranger is this version. All I did was reverse the order of the spans (which display counters of how many times panstart, pancancel, and panend have occurred). If the spans come after the SVG, then suddenly I can pan the circle, but only horizontally. But if the spans come before or aren't there, then panning gets abruptly cancelled after a short amount of finger movement. Ok, turns out it's the element that is scrolling, not the circle. However, version 41 behaves strangely on desktop too... when I first tried it, I couldn't get it to respond to mouse events at all, but then it started working, and then it stopped again, all by simply changing to different versions and back.
Update: In an attempt to get to the bottom of why the pancancel callback is being called, I instrumented my copy of hammer.js. The PanRecognizer directionTest function (line 1778) is returning false, causing the cancel to be generated. What happens is always some form of "nothing happened", resulting in hasMoved being false, or distance being 0 (I tried setting threshold to -1, but this just moved where the problem showed up), or direction being DIRECTION_NONE.
This still doesn't make a lot of sense - I see this happen even when I'm panning at a fast enough rate that I should never get zero movement between two events, and even if I was panning slowly I wouldn't expect this to be a problem.
Update 2:
I added the following instrumentation:
Hn1.on("hammer.input",function(ev) {
console.log("debug",ev)
})
When I do this, I see a source event pointercancel which suggests that the browser is cancelling the pointer motion in the middle of is actually being used. In reviewing the documentation for this event, none of the four reasons mentioned for a pointer event being cancelled applies in this case. What is going on here?!
Michael, I ran into the same problem as you. I've filed a bug report on GitHub with example and video. Interesting to see you uncovered it has something to do with "pointercancel" being triggered by the browser, but so far it hasn't helped me figure out how to patch this issue. Did you make any progress on the matter?
For reference, this StackOverflow thread talks about the same issue. Unfortunately, none of the fixes has so far worked for me for HammerJS.
EDIT:
It turns out that preventing the default "touchmove" event handling on the window will solve this problem for any SVG element on the page. That's great news, but it also disables all scrolling behaviour on the page. What I've done instead is catching the "touchmove" event on the SVG object, so any descendants in it will pan properly. I'm not yet sure if it has any bad side effects, but so far it has meant that my panning in SVG finally works on Android browsers!
For completeness' sake, here is the necessary code:
const options = { passive: false }; // needed because Chrome has this set to "true" by default for "touchmove" events
mySVG.addEventListener(
"touchmove",
e => e.preventDefault(),
options
);
I just remembered that I just need to add jquery.ui.touch-punch.js and it works fine on mobile
library:
http://touchpunch.furf.com/
Related
I'm trying to build a simple prototype in which I want to capture whether a user is either entering or leaving the website on desktop. Through 2 strips and a onmouseover/onmouseout I want to understand whether it's an inbound or outbound case. Hence the expected outcome is to get to the following sequences:
Outbound sequence: MouseIn: lowerStrip - MouseOut: lowerStrip - MouseIn: upperStrip - MouseOut: upperStrip
Inbound sequence: MouseIn: upperStrip - MouseOut: upperStrip - MouseIn: lowerStrip - MouseOut: lowerStrip
Unfortunately, the sequence produced by my prototype is not always the same - sometimes the 4 lines are printed to the console, sometimes only 2, and sometimes not even a single one. Seems like others have struggled with this same issue, be it in different languages and different contexts:
How do you Hover in ReactJS? - onMouseLeave not registered during fast hover over
onMouseEnter and onMouseLeave not functioning as expected
Mouseout and mouseleave not working
mouseenter and mouseleave in javascript not working
Regardless of some insights gathered from these solutions, I couldn't figure out how to actually make my solution more robuust, especially because I couldn't understand the actual root cause of the issue. Some where talking about speed being the issue, but even when scrolling over quite slow, the issue occurs.
Hence I'd like to understand what is actually causing this behaviour and what the best solution is to resolve this e.g. is it an issue in JS and could it be resolved with JS, is it an issue in JS and should it be resolved by CSS, is it another issue, ...
I attached my example below. The idea is to hover over the two strips, either coming from the bottom or the top. This should trigger the sequence mentioned earlier.
Thanks for your input.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template</title>
<style>
</style>
<style>
</style>
<script type='text/javascript'>
function init() {
document.getElementById('upperStrip').setAttribute('style', 'position: absolute; margin: 0 !important; top: 0px; width: ' + window.innerWidth +'px; border: 5px solid #ff0000');
document.getElementById('lowerStrip').setAttribute('style', 'position: absolute; margin: 0 !important; top: 10px; width: ' + window.innerWidth +'px; border: 5px solid #000000');
}
function onMouseIn(x) {
console.log("MouseIn: ", x.id);
}
function onMouseOut(x) {
console.log("MouseOut: ", x.id);
}
</script>
</head>
<body onload="init()">
<hr id='upperStrip' onmouseover="onMouseIn(this)" onmouseout="onMouseOut(this)">
<hr id='lowerStrip' onmouseover="onMouseIn(this)" onmouseout="onMouseOut(this)">
</body>
</html>
EDIT: 04/07/2020
Meanwhile I figured out the root cause:
"The mousemove event triggers when the mouse moves. But that doesn’t mean that every pixel leads to an event. The browser checks the mouse position from time to time. And if it notices changes then triggers the events. That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped." (https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave)
"In case of fast mouse movements, intermediate elements may be ignored, but one thing we know for sure: if the pointer “officially” entered an element (mouseover event generated), then upon leaving it we always get mouseout."
Based on that, I'm now looking for a solution that is robust for my particular case. Waiting for the events seems an option, but I can also not wait 'too long' because I'm trying to register an exit intent. Also I need to at least be able to generate a onmouseover before even having a chance to capture the onmouseout.
I found a source which mentioned that: "Mouse does not report its position by every pixel it passes, there are 20ms intervals between reports. If you manage to cross your control within this interval, it will catch no mouse events at all."
Hence I should be able to capture the 4 events in ~80ms? The question is how...
First of, this looks like a chromium bug and I already submitted a bug report. It's moving slowly so I'm opening a question here mainly for a workaround solution if anyone else has come across similar or related issues.
Also, code examples are not HTML5 compliant for readability reasons. I'm just showing a reduced example.
Description
In Chrome 78 and Edge beta, when there are 2+ nested iframes with different origin and there is an overlapping element, pointerout/pointerover events are misfiring between each click inside the iframe - only when there is at least 1px movement between pointerdown and pointerup. On Chrome 77, Edge stable, Firefox and Safari these events fire only when the pointer leaves the iframe or enters it respectively, which is the correct and expected behaviour.
When removing the overlapping element, Chrome 78 behaves as expected but Edge beta doesn't (Edge beta is unaffected by the overlapping element case).
When setting the iframe origins to be similar (ex. Both localhost), it behaves as expected in both browsers.
Reproduction
Start a server (like http-server for simplicity) in a folder containing the following files
a.html
<iframe src="http://127.0.0.1:[port]/b.html"></iframe>
<div style="height: 3px;
width: 300px;
position: absolute;
top: 159px;
left: 10px;
right: 0px;
background: green;"></div>
b.html
<iframe src="http://localhost:[port]/c.html"></iframe>
c.html
<button id="btn">Click</button>
<script type="text/javascript">
var btn = document.getElementById('btn');
btn.addEventListener('pointerdown', function() { console.log('down'); });
btn.addEventListener('pointerup', function() { console.log('up'); });
btn.addEventListener('pointerover', function() { console.log('over'); });
btn.addEventListener('pointerout', function() { console.log('out'); });
btn.addEventListener('pointerleave', function() { console.log('leave'); });
</script>
Navigate to localhost/a.html click button and check console for the event logs.
Note that the iframe origins in a.html and b.html are different. With that setting if you navigate to 127.0.0.1 it behaves correctly but on localhost it doesn't.
If you move the div in a.html outside the iframes it works. (Chrome 78 only)
Question
Any potential workarounds? Ideally through JS or CSS as messing with the iframe origin and domains is not always possible nor the best solution.
Regarding overlapping div, sometimes messing with z-index works but again it's not the best solution as iframes can have overlapping elements.
Just an idea for a workaround: you could compare the coordinates in the out/leave events to the last known position inside the button element. If the coordinates are the same then ignore the out/leave events. Also you may need to handle blur event as well.
So lets say you have a down handler that sets some variables (like current position, pressed state) and also subscribes to move event.
Your move handler just updates the current position.
Your out/leave handler compares the current position to the event position. If they are equal and there was no blur event then ignore this event and return from the handler. Otherwise cancel the press, reset variables, unsubscribe from move etc.
I am developing a web application:
http://filebox.vt.edu/users/sharni/Radio/spectrum.html
Clicking on the help button pops up a resizeable help window. However, if the cursor moves outside of the dialog when resizing, the dialog stops resizing! Does anyone know how to prevent this from happening? Still, it is possible to resize the window by moving the cursor very slowly.
Thanks in advance!
Hey i got short solution for you:
Step 1: Create layer that will appears when user works with UI. That layer will cover canvas.
var coverLayer = $('<div>').appendTo("body").css({ "width" : "100%", "height" : "100%", "z-index" : "2", "background-color" : "rgba(124, 124, 124, 0.5)", "position" : "absolute" }).hide();
Step 2: Make layer visible only when user works with UI.
$(".ui-dialog").on( "resizestart dragstart" , function( event, ui ) {
coverLayer.show();
// here you can add some code that will pause webgl while user works with ui, so resizing and moving will be faster and smoother
});
$(".ui-dialog").on( "resizestop dragstop" , function( event, ui ) {
coverLayer.hide();
// here you unpause webgl
});
Step 3 (optional): Pause webgl while user works with UI. Couse if he does, he isn't probably interested in canvas, so you can make other stuff happening faster and smoother...
PS: You had same problem with dragging dialog, so I fixed this too simply with adding dragstart/dragstop. You can also add more events there.
EDIT:
Why that problem happen?
I guess its because of resize event. It is happening in some short periods and is responsible for element redraw (setting new width and height). I also think that it can detect if another event is triggered, then resize is not triggered anymore.
Now, because you are using webgl that eats a lot of javascript calculation power, then short periods for resize event are not short anymore. Since then, element is not redrawed that often as you want and mouse will appear futher from element which probably trigger some event that cause stop calling resize.
If you stop webgl, calling period will be short again, so it could prevent that problem, but I'm not very sure about that...
I'm trying to implement MapBox on a layout which fills 100% of the page. I've disable the map zoom options but the problems comes when trying to scroll on touch devices where the map fills the viewport. Can I override this or have the browser treat it like an image?
Yes, you can stop the Mapbox listeners from preventing default actions by setting the leaflet function to do nothing:
L.DomEvent.preventDefault = function(e) {return;}
To remove the outline placed around the element by the browser that otherwise would have been prevented, you can add:
*:focus {
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-webkit-focus-ring-color: rgba(255, 255, 255, 0) !important;
}
This allowed scrolling on touch devices for me, though I've only tested on Android. Note that this may have other consequences on your application - these could probably be prevented by going into mapbox.js and removing this call from only the listeners that you need.
This example has been improved: the line that was missing is:
if (map.tap) map.tap.disable();
This will prevent the map from killing tap events on touch devices.
Remove the rest of the listeners as in this example - the one you probably still have is dragging.
A simple solution via CSS: pointer-events: none;
For those that want to dynamically enable / disable map box scrolling / zooming etc depending on page size (as I did) you can try:
$(document).ready(function() {
$(window).on('resize', updateMap);
});
function updateMap() {
var width = $(window).width();
if (width < 768) {
map.scrollWheelZoom.disable();
map.doubleClickZoom.disable();
map.dragging.disable();
if (map.tap) map.tap.disable();
}
else {
map.scrollWheelZoom.enable();
map.doubleClickZoom.enable();
map.dragging.enable();
if (map.tap) map.tap.enable();
}
}
Replace 768 by the appropriate magic number for your layout. Assumes jQuery.
The problem with the dragging parameter in mapbox is that it controls both the click-and-hold dragging with a mouse or other pointing device and the swiping motion drag using touch. Not sure any of the other answers clarify that as the main issue.
The heart of the problem that I think the OP is getting at is that we all like the click-dragging and think it's easy to use, but we don't like the touch-dragging because it interferes with the ability to scroll the entire page.
This is the line of code I ended up using. It depends on Modernizr and jQuery, but you could write something similar without them if you needed to.
// disable dragging the map on touch devices only
if ($('html').hasClass('touch')) map.dragging.disable();
I am working on a touch based JS application, I've studied Flex and Royal slider as examples. I noticed that both sliders acting similarly when getting touchmove event:
var started,touched ;
el.bind('touchstart',function(e){
started = Number(new Date()) ;
// Get pageX and pageY etc...
}) ;
el.bind('touchmove',function(e){
touched = Number(new Date()) ;
if (started-touched > 500) {
// Handle touch moves etc...
}
}) ;
My JS app works seamless without these, but why do they need to do this? Why they are waiting 500ms to get move datas?
I believe this is some kind of sensitivity setting. You only want to register a touch-move (drag) event if the user has been moving his or her finger across the device for at least 500ms (in this example).
This could be useful to differentiate between taps and drags. Otherwise, if the user would slightly move his/her finger when clicking, for example, a button, the app would also register a drag. As some controls accept both events, this could lead to erroneous behaviour.