vue3-touch-events
Version:
Simple touch events support for vue.js 3
533 lines (532 loc) • 26.4 kB
JavaScript
/**
* @project vue3-touch-events
* @author Robin Rodricks, Xavier Julien, Jerry Bendy
* @since 30/4/2021
* @url https://github.com/robinrodricks/vue3-touch-events
*/
// Helper function to get X coordinate from touch/mouse event
function touchX(event) {
var _a, _b, _c;
if (event.type.indexOf('mouse') !== -1) {
return event.clientX;
}
return (_c = (_b = (_a = event.touches) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.clientX) !== null && _c !== void 0 ? _c : 0;
}
// Helper function to get Y coordinate from touch/mouse event
function touchY(event) {
var _a, _b, _c;
if (event.type.indexOf('mouse') !== -1) {
return event.clientY;
}
return (_c = (_b = (_a = event.touches) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.clientY) !== null && _c !== void 0 ? _c : 0;
}
// Check for passive event listener support
const isPassiveSupported = (function () {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: function () {
supportsPassive = true;
return true;
}
});
window.addEventListener('test', null, opts);
}
catch (e) { }
return supportsPassive;
})();
// Main Vue Touch Events plugin
const Vue3TouchEvents = {
install(app, constructorOptions) {
// Default global options merged with constructor options
var globalOptions = Object.assign({}, {
// CORE
touchClass: '',
namespace: 'touch',
// CLICK/TAP
disableClick: false,
tapTolerance: 10, // px
touchHoldTolerance: 400, // ms
longTapTimeInterval: 400, // ms
rollOverFrequency: 100, // ms
// DRAG
dragFrequency: 10, // ms
dragOutside: false,
// SWIPE
swipeTolerance: 100, // px
swipeConeSize: 0.75, // number between 0 to 1
// ZOOM
zoomFrequency: 10, // ms
zoomDistance: 10, // px
zoomInOutDistance: 100, // px
// NOTE: When adding props here, update `index.d.ts` as well!!
}, constructorOptions);
/** Fired when the user performs a MOUSE DOWN on the object */
function touchStartEvent(event) {
var $this = this.$$touchObj, isTouchEvent = event.type.indexOf('touch') >= 0, isMouseEvent = event.type.indexOf('mouse') >= 0, $el = this;
if (isTouchEvent) {
$this.lastTouchStartTime = event.timeStamp;
}
if (isMouseEvent && $this.lastTouchStartTime && event.timeStamp - $this.lastTouchStartTime < 350) {
return;
}
if ($this.touchStarted) {
return;
}
addTouchClass(this);
$this.touchStarted = true; // always true while the element is being PRESSED
$this.touchMoved = false; // true only when the element is PRESSED and DRAGGED a bit
$this.swipeOutBounded = false;
$this.isZooming = false;
$this.startX = touchX(event);
$this.startY = touchY(event);
$this.currentX = 0; // always updated with the last mouse X/Y while over the element
$this.currentY = 0;
$this.touchStartTime = event.timeStamp;
// performance: only process swipe events if `swipe.*` event is registered on this element
$this.hasSwipe = hasEvent($this, 'swipe')
|| hasEvent($this, 'swipe.left') || hasEvent($this, 'swipe.right')
|| hasEvent($this, 'swipe.top') || hasEvent($this, 'swipe.bottom');
// performance: only process zoom events if `zoom.*` event is registered on this element
$this.hasZoom = hasEvent($this, 'zoom') || hasEvent($this, 'zoom.in') || hasEvent($this, 'zoom.out');
// performance: only start hold timer if the `hold` event is registered on this element
if (hasEvent($this, 'hold')) {
// Trigger touchhold event after `touchHoldTolerance` MS
$this.touchHoldTimer = setTimeout(function () {
$this.touchHoldTimer = null;
triggerEvent(event, $el, 'hold');
}, $this.options.touchHoldTolerance);
}
triggerEvent(event, this, 'press');
}
/**
Fired when the user MOVES the mouse over the window.
*/
function touchMoveEventWindow(event) {
// only process if pressed
var $this = this.$$touchObj;
if ($this.touchStarted == true) {
// process event and pass 'this' onward
touchMoveEvent(event, $this, false);
}
else {
// TODO: mobile support
// if rollover events are wanted and this is a mouse event
if (hasEvent($this, 'rollover') && event.clientX != null) {
var mouseEvent = event;
// if the mouse is actually over this object (within this obj's bounds)
const rect = $this.element.getBoundingClientRect();
if (mouseEvent.clientX >= rect.left && mouseEvent.clientX <= rect.right &&
mouseEvent.clientY >= rect.top && mouseEvent.clientY <= rect.bottom) {
// process rollovers only and pass 'this' onward
touchMoveEvent(event, $this, true);
}
}
}
}
/**
Fired when the user DRAGS the object or MOVES the mouse over the object.
*/
function touchMoveEvent(event, $this = null, onlyProcessRollover = false) {
if ($this == null)
$this = this.$$touchObj;
var curX = touchX(event);
var curY = touchY(event);
var movedAgain = ($this.currentX != curX) || ($this.currentY != curY);
$this.currentX = curX;
$this.currentY = curY;
// dont process if only rollover wanted!
if (!onlyProcessRollover) {
//--------------------------------------------------------------------------------------
// DRAG ONCE
//--------------------------------------------------------------------------------------
if (!$this.touchMoved) {
var tapTolerance = $this.options.tapTolerance;
$this.touchMoved = Math.abs($this.startX - $this.currentX) > tapTolerance ||
Math.abs($this.startY - $this.currentY) > tapTolerance;
// trigger `drag.once` only once after mouse FIRST moved while dragging the element
// (`touchMoved` is the flag that indicates we no longer need to trigger this)
if ($this.touchMoved) {
cancelTouchHoldTimer($this);
triggerEvent(event, $this.element, 'drag.once');
}
}
//--------------------------------------------------------------------------------------
// SWIPE
//--------------------------------------------------------------------------------------
// performance: only process swipe events if `swipe.*` event is registered on this element
else if ($this.hasSwipe && !$this.swipeOutBounded) {
var swipeOutBounded = $this.options.swipeTolerance;
// Process swipe events using cones
if (Math.abs($this.startX - $this.currentX) / Math.abs($this.startY - $this.currentY) > $this.options.swipeConeSize &&
Math.abs($this.startY - $this.currentY) / Math.abs($this.startX - $this.currentX) > $this.options.swipeConeSize) {
$this.swipeOutBounded = (Math.abs($this.startY - $this.currentY) < swipeOutBounded) && (Math.abs($this.startX - $this.currentX) < swipeOutBounded);
}
}
}
//--------------------------------------------------------------------------------------
// ROLL OVER
//--------------------------------------------------------------------------------------
// only trigger `rollover` event if cursor actually moved over this element
if (movedAgain && hasEvent($this, 'rollover')) {
// throttle the `rollover` event based on `rollOverFrequency`
var now = event.timeStamp;
if ($this.touchRollTime == null || now > ($this.touchRollTime + $this.options.rollOverFrequency)) {
$this.touchRollTime = now;
triggerEvent(event, $this.element, 'rollover');
}
}
// exit if only rollovers wanted!
if (onlyProcessRollover) {
return;
}
//--------------------------------------------------------------------------------------
// DRAG
//--------------------------------------------------------------------------------------
// only trigger `drag` event if cursor actually moved and if we are still dragging this element
if ($this.touchStarted && $this.touchMoved && movedAgain && hasEvent($this, 'drag')) {
// throttle the `drag` event based on `dragFrequency`
var now = event.timeStamp;
if ($this.touchDragTime == null || now > ($this.touchDragTime + $this.options.dragFrequency)) {
$this.touchDragTime = now;
triggerEvent(event, $this.element, 'drag');
}
}
//--------------------------------------------------------------------------------------
// ZOOM
//--------------------------------------------------------------------------------------
// only trigger `zoom` event if cursor actually moved
if ($this.touchStarted && $this.hasZoom) {
// throttle the `zoom` event based on `zoomFrequency`
var now = event.timeStamp;
if ($this.touchZoomTime == null || now > ($this.touchZoomTime + $this.options.zoomFrequency)) {
$this.touchZoomTime = now;
checkZoom($this, event);
}
}
}
function checkZoom($this, event) {
// get the list of changed touches from the event
const touches = event.changedTouches;
// check if exactly two fingers are being used
if (touches.length !== 2) {
// reset dragging state if fewer or more than 2 touches are detected
$this.isZooming = false;
return;
}
// calculate the distance between the two touch points (euclidean distance)
const newDistance = Math.sqrt(Math.pow(touches[0].clientX - touches[1].clientX, 2) + // horizontal distance
Math.pow(touches[0].clientY - touches[1].clientY, 2) // vertical distance
);
// initialize the gesture if it's not already active
if (!$this.isZooming) {
// mark the gesture as active and store the initial distance
$this.isZooming = true;
$this.initialZoomDistance = newDistance;
return;
}
// calculate the zoom factor based on the change in distance
const zoomFactor = newDistance / $this.initialZoomDistance;
//--------------------------------------------------------------------------------------
// ZOOM
//--------------------------------------------------------------------------------------
if (hasEvent($this, 'zoom')) {
// check if the zoom factor exceeds the threshold for zooming
if (Math.abs(zoomFactor - 1) > ($this.options.zoomDistance / $this.initialZoomDistance)) {
// trigger the zoom callback with the source and zoom factor
triggerEvent(event, $this.element, 'zoom', zoomFactor);
}
}
//--------------------------------------------------------------------------------------
// ZOOM IN/OUT
//--------------------------------------------------------------------------------------
if (hasEvent($this, 'zoom.in') || hasEvent($this, 'zoom.out')) {
// check if the distance change is significant enough to count as a zoom gesture
if (Math.abs(newDistance - $this.initialZoomDistance) > $this.options.zoomInOutDistance) {
// determine zoom direction
if (newDistance > $this.initialZoomDistance) {
// fingers moved apart = zoom in
triggerEvent(event, $this.element, 'zoom.in');
}
else {
// fingers moved closer = zoom out
triggerEvent(event, $this.element, 'zoom.out');
}
}
}
// reset the dragging state after detecting the pinch gesture
$this.isZooming = false;
}
function touchCancelEvent() {
var $this = this.$$touchObj;
if ($this.touchStarted == true) {
cancelTouchHoldTimer($this);
removeTouchClass(this);
$this.touchStarted = $this.touchMoved = false;
$this.startX = $this.startY = 0;
}
}
/** Fired when the user performs a MOUSE UP on the object (releases the mouse button or finger press) */
function touchEndEvent(event) {
var $this = this.$$touchObj;
if ($this.touchStarted == true) {
var isTouchEvent = event.type.indexOf('touch') >= 0;
var isMouseEvent = event.type.indexOf('mouse') >= 0;
if (isTouchEvent) {
$this.lastTouchEndTime = event.timeStamp;
}
var touchholdEnd = isTouchEvent && !$this.touchHoldTimer;
cancelTouchHoldTimer($this);
$this.touchStarted = false;
removeTouchClass(this);
if (isMouseEvent && $this.lastTouchEndTime && event.timeStamp - $this.lastTouchEndTime < 350) {
return;
}
//--------------------------------------------------------------------------------------
// RELEASE
//--------------------------------------------------------------------------------------
// trigger `end` event when touch stopped
triggerEvent(event, this, 'release');
//--------------------------------------------------------------------------------------
// LONGTAP / HOLD / TAP
//--------------------------------------------------------------------------------------
if (!$this.touchMoved) {
// detect if this is a longTap event or not
if (hasEvent($this, 'longtap') && event.timeStamp - $this.touchStartTime > $this.options.longTapTimeInterval) {
if (event.cancelable) {
event.preventDefault();
}
triggerEvent(event, this, 'longtap');
}
else if (hasEvent($this, 'hold') && touchholdEnd) {
if (event.cancelable) {
event.preventDefault();
}
return;
}
else {
// emit tap event
triggerEvent(event, this, 'tap');
}
}
//--------------------------------------------------------------------------------------
// SWIPE
//--------------------------------------------------------------------------------------
// only process swipe events if `swipe.*` event is registered on this element
else if ($this.hasSwipe && !$this.swipeOutBounded) {
var swipeOutBounded = $this.options.swipeTolerance, direction, distanceY = Math.abs($this.startY - $this.currentY), distanceX = Math.abs($this.startX - $this.currentX);
if (distanceY > swipeOutBounded || distanceX > swipeOutBounded) {
// Check which swipe direction it is based on the mouse movement
if (distanceX > swipeOutBounded) {
direction = $this.startX > $this.currentX ? 'left' : 'right';
}
else {
direction = $this.startY > $this.currentY ? 'top' : 'bottom';
}
// Only emit the specified event when it has modifiers
if (hasEvent($this, 'swipe.' + direction)) {
triggerEvent(event, this, 'swipe.' + direction, direction);
}
else {
// Emit a common event when it has no any modifier
triggerEvent(event, this, 'swipe', direction);
}
}
}
}
}
function mouseEnterEvent() {
addTouchClass(this);
}
function mouseLeaveEvent() {
removeTouchClass(this);
}
function hasEvent($this, eventType) {
var callbacks = $this.callbacks[eventType];
return (callbacks != null && callbacks.length > 0);
}
function triggerEvent(e, $el, eventType, param) {
var $this = $el.$$touchObj;
// get the subscribers for this event
var callbacks = $this.callbacks[eventType];
// exit if no subscribers to this particular event
if (callbacks == null || callbacks.length === 0) {
return null;
}
// per callback
for (var i = 0; i < callbacks.length; i++) {
var binding = callbacks[i];
if (binding.modifiers.stop) {
e.stopPropagation();
}
if (binding.modifiers.prevent) {
e.preventDefault();
}
// handle `self` modifier`
if (binding.modifiers.self && e.target !== e.currentTarget) {
continue;
}
if (typeof binding.value === 'function') {
if (param) {
binding.value(param, e);
}
else {
binding.value(e);
}
}
}
}
function addTouchClass($el) {
var className = $el.$$touchObj.options.touchClass;
className && $el.classList.add(className);
}
function removeTouchClass($el) {
var className = $el.$$touchObj.options.touchClass;
className && $el.classList.remove(className);
}
function cancelTouchHoldTimer($this) {
if ($this && $this.touchHoldTimer) {
clearTimeout($this.touchHoldTimer);
$this.touchHoldTimer = null;
}
}
function buildTouchObj($el, extraOptions) {
var touchObj = $el.$$touchObj || {
element: $el,
// an object contains all callbacks registered,
// key is event name, value is an array
callbacks: {},
// prevent bind twice, set to true when event bound
hasBindTouchEvents: false,
// default options, would be override by v-touch-options
options: globalOptions,
events: {},
};
if (extraOptions) {
touchObj.options = Object.assign({}, touchObj.options, extraOptions);
}
$el.$$touchObj = touchObj;
return $el.$$touchObj;
}
function addEvents(events) {
for (const eventName in events) {
if (events.hasOwnProperty(eventName)) {
const [target, handler] = events[eventName];
target.addEventListener(eventName, handler);
}
}
}
function removeEvents(events) {
for (const eventName in events) {
if (events.hasOwnProperty(eventName)) {
const [target, handler] = events[eventName];
target.removeEventListener(eventName, handler);
}
}
}
app.directive(globalOptions.namespace, {
beforeMount: function ($el, binding) {
// build a touch configuration object
var $this = buildTouchObj($el);
// declare passive option for the event listener. Defaults to { passive: true } if supported
var passiveOpt = isPassiveSupported ? { passive: true } : false;
// register callback
var eventType = binding.arg || 'tap';
switch (eventType) {
case 'swipe':
var _m = binding.modifiers;
if (_m.left || _m.right || _m.top || _m.bottom) {
for (var i in binding.modifiers) {
if (['left', 'right', 'top', 'bottom'].indexOf(i) >= 0) {
var _e = 'swipe.' + i;
$this.callbacks[_e] = $this.callbacks[_e] || [];
$this.callbacks[_e].push(binding);
}
}
}
else {
$this.callbacks.swipe = $this.callbacks.swipe || [];
$this.callbacks.swipe.push(binding);
}
break;
case 'press':
case 'drag':
if (binding.modifiers.disablePassive) {
// change the passive option for the `drag` event if disablePassive modifier exists
passiveOpt = false;
}
default:
$this.callbacks[eventType] = $this.callbacks[eventType] || [];
$this.callbacks[eventType].push(binding);
}
// prevent bind twice
if ($this.hasBindTouchEvents) {
return;
}
// ADD MOBILE EVENTS
if ($this.options.dragOutside) {
$this.events['touchstart'] = [$el, touchStartEvent];
$this.events['touchmove'] = [window, touchMoveEventWindow.bind($el)];
$this.events['touchcancel'] = [window, touchCancelEvent.bind($el)];
$this.events['touchend'] = [window, touchEndEvent.bind($el)];
}
else {
$this.events['touchstart'] = [$el, touchStartEvent];
$this.events['touchmove'] = [$el, touchMoveEventWindow];
$this.events['touchcancel'] = [$el, touchCancelEvent];
$this.events['touchend'] = [$el, touchEndEvent];
}
// ADD DESKTOP EVENTS
if (!$this.options.disableClick) {
if ($this.options.dragOutside) {
$this.events['mousedown'] = [$el, touchStartEvent];
$this.events['mousemove'] = [window, touchMoveEventWindow.bind($el)];
$this.events['mouseup'] = [window, touchEndEvent.bind($el)];
$this.events['mouseenter'] = [$el, mouseEnterEvent];
$this.events['mouseleave'] = [$el, mouseLeaveEvent];
}
else {
$this.events['mousedown'] = [$el, touchStartEvent];
$this.events['mousemove'] = [$el, touchMoveEvent];
$this.events['mouseup'] = [$el, touchEndEvent];
$this.events['mouseenter'] = [$el, mouseEnterEvent];
$this.events['mouseleave'] = [$el, mouseLeaveEvent];
}
}
// register all events
addEvents($this.events);
// set bind mark to true
$this.hasBindTouchEvents = true;
},
unmounted: function ($el) {
var touchObj = $el.$$touchObj;
cancelTouchHoldTimer(touchObj);
// unregister all events
if (touchObj && touchObj.events) {
removeEvents(touchObj.events);
touchObj.events = {};
}
// remove vars
delete $el.$$touchObj;
}
});
// Register additional directives for class
app.directive(`${globalOptions.namespace}-class`, {
beforeMount: function ($el, binding) {
buildTouchObj($el, {
touchClass: binding.value
});
}
});
// Register additional directives for options
app.directive(`${globalOptions.namespace}-options`, {
beforeMount: function ($el, binding) {
buildTouchObj($el, binding.value);
}
});
}
};
/*
* Exports
*/
export default Vue3TouchEvents;