1662 lines
47 KiB
JavaScript
1662 lines
47 KiB
JavaScript
'use strict';
|
|
|
|
function isNumber(subject) {
|
|
return typeof subject === 'number';
|
|
}
|
|
function isString(subject) {
|
|
return typeof subject === 'string';
|
|
}
|
|
function isBoolean(subject) {
|
|
return typeof subject === 'boolean';
|
|
}
|
|
function isObject(subject) {
|
|
return Object.prototype.toString.call(subject) === '[object Object]';
|
|
}
|
|
function mathAbs(n) {
|
|
return Math.abs(n);
|
|
}
|
|
function mathSign(n) {
|
|
return Math.sign(n);
|
|
}
|
|
function deltaAbs(valueB, valueA) {
|
|
return mathAbs(valueB - valueA);
|
|
}
|
|
function factorAbs(valueB, valueA) {
|
|
if (valueB === 0 || valueA === 0) return 0;
|
|
if (mathAbs(valueB) <= mathAbs(valueA)) return 0;
|
|
const diff = deltaAbs(mathAbs(valueB), mathAbs(valueA));
|
|
return mathAbs(diff / valueB);
|
|
}
|
|
function arrayKeys(array) {
|
|
return objectKeys(array).map(Number);
|
|
}
|
|
function arrayLast(array) {
|
|
return array[arrayLastIndex(array)];
|
|
}
|
|
function arrayLastIndex(array) {
|
|
return Math.max(0, array.length - 1);
|
|
}
|
|
function arrayIsLastIndex(array, index) {
|
|
return index === arrayLastIndex(array);
|
|
}
|
|
function arrayFromNumber(n, startAt = 0) {
|
|
return Array.from(Array(n), (_, i) => startAt + i);
|
|
}
|
|
function objectKeys(object) {
|
|
return Object.keys(object);
|
|
}
|
|
function objectsMergeDeep(objectA, objectB) {
|
|
return [objectA, objectB].reduce((mergedObjects, currentObject) => {
|
|
objectKeys(currentObject).forEach(key => {
|
|
const valueA = mergedObjects[key];
|
|
const valueB = currentObject[key];
|
|
const areObjects = isObject(valueA) && isObject(valueB);
|
|
mergedObjects[key] = areObjects ? objectsMergeDeep(valueA, valueB) : valueB;
|
|
});
|
|
return mergedObjects;
|
|
}, {});
|
|
}
|
|
function isMouseEvent(evt, ownerWindow) {
|
|
return typeof ownerWindow.MouseEvent !== 'undefined' && evt instanceof ownerWindow.MouseEvent;
|
|
}
|
|
|
|
function Alignment(align, viewSize) {
|
|
const predefined = {
|
|
start,
|
|
center,
|
|
end
|
|
};
|
|
function start() {
|
|
return 0;
|
|
}
|
|
function center(n) {
|
|
return end(n) / 2;
|
|
}
|
|
function end(n) {
|
|
return viewSize - n;
|
|
}
|
|
function measure(n, index) {
|
|
if (isString(align)) return predefined[align](n);
|
|
return align(viewSize, n, index);
|
|
}
|
|
const self = {
|
|
measure
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function EventStore() {
|
|
let listeners = [];
|
|
function add(node, type, handler, options = {
|
|
passive: true
|
|
}) {
|
|
let removeListener;
|
|
if ('addEventListener' in node) {
|
|
node.addEventListener(type, handler, options);
|
|
removeListener = () => node.removeEventListener(type, handler, options);
|
|
} else {
|
|
const legacyMediaQueryList = node;
|
|
legacyMediaQueryList.addListener(handler);
|
|
removeListener = () => legacyMediaQueryList.removeListener(handler);
|
|
}
|
|
listeners.push(removeListener);
|
|
return self;
|
|
}
|
|
function clear() {
|
|
listeners = listeners.filter(remove => remove());
|
|
}
|
|
const self = {
|
|
add,
|
|
clear
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Animations(ownerDocument, ownerWindow, update, render) {
|
|
const documentVisibleHandler = EventStore();
|
|
const timeStep = 1000 / 60;
|
|
let lastTimeStamp = null;
|
|
let lag = 0;
|
|
let animationFrame = 0;
|
|
function init() {
|
|
documentVisibleHandler.add(ownerDocument, 'visibilitychange', () => {
|
|
if (ownerDocument.hidden) reset();
|
|
});
|
|
}
|
|
function destroy() {
|
|
stop();
|
|
documentVisibleHandler.clear();
|
|
}
|
|
function animate(timeStamp) {
|
|
if (!animationFrame) return;
|
|
if (!lastTimeStamp) lastTimeStamp = timeStamp;
|
|
const elapsed = timeStamp - lastTimeStamp;
|
|
lastTimeStamp = timeStamp;
|
|
lag += elapsed;
|
|
while (lag >= timeStep) {
|
|
update(timeStep);
|
|
lag -= timeStep;
|
|
}
|
|
const lagOffset = lag / timeStep;
|
|
render(lagOffset);
|
|
if (animationFrame) ownerWindow.requestAnimationFrame(animate);
|
|
}
|
|
function start() {
|
|
if (animationFrame) return;
|
|
animationFrame = ownerWindow.requestAnimationFrame(animate);
|
|
}
|
|
function stop() {
|
|
ownerWindow.cancelAnimationFrame(animationFrame);
|
|
lastTimeStamp = null;
|
|
lag = 0;
|
|
animationFrame = 0;
|
|
}
|
|
function reset() {
|
|
lastTimeStamp = null;
|
|
lag = 0;
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy,
|
|
start,
|
|
stop,
|
|
update: () => update(timeStep),
|
|
render
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Axis(axis, contentDirection) {
|
|
const isRightToLeft = contentDirection === 'rtl';
|
|
const isVertical = axis === 'y';
|
|
const scroll = isVertical ? 'y' : 'x';
|
|
const cross = isVertical ? 'x' : 'y';
|
|
const sign = !isVertical && isRightToLeft ? -1 : 1;
|
|
const startEdge = getStartEdge();
|
|
const endEdge = getEndEdge();
|
|
function measureSize(nodeRect) {
|
|
const {
|
|
height,
|
|
width
|
|
} = nodeRect;
|
|
return isVertical ? height : width;
|
|
}
|
|
function getStartEdge() {
|
|
if (isVertical) return 'top';
|
|
return isRightToLeft ? 'right' : 'left';
|
|
}
|
|
function getEndEdge() {
|
|
if (isVertical) return 'bottom';
|
|
return isRightToLeft ? 'left' : 'right';
|
|
}
|
|
function direction(n) {
|
|
return n * sign;
|
|
}
|
|
const self = {
|
|
scroll,
|
|
cross,
|
|
startEdge,
|
|
endEdge,
|
|
measureSize,
|
|
direction
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Limit(min = 0, max = 0) {
|
|
const length = mathAbs(min - max);
|
|
function reachedMin(n) {
|
|
return n < min;
|
|
}
|
|
function reachedMax(n) {
|
|
return n > max;
|
|
}
|
|
function reachedAny(n) {
|
|
return reachedMin(n) || reachedMax(n);
|
|
}
|
|
function constrain(n) {
|
|
if (!reachedAny(n)) return n;
|
|
return reachedMin(n) ? min : max;
|
|
}
|
|
function removeOffset(n) {
|
|
if (!length) return n;
|
|
return n - length * Math.ceil((n - max) / length);
|
|
}
|
|
const self = {
|
|
length,
|
|
max,
|
|
min,
|
|
constrain,
|
|
reachedAny,
|
|
reachedMax,
|
|
reachedMin,
|
|
removeOffset
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Counter(max, start, loop) {
|
|
const {
|
|
constrain
|
|
} = Limit(0, max);
|
|
const loopEnd = max + 1;
|
|
let counter = withinLimit(start);
|
|
function withinLimit(n) {
|
|
return !loop ? constrain(n) : mathAbs((loopEnd + n) % loopEnd);
|
|
}
|
|
function get() {
|
|
return counter;
|
|
}
|
|
function set(n) {
|
|
counter = withinLimit(n);
|
|
return self;
|
|
}
|
|
function add(n) {
|
|
return clone().set(get() + n);
|
|
}
|
|
function clone() {
|
|
return Counter(max, get(), loop);
|
|
}
|
|
const self = {
|
|
get,
|
|
set,
|
|
add,
|
|
clone
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function DragHandler(axis, rootNode, ownerDocument, ownerWindow, target, dragTracker, location, animation, scrollTo, scrollBody, scrollTarget, index, eventHandler, percentOfView, dragFree, dragThreshold, skipSnaps, baseFriction, watchDrag) {
|
|
const {
|
|
cross: crossAxis,
|
|
direction
|
|
} = axis;
|
|
const focusNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
|
const nonPassiveEvent = {
|
|
passive: false
|
|
};
|
|
const initEvents = EventStore();
|
|
const dragEvents = EventStore();
|
|
const goToNextThreshold = Limit(50, 225).constrain(percentOfView.measure(20));
|
|
const snapForceBoost = {
|
|
mouse: 300,
|
|
touch: 400
|
|
};
|
|
const freeForceBoost = {
|
|
mouse: 500,
|
|
touch: 600
|
|
};
|
|
const baseSpeed = dragFree ? 43 : 25;
|
|
let isMoving = false;
|
|
let startScroll = 0;
|
|
let startCross = 0;
|
|
let pointerIsDown = false;
|
|
let preventScroll = false;
|
|
let preventClick = false;
|
|
let isMouse = false;
|
|
function init(emblaApi) {
|
|
if (!watchDrag) return;
|
|
function downIfAllowed(evt) {
|
|
if (isBoolean(watchDrag) || watchDrag(emblaApi, evt)) down(evt);
|
|
}
|
|
const node = rootNode;
|
|
initEvents.add(node, 'dragstart', evt => evt.preventDefault(), nonPassiveEvent).add(node, 'touchmove', () => undefined, nonPassiveEvent).add(node, 'touchend', () => undefined).add(node, 'touchstart', downIfAllowed).add(node, 'mousedown', downIfAllowed).add(node, 'touchcancel', up).add(node, 'contextmenu', up).add(node, 'click', click, true);
|
|
}
|
|
function destroy() {
|
|
initEvents.clear();
|
|
dragEvents.clear();
|
|
}
|
|
function addDragEvents() {
|
|
const node = isMouse ? ownerDocument : rootNode;
|
|
dragEvents.add(node, 'touchmove', move, nonPassiveEvent).add(node, 'touchend', up).add(node, 'mousemove', move, nonPassiveEvent).add(node, 'mouseup', up);
|
|
}
|
|
function isFocusNode(node) {
|
|
const nodeName = node.nodeName || '';
|
|
return focusNodes.includes(nodeName);
|
|
}
|
|
function forceBoost() {
|
|
const boost = dragFree ? freeForceBoost : snapForceBoost;
|
|
const type = isMouse ? 'mouse' : 'touch';
|
|
return boost[type];
|
|
}
|
|
function allowedForce(force, targetChanged) {
|
|
const next = index.add(mathSign(force) * -1);
|
|
const baseForce = scrollTarget.byDistance(force, !dragFree).distance;
|
|
if (dragFree || mathAbs(force) < goToNextThreshold) return baseForce;
|
|
if (skipSnaps && targetChanged) return baseForce * 0.5;
|
|
return scrollTarget.byIndex(next.get(), 0).distance;
|
|
}
|
|
function down(evt) {
|
|
const isMouseEvt = isMouseEvent(evt, ownerWindow);
|
|
isMouse = isMouseEvt;
|
|
preventClick = dragFree && isMouseEvt && !evt.buttons && isMoving;
|
|
isMoving = deltaAbs(target.get(), location.get()) >= 2;
|
|
if (isMouseEvt && evt.button !== 0) return;
|
|
if (isFocusNode(evt.target)) return;
|
|
pointerIsDown = true;
|
|
dragTracker.pointerDown(evt);
|
|
scrollBody.useFriction(0).useDuration(0);
|
|
target.set(location);
|
|
addDragEvents();
|
|
startScroll = dragTracker.readPoint(evt);
|
|
startCross = dragTracker.readPoint(evt, crossAxis);
|
|
eventHandler.emit('pointerDown');
|
|
}
|
|
function move(evt) {
|
|
const isTouchEvt = !isMouseEvent(evt, ownerWindow);
|
|
if (isTouchEvt && evt.touches.length >= 2) return up(evt);
|
|
const lastScroll = dragTracker.readPoint(evt);
|
|
const lastCross = dragTracker.readPoint(evt, crossAxis);
|
|
const diffScroll = deltaAbs(lastScroll, startScroll);
|
|
const diffCross = deltaAbs(lastCross, startCross);
|
|
if (!preventScroll && !isMouse) {
|
|
if (!evt.cancelable) return up(evt);
|
|
preventScroll = diffScroll > diffCross;
|
|
if (!preventScroll) return up(evt);
|
|
}
|
|
const diff = dragTracker.pointerMove(evt);
|
|
if (diffScroll > dragThreshold) preventClick = true;
|
|
scrollBody.useFriction(0.3).useDuration(0.75);
|
|
animation.start();
|
|
target.add(direction(diff));
|
|
evt.preventDefault();
|
|
}
|
|
function up(evt) {
|
|
const currentLocation = scrollTarget.byDistance(0, false);
|
|
const targetChanged = currentLocation.index !== index.get();
|
|
const rawForce = dragTracker.pointerUp(evt) * forceBoost();
|
|
const force = allowedForce(direction(rawForce), targetChanged);
|
|
const forceFactor = factorAbs(rawForce, force);
|
|
const speed = baseSpeed - 10 * forceFactor;
|
|
const friction = baseFriction + forceFactor / 50;
|
|
preventScroll = false;
|
|
pointerIsDown = false;
|
|
dragEvents.clear();
|
|
scrollBody.useDuration(speed).useFriction(friction);
|
|
scrollTo.distance(force, !dragFree);
|
|
isMouse = false;
|
|
eventHandler.emit('pointerUp');
|
|
}
|
|
function click(evt) {
|
|
if (preventClick) {
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
preventClick = false;
|
|
}
|
|
}
|
|
function pointerDown() {
|
|
return pointerIsDown;
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy,
|
|
pointerDown
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function DragTracker(axis, ownerWindow) {
|
|
const logInterval = 170;
|
|
let startEvent;
|
|
let lastEvent;
|
|
function readTime(evt) {
|
|
return evt.timeStamp;
|
|
}
|
|
function readPoint(evt, evtAxis) {
|
|
const property = evtAxis || axis.scroll;
|
|
const coord = `client${property === 'x' ? 'X' : 'Y'}`;
|
|
return (isMouseEvent(evt, ownerWindow) ? evt : evt.touches[0])[coord];
|
|
}
|
|
function pointerDown(evt) {
|
|
startEvent = evt;
|
|
lastEvent = evt;
|
|
return readPoint(evt);
|
|
}
|
|
function pointerMove(evt) {
|
|
const diff = readPoint(evt) - readPoint(lastEvent);
|
|
const expired = readTime(evt) - readTime(startEvent) > logInterval;
|
|
lastEvent = evt;
|
|
if (expired) startEvent = evt;
|
|
return diff;
|
|
}
|
|
function pointerUp(evt) {
|
|
if (!startEvent || !lastEvent) return 0;
|
|
const diffDrag = readPoint(lastEvent) - readPoint(startEvent);
|
|
const diffTime = readTime(evt) - readTime(startEvent);
|
|
const expired = readTime(evt) - readTime(lastEvent) > logInterval;
|
|
const force = diffDrag / diffTime;
|
|
const isFlick = diffTime && !expired && mathAbs(force) > 0.1;
|
|
return isFlick ? force : 0;
|
|
}
|
|
const self = {
|
|
pointerDown,
|
|
pointerMove,
|
|
pointerUp,
|
|
readPoint
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function NodeRects() {
|
|
function measure(node) {
|
|
const {
|
|
offsetTop,
|
|
offsetLeft,
|
|
offsetWidth,
|
|
offsetHeight
|
|
} = node;
|
|
const offset = {
|
|
top: offsetTop,
|
|
right: offsetLeft + offsetWidth,
|
|
bottom: offsetTop + offsetHeight,
|
|
left: offsetLeft,
|
|
width: offsetWidth,
|
|
height: offsetHeight
|
|
};
|
|
return offset;
|
|
}
|
|
const self = {
|
|
measure
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function PercentOfView(viewSize) {
|
|
function measure(n) {
|
|
return viewSize * (n / 100);
|
|
}
|
|
const self = {
|
|
measure
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ResizeHandler(container, eventHandler, ownerWindow, slides, axis, watchResize, nodeRects) {
|
|
const observeNodes = [container].concat(slides);
|
|
let resizeObserver;
|
|
let containerSize;
|
|
let slideSizes = [];
|
|
let destroyed = false;
|
|
function readSize(node) {
|
|
return axis.measureSize(nodeRects.measure(node));
|
|
}
|
|
function init(emblaApi) {
|
|
if (!watchResize) return;
|
|
containerSize = readSize(container);
|
|
slideSizes = slides.map(readSize);
|
|
function defaultCallback(entries) {
|
|
for (const entry of entries) {
|
|
if (destroyed) return;
|
|
const isContainer = entry.target === container;
|
|
const slideIndex = slides.indexOf(entry.target);
|
|
const lastSize = isContainer ? containerSize : slideSizes[slideIndex];
|
|
const newSize = readSize(isContainer ? container : slides[slideIndex]);
|
|
const diffSize = mathAbs(newSize - lastSize);
|
|
if (diffSize >= 0.5) {
|
|
emblaApi.reInit();
|
|
eventHandler.emit('resize');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
resizeObserver = new ResizeObserver(entries => {
|
|
if (isBoolean(watchResize) || watchResize(emblaApi, entries)) {
|
|
defaultCallback(entries);
|
|
}
|
|
});
|
|
ownerWindow.requestAnimationFrame(() => {
|
|
observeNodes.forEach(node => resizeObserver.observe(node));
|
|
});
|
|
}
|
|
function destroy() {
|
|
destroyed = true;
|
|
if (resizeObserver) resizeObserver.disconnect();
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollBody(location, offsetLocation, previousLocation, target, baseDuration, baseFriction) {
|
|
let bodyVelocity = 0;
|
|
let scrollDirection = 0;
|
|
let scrollDuration = baseDuration;
|
|
let scrollFriction = baseFriction;
|
|
let rawLocation = location.get();
|
|
let rawLocationPrevious = 0;
|
|
function seek(timeStep) {
|
|
const fixedDeltaTimeSeconds = timeStep / 1000;
|
|
const duration = scrollDuration * fixedDeltaTimeSeconds;
|
|
const diff = target.get() - location.get();
|
|
const isInstant = !scrollDuration;
|
|
let directionDiff = 0;
|
|
if (isInstant) {
|
|
bodyVelocity = 0;
|
|
previousLocation.set(target);
|
|
location.set(target);
|
|
directionDiff = diff;
|
|
} else {
|
|
previousLocation.set(location);
|
|
bodyVelocity += diff / duration;
|
|
bodyVelocity *= scrollFriction;
|
|
rawLocation += bodyVelocity;
|
|
location.add(bodyVelocity * fixedDeltaTimeSeconds);
|
|
directionDiff = rawLocation - rawLocationPrevious;
|
|
}
|
|
scrollDirection = mathSign(directionDiff);
|
|
rawLocationPrevious = rawLocation;
|
|
return self;
|
|
}
|
|
function settled() {
|
|
const diff = target.get() - offsetLocation.get();
|
|
return mathAbs(diff) < 0.001;
|
|
}
|
|
function duration() {
|
|
return scrollDuration;
|
|
}
|
|
function direction() {
|
|
return scrollDirection;
|
|
}
|
|
function velocity() {
|
|
return bodyVelocity;
|
|
}
|
|
function useBaseDuration() {
|
|
return useDuration(baseDuration);
|
|
}
|
|
function useBaseFriction() {
|
|
return useFriction(baseFriction);
|
|
}
|
|
function useDuration(n) {
|
|
scrollDuration = n;
|
|
return self;
|
|
}
|
|
function useFriction(n) {
|
|
scrollFriction = n;
|
|
return self;
|
|
}
|
|
const self = {
|
|
direction,
|
|
duration,
|
|
velocity,
|
|
seek,
|
|
settled,
|
|
useBaseFriction,
|
|
useBaseDuration,
|
|
useFriction,
|
|
useDuration
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollBounds(limit, location, target, scrollBody, percentOfView) {
|
|
const pullBackThreshold = percentOfView.measure(10);
|
|
const edgeOffsetTolerance = percentOfView.measure(50);
|
|
const frictionLimit = Limit(0.1, 0.99);
|
|
let disabled = false;
|
|
function shouldConstrain() {
|
|
if (disabled) return false;
|
|
if (!limit.reachedAny(target.get())) return false;
|
|
if (!limit.reachedAny(location.get())) return false;
|
|
return true;
|
|
}
|
|
function constrain(pointerDown) {
|
|
if (!shouldConstrain()) return;
|
|
const edge = limit.reachedMin(location.get()) ? 'min' : 'max';
|
|
const diffToEdge = mathAbs(limit[edge] - location.get());
|
|
const diffToTarget = target.get() - location.get();
|
|
const friction = frictionLimit.constrain(diffToEdge / edgeOffsetTolerance);
|
|
target.subtract(diffToTarget * friction);
|
|
if (!pointerDown && mathAbs(diffToTarget) < pullBackThreshold) {
|
|
target.set(limit.constrain(target.get()));
|
|
scrollBody.useDuration(25).useBaseFriction();
|
|
}
|
|
}
|
|
function toggleActive(active) {
|
|
disabled = !active;
|
|
}
|
|
const self = {
|
|
shouldConstrain,
|
|
constrain,
|
|
toggleActive
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollContain(viewSize, contentSize, snapsAligned, containScroll, pixelTolerance) {
|
|
const scrollBounds = Limit(-contentSize + viewSize, 0);
|
|
const snapsBounded = measureBounded();
|
|
const scrollContainLimit = findScrollContainLimit();
|
|
const snapsContained = measureContained();
|
|
function usePixelTolerance(bound, snap) {
|
|
return deltaAbs(bound, snap) < 1;
|
|
}
|
|
function findScrollContainLimit() {
|
|
const startSnap = snapsBounded[0];
|
|
const endSnap = arrayLast(snapsBounded);
|
|
const min = snapsBounded.lastIndexOf(startSnap);
|
|
const max = snapsBounded.indexOf(endSnap) + 1;
|
|
return Limit(min, max);
|
|
}
|
|
function measureBounded() {
|
|
return snapsAligned.map((snapAligned, index) => {
|
|
const {
|
|
min,
|
|
max
|
|
} = scrollBounds;
|
|
const snap = scrollBounds.constrain(snapAligned);
|
|
const isFirst = !index;
|
|
const isLast = arrayIsLastIndex(snapsAligned, index);
|
|
if (isFirst) return max;
|
|
if (isLast) return min;
|
|
if (usePixelTolerance(min, snap)) return min;
|
|
if (usePixelTolerance(max, snap)) return max;
|
|
return snap;
|
|
}).map(scrollBound => parseFloat(scrollBound.toFixed(3)));
|
|
}
|
|
function measureContained() {
|
|
if (contentSize <= viewSize + pixelTolerance) return [scrollBounds.max];
|
|
if (containScroll === 'keepSnaps') return snapsBounded;
|
|
const {
|
|
min,
|
|
max
|
|
} = scrollContainLimit;
|
|
return snapsBounded.slice(min, max);
|
|
}
|
|
const self = {
|
|
snapsContained,
|
|
scrollContainLimit
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollLimit(contentSize, scrollSnaps, loop) {
|
|
const max = scrollSnaps[0];
|
|
const min = loop ? max - contentSize : arrayLast(scrollSnaps);
|
|
const limit = Limit(min, max);
|
|
const self = {
|
|
limit
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollLooper(contentSize, limit, location, vectors) {
|
|
const jointSafety = 0.1;
|
|
const min = limit.min + jointSafety;
|
|
const max = limit.max + jointSafety;
|
|
const {
|
|
reachedMin,
|
|
reachedMax
|
|
} = Limit(min, max);
|
|
function shouldLoop(direction) {
|
|
if (direction === 1) return reachedMax(location.get());
|
|
if (direction === -1) return reachedMin(location.get());
|
|
return false;
|
|
}
|
|
function loop(direction) {
|
|
if (!shouldLoop(direction)) return;
|
|
const loopDistance = contentSize * (direction * -1);
|
|
vectors.forEach(v => v.add(loopDistance));
|
|
}
|
|
const self = {
|
|
loop
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollProgress(limit) {
|
|
const {
|
|
max,
|
|
length
|
|
} = limit;
|
|
function get(n) {
|
|
const currentLocation = n - max;
|
|
return length ? currentLocation / -length : 0;
|
|
}
|
|
const self = {
|
|
get
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollSnaps(axis, alignment, containerRect, slideRects, slidesToScroll) {
|
|
const {
|
|
startEdge,
|
|
endEdge
|
|
} = axis;
|
|
const {
|
|
groupSlides
|
|
} = slidesToScroll;
|
|
const alignments = measureSizes().map(alignment.measure);
|
|
const snaps = measureUnaligned();
|
|
const snapsAligned = measureAligned();
|
|
function measureSizes() {
|
|
return groupSlides(slideRects).map(rects => arrayLast(rects)[endEdge] - rects[0][startEdge]).map(mathAbs);
|
|
}
|
|
function measureUnaligned() {
|
|
return slideRects.map(rect => containerRect[startEdge] - rect[startEdge]).map(snap => -mathAbs(snap));
|
|
}
|
|
function measureAligned() {
|
|
return groupSlides(snaps).map(g => g[0]).map((snap, index) => snap + alignments[index]);
|
|
}
|
|
const self = {
|
|
snaps,
|
|
snapsAligned
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlideRegistry(containSnaps, containScroll, scrollSnaps, scrollContainLimit, slidesToScroll, slideIndexes) {
|
|
const {
|
|
groupSlides
|
|
} = slidesToScroll;
|
|
const {
|
|
min,
|
|
max
|
|
} = scrollContainLimit;
|
|
const slideRegistry = createSlideRegistry();
|
|
function createSlideRegistry() {
|
|
const groupedSlideIndexes = groupSlides(slideIndexes);
|
|
const doNotContain = !containSnaps || containScroll === 'keepSnaps';
|
|
if (scrollSnaps.length === 1) return [slideIndexes];
|
|
if (doNotContain) return groupedSlideIndexes;
|
|
return groupedSlideIndexes.slice(min, max).map((group, index, groups) => {
|
|
const isFirst = !index;
|
|
const isLast = arrayIsLastIndex(groups, index);
|
|
if (isFirst) {
|
|
const range = arrayLast(groups[0]) + 1;
|
|
return arrayFromNumber(range);
|
|
}
|
|
if (isLast) {
|
|
const range = arrayLastIndex(slideIndexes) - arrayLast(groups)[0] + 1;
|
|
return arrayFromNumber(range, arrayLast(groups)[0]);
|
|
}
|
|
return group;
|
|
});
|
|
}
|
|
const self = {
|
|
slideRegistry
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollTarget(loop, scrollSnaps, contentSize, limit, targetVector) {
|
|
const {
|
|
reachedAny,
|
|
removeOffset,
|
|
constrain
|
|
} = limit;
|
|
function minDistance(distances) {
|
|
return distances.concat().sort((a, b) => mathAbs(a) - mathAbs(b))[0];
|
|
}
|
|
function findTargetSnap(target) {
|
|
const distance = loop ? removeOffset(target) : constrain(target);
|
|
const ascDiffsToSnaps = scrollSnaps.map((snap, index) => ({
|
|
diff: shortcut(snap - distance, 0),
|
|
index
|
|
})).sort((d1, d2) => mathAbs(d1.diff) - mathAbs(d2.diff));
|
|
const {
|
|
index
|
|
} = ascDiffsToSnaps[0];
|
|
return {
|
|
index,
|
|
distance
|
|
};
|
|
}
|
|
function shortcut(target, direction) {
|
|
const targets = [target, target + contentSize, target - contentSize];
|
|
if (!loop) return target;
|
|
if (!direction) return minDistance(targets);
|
|
const matchingTargets = targets.filter(t => mathSign(t) === direction);
|
|
if (matchingTargets.length) return minDistance(matchingTargets);
|
|
return arrayLast(targets) - contentSize;
|
|
}
|
|
function byIndex(index, direction) {
|
|
const diffToSnap = scrollSnaps[index] - targetVector.get();
|
|
const distance = shortcut(diffToSnap, direction);
|
|
return {
|
|
index,
|
|
distance
|
|
};
|
|
}
|
|
function byDistance(distance, snap) {
|
|
const target = targetVector.get() + distance;
|
|
const {
|
|
index,
|
|
distance: targetSnapDistance
|
|
} = findTargetSnap(target);
|
|
const reachedBound = !loop && reachedAny(target);
|
|
if (!snap || reachedBound) return {
|
|
index,
|
|
distance
|
|
};
|
|
const diffToSnap = scrollSnaps[index] - targetSnapDistance;
|
|
const snapDistance = distance + shortcut(diffToSnap, 0);
|
|
return {
|
|
index,
|
|
distance: snapDistance
|
|
};
|
|
}
|
|
const self = {
|
|
byDistance,
|
|
byIndex,
|
|
shortcut
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function ScrollTo(animation, indexCurrent, indexPrevious, scrollBody, scrollTarget, targetVector, eventHandler) {
|
|
function scrollTo(target) {
|
|
const distanceDiff = target.distance;
|
|
const indexDiff = target.index !== indexCurrent.get();
|
|
targetVector.add(distanceDiff);
|
|
if (distanceDiff) {
|
|
if (scrollBody.duration()) {
|
|
animation.start();
|
|
} else {
|
|
animation.update();
|
|
animation.render(1);
|
|
animation.update();
|
|
}
|
|
}
|
|
if (indexDiff) {
|
|
indexPrevious.set(indexCurrent.get());
|
|
indexCurrent.set(target.index);
|
|
eventHandler.emit('select');
|
|
}
|
|
}
|
|
function distance(n, snap) {
|
|
const target = scrollTarget.byDistance(n, snap);
|
|
scrollTo(target);
|
|
}
|
|
function index(n, direction) {
|
|
const targetIndex = indexCurrent.clone().set(n);
|
|
const target = scrollTarget.byIndex(targetIndex.get(), direction);
|
|
scrollTo(target);
|
|
}
|
|
const self = {
|
|
distance,
|
|
index
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlideFocus(root, slides, slideRegistry, scrollTo, scrollBody, eventStore, eventHandler, watchFocus) {
|
|
const focusListenerOptions = {
|
|
passive: true,
|
|
capture: true
|
|
};
|
|
let lastTabPressTime = 0;
|
|
function init(emblaApi) {
|
|
if (!watchFocus) return;
|
|
function defaultCallback(index) {
|
|
const nowTime = new Date().getTime();
|
|
const diffTime = nowTime - lastTabPressTime;
|
|
if (diffTime > 10) return;
|
|
eventHandler.emit('slideFocusStart');
|
|
root.scrollLeft = 0;
|
|
const group = slideRegistry.findIndex(group => group.includes(index));
|
|
if (!isNumber(group)) return;
|
|
scrollBody.useDuration(0);
|
|
scrollTo.index(group, 0);
|
|
eventHandler.emit('slideFocus');
|
|
}
|
|
eventStore.add(document, 'keydown', registerTabPress, false);
|
|
slides.forEach((slide, slideIndex) => {
|
|
eventStore.add(slide, 'focus', evt => {
|
|
if (isBoolean(watchFocus) || watchFocus(emblaApi, evt)) {
|
|
defaultCallback(slideIndex);
|
|
}
|
|
}, focusListenerOptions);
|
|
});
|
|
}
|
|
function registerTabPress(event) {
|
|
if (event.code === 'Tab') lastTabPressTime = new Date().getTime();
|
|
}
|
|
const self = {
|
|
init
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Vector1D(initialValue) {
|
|
let value = initialValue;
|
|
function get() {
|
|
return value;
|
|
}
|
|
function set(n) {
|
|
value = normalizeInput(n);
|
|
}
|
|
function add(n) {
|
|
value += normalizeInput(n);
|
|
}
|
|
function subtract(n) {
|
|
value -= normalizeInput(n);
|
|
}
|
|
function normalizeInput(n) {
|
|
return isNumber(n) ? n : n.get();
|
|
}
|
|
const self = {
|
|
get,
|
|
set,
|
|
add,
|
|
subtract
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Translate(axis, container) {
|
|
const translate = axis.scroll === 'x' ? x : y;
|
|
const containerStyle = container.style;
|
|
let disabled = false;
|
|
function x(n) {
|
|
return `translate3d(${n}px,0px,0px)`;
|
|
}
|
|
function y(n) {
|
|
return `translate3d(0px,${n}px,0px)`;
|
|
}
|
|
function to(target) {
|
|
if (disabled) return;
|
|
containerStyle.transform = translate(axis.direction(target));
|
|
}
|
|
function toggleActive(active) {
|
|
disabled = !active;
|
|
}
|
|
function clear() {
|
|
if (disabled) return;
|
|
containerStyle.transform = '';
|
|
if (!container.getAttribute('style')) container.removeAttribute('style');
|
|
}
|
|
const self = {
|
|
clear,
|
|
to,
|
|
toggleActive
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlideLooper(axis, viewSize, contentSize, slideSizes, slideSizesWithGaps, snaps, scrollSnaps, location, slides) {
|
|
const roundingSafety = 0.5;
|
|
const ascItems = arrayKeys(slideSizesWithGaps);
|
|
const descItems = arrayKeys(slideSizesWithGaps).reverse();
|
|
const loopPoints = startPoints().concat(endPoints());
|
|
function removeSlideSizes(indexes, from) {
|
|
return indexes.reduce((a, i) => {
|
|
return a - slideSizesWithGaps[i];
|
|
}, from);
|
|
}
|
|
function slidesInGap(indexes, gap) {
|
|
return indexes.reduce((a, i) => {
|
|
const remainingGap = removeSlideSizes(a, gap);
|
|
return remainingGap > 0 ? a.concat([i]) : a;
|
|
}, []);
|
|
}
|
|
function findSlideBounds(offset) {
|
|
return snaps.map((snap, index) => ({
|
|
start: snap - slideSizes[index] + roundingSafety + offset,
|
|
end: snap + viewSize - roundingSafety + offset
|
|
}));
|
|
}
|
|
function findLoopPoints(indexes, offset, isEndEdge) {
|
|
const slideBounds = findSlideBounds(offset);
|
|
return indexes.map(index => {
|
|
const initial = isEndEdge ? 0 : -contentSize;
|
|
const altered = isEndEdge ? contentSize : 0;
|
|
const boundEdge = isEndEdge ? 'end' : 'start';
|
|
const loopPoint = slideBounds[index][boundEdge];
|
|
return {
|
|
index,
|
|
loopPoint,
|
|
slideLocation: Vector1D(-1),
|
|
translate: Translate(axis, slides[index]),
|
|
target: () => location.get() > loopPoint ? initial : altered
|
|
};
|
|
});
|
|
}
|
|
function startPoints() {
|
|
const gap = scrollSnaps[0];
|
|
const indexes = slidesInGap(descItems, gap);
|
|
return findLoopPoints(indexes, contentSize, false);
|
|
}
|
|
function endPoints() {
|
|
const gap = viewSize - scrollSnaps[0] - 1;
|
|
const indexes = slidesInGap(ascItems, gap);
|
|
return findLoopPoints(indexes, -contentSize, true);
|
|
}
|
|
function canLoop() {
|
|
return loopPoints.every(({
|
|
index
|
|
}) => {
|
|
const otherIndexes = ascItems.filter(i => i !== index);
|
|
return removeSlideSizes(otherIndexes, viewSize) <= 0.1;
|
|
});
|
|
}
|
|
function loop() {
|
|
loopPoints.forEach(loopPoint => {
|
|
const {
|
|
target,
|
|
translate,
|
|
slideLocation
|
|
} = loopPoint;
|
|
const shiftLocation = target();
|
|
if (shiftLocation === slideLocation.get()) return;
|
|
translate.to(shiftLocation);
|
|
slideLocation.set(shiftLocation);
|
|
});
|
|
}
|
|
function clear() {
|
|
loopPoints.forEach(loopPoint => loopPoint.translate.clear());
|
|
}
|
|
const self = {
|
|
canLoop,
|
|
clear,
|
|
loop,
|
|
loopPoints
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlidesHandler(container, eventHandler, watchSlides) {
|
|
let mutationObserver;
|
|
let destroyed = false;
|
|
function init(emblaApi) {
|
|
if (!watchSlides) return;
|
|
function defaultCallback(mutations) {
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === 'childList') {
|
|
emblaApi.reInit();
|
|
eventHandler.emit('slidesChanged');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
mutationObserver = new MutationObserver(mutations => {
|
|
if (destroyed) return;
|
|
if (isBoolean(watchSlides) || watchSlides(emblaApi, mutations)) {
|
|
defaultCallback(mutations);
|
|
}
|
|
});
|
|
mutationObserver.observe(container, {
|
|
childList: true
|
|
});
|
|
}
|
|
function destroy() {
|
|
if (mutationObserver) mutationObserver.disconnect();
|
|
destroyed = true;
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlidesInView(container, slides, eventHandler, threshold) {
|
|
const intersectionEntryMap = {};
|
|
let inViewCache = null;
|
|
let notInViewCache = null;
|
|
let intersectionObserver;
|
|
let destroyed = false;
|
|
function init() {
|
|
intersectionObserver = new IntersectionObserver(entries => {
|
|
if (destroyed) return;
|
|
entries.forEach(entry => {
|
|
const index = slides.indexOf(entry.target);
|
|
intersectionEntryMap[index] = entry;
|
|
});
|
|
inViewCache = null;
|
|
notInViewCache = null;
|
|
eventHandler.emit('slidesInView');
|
|
}, {
|
|
root: container.parentElement,
|
|
threshold
|
|
});
|
|
slides.forEach(slide => intersectionObserver.observe(slide));
|
|
}
|
|
function destroy() {
|
|
if (intersectionObserver) intersectionObserver.disconnect();
|
|
destroyed = true;
|
|
}
|
|
function createInViewList(inView) {
|
|
return objectKeys(intersectionEntryMap).reduce((list, slideIndex) => {
|
|
const index = parseInt(slideIndex);
|
|
const {
|
|
isIntersecting
|
|
} = intersectionEntryMap[index];
|
|
const inViewMatch = inView && isIntersecting;
|
|
const notInViewMatch = !inView && !isIntersecting;
|
|
if (inViewMatch || notInViewMatch) list.push(index);
|
|
return list;
|
|
}, []);
|
|
}
|
|
function get(inView = true) {
|
|
if (inView && inViewCache) return inViewCache;
|
|
if (!inView && notInViewCache) return notInViewCache;
|
|
const slideIndexes = createInViewList(inView);
|
|
if (inView) inViewCache = slideIndexes;
|
|
if (!inView) notInViewCache = slideIndexes;
|
|
return slideIndexes;
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy,
|
|
get
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlideSizes(axis, containerRect, slideRects, slides, readEdgeGap, ownerWindow) {
|
|
const {
|
|
measureSize,
|
|
startEdge,
|
|
endEdge
|
|
} = axis;
|
|
const withEdgeGap = slideRects[0] && readEdgeGap;
|
|
const startGap = measureStartGap();
|
|
const endGap = measureEndGap();
|
|
const slideSizes = slideRects.map(measureSize);
|
|
const slideSizesWithGaps = measureWithGaps();
|
|
function measureStartGap() {
|
|
if (!withEdgeGap) return 0;
|
|
const slideRect = slideRects[0];
|
|
return mathAbs(containerRect[startEdge] - slideRect[startEdge]);
|
|
}
|
|
function measureEndGap() {
|
|
if (!withEdgeGap) return 0;
|
|
const style = ownerWindow.getComputedStyle(arrayLast(slides));
|
|
return parseFloat(style.getPropertyValue(`margin-${endEdge}`));
|
|
}
|
|
function measureWithGaps() {
|
|
return slideRects.map((rect, index, rects) => {
|
|
const isFirst = !index;
|
|
const isLast = arrayIsLastIndex(rects, index);
|
|
if (isFirst) return slideSizes[index] + startGap;
|
|
if (isLast) return slideSizes[index] + endGap;
|
|
return rects[index + 1][startEdge] - rect[startEdge];
|
|
}).map(mathAbs);
|
|
}
|
|
const self = {
|
|
slideSizes,
|
|
slideSizesWithGaps,
|
|
startGap,
|
|
endGap
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function SlidesToScroll(axis, viewSize, slidesToScroll, loop, containerRect, slideRects, startGap, endGap, pixelTolerance) {
|
|
const {
|
|
startEdge,
|
|
endEdge,
|
|
direction
|
|
} = axis;
|
|
const groupByNumber = isNumber(slidesToScroll);
|
|
function byNumber(array, groupSize) {
|
|
return arrayKeys(array).filter(i => i % groupSize === 0).map(i => array.slice(i, i + groupSize));
|
|
}
|
|
function bySize(array) {
|
|
if (!array.length) return [];
|
|
return arrayKeys(array).reduce((groups, rectB, index) => {
|
|
const rectA = arrayLast(groups) || 0;
|
|
const isFirst = rectA === 0;
|
|
const isLast = rectB === arrayLastIndex(array);
|
|
const edgeA = containerRect[startEdge] - slideRects[rectA][startEdge];
|
|
const edgeB = containerRect[startEdge] - slideRects[rectB][endEdge];
|
|
const gapA = !loop && isFirst ? direction(startGap) : 0;
|
|
const gapB = !loop && isLast ? direction(endGap) : 0;
|
|
const chunkSize = mathAbs(edgeB - gapB - (edgeA + gapA));
|
|
if (index && chunkSize > viewSize + pixelTolerance) groups.push(rectB);
|
|
if (isLast) groups.push(array.length);
|
|
return groups;
|
|
}, []).map((currentSize, index, groups) => {
|
|
const previousSize = Math.max(groups[index - 1] || 0);
|
|
return array.slice(previousSize, currentSize);
|
|
});
|
|
}
|
|
function groupSlides(array) {
|
|
return groupByNumber ? byNumber(array, slidesToScroll) : bySize(array);
|
|
}
|
|
const self = {
|
|
groupSlides
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function Engine(root, container, slides, ownerDocument, ownerWindow, options, eventHandler) {
|
|
// Options
|
|
const {
|
|
align,
|
|
axis: scrollAxis,
|
|
direction,
|
|
startIndex,
|
|
loop,
|
|
duration,
|
|
dragFree,
|
|
dragThreshold,
|
|
inViewThreshold,
|
|
slidesToScroll: groupSlides,
|
|
skipSnaps,
|
|
containScroll,
|
|
watchResize,
|
|
watchSlides,
|
|
watchDrag,
|
|
watchFocus
|
|
} = options;
|
|
// Measurements
|
|
const pixelTolerance = 2;
|
|
const nodeRects = NodeRects();
|
|
const containerRect = nodeRects.measure(container);
|
|
const slideRects = slides.map(nodeRects.measure);
|
|
const axis = Axis(scrollAxis, direction);
|
|
const viewSize = axis.measureSize(containerRect);
|
|
const percentOfView = PercentOfView(viewSize);
|
|
const alignment = Alignment(align, viewSize);
|
|
const containSnaps = !loop && !!containScroll;
|
|
const readEdgeGap = loop || !!containScroll;
|
|
const {
|
|
slideSizes,
|
|
slideSizesWithGaps,
|
|
startGap,
|
|
endGap
|
|
} = SlideSizes(axis, containerRect, slideRects, slides, readEdgeGap, ownerWindow);
|
|
const slidesToScroll = SlidesToScroll(axis, viewSize, groupSlides, loop, containerRect, slideRects, startGap, endGap, pixelTolerance);
|
|
const {
|
|
snaps,
|
|
snapsAligned
|
|
} = ScrollSnaps(axis, alignment, containerRect, slideRects, slidesToScroll);
|
|
const contentSize = -arrayLast(snaps) + arrayLast(slideSizesWithGaps);
|
|
const {
|
|
snapsContained,
|
|
scrollContainLimit
|
|
} = ScrollContain(viewSize, contentSize, snapsAligned, containScroll, pixelTolerance);
|
|
const scrollSnaps = containSnaps ? snapsContained : snapsAligned;
|
|
const {
|
|
limit
|
|
} = ScrollLimit(contentSize, scrollSnaps, loop);
|
|
// Indexes
|
|
const index = Counter(arrayLastIndex(scrollSnaps), startIndex, loop);
|
|
const indexPrevious = index.clone();
|
|
const slideIndexes = arrayKeys(slides);
|
|
// Animation
|
|
const update = ({
|
|
dragHandler,
|
|
scrollBody,
|
|
scrollBounds,
|
|
options: {
|
|
loop
|
|
}
|
|
}, timeStep) => {
|
|
if (!loop) scrollBounds.constrain(dragHandler.pointerDown());
|
|
scrollBody.seek(timeStep);
|
|
};
|
|
const render = ({
|
|
scrollBody,
|
|
translate,
|
|
location,
|
|
offsetLocation,
|
|
scrollLooper,
|
|
slideLooper,
|
|
dragHandler,
|
|
animation,
|
|
eventHandler,
|
|
scrollBounds,
|
|
options: {
|
|
loop
|
|
}
|
|
}, lagOffset) => {
|
|
const shouldSettle = scrollBody.settled();
|
|
const withinBounds = !scrollBounds.shouldConstrain();
|
|
const hasSettled = loop ? shouldSettle : shouldSettle && withinBounds;
|
|
if (hasSettled && !dragHandler.pointerDown()) {
|
|
animation.stop();
|
|
eventHandler.emit('settle');
|
|
}
|
|
if (!hasSettled) eventHandler.emit('scroll');
|
|
const interpolatedLocation = location.get() * lagOffset + previousLocation.get() * (1 - lagOffset);
|
|
offsetLocation.set(interpolatedLocation);
|
|
if (loop) {
|
|
scrollLooper.loop(scrollBody.direction());
|
|
slideLooper.loop();
|
|
}
|
|
translate.to(offsetLocation.get());
|
|
};
|
|
const animation = Animations(ownerDocument, ownerWindow, timeStep => update(engine, timeStep), lagOffset => render(engine, lagOffset));
|
|
// Shared
|
|
const friction = 0.68;
|
|
const startLocation = scrollSnaps[index.get()];
|
|
const location = Vector1D(startLocation);
|
|
const previousLocation = Vector1D(startLocation);
|
|
const offsetLocation = Vector1D(startLocation);
|
|
const target = Vector1D(startLocation);
|
|
const scrollBody = ScrollBody(location, offsetLocation, previousLocation, target, duration, friction);
|
|
const scrollTarget = ScrollTarget(loop, scrollSnaps, contentSize, limit, target);
|
|
const scrollTo = ScrollTo(animation, index, indexPrevious, scrollBody, scrollTarget, target, eventHandler);
|
|
const scrollProgress = ScrollProgress(limit);
|
|
const eventStore = EventStore();
|
|
const slidesInView = SlidesInView(container, slides, eventHandler, inViewThreshold);
|
|
const {
|
|
slideRegistry
|
|
} = SlideRegistry(containSnaps, containScroll, scrollSnaps, scrollContainLimit, slidesToScroll, slideIndexes);
|
|
const slideFocus = SlideFocus(root, slides, slideRegistry, scrollTo, scrollBody, eventStore, eventHandler, watchFocus);
|
|
// Engine
|
|
const engine = {
|
|
ownerDocument,
|
|
ownerWindow,
|
|
eventHandler,
|
|
containerRect,
|
|
slideRects,
|
|
animation,
|
|
axis,
|
|
dragHandler: DragHandler(axis, root, ownerDocument, ownerWindow, target, DragTracker(axis, ownerWindow), location, animation, scrollTo, scrollBody, scrollTarget, index, eventHandler, percentOfView, dragFree, dragThreshold, skipSnaps, friction, watchDrag),
|
|
eventStore,
|
|
percentOfView,
|
|
index,
|
|
indexPrevious,
|
|
limit,
|
|
location,
|
|
offsetLocation,
|
|
previousLocation,
|
|
options,
|
|
resizeHandler: ResizeHandler(container, eventHandler, ownerWindow, slides, axis, watchResize, nodeRects),
|
|
scrollBody,
|
|
scrollBounds: ScrollBounds(limit, offsetLocation, target, scrollBody, percentOfView),
|
|
scrollLooper: ScrollLooper(contentSize, limit, offsetLocation, [location, offsetLocation, previousLocation, target]),
|
|
scrollProgress,
|
|
scrollSnapList: scrollSnaps.map(scrollProgress.get),
|
|
scrollSnaps,
|
|
scrollTarget,
|
|
scrollTo,
|
|
slideLooper: SlideLooper(axis, viewSize, contentSize, slideSizes, slideSizesWithGaps, snaps, scrollSnaps, offsetLocation, slides),
|
|
slideFocus,
|
|
slidesHandler: SlidesHandler(container, eventHandler, watchSlides),
|
|
slidesInView,
|
|
slideIndexes,
|
|
slideRegistry,
|
|
slidesToScroll,
|
|
target,
|
|
translate: Translate(axis, container)
|
|
};
|
|
return engine;
|
|
}
|
|
|
|
function EventHandler() {
|
|
let listeners = {};
|
|
let api;
|
|
function init(emblaApi) {
|
|
api = emblaApi;
|
|
}
|
|
function getListeners(evt) {
|
|
return listeners[evt] || [];
|
|
}
|
|
function emit(evt) {
|
|
getListeners(evt).forEach(e => e(api, evt));
|
|
return self;
|
|
}
|
|
function on(evt, cb) {
|
|
listeners[evt] = getListeners(evt).concat([cb]);
|
|
return self;
|
|
}
|
|
function off(evt, cb) {
|
|
listeners[evt] = getListeners(evt).filter(e => e !== cb);
|
|
return self;
|
|
}
|
|
function clear() {
|
|
listeners = {};
|
|
}
|
|
const self = {
|
|
init,
|
|
emit,
|
|
off,
|
|
on,
|
|
clear
|
|
};
|
|
return self;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
align: 'center',
|
|
axis: 'x',
|
|
container: null,
|
|
slides: null,
|
|
containScroll: 'trimSnaps',
|
|
direction: 'ltr',
|
|
slidesToScroll: 1,
|
|
inViewThreshold: 0,
|
|
breakpoints: {},
|
|
dragFree: false,
|
|
dragThreshold: 10,
|
|
loop: false,
|
|
skipSnaps: false,
|
|
duration: 25,
|
|
startIndex: 0,
|
|
active: true,
|
|
watchDrag: true,
|
|
watchResize: true,
|
|
watchSlides: true,
|
|
watchFocus: true
|
|
};
|
|
|
|
function OptionsHandler(ownerWindow) {
|
|
function mergeOptions(optionsA, optionsB) {
|
|
return objectsMergeDeep(optionsA, optionsB || {});
|
|
}
|
|
function optionsAtMedia(options) {
|
|
const optionsAtMedia = options.breakpoints || {};
|
|
const matchedMediaOptions = objectKeys(optionsAtMedia).filter(media => ownerWindow.matchMedia(media).matches).map(media => optionsAtMedia[media]).reduce((a, mediaOption) => mergeOptions(a, mediaOption), {});
|
|
return mergeOptions(options, matchedMediaOptions);
|
|
}
|
|
function optionsMediaQueries(optionsList) {
|
|
return optionsList.map(options => objectKeys(options.breakpoints || {})).reduce((acc, mediaQueries) => acc.concat(mediaQueries), []).map(ownerWindow.matchMedia);
|
|
}
|
|
const self = {
|
|
mergeOptions,
|
|
optionsAtMedia,
|
|
optionsMediaQueries
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function PluginsHandler(optionsHandler) {
|
|
let activePlugins = [];
|
|
function init(emblaApi, plugins) {
|
|
activePlugins = plugins.filter(({
|
|
options
|
|
}) => optionsHandler.optionsAtMedia(options).active !== false);
|
|
activePlugins.forEach(plugin => plugin.init(emblaApi, optionsHandler));
|
|
return plugins.reduce((map, plugin) => Object.assign(map, {
|
|
[plugin.name]: plugin
|
|
}), {});
|
|
}
|
|
function destroy() {
|
|
activePlugins = activePlugins.filter(plugin => plugin.destroy());
|
|
}
|
|
const self = {
|
|
init,
|
|
destroy
|
|
};
|
|
return self;
|
|
}
|
|
|
|
function EmblaCarousel(root, userOptions, userPlugins) {
|
|
const ownerDocument = root.ownerDocument;
|
|
const ownerWindow = ownerDocument.defaultView;
|
|
const optionsHandler = OptionsHandler(ownerWindow);
|
|
const pluginsHandler = PluginsHandler(optionsHandler);
|
|
const mediaHandlers = EventStore();
|
|
const eventHandler = EventHandler();
|
|
const {
|
|
mergeOptions,
|
|
optionsAtMedia,
|
|
optionsMediaQueries
|
|
} = optionsHandler;
|
|
const {
|
|
on,
|
|
off,
|
|
emit
|
|
} = eventHandler;
|
|
const reInit = reActivate;
|
|
let destroyed = false;
|
|
let engine;
|
|
let optionsBase = mergeOptions(defaultOptions, EmblaCarousel.globalOptions);
|
|
let options = mergeOptions(optionsBase);
|
|
let pluginList = [];
|
|
let pluginApis;
|
|
let container;
|
|
let slides;
|
|
function storeElements() {
|
|
const {
|
|
container: userContainer,
|
|
slides: userSlides
|
|
} = options;
|
|
const customContainer = isString(userContainer) ? root.querySelector(userContainer) : userContainer;
|
|
container = customContainer || root.children[0];
|
|
const customSlides = isString(userSlides) ? container.querySelectorAll(userSlides) : userSlides;
|
|
slides = [].slice.call(customSlides || container.children);
|
|
}
|
|
function createEngine(options) {
|
|
const engine = Engine(root, container, slides, ownerDocument, ownerWindow, options, eventHandler);
|
|
if (options.loop && !engine.slideLooper.canLoop()) {
|
|
const optionsWithoutLoop = Object.assign({}, options, {
|
|
loop: false
|
|
});
|
|
return createEngine(optionsWithoutLoop);
|
|
}
|
|
return engine;
|
|
}
|
|
function activate(withOptions, withPlugins) {
|
|
if (destroyed) return;
|
|
optionsBase = mergeOptions(optionsBase, withOptions);
|
|
options = optionsAtMedia(optionsBase);
|
|
pluginList = withPlugins || pluginList;
|
|
storeElements();
|
|
engine = createEngine(options);
|
|
optionsMediaQueries([optionsBase, ...pluginList.map(({
|
|
options
|
|
}) => options)]).forEach(query => mediaHandlers.add(query, 'change', reActivate));
|
|
if (!options.active) return;
|
|
engine.translate.to(engine.location.get());
|
|
engine.animation.init();
|
|
engine.slidesInView.init();
|
|
engine.slideFocus.init(self);
|
|
engine.eventHandler.init(self);
|
|
engine.resizeHandler.init(self);
|
|
engine.slidesHandler.init(self);
|
|
if (engine.options.loop) engine.slideLooper.loop();
|
|
if (container.offsetParent && slides.length) engine.dragHandler.init(self);
|
|
pluginApis = pluginsHandler.init(self, pluginList);
|
|
}
|
|
function reActivate(withOptions, withPlugins) {
|
|
const startIndex = selectedScrollSnap();
|
|
deActivate();
|
|
activate(mergeOptions({
|
|
startIndex
|
|
}, withOptions), withPlugins);
|
|
eventHandler.emit('reInit');
|
|
}
|
|
function deActivate() {
|
|
engine.dragHandler.destroy();
|
|
engine.eventStore.clear();
|
|
engine.translate.clear();
|
|
engine.slideLooper.clear();
|
|
engine.resizeHandler.destroy();
|
|
engine.slidesHandler.destroy();
|
|
engine.slidesInView.destroy();
|
|
engine.animation.destroy();
|
|
pluginsHandler.destroy();
|
|
mediaHandlers.clear();
|
|
}
|
|
function destroy() {
|
|
if (destroyed) return;
|
|
destroyed = true;
|
|
mediaHandlers.clear();
|
|
deActivate();
|
|
eventHandler.emit('destroy');
|
|
eventHandler.clear();
|
|
}
|
|
function scrollTo(index, jump, direction) {
|
|
if (!options.active || destroyed) return;
|
|
engine.scrollBody.useBaseFriction().useDuration(jump === true ? 0 : options.duration);
|
|
engine.scrollTo.index(index, direction || 0);
|
|
}
|
|
function scrollNext(jump) {
|
|
const next = engine.index.add(1).get();
|
|
scrollTo(next, jump, -1);
|
|
}
|
|
function scrollPrev(jump) {
|
|
const prev = engine.index.add(-1).get();
|
|
scrollTo(prev, jump, 1);
|
|
}
|
|
function canScrollNext() {
|
|
const next = engine.index.add(1).get();
|
|
return next !== selectedScrollSnap();
|
|
}
|
|
function canScrollPrev() {
|
|
const prev = engine.index.add(-1).get();
|
|
return prev !== selectedScrollSnap();
|
|
}
|
|
function scrollSnapList() {
|
|
return engine.scrollSnapList;
|
|
}
|
|
function scrollProgress() {
|
|
return engine.scrollProgress.get(engine.location.get());
|
|
}
|
|
function selectedScrollSnap() {
|
|
return engine.index.get();
|
|
}
|
|
function previousScrollSnap() {
|
|
return engine.indexPrevious.get();
|
|
}
|
|
function slidesInView() {
|
|
return engine.slidesInView.get();
|
|
}
|
|
function slidesNotInView() {
|
|
return engine.slidesInView.get(false);
|
|
}
|
|
function plugins() {
|
|
return pluginApis;
|
|
}
|
|
function internalEngine() {
|
|
return engine;
|
|
}
|
|
function rootNode() {
|
|
return root;
|
|
}
|
|
function containerNode() {
|
|
return container;
|
|
}
|
|
function slideNodes() {
|
|
return slides;
|
|
}
|
|
const self = {
|
|
canScrollNext,
|
|
canScrollPrev,
|
|
containerNode,
|
|
internalEngine,
|
|
destroy,
|
|
off,
|
|
on,
|
|
emit,
|
|
plugins,
|
|
previousScrollSnap,
|
|
reInit,
|
|
rootNode,
|
|
scrollNext,
|
|
scrollPrev,
|
|
scrollProgress,
|
|
scrollSnapList,
|
|
scrollTo,
|
|
selectedScrollSnap,
|
|
slideNodes,
|
|
slidesInView,
|
|
slidesNotInView
|
|
};
|
|
activate(userOptions, userPlugins);
|
|
setTimeout(() => eventHandler.emit('init'), 0);
|
|
return self;
|
|
}
|
|
EmblaCarousel.globalOptions = undefined;
|
|
|
|
module.exports = EmblaCarousel;
|
|
//# sourceMappingURL=embla-carousel.cjs.js.map
|