import { isAndroid } from "../utils/device.js"; import { matches } from "../utils/util.js"; const SWIPE_THRESHOLD = 40; /** * Controls all touch interactions and navigations for * a presentation. */ export default class Touch { constructor(Reveal) { this.Reveal = Reveal; // Holds information about the currently ongoing touch interaction this.touchStartX = 0; this.touchStartY = 0; this.touchStartCount = 0; this.touchCaptured = false; this.onPointerDown = this.onPointerDown.bind(this); this.onPointerMove = this.onPointerMove.bind(this); this.onPointerUp = this.onPointerUp.bind(this); this.onTouchStart = this.onTouchStart.bind(this); this.onTouchMove = this.onTouchMove.bind(this); this.onTouchEnd = this.onTouchEnd.bind(this); } /** * */ bind() { let revealElement = this.Reveal.getRevealElement(); if ("onpointerdown" in window) { // Use W3C pointer events revealElement.addEventListener("pointerdown", this.onPointerDown, false); revealElement.addEventListener("pointermove", this.onPointerMove, false); revealElement.addEventListener("pointerup", this.onPointerUp, false); } else if (window.navigator.msPointerEnabled) { // IE 10 uses prefixed version of pointer events revealElement.addEventListener( "MSPointerDown", this.onPointerDown, false ); revealElement.addEventListener( "MSPointerMove", this.onPointerMove, false ); revealElement.addEventListener("MSPointerUp", this.onPointerUp, false); } else { // Fall back to touch events revealElement.addEventListener("touchstart", this.onTouchStart, false); revealElement.addEventListener("touchmove", this.onTouchMove, false); revealElement.addEventListener("touchend", this.onTouchEnd, false); } } /** * */ unbind() { let revealElement = this.Reveal.getRevealElement(); revealElement.removeEventListener("pointerdown", this.onPointerDown, false); revealElement.removeEventListener("pointermove", this.onPointerMove, false); revealElement.removeEventListener("pointerup", this.onPointerUp, false); revealElement.removeEventListener( "MSPointerDown", this.onPointerDown, false ); revealElement.removeEventListener( "MSPointerMove", this.onPointerMove, false ); revealElement.removeEventListener("MSPointerUp", this.onPointerUp, false); revealElement.removeEventListener("touchstart", this.onTouchStart, false); revealElement.removeEventListener("touchmove", this.onTouchMove, false); revealElement.removeEventListener("touchend", this.onTouchEnd, false); } /** * Checks if the target element prevents the triggering of * swipe navigation. */ isSwipePrevented(target) { // Prevent accidental swipes when scrubbing timelines if (matches(target, "video, audio")) return true; while (target && typeof target.hasAttribute === "function") { if (target.hasAttribute("data-prevent-swipe")) return true; target = target.parentNode; } return false; } /** * Handler for the 'touchstart' event, enables support for * swipe and pinch gestures. * * @param {object} event */ onTouchStart(event) { if (this.isSwipePrevented(event.target)) return true; this.touchStartX = event.touches[0].clientX; this.touchStartY = event.touches[0].clientY; this.touchStartCount = event.touches.length; } /** * Handler for the 'touchmove' event. * * @param {object} event */ onTouchMove(event) { if (this.isSwipePrevented(event.target)) return true; let config = this.Reveal.getConfig(); // Each touch should only trigger one action if (!this.touchCaptured) { this.Reveal.onUserInput(event); let currentX = event.touches[0].clientX; let currentY = event.touches[0].clientY; // There was only one touch point, look for a swipe if (event.touches.length === 1 && this.touchStartCount !== 2) { let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true, }); let deltaX = currentX - this.touchStartX, deltaY = currentY - this.touchStartY; if (deltaX > SWIPE_THRESHOLD && Math.abs(deltaX) > Math.abs(deltaY)) { this.touchCaptured = true; if (config.navigationMode === "linear") { if (config.rtl) { this.Reveal.next(); } else { this.Reveal.prev(); } } else { this.Reveal.left(); } } else if ( deltaX < -SWIPE_THRESHOLD && Math.abs(deltaX) > Math.abs(deltaY) ) { this.touchCaptured = true; if (config.navigationMode === "linear") { if (config.rtl) { this.Reveal.prev(); } else { this.Reveal.next(); } } else { this.Reveal.right(); } } else if (deltaY > SWIPE_THRESHOLD && availableRoutes.up) { this.touchCaptured = true; if (config.navigationMode === "linear") { this.Reveal.prev(); } else { this.Reveal.up(); } } else if (deltaY < -SWIPE_THRESHOLD && availableRoutes.down) { this.touchCaptured = true; if (config.navigationMode === "linear") { this.Reveal.next(); } else { this.Reveal.down(); } } // If we're embedded, only block touch events if they have // triggered an action if (config.embedded) { if (this.touchCaptured || this.Reveal.isVerticalSlide()) { event.preventDefault(); } } // Not embedded? Block them all to avoid needless tossing // around of the viewport in iOS else { event.preventDefault(); } } } // There's a bug with swiping on some Android devices unless // the default action is always prevented else if (isAndroid) { event.preventDefault(); } } /** * Handler for the 'touchend' event. * * @param {object} event */ onTouchEnd(event) { this.touchCaptured = false; } /** * Convert pointer down to touch start. * * @param {object} event */ onPointerDown(event) { if ( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchStart(event); } } /** * Convert pointer move to touch move. * * @param {object} event */ onPointerMove(event) { if ( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchMove(event); } } /** * Convert pointer up to touch end. * * @param {object} event */ onPointerUp(event) { if ( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) { event.touches = [{ clientX: event.clientX, clientY: event.clientY }]; this.onTouchEnd(event); } } }