Cross-Frame events on IE Edge break HammerJS (v2) - javascript

I'm facing a strange problem that I think leaves HammerJS internal event loop with a stuck event that ruins subsequent detections.
This only happens on Internet Explorer Edge on a Touch Device with PointerEvents.
Basically, when using HammerJS for a PAN event (panstart -> panmove -> panend), and you cross the current frame boundary (for example, into an IFRAME, or just outside the browser window) AND you release your finger there, then HammerJS never receives the CANCEL event and the session kind of stays stuck.
From then on, all gestures are reported incorrectly, with one more finger ('pointer') than you're using: For example, it will report a PINCH or ROTATE (2 pointers) just tapping (1 pointer) and so on.
I haven't found a way to reset the Hammer Manager once it enters this ghost state. This breaks my app.
I've prepared a Fiddle with a full working example. Please execute it under a Windows/Touch device !
https://jsfiddle.net/28cxrupv/5/
I'd like to know, either how to detect the out-of-bounds event, or just how could I manually reset the Hammer Manager instance if I am able to detect myself by other means that there are stuck events.
UPDATE
I've found in my investigations that the problem is at the lowest level in HammerJS: the PointerEvents handler has an array of detected pointers this.store and there's the stuck event with an old timestamp.

I've found a way to patch Hammer.JS so it can detect stuck pointers. I don't know if this is wrong, but apparently it works!
On HammerJS PointerEvents handler, there's an array this.store that keeps all current pointer events. It's there where, when we pan out of the window and release the touch, a stuck event is kept forever.
Clearing this array causes Hammer to go back to normal again.
I just added a condition where, if we are processing a Primary touch (start of a gesture?), and the store is not empty, it clears the store automatically.
How it works is, on the next interaction with the stuck hammer instance, the internal store gets reset and the gesture is interpreted properly.
On Hammer.js 2.0.6, around line 885
/**
* handle mouse events
* #param {Object} ev
*/
handler: function PEhandler(ev) {
var store = this.store;
var removePointer = false;
var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
var isTouch = (pointerType == INPUT_TYPE_TOUCH);
// get index of the event in the store
var storeIndex = inArray(store, ev.pointerId, 'pointerId');
// start and mouse must be down
if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
// NEW CONDITION: Check the store is empty on a new gesture
// http://stackoverflow.com/questions/35618107/cross-frame-events-on-ie-edge-break-hammerjs-v2
if (ev.isPrimary && store.length) {
window.console.warn ("Store should be 0 on a primary touch! Clearing Stuck Event!");
this.reset();
}
if (storeIndex < 0) {
store.push(ev);
storeIndex = store.length - 1;
}
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
removePointer = true;
}
// it not found, so the pointer hasn't been down (so it's probably a hover)
if (storeIndex < 0) {
return;
}
// update the event in the store
store[storeIndex] = ev;
this.callback(this.manager, eventType, {
pointers: store,
changedPointers: [ev],
pointerType: pointerType,
srcEvent: ev
});
if (removePointer) {
// remove from the store
store.splice(storeIndex, 1);
}
}
});
and also I define the function "reset":
/**
* Reset internal state
*/
reset: function() {
this.store = (this.manager.session.pointerEvents = []);
},

Related

How do I stop from creating an endless sequence of events?

I have a set of user preferences with a listener on change.
A few of the preferences need to interact. For instance, if I change preference A to "off" then Preference D should also be shown as "off."
I realize what I have is a simple implementation - which worked when all preferences were independent. But if the user changes Preference A, and my code checks for their choice then changes Preference D for them, then I get endless triggering of events. How do I gracefully design around this trap I've created?
$('.updateUserDetails').on('change', function (e) {
console.log('Detected change in user prefs...');
var chromeLog = $("#loggingModeAccount").is(':checked') ? 1 : 0;
var historyPosition = parseInt($("#historyPosDD").val(), 10);
if (chromeLog === 0){
console.log('Turning history OFF in Chrome...');
$("#historyPosDD").val("127");
historyPosition = 127; // Set to default
} else {
console.log('Chrome logging ON...');
$("#historyPosDD").val("3");
historyPosition = 3; // Set to default
});
Both #loggingModeAccount and #historyPosDD have the class . updateUserDetails. So when my code updates $("#historyPosDD").val based on the change to #loggingModeAccount I create an endless loop.
I've tried including e.stopPropagation(); in various places with no luck...
How do I design my way out of this? Any help/direction appreciated.

find if camera has attached or detached keyboard control to canvas in Babylonjs

I have a program in BabylonJS that has multiple cameras (free camera, scene, follow, and sometimes an arc camera). I set the active camera(s) and attach control (keyboard based movement) using the following:
scene.activeCameras[0] = camera;
camera.attachControl(canvas);
There are times I bring up html forms in my program and allow the user to type in the form (or temporarily turn the controls over to custom keydown / keyup controls) so I use the following to detach the control (so that the typed keys are not intercepted by the canvas scene):
for (var i = 0;i < scene.activeCameras.length;i++) {
scene.activeCameras[i].detachControl(canvas);
}
Then I attach again when done. This works great, but there are times when I need to test if the control is attached or not.
Currently, I set an outside variable (but I have to place it in many locations in my code) but I was wondering if there is a function in BabylonJS that tells you the attached control camera name or boolean true / false if camera control is currently attached?
you can test scene.activeCameras[i].inputs.attachedElement
Using #DavidCatuhe 's answer I was able to write this function others might find useful. Thanks!
function iscamaraattached() {
var attached = false;
if (scene.activeCameras != null) {
for (var i=0;i < scene.activeCameras.length;i++) {
if (scene.activeCameras[i].inputs.attachedElement != null) {
console.log("i=" + i + " - " + scene.activeCameras[i].inputs.attachedElement.id);
attached = true;
}
}
}
return attached;
}

TouchMove Event on iOS

I am currently developing a web application for iPad. I have a html canvas, where I am wanting to track the coordinates of the touchmove event.
I can track the touchstart event, which outputs the coordinate I have pressed, but when I try and output my current coordinates as I am moving across the canvas, it is doing nothing, I have the following code which is using angular2, where this.currentArrowPoint is the label I am outputting the values to on the screen:
e.preventDefault();
this.currentArrowPoint = ["a", "b"];
this.currentYPosition = ["e.changedTouches[0].pageY];
this.currentArrowPoint = [this.currentYPosition];
It is definitely entering the mousemove, as I am getting the a and b back, but then when I output anything relating to the event e, I get absolutely no output, not even if I try and output just e. This works on everything but apple devices, so wondering if I am missing something special about apple browsers or devices?
Thanks
As you can read here, only Chrome and Firefox support changedTouches in a TouchEvent, which in turn means that Safari does not support this.
I also guess you did not mean to put e.changedTouches[0].pageY in between quotes?
You should try and track touches based on their index on touchstart (such a waste why they didn't call this touchdown....) and figure out on touchmove if any of these touches have changed. You are using a canvas, so I can imagine you are trying to draw something. You could stop drawing once you count more than 1 touch. This way you are always sure you are tracking the right finger (or toe.. I'm not judging)
I find walk around solution if you want do swipe or something like that on iOS 13 with 3 event listener.
const someFucntion = () =>{
let touchstartX = 0;
let touchendX = 0;
const gesturedZone = this.mainWrapperRef.current; // ref, querySelector or something else
gesturedZone.addEventListener('touchstart', (event) => {
if (event.touches.length === 1) {
touchstartX = event.touches[0].screenX;
}
}, false);
gesturedZone.addEventListener('touchmove', (event) => {
if (event.touches.length === 1) {
touchendX = event.touches[0].screenX;
}
}, false);
gesturedZone.addEventListener('touchend', () => {
this.handleGesture(touchstartX, touchendX);
}, false);
}
const handleGesture = (touchstartX, touchendX) => {
// do some staff
};

handle both mouse and touch events on touch screens

I'm writing web application which should support both mouse and touch interactions.
For testing I use touch screen device with Windows 7. I've tried to sniff touch events in latest Firefox and Chrome canary and got the following results:
On touch Firefox fires touch and corresponding mouse event.
Chrome fires touchstart/mousedown, touchend/mouseup pairs, but mousemove fired in very strange manner: one/two times while touchmove.
All mouse events handled as always.
Is there any way to handle mouse and touch evens simultaneously on modern touch screens? If Firefox fires a pair of touch and mouse event what happens on touchmove with mousemove in Chrome? Should I translate all mouse events to touch or vice versa? I hope to find right way to create responsive interface.
You can't really predict in advance which events to listen for (eg. for all you know a USB touch screen could get plugged in after your page has loaded).
Instead, you should always listen to both the touch events and mouse events, but call preventDefault() on the touch events you handle to prevent (now redundant) mouse events from being fired for them. See http://www.html5rocks.com/en/mobile/touchandmouse/ for details.
You should rather check availability of touch interface and bind events according to that.
You can do something like this:
(function () {
if ('ontouchstart' in window) {
window.Evt = {
PUSH : 'touchstart',
MOVE : 'touchmove',
RELEASE : 'touchend'
};
} else {
window.Evt = {
PUSH : 'mousedown',
MOVE : 'mousemove',
RELEASE : 'mouseup'
};
}
}());
// and then...
document.getElementById('mydiv').addEventListener(Evt.PUSH, myStartDragHandler, false);
If you want to handle both in same time and browser does not translate well touch events into mouse events, you can catch touch events and stop them - then corresponding mouse event shouldn't be fired by browser (you won't have double events) and you can fire it yourself as mouse event or just handle it.
var mydiv = document.getElementsById('mydiv');
mydiv.addEventListener('mousemove', myMoveHandler, false);
mydiv.addEventListener('touchmove', function (e) {
// stop touch event
e.stopPropagation();
e.preventDefault();
// translate to mouse event
var clkEvt = document.createEvent('MouseEvent');
clkEvt.initMouseEvent('mousemove', true, true, window, e.detail,
e.touches[0].screenX, e.touches[0].screenY,
e.touches[0].clientX, e.touches[0].clientY,
false, false, false, false,
0, null);
mydiv.dispatchEvent(clkEvt);
// or just handle touch event
myMoveHandler(e);
}, false);
The solutions on this thread are outdated - for those (like me) who still land here in 2021, there is a new W3 specification for pointer events. These events combine mouse and touch into one.
https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
https://www.w3.org/TR/pointerevents/
MouseEvents and TouchEvents do not technically provide exactly the same functionality, but for most purposes , they can be used interchangeably. This solution does not favor one over the other, as the user may have both a mouse and a touch screen. Instead, it allows the user to use which ever input device they wish, as long as they wait at least five seconds before changing inputs. This solution ignores mouse pointer emulation on touchscreen devices when the screen is tapped.
var lastEvent = 3 ;
var MOUSE_EVENT = 1;
var TOUCH_EVENT = 2 ;
element.addEventListener('touchstart', function(event)
{
if (lastEvent === MOUSE_EVENT )
{
var time = Date.now() - eventTime ;
if ( time > 5000 )
{
eventTime = Date.now() ;
lastEvent = TOUCH_EVENT ;
interactionStart(event) ;
}
}
else
{
lastEvent = TOUCH_EVENT ; ;
eventTime = Date.now() ;
interactionStart(event) ;
}
}) ;
element.addEventListener('mousedown', function(event)
{
if (lastEvent === TOUCH_EVENT )
{
var time = Date.now() - eventTime ;
if ( time > 5000 )
{
eventTime = Date.now() ;
lastEvent = MOUSE_EVENT ;
interactionStart(event) ;
}
}
else
{
lastEvent= MOUSE_EVENT ;
eventTime = Date.now() ;
interactionStart(event) ;
}
}) ;
function interactionStart(event) // handle interaction (touch or click ) here.
{...}
This is by no means a win all solution, I have used this a few times , and have not found problems with it, but to be fair i usually just use it to start animation when a canvas it tapped , or to provide logic to turn a div into a button. I leave it to you all to use this code , find improvements and help to improve this code.(If you do not find a better solution ).
I found this thread because I have a similar & more complex problem:
supposing we create a js enabled scrollable area with arrows NEXT/PREVIOUS which we want not only to respond to touch and mouse events but also to fire them repeatedly while the user continues to press the screen or hold down his/her mouse!
Repetition of events would make my next button to advance 2 positions instead one!
With the help of closures everything seems possible:
(1) First create a self invoking function for variable isolation:
(function(myScroll, $, window, document, undefined){
...
}(window.myScroll = window.myScroll || {}, jQuery, window, document));
(2) Then, add your private variables that will hold internal state from setTimeout():
/*
* Primary events for handlers that respond to more than one event and devices
* that produce more than one, like touch devices.
* The first event in browser's queue hinders all subsequent for the specific
* key intended to be used by a handler.
* Every key points to an object '{primary: <event type>}'.
*/
var eventLock = {};
// Process ids based on keys.
var pids = {};
// Some defaults
var defaults = {
pressDelay: 100 // ms between successive calls for continuous press by mouse or touch
}
(3) The event lock functions:
function getEventLock(evt, key){
if(typeof(eventLock[key]) == 'undefined'){
eventLock[key] = {};
eventLock[key].primary = evt.type;
return true;
}
if(evt.type == eventLock[key].primary)
return true;
else
return false;
}
function primaryEventLock(evt, key){
eventLock[key].primary = evt.type;
}
(4) Attach your event handlers:
function init(){
$('sth').off('mousedown touchstart', previousStart).on('mousedown touchstart', previousStart);
$('sth').off('mouseup touchend', previousEnd).on('mouseup touchend', previousEnd);
// similar for 'next*' handlers
}
Firing of events mousedown and touchstart will produce double calls for handlers on devices that support both (probably touch fires first). The same applies to mouseup and touchend.
We know that input devices (whole graphic environments actually) produce events sequentially so we don't care which fires first as long a special key is set at private eventLock.next.primary and eventLock.previous.primary for the first events captured from handlers next*() and previous*() respectively.
That key is the event type so that the second, third etc. event are always losers, they don't acquire the lock with the help of the lock functions eventLock() and primaryEventLock().
(5) The above can be seen at the definition of the event handlers:
function previousStart(evt){
// 'race' condition/repetition between 'mousedown' and 'touchstart'
if(!getEventLock(evt, 'previous'))
return;
// a. !!!you have to implement this!!!
previous(evt.target);
// b. emulate successive events of this type
pids.previous = setTimeout(closure, defaults.pressDelay);
// internal function repeats steps (a), (b)
function closure(){
previous(evt.target);
primaryEventLock(evt, 'previous');
pids.previous = setTimeout(closure, defaults.pressDelay);
}
};
function previousEnd(evt){
clearTimeout(pids.previous);
};
Similar for nextStart and nextEnd.
The idea is that whoever comes after the first (touch or mouse) does not acquire a lock with the help of function eventLock(evt, key) and stops there.
The only way to open this lock is to fire the termination event handlers *End() at step (4): previousEnd and nextEnd.
I also handle the problem of touch devices attached in the middle of the session with a very smart way: I noticed that a continuous press longer than defaults.pressDelay produces successive calls of the callback function only for the primary event at that time (the reason is that no end event handler terminates the callabck)!
touchstart event
closure
closure
....
touchend event
I define primary the device the user is using so, all you have to do is just press longer and immediately your device becomes primary with the help of primaryEventLock(evt, 'previous') inside the closure!
Also, note that the time it takes to execute previous(event.target) should be smaller than defaults.pressDelay.
(6) Finally, let's expose init() to the global scope:
myScroll.init = init;
You should replace the call to previous(event.target) with the problem at hand: fiddle.
Also, note that at (5b) there is a solution to another popular question how do we pass arguments to a function called from setTimeout(), i.e. setTimeout(previous, defaults.pressDelay) lacks an argument passing mechanism.
I have been using this jQuery helper to bind both touch and click events.
(function ($) {
$.fn.tclick = function (onclick) {
this.bind("touchstart", function (e) { onclick.call(this, e); e.stopPropagation(); e.preventDefault(); });
this.bind("click", function (e) { onclick.call(this, e); }); //substitute mousedown event for exact same result as touchstart
return this;
};
})(jQuery);

What is the best way to handle multiple key events in Javascript?

Pressing space bar in game will make a character shoot, pressing space bar when a confirmation box is shown will make this box disappear and pressing space bar in a highscore form will add a space in an input box. In this example there are several events for the same key, but only one is fired at a time.
Is there a general (or specific for Javascript) method or way of programming to add events to a certain key, so they are only executed under certain circumstances?
Of course it can be done like this:
var inGame = true|false;
var inConfirmationBox = true|false;
function spaceBarHandler(){
if(inGame){ /*shoot*/}
else if(inConfirmationBox){ /*remove box*/}
}
document.onkeydown = function(){ /* call space bar handler if space bar was pressed */ };
But this is a very confusing way of programming, since specific actions are mixed together in a space bar handler function, which makes maintenance hard.
What is the best way to handle multiple events for one key, such that these events are only fired under certain circumstances?
Functions are first-class objects in javascript, which makes them really powerful. Because of this, your problem can be solved very elegantly.
// the whole thing can be encapsulated
// into an object actually
function spaceBarHandler() {
var state = spaceBarHandler.state;
var actions = spaceBarHandler.actions;
// execute function if exists
if (actions[state]) {
actions[state]();
}
}
// spaceBar actions
spaceBarHandler.actions = {
shoot: function() {
// bang bang
},
removeBox: function() {
// do it...
}
};
// change current state from outside
// we are in the game
spaceBarHandler.state = "shoot";
// change current state from outside
// confirmation box is shown
spaceBarHandler.state = "removeBox";
All these cases will be handled by one function. If you want to extend with another case, you just add another function to the actions object. Notice how the whole thing is encapsulated into one object.
you could instead add and remove the event listener as needed.
let's assume you're using a javascript framework (if you're not, then you probably should be considering the amount of JS code involved in a game like this)
using PrototypeJS:
when game starts,
document.observe("keydown",shootHandler());
when the message box is created,
function createBox(text) {
...snip
document.observe("keydown",closeBox());
document.fire("game:pause");
}
and, for example
var paused = false;
function shoothandler() {
if (!paused) {
alert("pew! pew!");
}
}
function closeBox() {
$('messagebox').remove();
document.fire("game:unpaused");
document.stopObserving("keydown",closeBox());
}
document.observe("game:paused", function() { paused = true;});
document.observe("game:unpaused", function() { paused = false;});
document.observe("game:over", function() { document.stopObserving("keydown",shootHandler());});
I haven't included the high score screen but the theory is the same.
As you can see, I also used custom events to notify the pause status. The same event could also be fire by a puase button in the interface, etc...
Attach event listeners to individual elements instead of the entire document.
document.getElementById('highscore').onkeypress = function(keyEvent) {
if (is_spacebar(keyEvent)) //Do something...
};
document.getElementById('game').onkeypress = function(keyEvent) {
if (is_spacebar(keyEvent)) //Do something else...
};
This is a simplistic example. You will probably have to deal with event bubbling which can be controlled when using addEventListener() to attach functions to events. Given browser (IE) compatibility issues involving this, some JS library should be used to deal with events.
There are a few ways, typically involving code-branching for IE's ‘special’ event model.
One way is to stop keypresses handled further down from bubbling up to the document key handler:
confirmationbox.onkeydown = function(event) {
if (event === undefined) event = window.event;
// do something with event.keyCode
if ('stopPropagation' in event) // standards browsers
event.stopPropagation();
else if ('cancelBubble' in event) // IE before version 9
event.cancelBubble = true;
};
document.onkeydown = ... // will not be called for keydowns inside confirmationbox
Another way would be to check the event target element to see if it's in the box:
document.onkeydown = function(event) {
if (event === undefined) event = window.event;
var target = 'target' in event ? event.target : event.srcElement; // srcElement is for IE<9
if (target === containerbox || isDescendantOf(target, containerbox) {
// do containerbox stuff
} else {
// do other stuff
}
};
function isDescendantOf(element, ancestor) {
while (element = element.parentNode)
if (element === ancestor)
return true;
return false;
}

Categories