I need to add a simple left/right swipe gesture so that the 'selected' image cycles when swiped on mobile, similar to clicking the buttons in the hero component, also similar to pressing the left/right arrow keys on a keyboard
I don't have the most experience with JavaScript so if anyone could tell me what exactly to write and where so that I can completely wrap up this project.
Here is a demo: http://nufaith.ca/justinatkins/
Code:
Vue.component('hero-bg', {
template: `
<div class="hero-bg">
<div class="hero">
<img id="pushed" :src="selected"/>
</div>
</div>
`,
props: ['selected']
});
Vue.component('hero-bg-empty', {
template: `
<div class="hero-bg">
<div class="hero">
<span style="display:block;height:100px;"></span>
</div>
</div>
`
});
Vue.component('hero', {
template: `
<div>
<topbar v-if="!gridEnabled"></topbar>
<topbar2 v-if="gridEnabled"></topbar2>
<hero-bg :selected="selectedItem.img" v-if="!gridEnabled"></hero-bg>
<hero-bg-empty v-if="gridEnabled"></hero-bg-empty>
<div class="hero-container" v-if="!gridEnabled">
<div class="hero">
<img :src="selectedItem.img" v-if="thing" alt=""/>
</div>
<div class="hero-desc">
<button class="control left" #click="previous">
<i class="zmdi zmdi-chevron-left"></i>
</button>
<span class="hero-desc-title" v-html="title"></span>
<button class="control right" #click="next">
<i class="zmdi zmdi-chevron-right"></i>
</button>
<br/>
<button class="view-all-button" #click="enableGrid">OVERVIEW</button>
</div>
</div>
</div>
`,
data() {
return {
gridEnabled: false,
selected: 0,
thing: true
};
},
computed: {
selectedItem() {
return info[this.selected];
},
title() {
const comma = this.selectedItem.title.indexOf(',');
const len = this.selectedItem.title.length;
const strBeginning = this.selectedItem.title.substring(comma, 0);
const strEnd = this.selectedItem.title.substring(comma, len);
if (this.selectedItem.title.includes(',')) {
return `<span>${strBeginning}<span class="font-regular font-muted">${strEnd}</span></span>`;
}
return this.selectedItem.title;
},
maxImages() {
return info.length - 1;
}
},
created() {
window.addEventListener('keydown', e => {
if (e.keyCode === 37) {
this.previous();
return;
}
if (e.keyCode === 39) {
this.next();
return;
}
});
Event.$on('updateImg', index => {
this.selected = index;
this.gridEnabled = !this.gridEnabled;
});
},
methods: {
next() {
this.selected === this.maxImages ? (this.selected = 0) : (this.selected += 1);
},
previous() {
this.selected === 0 ? (this.selected = this.maxImages) : (this.selected -= 1);
},
enableGrid() {
this.gridEnabled = !this.gridEnabled;
window.scroll(0, 0);
Event.$emit('enableGrid');
}
}
});
This is how I implemented a simple swipe gesture in one of my projects. You may check this out.
Code:
touchableElement.addEventListener('touchstart', function (event) {
touchstartX = event.changedTouches[0].screenX;
touchstartY = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', function (event) {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
handleGesture();
}, false);
function handleGesture() {
if (touchendX < touchstartX) {
console.log('Swiped Left');
}
if (touchendX > touchstartX) {
console.log('Swiped Right');
}
if (touchendY < touchstartY) {
console.log('Swiped Up');
}
if (touchendY > touchstartY) {
console.log('Swiped Down');
}
if (touchendY === touchstartY) {
console.log('Tap');
}
}
Basically, touchableElement mentioned here, refers to the DOM Element that will receive the touch event. If you want to activate swipe options on your entire screen, then you may use your body tag as the touchable element. Or you may configure any specific div element as the touchable element, in case you just want the swipe gesture on that specific div.
On that touchableElement, we are adding 2 event-listeners here:
touchstart:
this is when user starts swiping. We take that initial coordinates (x,y) and
store them into touchstartX, touchstartY respectively.
touchend: this is when user stops swiping. We take that final coordinates (x, y) and store them into touchendX, touchendY respectively.
Keep in mind that, the origin of these coordinates is the top left corner of the screen. x-coordinate increases as you go from left to right and y-coordinate increases as you go from top to bottom.
Then, in handleGesture(), we just compare those 2 pair of coordinates (touchstartX, touchstartY) and (touchendX, touchendY), to detect different types of swipe gesture (up, down, left, right):
touchendX < touchstartX: says that, user started swiping at a higher X value & stopped swiping at a lower X value. That means, swiped from right to left (Swiped Left).
touchendX > touchstartX: says that, user started swiping at a lower X value & stopped swiping at a higher X value. That means, swiped from left to right (Swiped Right).
touchendY < touchstartY: says that, user started swiping at a higher Y value & stopped swiping at a lower Y value. That means, swiped from bottom to top (Swiped Up).
touchendY > touchstartY: says that, user started swiping at a lower Y value & stopped swiping at a higher Y value. That means, swiped from top to bottom (Swiped Down).
You may add the code for these 4 different events (Swipe Up/Down/Left/Right), on the corresponding if blocks, as shown on the code.
I took smmehrab’s answer, added some thresholds to avoid accidental swipes, and turned it into a little library. Might come in handy, so here it is:
export default class TouchEvent
{
static SWPIE_THRESHOLD = 50 // Minumum difference in pixels at which a swipe gesture is detected
static SWIPE_LEFT = 1
static SWIPE_RIGHT = 2
static SWIPE_UP = 3
static SWIPE_DOWN = 4
constructor(startEvent, endEvent)
{
this.startEvent = startEvent
this.endEvent = endEvent || null
}
isSwipeLeft()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_LEFT
}
isSwipeRight()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_RIGHT
}
isSwipeUp()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_UP
}
isSwipeDown()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_DOWN
}
getSwipeDirection()
{
let start = this.startEvent.changedTouches[0]
let end = this.endEvent.changedTouches[0]
if (!start || !end) {
return null
}
let horizontalDifference = start.screenX - end.screenX
let verticalDifference = start.screenY - end.screenY
// Horizontal difference dominates
if (Math.abs(horizontalDifference) > Math.abs(verticalDifference)) {
if (horizontalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_LEFT
} else if (horizontalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_RIGHT
}
// Verical or no difference dominates
} else {
if (verticalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_UP
} else if (verticalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_DOWN
}
}
return null
}
setEndEvent(endEvent)
{
this.endEvent = endEvent
}
}
How to use
Simply feed it the events from touchstart and touchend:
import TouchEvent from '#/TouchEvent'
let touchEvent = null;
document.addEventListener('touchstart', (event) => {
touchEvent = new TouchEvent(event);
});
document.addEventListener('touchend', handleSwipe);
function handleSwipe(event)
{
if (!touchEvent) {
return;
}
touchEvent.setEndEvent(event);
if (touchEvent.isSwipeRight()) {
// Do something
} else if (touchEvent.isSwipeLeft()) {
// Do something different
}
// Reset event for next touch
touchEvent = null;
}
This sounds like a job for Hammer.JS, unless you're trying to avoid dependencies. They have good documentation and examples for getting started
My Vue knowledge is next to nothing, so I'm wary of this becoming a blind leading the blind scenario, but the first thing you'll have to do is add the dependency using either npm or yarn - then add it to the top of your file using
import Hammer from 'hammerjs'
Try adding the below code right above this line: Event.$on('updateImg', index => {
const swipeableEl = document.getElementsByClassName('.hero')[0];
this.hammer = Hammer(swipeableEl)
this.hammer.on('swipeleft', () => this.next())
this.hammer.on('swiperight', () => this.previous())
If it doesn't work you'll have to check your developer tools / console log to see if it's logged any useful errors.
This codepen might be a useful resource too:
Good luck.
Using #smmehrab answer, I created a Vue 3 composable that also works for SSR builds.
import { onMounted, Ref } from 'vue'
export type SwipeCallback = (event: TouchEvent) => void;
export type SwipeOptions = {
directinoal_threshold?: number; // Pixels offset to trigger swipe
};
export const useSwipe = (touchableElement: HTMLElement = null, options: Ref<SwipeOptions> = ref({
directinoal_threshold: 10
})) => {
const touchStartX = ref(0);
const touchEndX = ref(0);
const touchStartY = ref(0);
const touchEndY = ref(0);
onMounted(() => {
if (!touchableElement)
touchableElement = document.body;
touchableElement.addEventListener('touchstart', (event) => {
touchStartX.value = event.changedTouches[0].screenX;
touchStartY.value = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', (event) => {
touchEndX.value = event.changedTouches[0].screenX;
touchEndY.value = event.changedTouches[0].screenY;
handleGesture(event);
}, false);
});
const onSwipeLeft: Array<SwipeCallback> = [];
const onSwipeRight: Array<SwipeCallback> = [];
const onSwipeUp: Array<SwipeCallback> = [];
const onSwipeDown: Array<SwipeCallback> = [];
const onTap: Array<SwipeCallback> = [];
const addEventListener = (arr: Array<SwipeCallback>, callback: SwipeCallback) => {
arr.push(callback);
};
const handleGesture = (event: TouchEvent) => {
if (touchEndX.value < touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeLeft.forEach(callback => callback(event));
}
if (touchEndX.value > touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeRight.forEach(callback => callback(event));
}
if (touchEndY.value < touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeUp.forEach(callback => callback(event));
}
if (touchEndY.value > touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeDown.forEach(callback => callback(event));
}
if (touchEndY.value === touchStartY.value) {
onTap.forEach(callback => callback(event));
}
}
return {
onSwipeLeft: (callback: SwipeCallback) => addEventListener(onSwipeLeft, callback),
onSwipeRight: (callback: SwipeCallback) => addEventListener(onSwipeRight, callback),
onSwipeUp: (callback: SwipeCallback) => addEventListener(onSwipeUp, callback),
onSwipeDown: (callback: SwipeCallback) => addEventListener(onSwipeDown, callback),
onTap: (callback: SwipeCallback) => addEventListener(onTap, callback)
}
}
Example usage:
const { onSwipeLeft, onSwipeRight } = useSwipe(document.body);
onSwipeLeft((e:TouchEvent) => {
//logic
});
Related
When I click on the button, if the value of the button is 0, then the text will not show in that space. I mean, how can it be done without showing -10?
console.log(variable)
The this is the error I'm getting ..
const Mobile = () => {
const [charge, setCharge] = useState(20)
const setMobileCharge = () => setCharge(charge - 10)
if (setMobileCharge === 0) {
return 0;
}
return (
<div>
<h3>Charge : {charge}</h3>
<button onClick={setMobileCharge}>Battery down</button>
</div>
);
};
export default Mobile;
this happens because the react "ticks" your render is not fully synced with the state ... so to prevent that you can set the new state like that
setCharge(oldCharge => oldCharge -10 )
you can add a condition like below,
const Mobile = () => {
const [charge, setCharge] = useState(20)
const setMobileCharge = () => {
if(charge > 0) setCharge(charge - 10)} //here
if (setMobileCharge === 0) {
return 0;
}
return (
<div>
<h3>Charge : {charge === 0 ? "" : charge}</h3> //here
<button onClick={setMobileCharge}>Battery down</button>
</div>
);
};
I'm don't quite understand your question.
But if you want when 0 not to decrease, you can :
const setMobileCharge = () => {
if (charge > 0) {
setCharge(charge - 10)
}
}
If you want when 0 Mobile component disappear you can:
if (charge === 0) {
return 0;
}
What I'm trying to build is a card with a photo as a background image. The idea is that when I hover on the photo (MouseEnter) the photo changes from a black and white one, to a colored one, but when I move the mouse horizontally on the photo the background image changes to another photo (the effect is similar to the apple album preview).
Now I got the code to work, but when I enter with the mouse on the photo the effect of the mouse enter is not even visible, because the mouse move function triggers right after that.
So the effect I want to get is: when I move the mouse over the photo, it changes to a colored one. THEN when I move the mouse again the scroll effect starts to work and I can change photo scrolling.
Here is the code:
import React, { useState } from "react";
import { Link } from "react-router-dom";
import $ from 'jquery';
const AboutCard = ({id, name, role, description, link, image, colored_image}) => {
const [backImg, setBackImg] = useState(0);
var active = 0;
var currentProfile = 0;
var unitPhoto= 0;
const handeMouseEnter = (event) => {
setBackImg(colored_image);
};
const handleMouseMove = (event) => {
unitPhoto = 100/(image.length-1);
var offset = $('#'+event.target.attributes.id.value).offset();
let x = event.pageX- offset.left;
let w =$('#'+event.target.attributes.id.value).width();
x = (x*100)/w;
let index = Math.floor(x/unitPhoto);
if(index >= image.length){
index = image.length-1;
}
if(index <0){
return;
}
if(index !== active || index != colored_image){
setBackImg(index);
active = index;
}
};
const handleTouchMove = (event) => {
unitPhoto = 100/(image.length-1);
var offset = $('#'+event.target.attributes.id.value).offset();
let x = event.touches[0].pageX- offset.left;
let w = $('#'+event.target.attributes.id.value).width();
x = (x*100)/w;
let index = Math.floor(x/unitPhoto);
if(index === 0){
setBackImg(colored_image);
return;
}
if(index >= image.length-1){
index = image.length-2;
}
if(index <0){
return;
}
if(index !== active || index !== colored_image){
setBackImg(index);
active = index;
}
};
$(document).on('mousemove', function(event){
if($('#'+event.target.attributes.id.value).attr('class') !== 'profileImage'){
setBackImg(0);
}
})
$(document).on('touchend', function(){
setBackImg(0);
})
return (
<div className="aboutDiv" id={id}>
<Link to={`${link}`}>
<div
onMouseEnter={(ev) => handeMouseEnter(ev)}
onMouseMove={(ev)=> handleMouseMove(ev)}
onTouchMove={(ev)=>
handleTouchMove(ev)}
id={'image-'+id}
style={{
background: `url(${image[backImg]})`,
}}
className="profileImage"
>
</div>
</Link>
<h3 className="profileName">{name}</h3>
<p className="profileRole">{role}</p>
</div>
);
};
export default AboutCard;
Any tips?
Is there any angular specific way to determine if a user is scrolling upwards or downwards?
I came across solutions which all were either about jQuery or pure JavaScript.
I tried below JSFiddle but not getting it right, it always shows scrolling up.
JSFiddle Demo
Here's how I tried it:
this.currentPosition = window.pageYOffset;
onContentScrolled(e) {
let scroll = window.pageYOffset;
if (scroll > this.currentPosition) {
console.log('scrollDown');
} else {
console.log('scrollUp');
}
this.currentPosition = scroll;
}
The fiddle works all fine. But I want to know the correct way to implement in angular component.
It only outputs "scrollUp" every time. Can anyone tell me what I am doing wrong here. I think it's with the global currentPosition variable but don't know how to proceed further.
It depends on where you want to add the listener (to a specific component, to the document, to the body, etc). A silver bullet is to use a regular event listener. As an example let's listen to scrolling events on the document. You can inject the document in the constructor (just in case you want to use something like SSR in the future):
Stackblitz demo
constructor(#Inject(DOCUMENT) private _document: Document) {
this._document.addEventListener('scroll', this.onContentScrolled);
}
ngOnDestroy() {
this._document.removeEventListener('scroll', this.onContentScrolled);
}
onContentScrolled = (e) => {
let scroll = window.pageYOffset;
if (scroll > this.currentPosition) {
console.log('scrollDown');
} else {
console.log('scrollUp');
}
this.currentPosition = scroll;
}
If you want to do the same thing with the scrollbar contained within a component, you can use the #HostListener('scroll') to decorate the listener method inside the component you want to listen to scrolling events.
Stackblitz demo
#HostListener("scroll", ['$event.target'])
onContentScrolled(e: HTMLElement) {
let scroll = e.scrollTop;
if (scroll > this.currentPosition) {
console.log("scrollDown");
} else {
console.log("scrollUp");
}
this.currentPosition = scroll;
}
my observable version of it: StackBlitz
Maybe I have lot more complex structure in my app, which includes dynamic content from various components, so I tried below and it worked seamlessly!
private scrollChangeCallback: () => void;
currentPosition: any;
startPosition: number;
ngAfterViewInit() {
this.scrollChangeCallback = () => this.onContentScrolled(event);
window.addEventListener('scroll', this.scrollChangeCallback, true);
}
onContentScrolled(e) {
this.startPosition = e.srcElement.scrollTop;
let scroll = e.srcElement.scrollTop;
if (scroll > this.currentPosition) {
this.showButton = false;
} else {
this.showButton = true;
}
this.currentPosition = scroll;
}
ngOnDestroy() {
window.removeEventListener('scroll', this.scrollChangeCallback, true);
}
e.srcElement works like a charm!
And thanks for all solutions above! They weren't wrong just didn't fit to my app
I know this is pretty late, but here's what worked for me in angular 9.
Typescript
lastScrollTop = 0
scrollHandler(event) {
let currentScrollTop = event.currentTarget.scrollTop
if (currentScrollTop > this.lastScrollTop) this.handleScrollDown()
else this.handleScrollUp()
this.lastScrollTop = currentScrollTop
}
HTML
<div class="container border" (scroll)="scrollHandler($event)">
</div>
this work like a charm for me using Angular 13
----------------------------HTML CODE------------------------------------------------
<div class="list"
(scroll)="onScroll($event)">
here you list or content to paginate /scroll
</div>
-----------------------------TS CODE---------------------------------------------------
startPosition: number = 0; //global variable
onScroll(event: any) {
if (
this.startPosition <=
event.target.offsetHeight + event.target.scrollTop + 50
) {
console.log('scrolled down!!', event);
} else {
console.log('scrolled up!!', event);
}
this.startPosition =
event.target.offsetHeight + event.target.scrollTop + 50;
}
TO DO refactor ,Hope this helps you ,have a nice coding ^^
you can use:
lastScrollTop = 0;
hold = false;
constructor(#Inject(DOCUMENT) private _document: Document) {
this._document.addEventListener('scroll', this.onContentScrolled);
}
onContentScrolled(e) {
const windowHeight = window.innerHeight;
const st = document.documentElement.scrollTop;
if (document.documentElement.scrollTop < 200) {
this.mainSectionOneIndex = 0;
}
if (st > this.lastScrollTop) {
if (this.hold === false) {
this.hold = true;
console.log('down');
setTimeout(() => {
this.hold = false;
}, 500);
}
} else {
if (this.hold === false) {
this.hold = true;
console.log('up');
setTimeout(() => {
this.hold = false;
}, 500);
}
}
this.lastScrollTop = st <= 0 ? 0 : st;
}
This is a relatively simple problem, but I haven't been able to solve it. I have built the following carousel / slider:
const BlogPostCardSlider = ({ children }) => {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
const wrapperRef = useRef(null);
const firstRenderRef = useRef(true);
useEffect(() => {
if (firstRenderRef.current) {
//this is checking whether its the first render of the component. If it is, we dont want useEffect to run.
firstRenderRef.current = false;
} else if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveRight = () => {
if (activeSlide + 1 >= children.length) {
return children.length - 1;
}
return activeSlide + 1;
};
const moveLeft = () => {
if (activeSlide - 1 <= 0) {
return 0;
}
return activeSlide - 1;
};
return (
<div id="trap" tabIndex="0">
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
{children.map((child, i) => {
return (
<SlideLink
key={`slideLink-${i}`}
isActive={activeSlide === i}
onClick={e => {
setActiveSlide(i);
}}
>
{i + 1}
</SlideLink>
);
})}
<Wrapper
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
ref={wrapperRef}
>
{children.map((child, i) => {
return (
<Slide
key={`slide-${i}`}
ref={i === activeSlide ? activeSlideRef : null}
>
{child}
</Slide>
);
})}
</Wrapper>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</div>
);
};
export default BlogPostCardSlider;
It displays its children as Slides in a carousel. You can navigate the carousel by pressing the 'NEXT' or 'PREVIOUS' buttons. There is also a component that shows you what slide you're on (e.g.: 1 2 *3* 4 etc.). I call these the SlideLink(s). Whenever the activeSlide updates, the SlideLink will update and highlight you the new current Slide.
Lastly, you can also navigate by scrolling. And here's the problem:
Whenever somebody scrolls, I am checking what slide they're on by doing some calculations:
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
...the result of this is that setActiveSlide is only called once the slide completely in view. This results in a laggy experience : the user has scrolled to the next slide, the next slide is in view, but the calculation is not completed yet (since it is only completed once the Slide is 100% in view), so the active SlideLink gets updated very late in the process.
How would I solve this? Is there some way to optimistically update this?
You can modify the calculations little bit so that when the slide is more than 50% in the view, set the active property to that one.
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
setActiveSlide(Math.round(scrollLeft / width) + 1);
}}
Example: Carousel with width: 800px, Total slides: 3. So, scrollLeft will vary from 0 to 1600
From 0-400: Active slide = Math.round(scrollLeft / width) = 0th index
(or first slide)
From 400-1200: Active slide = 1st index (or second
slide) since it is more in the view
From 1200-1600: Active slide =
2nd index (or third slide) since it is more in the view
Hope it helps. Revert for any doubts.
At first sight I thought this should be a quick fix. Turned out a bit more was necessary. I create a working example here:
https://codesandbox.io/s/dark-field-g78zb?fontsize=14&hidenavigation=1&theme=dark
The main point is that setActiveSide should not be called during the onScroll event, but rather when the user is done with scrolling. This is achieved in the example using the onIdle react hook.
If this is for a double-click:
window.addEventListener("dblclick", function(event) { }, false);
How can I capture a triple-click? This is for a pinned tab in Google Chrome.
You need to write your own triple-click implementation because no native event exists to capture 3 clicks in a row. Fortunately, modern browsers have event.detail, which the MDN documentation describes as:
A count of consecutive clicks that happened in a short amount of time, incremented by one.
This means you can simply check the value of this property and see if it is 3:
window.addEventListener('click', function (evt) {
if (evt.detail === 3) {
alert('triple click!');
}
});
Working demo: http://jsfiddle.net/L6d0p4jo/
If you need support for IE 8, the best approach is to capture a double-click, followed by a triple-click — something like this, for example:
var timer, // timer required to reset
timeout = 200; // timer reset in ms
window.addEventListener("dblclick", function (evt) {
timer = setTimeout(function () {
timer = null;
}, timeout);
});
window.addEventListener("click", function (evt) {
if (timer) {
clearTimeout(timer);
timer = null;
executeTripleClickFunction();
}
});
Working demo: http://jsfiddle.net/YDFLV/
The reason for this is that old IE browsers will not fire two consecutive click events for a double click. Don't forget to use attachEvent in place of addEventListener for IE 8.
Since DOM Level 2 you could use mouse click handler and check the detail parameter of event which should be interpreted as:
The detail attribute inherited from UIEvent indicates the number of times a mouse button has been pressed and released over the same screen location during a user action. The attribute value is 1 when the user begins this action and increments by 1 for each full sequence of pressing and releasing. If the user moves the mouse between the mousedown and mouseup the value will be set to 0, indicating that no click is occurring.
So the value of detail === 3 will give you the triple-click event.
More information in specification at http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MouseEvent.
Thanks to #Nayuki https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail - a DOM3 extension which is WIP https://w3c.github.io/uievents/
Here is the real Triple click event, which triggers only when all of three clicks fired with equal interval.
// Default settings
var minClickInterval = 100,
maxClickInterval = 500,
minPercentThird = 85.0,
maxPercentThird = 130.0;
// Runtime
var hasOne = false,
hasTwo = false,
time = [0, 0, 0],
diff = [0, 0];
$('#btn').on('click', function() {
var now = Date.now();
// Clear runtime after timeout fot the 2nd click
if (time[1] && now - time[1] >= maxClickInterval) {
clearRuntime();
}
// Clear runtime after timeout fot the 3rd click
if (time[0] && time[1] && now - time[0] >= maxClickInterval) {
clearRuntime();
}
// Catch the third click
if (hasTwo) {
time[2] = Date.now();
diff[1] = time[2] - time[1];
var deltaPercent = 100.0 * (diff[1] / diff[0]);
if (deltaPercent >= minPercentThird && deltaPercent <= maxPercentThird) {
alert("Triple Click!");
}
clearRuntime();
}
// Catch the first click
else if (!hasOne) {
hasOne = true;
time[0] = Date.now();
}
// Catch the second click
else if (hasOne) {
time[1] = Date.now();
diff[0] = time[1] - time[0];
(diff[0] >= minClickInterval && diff[0] <= maxClickInterval) ?
hasTwo = true : clearRuntime();
}
});
var clearRuntime = function() {
hasOne = false;
hasTwo = false;
time[0] = 0;
time[1] = 0;
time[2] = 0;
diff[0] = 0;
diff[1] = 0;
};
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Click button three times with equal interval
<button id="btn">Click me</button>
Also, I wrote jquery plugin TrplClick, which enables 'trplclick' event
it's very simple if you do it right, and you can even catch single, double, triple, ... clicks as you like. plain javascript, customizable click delay (timeout):
var clicks = 0;
var timer, timeout = 350;
var doubleClick = function(e) {
console.log('doubleClick');
}
var tripleClick = function(e) {
console.log('tripleClick');
}
// click timer
yourcontainer.addEventListener('click', function(e) {
clearTimeout(timer);
clicks++;
var evt = e;
timer = setTimeout(function() {
if(clicks==2) doubleClick(evt);
if(clicks==3) tripleClick(evt);
clicks = 0;
}, timeout);
});
pseudo-code:
var clicks = 0
onclick:
clicks++;
setTimer(resetClicksToZero);
if clicks == 3: tripleclickdetected(); clicks = 0;
I am working on a javascript code editor and I had to listen for triple click and here is the solution that will work for most browsers:
// Function to get mouse position
var getMousePosition = function (mouseEvent) {
var currentObject = container;
var currentLeft = 0;
var currentTop = 0;
do {
currentLeft += currentObject.offsetLeft;
currentTop += currentObject.offsetTop;
currentObject = currentObject.offsetParent;
} while (currentObject != document.body);
return {
x: mouseEvent.pageX - currentLeft,
y: mouseEvent.pageY - currentTop
}
}
// We will need a counter, the old position and a timer
var clickCounter = 0;
var clickPosition = {
x: null,
y: null
};
var clickTimer;
// The listener (container may be any HTML element)
container.addEventListener('click', function (event) {
// Get the current mouse position
var mousePosition = getMousePosition(event);
// Function to reset the data
var resetClick = function () {
clickCounter = 0;
var clickPosition = {
x: null,
y: null
};
}
// Function to wait for the next click
var conserveClick = function () {
clickPosition = mousePosition;
clearTimeout(clickTimer);
clickTimer = setTimeout(resetClick, 250);
}
// If position has not changed
if (clickCounter && clickPosition.x == mousePosition.x && clickPosition.y == mousePosition.y) {
clickCounter++;
if (clickCounter == 2) {
// Do something on double click
} else {
// Do something on triple click
resetClick();
}
conserveClick();
} else {
// Do something on single click
conserveClick();
}
});
Tested on Firefox 12, Google Chrome 19, Opera 11.64, Internet Explorer 9
This approach checks if the user has not changed cursor's position, you still can do something when you have single click or double click. Hope this solution will help everybody who will need to implement a triple click event listener :)
Configurable n-clicks event detector factory
const nClicks = (minClickStreak, maxClickInterval = 500, resetImmediately = true) => {
let timerId = 0
let clickCount = 0
let lastTarget = null
const reset = () => {
timerId = 0
clickCount = 0
lastTarget = null
}
return (originalEventHandler) => (e) => {
if (lastTarget == null || lastTarget == e.target) { // 2. unless we clicked same target
clickCount++ // 3. then increment click count
clearTimeout(timerId)
}
lastTarget = e.target
timerId = setTimeout(reset, maxClickInterval) // 1. reset state within set time
if (clickCount >= minClickStreak) {
originalEventHandler(e)
if (resetImmediately) {
clickCount = 0
}
}
}
}
Usage
table.addEventListener('click', nClicks(2)(e => { // double click
selectCell(e.target)
}))
table.addEventListener('click', nClicks(3)(e => { // triple click
selectRow(e.target)
}))