Click event not firing on touchscreen when finger moves a bit - javascript

Click event is working fine when using mouse with computer. Even when I put mouse button down on button move cursor and then release mouse button inside button area, click event is firing. But same with touchscreen it is not working. I know that reason is that in touchscreen that kind of dragging is considered as scrolling. Click event is fired when I don't move finger too much on button. So only down and up without moving. My client has problem that they are moving finger too much and it is too hard to get click event. Is it possible to set bigger threshold for how much finger can move that it is still considered as click and not scroll?
I found this article where touch events are handled byself and translated them to click event. http://phonegap-tips.com/articles/fast-touch-event-handling-eliminate-click-delay.html I would not to like to go this road.
Have you any suggestion how can I solve this?
Here is more detail about touch events https://developer.mozilla.org/en-US/docs/Web/API/Touch_events Look at Handling clicks there is described how click is working in touchscreens. Still I didn't managed to work. Few months ago I but evt.preventDefault(); to my touchmove event handler and it did fix problem but currently it seems not.
EDIT:2019.11.5
Here is what was working earlier but no anymore:
html
<body (touchmove)="touchMoveEvent($event)"></body>
TypeScript
touchMoveEvent(ev: Event): void
{
ev.preventDefault();
}
And here is basic angular example of button and click handler which is not working if user is moving finger too much. I haven't check what is threshold but my I assume it is something near 10px-20px.
<button (click)="onClickEventHandler($event)">Press button</button>
onClickEventHandler(ev: Event) {
//do the thing here
}
I have tested touchscreen functionality with chrome's devtools toggle device toolbar.

Here is a nice solution. by using the touchstart and touchend events you can measure the distance between the 2 points and fire a click event if the events where close (in terms of pixels). read my comments.
class ScrollToClick {
constructor(elem, maxDistance = 20) {
this.elem = elem;
this.start = null;
this.maxDistance = maxDistance;
// Bind the touches event to the element
this.bindTouchEvents();
}
bindTouchEvents() {
this.elem.addEventListener('touchstart', this.onTouchStart.bind(this), false);
this.elem.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
onTouchStart(e) {
// hold the touch start position
this.start = e.touches[0];
// clear the position after 2000 mil (could be set for less).
setTimeout(() => { this.start = null; }, 2000);
}
onTouchEnd(e) {
// if the timeout was called, there will be no start position
if (!this.start) { return; }
// calculate the distance between start and end position
const end = e.changedTouches[0],
dx = Math.pow(this.start.pageX - end.pageX, 2),
dy = Math.pow(this.start.pageY - end.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire
// a click event. (default is 20 but you can override it through the constructor)
if (distance <= this.maxDistance) {
this.elem.click();
}
// clear the start position again
this.start = null;
}
}
Then you can use it with any element like so:
// use any element you wish (here I'm using the body)
const elem = document.body;
// initialize the class with the given element
new ScrollToClick(elem);
// listen to a click event on this element.
elem.addEventListener('click', (e) => {
console.log('Clicked');
})

This is a GitHub Issue that seems to be similar. I am not a JS dev so I am not sure but hope this helps.

My final solution is here. I forgot to mention in text that I am using Angular although I but in tag.
So I made Angular directive and but in AfikDeri's suggestion which was really close with directive style code.
import { Directive, ElementRef, Input, OnInit } from '#angular/core';
#Directive({
selector: '[touchClick]'
})
export class TouchClickDirective implements OnInit {
#Input() maxDistance = 100;
#Input() maxTime = 2000;
#Input() touchClick: boolean;
start: Touch;
constructor(private elem: ElementRef) {
this.start = null;
}
ngOnInit(): void {
// Bind the touches event to the element
this.bindTouchEvents();
}
bindTouchEvents() {
this.elem.nativeElement.addEventListener('touchstart', this.onTouchStart.bind(this), false);
this.elem.nativeElement.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
onTouchStart(e: TouchEvent) {
// hold the touch start position
this.start = e.touches[0];
// clear the position after 2000 mil (could be set for less).
setTimeout(() => {
this.start = null;
}, this.maxTime);
}
onTouchEnd(e: TouchEvent) {
// if the timeout was called, there will be no start position
if (!this.start) {
return;
}
// calculate the distance between start and end position
const end = e.changedTouches[0],
dx = Math.pow(this.start.pageX - end.pageX, 2),
dy = Math.pow(this.start.pageY - end.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire
// a click event. (default is 20 but you can override it through the constructor)
if (distance <= this.maxDistance) {
this.elem.nativeElement.click();
}
// clear the start position again
this.start = null;
}
}
And here is how it can be used
<button mat-flat-button [touchClick] [maxDistance]="100" [maxTime]="300" (click)="doWarning()">
Generate Warning
</button>

I worked out a quick solution to this problem based only on external value state set on different event listeners. Btn click fn will be triggered on touchend event if moveState variable will not change value by touchmove event. Touch start is always resetting state.
const moveState = false;
btn.addEventListener("click", (e) => handleBtnClick(e));
btn.addEventListener("touchstart", (e) => handleBtnTouchStart(e));
btn.addEventListener("touchmove", (e) => handleBtnTouchMove(e));
btn.addEventListener("touchend", (e) => handleBtnClick(e));
function handleHotspotTouchStart(e){
moveState = false;
}
function handleHotspotTouchMove(e){
moveState = true;
}
function handleBtnClick(e){
e.preventDefault;
if(e.type === 'touchend'){
if(moveState) return;
}
// trigger btn click action for both cursor click and touch if no movement detected
btnClick();
}

To add to the accepted answer, here is my react implementation:
import React, { useState } from 'react';
import './Button.css';
interface ButtonProps {
className: string,
value: string,
icon?: string,
onClick: () => void,
onPointerDown?: () => void,
onPointerUp?: () => void,
style?: React.CSSProperties,
}
function Button(props: ButtonProps): JSX.Element {
const [touchStart, setTouchStart] = useState(null);
const onTouchStart = (e) => {
// store the touchStart position
setTouchStart(e.touches[0]);
// clear the position after 2000ms
setTimeout(() => setTouchStart(null), 2000);
};
const onTouchEnd = (e) => {
// if the timeout was called, there will be no touchStart position
if (!touchStart) return;
// calculate the distance between touchStart and touchEnd position
const touchEnd = e.changedTouches[0],
dx = Math.pow(touchStart.pageX - touchEnd.pageX, 2),
dy = Math.pow(touchStart.pageY - touchEnd.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire a click event.
if (distance <= 50 && distance > 5) {
props.onClick();
}
// clear the start position again
setTouchStart(null);
};
return (
<button
className={`${props.className}`}
onClick={props.onClick}
onPointerDown={props.onPointerDown}
onPointerUp={props.onPointerUp}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
style={props.style}
>
{props.icon ? <img className="button__icon" src={props.icon} alt=""/> : ''}
{props.value}
</button>
);
}
export default Button;

Related

How to add a window event listener (within a functional component) that calls back function received as props

I'm working on my first React web app and got stuck creating a Knob/dial component that will be re-used hundreds of times throughout the app.
The user interaction will be handled within 3 elements, the Knob, the Knob's Parent, and the window object.
The Knob will receive a value as props, and represent this value visually, and it will also allow user interaction, changing the value dynamically with click and drag mouse movements.
The user interaction expected is:
< Knob onMouseDown > -> will set the Knob state "isChanging" to true, store mouse coordinates in the Parent state, and add 2 event listeners in the window object:
window.AddEventListener('onmousemove' -> if Knob state "isChanging" is true, calculate the new value (calling back a function passed as props to the Knob), and store it in the Parent state)
window.addEventListener('onmouseup' -> set Knob state "isChanging" to false)
The Knob will receive the value property dynamically (as part of the Parent state). I also wanna pass, as props to the Knob, the callback functions that will handle the mouse movements.
The thing is that I'm having trouble getting the window events to work..
Although I find out that the onMouseDown callback is working fine (altering the Knob state, and the mouseAt state value in the Parent), nothing that is set in the window event listeners is being executed when I move the mouse after clicking down in the Knob.
atm the code is as follows
class Parent extends Component {
constructor(props) {
super(props);
this.state={
value: 10,
min: 0,
max: 100,
mouseAt: null;
}
}
calcValue = (operation) => {
if(operation === 'increase') {
// Increase logic here
// Will sure have some call of this.setState({value: x})
}
if( operation === 'decrease') {
// Decrease logic here
// Will sure have some call of this.setState({value: x})
}
}
onMouseMove = (e) => {
if (e.clientY < this.state.mouseAt) {
this.calcValue('increase');
}
if (e.clientY > this.state.mouseAt) {
this.calcValue('decrease');
}
this.setState({mouseAt: e.clientY});
};
startMove = (e) => {
this.setState({mouseAt: e.clientY});
}
render() {
return (
<div>
<Knob windowMouseMove={this.onMouseMove} startMove={this.startMove} value={this.state.value) min={this.state.min} max={this.state.max} radius={15}></Knob>
</div>
)
}
}
and then in the Knob component:
function describeArc(x, y, radius, startAngle, endAngle){
// return svg path code
}
const Knob = (props) => {
const [isChanging, setChangeState] = useState(false);
useEffect(() => {
if (isChanging) {
window.addEventListener('onmousemove', props.windowMouseMove);
window.addEventListener('onmouseup', stopMoving);
};
return () => {
if (!isChanging) {
window.removeEventListener('mousemove', props.windowMouseMove);
window.removeEventListener('mouseup', stopMoving);
}
}
}, [isChanging])
const startMoving = (e) => {
setChangeState(true);
props.startMove(e);
}
const stopMoving = () => {
setChangeState(false);
}
return (
<svg onMouseDown={startMoving}>
<path id="arc1" fill="none" stroke="446688" strokeWidth="5"
d={describeArc(27.5, 27.5, props.radius, -160, 160)}/>
<path id="arc2" fill="none" stroke="#4169' strokeWidth="5"
d={decribeArc(27.5, 27.5, props.radius, radProps(props.value, props.min, props.max), 160)}/>
</svg>
)
}
I also have tried adding the window event listeners directly in the onMouseDown callback, but without success.
Tracking the state between callback calls, I found that the last callback being executed is the one in the body of useEffect, with "isChanging" set to true, even though no window event listener is being executed.
Any help on how to pull this off is appreciated.
Thanks.
[SOLVED] - The solution was simple, instead of window.addEventListener, I used window.onmousemove = props.windowMouseMove, and window.onmouseup = stopMoving, and before unmounting window.onmousemove = null and window.onmouseup = null.

How to remove scroll event listener?

I am trying to remove scroll event listener when I scroll to some element. What I am trying to do is call a click event when some elements are in a viewport. The problem is that the click event keeps calling all the time or after first call not at all. (Sorry - difficult to explain) and I would like to remove the scroll event to stop calling the click function.
My code:
window.addEventListener('scroll', () => {
window.onscroll = slideMenu;
// offsetTop - the distance of the current element relative to the top;
if (window.scrollY > elementTarget.offsetTop) {
const scrolledPx = (window.scrollY - elementTarget.offsetTop);
// going forward one step
if (scrolledPx < viewportHeight) {
// console.log('section one');
const link = document.getElementById('2');
if (link.stopclik === undefined) {
link.click();
link.stopclik = true;
}
}
// SECOND STEP
if (viewportHeight < scrolledPx && (viewportHeight * 2) > scrolledPx) {
console.log('section two');
// Initial state
let scrollPos = 0;
window.addEventListener('scroll', () => {
if ((document.body.getBoundingClientRect()).top > scrollPos) { // UP
const link1 = document.getElementById('1');
link1.stopclik = undefined;
if (link1.stopclik === undefined) {
link1.click();
link1.stopclik = true;
}
} else {
console.log('down');
}
// saves the new position for iteration.
scrollPos = (document.body.getBoundingClientRect()).top;
});
}
if ((viewportHeight * 2) < scrolledPx && (viewportHeight * 3) > scrolledPx) {
console.log('section three');
}
const moveInPercent = scrolledPx / base;
const move = -1 * (moveInPercent);
innerWrapper.style.transform = `translate(${move}%)`;
}
});
You can only remove event listeners on external functions. You cannot remove event listeners on anonymous functions, like you have used.
Replace this code
window.addEventListener('scroll', () => { ... };
and do this instead
window.addEventListener('scroll', someFunction);
Then move your function logic into the function
function someFunction() {
// add logic here
}
You can then remove the click listener when some condition is met i.e. when the element is in the viewport
window.removeEventListener('scroll', someFunction);
Instead of listening to scroll event you should try using Intersection Observer (IO) Listening to scroll event and calculating the position of elements on each scroll can be bad for performance. With IO you can use a callback function whenever two elements on the page are intersecting with each other or intersecting with the viewport.
To use IO you first have to specify the options for IO. Since you want to check if your element is in view, leave the root element out.
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Then you specify which elements you want to watch:
let target = slideMenu; //document.querySelector('#oneElement') or document.querySelectorAll('.multiple-elements')
observer.observe(target); // if you have multiple elements, loop through them to add observer
Lastly you have to define what should happen once the element is in the viewport:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
});
};
You can also unobserve an element if you don't need the observer anymore.
Check this polyfill from w3c to support older browsers.
Here is my scenario/code, call removeEventListener as return() in the useEffect hook.
const detectPageScroll = () => {
if (window.pageYOffset > YOFFSET && showDrawer) {
// do something
}
};
React.useEffect(() => {
if (showDrawer) {
window.addEventListener("scroll", detectPageScroll);
}
return () => {
window.removeEventListener("scroll", detectPageScroll);
};
}, [showDrawer]);

React music player Switching from mouse eventListener to window eventListener

Im making a simple music player, and the problem im facing is that the player has a progress bar, however on mousemove, if i drag outside of its parent container, it loses focus. My idea was to listen to the window instead which kinda worked, but now it listens to any kind of mouseevents, like the play/pause etc.
Can i switch to a window event listener on mouseleave for example, or do anyone have any idea on how to do this?
Some of the code i used, the innerProgressbar is the line that i can drag, and also updates for the song time. The outerProgress is its parent container, which is 6px in height.
mouseMove = (e) => {
if(this.songSeeking){
const{ song } = this.state;
let mouseX = e.clientX - this.outerProgress.offsetLeft;
let newTime = mouseX * song.duration/ this.outerProgress.offsetWidth;
song.currentTime = newTime;
this.innerProgress.style.width = mouseX + 'px';
}
}
mousedown = (e) => {
const { song } = this.state;
if(!this.songSeeking){
this.songSeeking = true;
song.pause();
this.setState({playing: false});
}
mouseup = (e) => {
const { song } = this.state;
if(this.songSeeking){
this.songSeeking = false
song.play();
this.setState({playing: true});
}
}

How to know scroll to element is done in Javascript?

I am using Javascript method Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
Is there any way I can get to know when the scroll is over. Say there was an animation, or I have set {behavior: smooth}.
I am assuming scrolling is async and want to know if there is any callback like mechanism to it.
There is no scrollEnd event, but you can listen for the scroll event and check if it is still scrolling the window:
var scrollTimeout;
addEventListener('scroll', function(e) {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(function() {
console.log('Scroll ended');
}, 100);
});
2022 Update:
The CSS specs recently included the overscroll and scrollend proposal, this proposal adds a few CSS overscroll attributes, and more importantly to us, a scrollend event.
Browsers are still working on implementing it. (It's already available in Chromium under the Web Platforms Experiments flag.)
We can feature-detect it by simply looking for
if (window.onscrollend !== undefined) {
// we have a scrollend event
}
While waiting for implementations everywhere, the remaining of this answer is still useful if you want to build a polyfill:
For this "smooth" behavior, all the specs say[said] is
When a user agent is to perform a smooth scroll of a scrolling box box to position, it must update the scroll position of box in a user-agent-defined fashion over a user-agent-defined amount of time.
(emphasis mine)
So not only is there no single event that will fire once it's completed, but we can't even assume any stabilized behavior between different browsers.
And indeed, current Firefox and Chrome already differ in their behavior:
Firefox seems to have a fixed duration set, and whatever the distance to scroll is, it will do it in this fixed duration ( ~500ms )
Chrome on the other hand will use a speed, that is, the duration of the operation will vary based on the distance to scroll, with an hard-limit of 3s.
So this already disqualifies all the timeout based solutions for this problem.
Now, one of the answers here has proposed to use an IntersectionObserver, which is not a too bad solution, but which is not too portable, and doesn't take the inline and block options into account.
So the best might actually be to check regularly if we did stop scrolling. To do this in a non-invasive way, we can start an requestAnimationFrame powered loop, so that our checks are performed only once per frame.
Here one such implementation, which will return a Promise that will get resolved once the scroll operation has finished.
Note: This code misses a way to check if the operation succeeded, since if an other scroll operation happens on the page, all current ones are cancelled, but I'll leave this as an exercise for the reader.
const buttons = [ ...document.querySelectorAll( 'button' ) ];
document.addEventListener( 'click', ({ target }) => {
// handle delegated event
target = target.closest('button');
if( !target ) { return; }
// find where to go next
const next_index = (buttons.indexOf(target) + 1) % buttons.length;
const next_btn = buttons[next_index];
const block_type = target.dataset.block;
// make it red
document.body.classList.add( 'scrolling' );
smoothScroll( next_btn, { block: block_type })
.then( () => {
// remove the red
document.body.classList.remove( 'scrolling' );
} )
});
/*
*
* Promised based scrollIntoView( { behavior: 'smooth' } )
* #param { Element } elem
** ::An Element on which we'll call scrollIntoView
* #param { object } [options]
** ::An optional scrollIntoViewOptions dictionary
* #return { Promise } (void)
** ::Resolves when the scrolling ends
*
*/
function smoothScroll( elem, options ) {
return new Promise( (resolve) => {
if( !( elem instanceof Element ) ) {
throw new TypeError( 'Argument 1 must be an Element' );
}
let same = 0; // a counter
let lastPos = null; // last known Y position
// pass the user defined options along with our default
const scrollOptions = Object.assign( { behavior: 'smooth' }, options );
// let's begin
elem.scrollIntoView( scrollOptions );
requestAnimationFrame( check );
// this function will be called every painting frame
// for the duration of the smooth scroll operation
function check() {
// check our current position
const newPos = elem.getBoundingClientRect().top;
if( newPos === lastPos ) { // same as previous
if(same ++ > 2) { // if it's more than two frames
/* #todo: verify it succeeded
* if(isAtCorrectPosition(elem, options) {
* resolve();
* } else {
* reject();
* }
* return;
*/
return resolve(); // we've come to an halt
}
}
else {
same = 0; // reset our counter
lastPos = newPos; // remember our current position
}
// check again next painting frame
requestAnimationFrame(check);
}
});
}
p {
height: 400vh;
width: 5px;
background: repeat 0 0 / 5px 10px
linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
background: red;
}
<button data-block="center">scroll to next button <code>block:center</code></button>
<p></p>
<button data-block="start">scroll to next button <code>block:start</code></button>
<p></p>
<button data-block="nearest">scroll to next button <code>block:nearest</code></button>
<p></p>
<button>scroll to top</button>
You can use IntersectionObserver, check if element .isIntersecting at IntersectionObserver callback function
const element = document.getElementById("box");
const intersectionObserver = new IntersectionObserver((entries) => {
let [entry] = entries;
if (entry.isIntersecting) {
setTimeout(() => alert(`${entry.target.id} is visible`), 100)
}
});
// start observing
intersectionObserver.observe(element);
element.scrollIntoView({behavior: "smooth"});
body {
height: calc(100vh * 2);
}
#box {
position: relative;
top:500px;
}
<div id="box">
box
</div>
I stumbled across this question as I wanted to focus a particular input after the scrolling is done (so that I keep the smooth scrolling).
If you have the same usecase as me, you don't actually need to wait for the scroll to be finished to focus your input, you can simply disable the scrolling of focus.
Here is how it's done:
window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });
cf: https://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932
Btw this particular issue (of waiting for scroll to finish before executing an action) is discussed in CSSWG GitHub here: https://github.com/w3c/csswg-drafts/issues/3744
Solution that work for me with rxjs
lang: Typescript
scrollToElementRef(
element: HTMLElement,
options?: ScrollIntoViewOptions,
emitFinish = false,
): void | Promise<boolean> {
element.scrollIntoView(options);
if (emitFinish) {
return fromEvent(window, 'scroll')
.pipe(debounceTime(100), first(), mapTo(true)).toPromise();
}
}
Usage:
const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, {behavior: 'smooth'}, true).then(() => {
// scroll finished do something
})
These answers above leave the event handler in place even after the scrolling is done (so that if the user scrolls, their method keeps getting called). They also don't notify you if there's no scrolling required. Here's a slightly better answer:
$("#mybtn").click(function() {
$('html, body').animate({
scrollTop: $("div").offset().top
}, 2000);
$("div").html("Scrolling...");
callWhenScrollCompleted(() => {
$("div").html("Scrolling is completed!");
});
});
// Wait for scrolling to stop.
function callWhenScrollCompleted(callback, checkTimeout = 200, parentElement = $(window)) {
const scrollTimeoutFunction = () => {
// Scrolling is complete
parentElement.off("scroll");
callback();
};
let scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
parentElement.on("scroll", () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
});
}
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
i'm not an expert in javascript but i made this with jQuery. i hope it helps
$("#mybtn").click(function() {
$('html, body').animate({
scrollTop: $("div").offset().top
}, 2000);
});
$( window ).scroll(function() {
$("div").html("scrolling");
if($(window).scrollTop() == $("div").offset().top) {
$("div").html("Ended");
}
})
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>
I recently needed callback method of element.scrollIntoView(). So tried to use the Krzysztof Podlaski's answer.
But I could not use it as is. I modified a little.
import { fromEvent, lastValueFrom } from 'rxjs';
import { debounceTime, first, mapTo } from 'rxjs/operators';
/**
* This function allows to get a callback for the scrolling end
*/
const scrollToElementRef = (parentEle, childEle, options) => {
// If parentEle.scrollTop is 0, the parentEle element does not emit 'scroll' event. So below is needed.
if (parentEle.scrollTop === 0) return Promise.resolve(1);
childEle.scrollIntoView(options);
return lastValueFrom(
fromEvent(parentEle, 'scroll').pipe(
debounceTime(100),
first(),
mapTo(true)
)
);
};
How to use
scrollToElementRef(
scrollableContainerEle,
childrenEle,
{
behavior: 'smooth',
block: 'end',
inline: 'nearest',
}
).then(() => {
// Do whatever you want ;)
});

How to make ondblclick event works on phone?

I want to achieve the double click event on a specific div like this:
<div id="divID" ondblclick = 'alert("double click!!");' >
it worked on the google chrome browser but when I open it with phone it didn't work, by the way the single click worked.
ps: i added this two things
<meta name="viewport" content="width=device-width, initial scale=1,user-scalable=no">
and this
body {
-ms-touch-action: manipulation;
touch-action: manipulation;}
but it didnt work!
I got the same issue. On touch devices, if you want to detect a double-tap gesture and you use the ondblclick event in most cases it will not work and also the problem is it will also fire an onclick. One of the solution is to implement a double tap detection pattern using the following code sample:
var doubletapDeltaTime_ = 700;
var doubletap1Function_ = null;
var doubletap2Function_ = null;
var doubletapTimer = null;
function tap(singleTapFunc, doubleTapFunc) {
if (doubletapTimer==null) {
// First tap, we wait X ms to the second tap
doubletapTimer_ = setTimeout(doubletapTimeout_, doubletapDeltaTime_);
doubletap1Function_ = singleTapFunc;
doubletap2Function_ = doubleTapFunc;
} else {
// Second tap
clearTimeout(doubletapTimer);
doubletapTimer_ = null;
doubletap2Function_();
}
}
function doubletapTimeout() {
// Wait for second tap timeout
doubletap1Function_();
doubleTapTimer_ = null;
}
And you can call it like
<div id="divID" onclick="tap(tapOnce, tapTwice)" >
tapOnce and tapTwice are your functions which will be called in respective cases. This solution will work on browsers too.
Reference
Here is the external function 'doubletap' which can be helpful:
/*
* jQuery Double Tap
* Developer: Sergey Margaritov (sergey#margaritov.net)
* Date: 22.10.2013
* Based on jquery documentation http://learn.jquery.com/events/event-extensions/
*/
(function($){
$.event.special.doubletap = {
bindType: 'touchend',
delegateType: 'touchend',
handle: function(event) {
var handleObj = event.handleObj,
targetData = jQuery.data(event.target),
now = new Date().getTime(),
delta = targetData.lastTouch ? now - targetData.lastTouch : 0,
delay = delay == null ? 300 : delay;
if (delta < delay && delta > 30) {
targetData.lastTouch = null;
event.type = handleObj.origType;
['clientX', 'clientY', 'pageX', 'pageY'].forEach(function(property) {
event[property] = event.originalEvent.changedTouches[0][property];
})
// let jQuery handle the triggering of "doubletap" event handlers
handleObj.handler.apply(this, arguments);
} else {
targetData.lastTouch = now;
}
}
};
})(jQuery);
Load jQuery Mobile into your project and try using taphold or some of the other mobile specific touch events that are available to you through that API.
Here's the jQuery Mobile documentation with all the events you can use: http://api.jquerymobile.com/category/events/
Here is the snippet for TS React users. Pass in the click event, so that double click is only invoked if the same element is clicked twice
import React from "react";
type CallBack = () => any;
type TapParams = { onSingleTap?: CallBack; onDoubleTap?: CallBack };
var DELTA_TIME_THRESHOLD_MS = 700;
var timer: NodeJS.Timeout | null = null;
var target: EventTarget;
export function tap(
e: React.MouseEvent,
{ onSingleTap, onDoubleTap }: TapParams
) {
if (timer == null) {
// First tap
onSingleTap?.();
timer = setTimeout(() => {
timer = null;
}, DELTA_TIME_THRESHOLD_MS);
} else {
// Second tap
if (e.target === target) {
onDoubleTap?.();
}
clearTimeout(timer);
timer = null;
}
target = e.target;
}
Usage
<div
onClick={(e) => tap(e, { onSingleTap, onDoubleTap })}
>Tap or doubletap</div>
Using only JavaScript
You can use "touchstart" event for a single touch,
but with calculating the time when he should click again
I used 400 (0.4s) as it's the longer duration between two touches
It's only an estimate, but it's still a reasonable time
let expired
let doubleClick = function () {
console.log('double click')
}
let doubleTouch = function (e) {
if (e.touches.length === 1) {
if (!expired) {
expired = e.timeStamp + 400
} else if (e.timeStamp <= expired) {
// remove the default of this event ( Zoom )
e.preventDefault()
doubleClick()
// then reset the variable for other "double Touches" event
expired = null
} else {
// if the second touch was expired, make it as it's the first
expired = e.timeStamp + 400
}
}
}
let element = document.getElementById('btn')
element.addEventListener('touchstart', doubleTouch)
element.addEventListener('dblclick', doubleClick)
In case of this error :
Unable to preventDefault inside passive event listener due to target being treated as passive.
event.preventDefault( ) not working if element = "document" or "document.body"
So the solution of that, you should have a full page div container :
let element = document.getElementById('container')
element.style.minWidth = '100vw'
element.style.minHeight = '100vh'
document.body.style.margin = '0px'
element.addEventListener('touchstart', elementTouch)
element.addEventListener('dblclick', doubleClick)

Categories

Resources