stroll.js
Version:
stroll.js is strolling along in your viewport
322 lines (252 loc) • 9.86 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.stroll = factory());
}(this, (function () { 'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
function createOptions(defaults, options, adapter) {
return Object.assign({
ignoreUserScroll: false,
allowInvalidPositions: false,
offset: { x: 0, y: 0 },
duration: 500,
focus: true,
// Robert Penner's easeInOutQuad - http://robertpenner.com/easing/
easing: function easing(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
}, defaults, options, {
start: adapter.getCurrentPosition()
});
}
function resolveDuration(duration, distance) {
var type = typeof duration === 'undefined' ? 'undefined' : _typeof(duration);
if (type === 'number') return duration;
if (type === 'function') return duration(distance);
var parsed = parseFloat(duration);
if (isNaN(parsed)) {
throw new Error('invalid duration');
}
return parsed;
}
function resolveOffset(offset, adapter) {
var type = typeof offset === 'undefined' ? 'undefined' : _typeof(offset);
if (type === 'number') {
return adapter.createPositionFromNumber(offset);
}
if (type === 'object') {
if (typeof offset.x === 'undefined') offset.x = 0;
if (typeof offset.y === 'undefined') offset.y = 0;
return offset;
}
throw new Error('invalid offset');
}
function positionAbsolute(distance) {
return { x: Math.abs(distance.x), y: Math.abs(distance.y) };
}
function positionCompare(pos1, pos2) {
return pos1.x === pos2.x && pos1.y === pos2.y;
}
function positionAdd(pos1, pos2) {
return { x: pos1.x + pos2.x, y: pos1.y + pos2.y };
}
function ease(easing, timeElapsed, start, target, duration) {
return {
x: Math.round(easing(timeElapsed, start.x, target.x, duration)),
y: Math.round(easing(timeElapsed, start.y, target.y, duration))
};
}
function createLoop(stroll, options) {
var adapter = options.adapter;
function startLoop(resolve) {
var animationFrame = void 0;
var timeStart = void 0;
var lastPosition = void 0;
function loop(timeCurrent) {
if (!timeStart) {
timeStart = timeCurrent;
}
var timeElapsed = timeCurrent - timeStart;
var newPosition = ease(options.easing, timeElapsed, options.start, options.target, options.duration);
if (!options.allowInvalidPositions && adapter.isPositionOutsideOfElement(newPosition)) {
return stroll.currentStroll('invalid_position');
}
if (!options.ignoreUserScroll && lastPosition && !positionCompare(lastPosition, adapter.getCurrentPosition())) {
return stroll.currentStroll('user_scrolled');
}
if (timeElapsed > options.duration) {
done(resolve);
} else {
adapter.scrollTo(newPosition);
lastPosition = adapter.getCurrentPosition();
next();
}
}
function next() {
animationFrame = window.requestAnimationFrame(loop);
}
if (stroll.currentStroll) {
stroll.currentStroll('new_stroll');
}
next();
stroll.currentStroll = function (cancelReason) {
window.cancelAnimationFrame(animationFrame);
resolve({ wasCancelled: true, cancelReason: cancelReason });
};
return stroll.currentStroll;
}
function done(resolve) {
stroll.currentStroll = null;
// easing might create small offsets from the requested target
adapter.scrollTo(positionAdd(options.start, options.target));
// keyboard navigation might reset the scroll position
// this sets focus to the element to prevent such problems
if (options.element && options.focus) {
if (!options.element.hasAttribute('tabindex')) {
options.element.setAttribute('tabindex', '-1');
}
options.element.focus();
}
resolve({ wasCancelled: false });
}
return { start: startLoop };
}
function createStroller(adapter) {
function stroll(target) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return new Promise(function (resolve) {
var strollOptions = createOptions(stroll.DEFAULTS, options, adapter);
var offset = resolveOffset(strollOptions.offset, adapter);
var strollTarget = adapter.resolveTarget(target, strollOptions.start, offset);
var duration = resolveDuration(strollOptions.duration, positionAbsolute(strollTarget.target));
var loop = createLoop(stroll, Object.assign({}, strollOptions, strollTarget, {
duration: duration, offset: offset, adapter: adapter
}));
loop.start(resolve);
});
}
stroll.currentStroll = null;
stroll.relative = function (offset) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return stroll(null, Object.assign({ offset: offset }, options));
};
stroll.DEFAULTS = {};
return stroll;
}
var _typeof$1 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var d = typeof document !== 'undefined' ? document : {};
var w = typeof window !== 'undefined' ? window : {};
function resolveTargetFactory(el, createPositionFromNumber) {
return function resolveTarget(userTarget, start, offset) {
var type = typeof userTarget === 'undefined' ? 'undefined' : _typeof$1(userTarget);
var target = type === 'string' ? el.querySelector(userTarget) : userTarget;
var result = {};
if (userTarget === null || type === 'undefined') {
var zero = { x: 0, y: 0 };
return resolveTarget(offset, zero, zero);
}
if (type === 'number') {
var pos = createPositionFromNumber(userTarget);
// add start and offset to axes, that are not 0 by default
pos.x = pos.x - start.x + offset.x;
pos.y = pos.y - start.y + offset.y;
result.target = pos;
result.focus = false;
return result;
}
if (type === 'object' && !(userTarget instanceof w.HTMLElement)) {
result.target = {
x: (userTarget.x || 0) - start.x + offset.x,
y: (userTarget.y || 0) - start.y + offset.y
};
result.focus = false;
return result;
}
if (!target || !target.getBoundingClientRect) {
throw new Error('invalid target');
}
var boundingRect = target.getBoundingClientRect();
result.element = target;
result.target = {
x: boundingRect.left + offset.x,
y: boundingRect.top + offset.y
};
return result;
};
}
function createPositionFromNumberFactory(calculateMaxima) {
return function (number) {
var maxima = calculateMaxima();
var primaryAxis = maxima.y > maxima.x ? 'y' : 'x';
var result = {};
result[primaryAxis] = number;
result[primaryAxis === 'x' ? 'y' : 'x'] = 0;
return result;
};
}
function isPositionOutsideOfElementFactory(calculateMaxima) {
return function (pos) {
if ((typeof pos === 'undefined' ? 'undefined' : _typeof$1(pos)) !== 'object') return true;
var maxima = calculateMaxima();
return pos.x < 0 || pos.x > maxima.x || pos.y < 0 || pos.y > maxima.y;
};
}
function createBaseAdapter(el, calculateMaxima) {
var createPositionFromNumber = createPositionFromNumberFactory(calculateMaxima);
var isPositionOutsideOfElement = isPositionOutsideOfElementFactory(calculateMaxima);
var resolveTarget = resolveTargetFactory(el, createPositionFromNumber);
return { createPositionFromNumber: createPositionFromNumber, resolveTarget: resolveTarget, isPositionOutsideOfElement: isPositionOutsideOfElement };
}
function createWindowAdapter() {
function calculateMaxima() {
var documentWidth = Math.max(d.body.scrollWidth, d.body.offsetWidth, d.documentElement.clientWidth, d.documentElement.scrollWidth, d.documentElement.offsetWidth) - w.innerWidth;
var x = Math.max(0, documentWidth);
var documentHeight = Math.max(d.body.scrollHeight, d.body.offsetHeight, d.documentElement.clientHeight, d.documentElement.scrollHeight, d.documentElement.offsetHeight) - w.innerHeight;
var y = Math.max(0, documentHeight);
return { x: x, y: y };
}
return Object.assign(createBaseAdapter(d, calculateMaxima), {
getCurrentPosition: function getCurrentPosition() {
return {
x: w.scrollX || w.pageXOffset,
y: w.scrollY || w.pageYOffset
};
},
scrollTo: function scrollTo(pos) {
w.scrollTo(pos.x, pos.y);
}
});
}
function createElementAdapter(el) {
function calculateMaxima() {
return {
x: Math.max(0, el.scrollWidth - el.clientWidth),
y: Math.max(0, el.scrollHeight - el.clientHeight)
};
}
return Object.assign(createBaseAdapter(el, calculateMaxima), {
getCurrentPosition: function getCurrentPosition() {
return { x: el.scrollLeft, y: el.scrollTop };
},
scrollTo: function scrollTo(pos) {
el.scrollLeft = pos.x;
el.scrollTop = pos.y;
}
});
}
var publicAdapters = {
element: createElementAdapter
};
var stroll = createStroller(createWindowAdapter());
stroll.factory = function (el) {
var adapter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'element';
if (typeof adapter === 'string') {
adapter = publicAdapters[adapter];
}
return createStroller(adapter(el));
};
return stroll;
})));