embla-carousel-wheel-gestures
Version:
wheel gestures for embla carousel
690 lines (559 loc) • 21.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.EmblaCarouselWheelGestures = factory());
}(this, (function () { 'use strict';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
var DECAY = 0.996;
/**
* movement projection based on velocity
* @param velocityPxMs
* @param decay
*/
var projection = function projection(velocityPxMs, decay) {
if (decay === void 0) {
decay = DECAY;
}
return velocityPxMs * decay / (1 - decay);
};
function lastOf(array) {
return array[array.length - 1];
}
function average(numbers) {
return numbers.reduce(function (a, b) {
return a + b;
}) / numbers.length;
}
var clamp = function clamp(value, min, max) {
return Math.min(Math.max(min, value), max);
};
function addVectors(v1, v2) {
if (v1.length !== v2.length) {
throw new Error('vectors must be same length');
}
return v1.map(function (val, i) {
return val + v2[i];
});
}
function absMax(numbers) {
return Math.max.apply(Math, numbers.map(Math.abs));
} // eslint-disable-next-line @typescript-eslint/ban-types
function deepFreeze(o) {
Object.freeze(o);
Object.values(o).forEach(function (value) {
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return o;
}
function EventBus() {
var listeners = {};
function on(type, listener) {
listeners[type] = (listeners[type] || []).concat(listener);
return function () {
return off(type, listener);
};
}
function off(type, listener) {
listeners[type] = (listeners[type] || []).filter(function (l) {
return l !== listener;
});
}
function dispatch(type, data) {
if (!(type in listeners)) return;
listeners[type].forEach(function (l) {
return l(data);
});
}
return deepFreeze({
on: on,
off: off,
dispatch: dispatch
});
}
function WheelTargetObserver(eventListener) {
var targets = []; // add event listener to target element
var observe = function observe(target) {
target.addEventListener('wheel', eventListener, {
passive: false
});
targets.push(target);
return function () {
return unobserve(target);
};
}; /// remove event listener from target element
var unobserve = function unobserve(target) {
target.removeEventListener('wheel', eventListener);
targets = targets.filter(function (t) {
return t !== target;
});
}; // stops watching all of its target elements for visibility changes.
var disconnect = function disconnect() {
targets.forEach(unobserve);
};
return deepFreeze({
observe: observe,
unobserve: unobserve,
disconnect: disconnect
});
}
var LINE_HEIGHT = 16 * 1.125;
var PAGE_HEIGHT = typeof window !== 'undefined' && window.innerHeight || 800;
var DELTA_MODE_UNIT = [1, LINE_HEIGHT, PAGE_HEIGHT];
function normalizeWheel(e) {
var deltaX = e.deltaX * DELTA_MODE_UNIT[e.deltaMode];
var deltaY = e.deltaY * DELTA_MODE_UNIT[e.deltaMode];
var deltaZ = (e.deltaZ || 0) * DELTA_MODE_UNIT[e.deltaMode];
return {
timeStamp: e.timeStamp,
axisDelta: [deltaX, deltaY, deltaZ]
};
}
var reverseAll = [-1, -1, -1];
function reverseAxisDeltaSign(wheel, reverseSign) {
if (!reverseSign) {
return wheel;
}
var multipliers = reverseSign === true ? reverseAll : reverseSign.map(function (shouldReverse) {
return shouldReverse ? -1 : 1;
});
return _extends({}, wheel, {
axisDelta: wheel.axisDelta.map(function (delta, i) {
return delta * multipliers[i];
})
});
}
var DELTA_MAX_ABS = 700;
var clampAxisDelta = function clampAxisDelta(wheel) {
return _extends({}, wheel, {
axisDelta: wheel.axisDelta.map(function (delta) {
return clamp(delta, -DELTA_MAX_ABS, DELTA_MAX_ABS);
})
});
};
var ACC_FACTOR_MIN = 0.6;
var ACC_FACTOR_MAX = 0.96;
var WHEELEVENTS_TO_MERGE = 2;
var WHEELEVENTS_TO_ANALAZE = 5;
var configDefaults = /*#__PURE__*/deepFreeze({
preventWheelAction: true,
reverseSign: [true, true, false]
});
var WILL_END_TIMEOUT_DEFAULT = 400;
function createWheelGesturesState() {
return {
isStarted: false,
isStartPublished: false,
isMomentum: false,
startTime: 0,
lastAbsDelta: Infinity,
axisMovement: [0, 0, 0],
axisVelocity: [0, 0, 0],
accelerationFactors: [],
scrollPoints: [],
scrollPointsToMerge: [],
willEndTimeout: WILL_END_TIMEOUT_DEFAULT
};
}
function WheelGestures(optionsParam) {
if (optionsParam === void 0) {
optionsParam = {};
}
var _EventBus = EventBus(),
on = _EventBus.on,
off = _EventBus.off,
dispatch = _EventBus.dispatch;
var config = configDefaults;
var state = createWheelGesturesState();
var currentEvent;
var negativeZeroFingerUpSpecialEvent = false;
var prevWheelEventState;
var feedWheel = function feedWheel(wheelEvents) {
if (Array.isArray(wheelEvents)) {
wheelEvents.forEach(function (wheelEvent) {
return processWheelEventData(wheelEvent);
});
} else {
processWheelEventData(wheelEvents);
}
};
var updateOptions = function updateOptions(newOptions) {
if (newOptions === void 0) {
newOptions = {};
}
if (Object.values(newOptions).some(function (option) {
return option === undefined || option === null;
})) {
console.error('updateOptions ignored! undefined & null options not allowed');
return config;
}
return config = deepFreeze(_extends({}, configDefaults, config, newOptions));
};
var publishWheel = function publishWheel(additionalData) {
var wheelEventState = _extends({
event: currentEvent,
isStart: false,
isEnding: false,
isMomentumCancel: false,
isMomentum: state.isMomentum,
axisDelta: [0, 0, 0],
axisVelocity: state.axisVelocity,
axisMovement: state.axisMovement,
get axisMovementProjection() {
return addVectors(wheelEventState.axisMovement, wheelEventState.axisVelocity.map(function (velocity) {
return projection(velocity);
}));
}
}, additionalData);
dispatch('wheel', _extends({}, wheelEventState, {
previous: prevWheelEventState
})); // keep reference without previous, otherwise we would create a long chain
prevWheelEventState = wheelEventState;
}; // should prevent when there is mainly movement on the desired axis
var shouldPreventDefault = function shouldPreventDefault(deltaMaxAbs, axisDelta) {
var _config = config,
preventWheelAction = _config.preventWheelAction;
var deltaX = axisDelta[0],
deltaY = axisDelta[1],
deltaZ = axisDelta[2];
if (typeof preventWheelAction === 'boolean') return preventWheelAction;
switch (preventWheelAction) {
case 'x':
return Math.abs(deltaX) >= deltaMaxAbs;
case 'y':
return Math.abs(deltaY) >= deltaMaxAbs;
case 'z':
return Math.abs(deltaZ) >= deltaMaxAbs;
default:
console.warn('unsupported preventWheelAction value: ' + preventWheelAction, 'warn');
return false;
}
};
var processWheelEventData = function processWheelEventData(wheelEvent) {
var _clampAxisDelta = clampAxisDelta(reverseAxisDeltaSign(normalizeWheel(wheelEvent), config.reverseSign)),
axisDelta = _clampAxisDelta.axisDelta,
timeStamp = _clampAxisDelta.timeStamp;
var deltaMaxAbs = absMax(axisDelta);
if (wheelEvent.preventDefault && shouldPreventDefault(deltaMaxAbs, axisDelta)) {
wheelEvent.preventDefault();
}
if (!state.isStarted) {
start();
} // check if user started scrolling again -> cancel
else if (state.isMomentum && deltaMaxAbs > Math.max(2, state.lastAbsDelta * 2)) {
end(true);
start();
} // special finger up event on windows + blink
if (deltaMaxAbs === 0 && Object.is && Object.is(wheelEvent.deltaX, -0)) {
negativeZeroFingerUpSpecialEvent = true; // return -> zero delta event should not influence velocity
return;
}
currentEvent = wheelEvent;
state.axisMovement = addVectors(state.axisMovement, axisDelta);
state.lastAbsDelta = deltaMaxAbs;
state.scrollPointsToMerge.push({
axisDelta: axisDelta,
timeStamp: timeStamp
});
mergeScrollPointsCalcVelocity(); // only wheel event (move) and not start/end get the delta values
publishWheel({
axisDelta: axisDelta,
isStart: !state.isStartPublished
}); // state.isMomentum ? MOMENTUM_WHEEL : WHEEL, { axisDelta })
// publish start after velocity etc. have been updated
state.isStartPublished = true; // calc debounced end function, to recognize end of wheel event stream
willEnd();
};
var mergeScrollPointsCalcVelocity = function mergeScrollPointsCalcVelocity() {
if (state.scrollPointsToMerge.length === WHEELEVENTS_TO_MERGE) {
state.scrollPoints.unshift({
axisDeltaSum: state.scrollPointsToMerge.map(function (b) {
return b.axisDelta;
}).reduce(addVectors),
timeStamp: average(state.scrollPointsToMerge.map(function (b) {
return b.timeStamp;
}))
}); // only update velocity after a merged scrollpoint was generated
updateVelocity(); // reset toMerge array
state.scrollPointsToMerge.length = 0; // after calculation of velocity only keep the most recent merged scrollPoint
state.scrollPoints.length = 1;
if (!state.isMomentum) {
detectMomentum();
}
} else if (!state.isStartPublished) {
updateStartVelocity();
}
};
var updateStartVelocity = function updateStartVelocity() {
state.axisVelocity = lastOf(state.scrollPointsToMerge).axisDelta.map(function (d) {
return d / state.willEndTimeout;
});
};
var updateVelocity = function updateVelocity() {
// need to have two recent points to calc velocity
var _state$scrollPoints = state.scrollPoints,
latestScrollPoint = _state$scrollPoints[0],
prevScrollPoint = _state$scrollPoints[1];
if (!prevScrollPoint || !latestScrollPoint) {
return;
} // time delta
var deltaTime = latestScrollPoint.timeStamp - prevScrollPoint.timeStamp;
if (deltaTime <= 0) {
console.warn('invalid deltaTime');
return;
} // calc the velocity per axes
var velocity = latestScrollPoint.axisDeltaSum.map(function (d) {
return d / deltaTime;
}); // calc the acceleration factor per axis
var accelerationFactor = velocity.map(function (v, i) {
return v / (state.axisVelocity[i] || 1);
});
state.axisVelocity = velocity;
state.accelerationFactors.push(accelerationFactor);
updateWillEndTimeout(deltaTime);
};
var updateWillEndTimeout = function updateWillEndTimeout(deltaTime) {
// use current time between events rounded up and increased by a bit as timeout
var newTimeout = Math.ceil(deltaTime / 10) * 10 * 1.2; // double the timeout, when momentum was not detected yet
if (!state.isMomentum) {
newTimeout = Math.max(100, newTimeout * 2);
}
state.willEndTimeout = Math.min(1000, Math.round(newTimeout));
};
var accelerationFactorInMomentumRange = function accelerationFactorInMomentumRange(accFactor) {
// when main axis is the the other one and there is no movement/change on the current one
if (accFactor === 0) return true;
return accFactor <= ACC_FACTOR_MAX && accFactor >= ACC_FACTOR_MIN;
};
var detectMomentum = function detectMomentum() {
if (state.accelerationFactors.length >= WHEELEVENTS_TO_ANALAZE) {
if (negativeZeroFingerUpSpecialEvent) {
negativeZeroFingerUpSpecialEvent = false;
if (absMax(state.axisVelocity) >= 0.2) {
recognizedMomentum();
return;
}
}
var recentAccelerationFactors = state.accelerationFactors.slice(WHEELEVENTS_TO_ANALAZE * -1); // check recent acceleration / deceleration factors
// all recent need to match, if any did not match
var detectedMomentum = recentAccelerationFactors.every(function (accFac) {
// when both axis decelerate exactly in the same rate it is very likely caused by momentum
var sameAccFac = !!accFac.reduce(function (f1, f2) {
return f1 && f1 < 1 && f1 === f2 ? 1 : 0;
}); // check if acceleration factor is within momentum range
var bothAreInRangeOrZero = accFac.filter(accelerationFactorInMomentumRange).length === accFac.length; // one the requirements must be fulfilled
return sameAccFac || bothAreInRangeOrZero;
});
if (detectedMomentum) {
recognizedMomentum();
} // only keep the most recent events
state.accelerationFactors = recentAccelerationFactors;
}
};
var recognizedMomentum = function recognizedMomentum() {
state.isMomentum = true;
};
var start = function start() {
state = createWheelGesturesState();
state.isStarted = true;
state.startTime = Date.now();
prevWheelEventState = undefined;
negativeZeroFingerUpSpecialEvent = false;
};
var willEnd = function () {
var willEndId;
return function () {
clearTimeout(willEndId);
willEndId = setTimeout(end, state.willEndTimeout);
};
}();
var end = function end(isMomentumCancel) {
if (isMomentumCancel === void 0) {
isMomentumCancel = false;
}
if (!state.isStarted) return;
if (state.isMomentum && isMomentumCancel) {
publishWheel({
isEnding: true,
isMomentumCancel: true
});
} else {
publishWheel({
isEnding: true
});
}
state.isMomentum = false;
state.isStarted = false;
};
var _WheelTargetObserver = WheelTargetObserver(feedWheel),
observe = _WheelTargetObserver.observe,
unobserve = _WheelTargetObserver.unobserve,
disconnect = _WheelTargetObserver.disconnect;
updateOptions(optionsParam);
return deepFreeze({
on: on,
off: off,
observe: observe,
unobserve: unobserve,
disconnect: disconnect,
feedWheel: feedWheel,
updateOptions: updateOptions
});
}
var defaultOptions = {
active: true,
breakpoints: {},
wheelDraggingClass: 'is-wheel-dragging',
forceWheelAxis: undefined,
target: undefined
};
WheelGesturesPlugin.globalOptions = undefined;
function WheelGesturesPlugin(userOptions) {
if (userOptions === void 0) {
userOptions = {};
}
var options;
var cleanup = function cleanup() {};
function init(embla, optionsHandler) {
var _options$target, _options$forceWheelAx;
var mergeOptions = optionsHandler.mergeOptions,
optionsAtMedia = optionsHandler.optionsAtMedia;
var optionsBase = mergeOptions(defaultOptions, WheelGesturesPlugin.globalOptions);
var allOptions = mergeOptions(optionsBase, userOptions);
options = optionsAtMedia(allOptions);
var engine = embla.internalEngine();
var targetNode = (_options$target = options.target) != null ? _options$target : embla.containerNode().parentNode;
var wheelAxis = (_options$forceWheelAx = options.forceWheelAxis) != null ? _options$forceWheelAx : engine.options.axis;
var wheelGestures = WheelGestures({
preventWheelAction: wheelAxis,
reverseSign: [true, true, false]
});
var unobserveTargetNode = wheelGestures.observe(targetNode);
var offWheel = wheelGestures.on('wheel', handleWheel);
var isStarted = false;
var startEvent;
function wheelGestureStarted(state) {
try {
startEvent = new MouseEvent('mousedown', state.event);
dispatchEvent(startEvent);
} catch (e) {
// Legacy Browsers like IE 10 & 11 will throw when attempting to create the Event
{
console.warn('Legacy browser requires events-polyfill (https://github.com/xiel/embla-carousel-wheel-gestures#legacy-browsers)');
}
return cleanup();
}
isStarted = true;
addNativeMouseEventListeners();
if (options.wheelDraggingClass) {
targetNode.classList.add(options.wheelDraggingClass);
}
}
function wheelGestureEnded(state) {
isStarted = false;
dispatchEvent(createRelativeMouseEvent('mouseup', state));
removeNativeMouseEventListeners();
if (options.wheelDraggingClass) {
targetNode.classList.remove(options.wheelDraggingClass);
}
}
function addNativeMouseEventListeners() {
document.documentElement.addEventListener('mousemove', preventNativeMouseHandler, true);
document.documentElement.addEventListener('mouseup', preventNativeMouseHandler, true);
document.documentElement.addEventListener('mousedown', preventNativeMouseHandler, true);
}
function removeNativeMouseEventListeners() {
document.documentElement.removeEventListener('mousemove', preventNativeMouseHandler, true);
document.documentElement.removeEventListener('mouseup', preventNativeMouseHandler, true);
document.documentElement.removeEventListener('mousedown', preventNativeMouseHandler, true);
}
function preventNativeMouseHandler(e) {
if (isStarted && e.isTrusted) {
e.stopImmediatePropagation();
}
}
function createRelativeMouseEvent(type, state) {
var moveX, moveY;
if (wheelAxis === engine.options.axis) {
var _state$axisMovement = state.axisMovement;
moveX = _state$axisMovement[0];
moveY = _state$axisMovement[1];
} else {
var _state$axisMovement2 = state.axisMovement;
moveY = _state$axisMovement2[0];
moveX = _state$axisMovement2[1];
} // prevent skipping slides
if (!engine.options.skipSnaps && !engine.options.dragFree) {
var maxX = engine.containerRect.width;
var maxY = engine.containerRect.height;
moveX = moveX < 0 ? Math.max(moveX, -maxX) : Math.min(moveX, maxX);
moveY = moveY < 0 ? Math.max(moveY, -maxY) : Math.min(moveY, maxY);
}
return new MouseEvent(type, {
clientX: startEvent.clientX + moveX,
clientY: startEvent.clientY + moveY,
screenX: startEvent.screenX + moveX,
screenY: startEvent.screenY + moveY,
movementX: moveX,
movementY: moveY,
button: 0,
bubbles: true,
cancelable: true,
composed: true
});
}
function dispatchEvent(event) {
embla.containerNode().dispatchEvent(event);
}
function handleWheel(state) {
var _state$axisDelta = state.axisDelta,
deltaX = _state$axisDelta[0],
deltaY = _state$axisDelta[1];
var primaryAxisDelta = wheelAxis === 'x' ? deltaX : deltaY;
var crossAxisDelta = wheelAxis === 'x' ? deltaY : deltaX;
var isRelease = state.isMomentum && state.previous && !state.previous.isMomentum;
var isEndingOrRelease = state.isEnding && !state.isMomentum || isRelease;
var primaryAxisDeltaIsDominant = Math.abs(primaryAxisDelta) > Math.abs(crossAxisDelta);
if (primaryAxisDeltaIsDominant && !isStarted && !state.isMomentum) {
wheelGestureStarted(state);
}
if (!isStarted) return;
if (isEndingOrRelease) {
wheelGestureEnded(state);
} else {
dispatchEvent(createRelativeMouseEvent('mousemove', state));
}
}
cleanup = function cleanup() {
unobserveTargetNode();
offWheel();
removeNativeMouseEventListeners();
};
}
var self = {
name: 'wheelGestures',
options: userOptions,
init: init,
destroy: function destroy() {
return cleanup();
}
};
return self;
}
return WheelGesturesPlugin;
})));
//# sourceMappingURL=embla-carousel-wheel-gestures.umd.development.js.map