sweet-scroll
Version:
Modern and the sweet smooth scroll library.
691 lines (673 loc) • 27.6 kB
JavaScript
/*! @preserve sweet-scroll v4.0.0 - tsuyoshiwada | MIT License */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.SweetScroll = factory());
}(this, function () { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
// @link https://github.com/JedWatson/exenv/blob/master/index.js
var canUseDOM = !!(typeof window !== 'undefined' &&
window.document &&
window.document.createElement);
var canUseHistory = !canUseDOM
? false
: window.history && 'pushState' in window.history && window.location.protocol !== 'file:';
var canUsePassiveOption = (function () {
var support = false;
if (!canUseDOM) {
return support;
}
/* tslint:disable:no-empty */
try {
var win = window;
var opts = Object.defineProperty({}, 'passive', {
get: function () {
support = true;
},
});
win.addEventListener('test', null, opts);
win.removeEventListener('test', null, opts);
}
catch (e) { }
/* tslint:enable */
return support;
})();
var isString = function (obj) { return typeof obj === 'string'; };
var isFunction = function (obj) { return typeof obj === 'function'; };
var isArray = function (obj) { return Array.isArray(obj); };
var isNumeric = function (obj) { return !isArray(obj) && obj - parseFloat(obj) + 1 >= 0; };
var hasProp = function (obj, key) { return obj && obj.hasOwnProperty(key); };
var raf = canUseDOM
? window.requestAnimationFrame.bind(window)
: null;
var caf = canUseDOM
? window.cancelAnimationFrame.bind(window)
: null;
/* tslint:disable:curly */
/* tslint:disable:no-conditional-assignment */
var cos = Math.cos, sin = Math.sin, pow = Math.pow, sqrt = Math.sqrt, PI = Math.PI;
var easings = {
linear: function (p) { return p; },
easeInQuad: function (_, t, b, c, d) { return c * (t /= d) * t + b; },
easeOutQuad: function (_, t, b, c, d) { return -c * (t /= d) * (t - 2) + b; },
easeInOutQuad: function (_, t, b, c, d) {
return (t /= d / 2) < 1 ? (c / 2) * t * t + b : (-c / 2) * (--t * (t - 2) - 1) + b;
},
easeInCubic: function (_, t, b, c, d) { return c * (t /= d) * t * t + b; },
easeOutCubic: function (_, t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; },
easeInOutCubic: function (_, t, b, c, d) {
return (t /= d / 2) < 1 ? (c / 2) * t * t * t + b : (c / 2) * ((t -= 2) * t * t + 2) + b;
},
easeInQuart: function (_, t, b, c, d) { return c * (t /= d) * t * t * t + b; },
easeOutQuart: function (_, t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; },
easeInOutQuart: function (_, t, b, c, d) {
return (t /= d / 2) < 1 ? (c / 2) * t * t * t * t + b : (-c / 2) * ((t -= 2) * t * t * t - 2) + b;
},
easeInQuint: function (_, t, b, c, d) { return c * (t /= d) * t * t * t * t + b; },
easeOutQuint: function (_, t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; },
easeInOutQuint: function (_, t, b, c, d) {
return (t /= d / 2) < 1
? (c / 2) * t * t * t * t * t + b
: (c / 2) * ((t -= 2) * t * t * t * t + 2) + b;
},
easeInSine: function (_, t, b, c, d) { return -c * cos((t / d) * (PI / 2)) + c + b; },
easeOutSine: function (_, t, b, c, d) { return c * sin((t / d) * (PI / 2)) + b; },
easeInOutSine: function (_, t, b, c, d) { return (-c / 2) * (cos((PI * t) / d) - 1) + b; },
easeInExpo: function (_, t, b, c, d) { return (t === 0 ? b : c * pow(2, 10 * (t / d - 1)) + b); },
easeOutExpo: function (_, t, b, c, d) { return (t === d ? b + c : c * (-pow(2, (-10 * t) / d) + 1) + b); },
easeInOutExpo: function (_, t, b, c, d) {
if (t === 0)
return b;
if (t === d)
return b + c;
if ((t /= d / 2) < 1)
return (c / 2) * pow(2, 10 * (t - 1)) + b;
return (c / 2) * (-pow(2, -10 * --t) + 2) + b;
},
easeInCirc: function (_, t, b, c, d) { return -c * (sqrt(1 - (t /= d) * t) - 1) + b; },
easeOutCirc: function (_, t, b, c, d) { return c * sqrt(1 - (t = t / d - 1) * t) + b; },
easeInOutCirc: function (_, t, b, c, d) {
return (t /= d / 2) < 1
? (-c / 2) * (sqrt(1 - t * t) - 1) + b
: (c / 2) * (sqrt(1 - (t -= 2) * t) + 1) + b;
},
};
var $$ = function (selector) {
return Array.prototype.slice.call((!selector ? [] : document.querySelectorAll(selector)));
};
var $ = function (selector) { return $$(selector).shift() || null; };
var isElement = function (obj) { return obj instanceof Element; };
var isWindow = function ($el) { return $el === window; };
var isRootContainer = function ($el) {
return $el === document.documentElement || $el === document.body;
};
var matches = function ($el, selector) {
if (isElement(selector)) {
return $el === selector;
}
var results = $$(selector);
var i = results.length;
// tslint:disable-next-line no-empty
while (--i >= 0 && results[i] !== $el) { }
return i > -1;
};
var getHeight = function ($el) {
return Math.max($el.scrollHeight, $el.clientHeight, $el.offsetHeight);
};
var getWidth = function ($el) {
return Math.max($el.scrollWidth, $el.clientWidth, $el.offsetWidth);
};
var getSize = function ($el) { return ({
width: getWidth($el),
height: getHeight($el),
}); };
var getViewportAndElementSizes = function ($el) {
var isRoot = isWindow($el) || isRootContainer($el);
return {
viewport: {
width: isRoot
? Math.min(window.innerWidth, document.documentElement.clientWidth)
: $el.clientWidth,
height: isRoot ? window.innerHeight : $el.clientHeight,
},
size: isRoot
? {
width: Math.max(getWidth(document.body), getWidth(document.documentElement)),
height: Math.max(getHeight(document.body), getHeight(document.documentElement)),
}
: getSize($el),
};
};
var directionMethodMap = {
y: 'scrollTop',
x: 'scrollLeft',
};
var directionPropMap = {
y: 'pageYOffset',
x: 'pageXOffset',
};
var getScroll = function ($el, direction) {
return isWindow($el) ? $el[directionPropMap[direction]] : $el[directionMethodMap[direction]];
};
var setScroll = function ($el, offset, direction) {
if (isWindow($el)) {
var top_1 = direction === 'y';
$el.scrollTo(!top_1 ? offset : $el.pageXOffset, top_1 ? offset : $el.pageYOffset);
}
else {
$el[directionMethodMap[direction]] = offset;
}
};
var getOffset = function ($el, $context) {
var rect = $el.getBoundingClientRect();
if (rect.width || rect.height) {
var scroll_1 = { top: 0, left: 0 };
var $ctx = void 0;
if (isWindow($context) || isRootContainer($context)) {
$ctx = document.documentElement;
scroll_1.top = window[directionPropMap.y];
scroll_1.left = window[directionPropMap.x];
}
else {
$ctx = $context;
var cRect = $ctx.getBoundingClientRect();
scroll_1.top = cRect.top * -1 + $ctx[directionMethodMap.y];
scroll_1.left = cRect.left * -1 + $ctx[directionMethodMap.x];
}
return {
top: rect.top + scroll_1.top - $ctx.clientTop,
left: rect.left + scroll_1.left - $ctx.clientLeft,
};
}
return rect;
};
var wheelEventName = (function () {
if (!canUseDOM) {
return 'wheel';
}
return 'onwheel' in document ? 'wheel' : 'mousewheel';
})();
var eventName = function (name) { return (name === 'wheel' ? wheelEventName : name); };
var apply = function ($el, method, event, listener, passive) {
event.split(' ').forEach(function (name) {
$el[method](eventName(name), listener, canUsePassiveOption ? { passive: passive } : false);
});
};
var addEvent = function ($el, event, listener, passive) { return apply($el, 'addEventListener', event, listener, passive); };
var removeEvent = function ($el, event, listener, passive) { return apply($el, 'removeEventListener', event, listener, passive); };
var reRelativeToken = /^(\+|-)=(\d+(?:\.\d+)?)$/;
var parseCoordinate = function (coordinate, enableVertical) {
var res = { top: 0, left: 0, relative: false };
// Object ({ top: {n}, left: {n} })
if (hasProp(coordinate, 'top') || hasProp(coordinate, 'left')) {
res = __assign({}, res, coordinate);
// Array ([{n}, [{n}])
}
else if (isArray(coordinate)) {
if (coordinate.length > 1) {
res.top = coordinate[0];
res.left = coordinate[1];
}
else if (coordinate.length === 1) {
res.top = enableVertical ? coordinate[0] : 0;
res.left = !enableVertical ? coordinate[0] : 0;
}
else {
return null;
}
// Number
}
else if (isNumeric(coordinate)) {
if (enableVertical) {
res.top = coordinate;
}
else {
res.left = coordinate;
}
// String ('+={n}', '-={n}')
}
else if (isString(coordinate)) {
var m = coordinate.trim().match(reRelativeToken);
if (!m) {
return null;
}
var op = m[1];
var val = parseInt(m[2], 10);
if (op === '+') {
res.top = enableVertical ? val : 0;
res.left = !enableVertical ? val : 0;
}
else {
res.top = enableVertical ? -val : 0;
res.left = !enableVertical ? -val : 0;
}
res.relative = true;
}
else {
return null;
}
return res;
};
var defaultOptions = {
trigger: '[data-scroll]',
header: '[data-scroll-header]',
duration: 1000,
easing: 'easeOutQuint',
offset: 0,
vertical: true,
horizontal: false,
cancellable: true,
updateURL: false,
preventDefault: true,
stopPropagation: true,
// Callbacks
before: null,
after: null,
cancel: null,
complete: null,
step: null,
};
var CONTAINER_CLICK_EVENT = 'click';
var CONTAINER_STOP_EVENT = 'wheel touchstart touchmove';
var SweetScroll = /** @class */ (function () {
/**
* Constructor
*/
function SweetScroll(options, container) {
var _this = this;
this.$el = null;
this.ctx = {
$trigger: null,
opts: null,
progress: false,
pos: null,
startPos: null,
easing: null,
start: 0,
id: 0,
cancel: false,
hash: null,
};
/**
* Handle each frame of the animation.
*/
this.loop = function (time) {
var _a = _this, $el = _a.$el, ctx = _a.ctx;
if (!ctx.start) {
ctx.start = time;
}
if (!ctx.progress || !$el) {
_this.stop();
return;
}
var options = ctx.opts;
var offset = ctx.pos;
var start = ctx.start;
var startOffset = ctx.startPos;
var easing = ctx.easing;
var duration = options.duration;
var directionMap = { top: 'y', left: 'x' };
var timeElapsed = time - start;
var t = Math.min(1, Math.max(timeElapsed / duration, 0));
Object.keys(offset).forEach(function (key) {
var value = offset[key];
var initial = startOffset[key];
var delta = value - initial;
if (delta !== 0) {
var val = easing(t, duration * t, 0, 1, duration);
setScroll($el, Math.round(initial + delta * val), directionMap[key]);
}
});
if (timeElapsed <= duration) {
_this.hook(options, 'step', t);
ctx.id = SweetScroll.raf(_this.loop);
}
else {
_this.stop(true);
}
};
/**
* Handling of container click event.
*/
this.handleClick = function (e) {
var opts = _this.opts;
var $el = e.target;
for (; $el && $el !== document; $el = $el.parentNode) {
if (!matches($el, opts.trigger)) {
continue;
}
var dataOptions = JSON.parse($el.getAttribute('data-scroll-options') || '{}');
var data = $el.getAttribute('data-scroll');
var to = data || $el.getAttribute('href');
var options = __assign({}, opts, dataOptions);
var preventDefault = options.preventDefault, stopPropagation = options.stopPropagation, vertical = options.vertical, horizontal = options.horizontal;
if (preventDefault) {
e.preventDefault();
}
if (stopPropagation) {
e.stopPropagation();
}
// Passes the trigger element to callback
_this.ctx.$trigger = $el;
if (horizontal && vertical) {
_this.to(to, options);
}
else if (vertical) {
_this.toTop(to, options);
}
else if (horizontal) {
_this.toLeft(to, options);
}
break;
}
};
/**
* Handling of container stop events.
*/
this.handleStop = function (e) {
var ctx = _this.ctx;
var opts = ctx.opts;
if (opts && opts.cancellable) {
ctx.cancel = true;
_this.stop();
}
else {
e.preventDefault();
}
};
this.opts = __assign({}, defaultOptions, (options || {}));
var $container = null;
if (canUseDOM) {
if (typeof container === 'string') {
$container = $(container);
}
else if (container != null) {
$container = container;
}
else {
$container = window;
}
}
this.$el = $container;
if ($container) {
this.bind(true, false);
}
}
/**
* SweetScroll instance factory.
*/
SweetScroll.create = function (options, container) {
return new SweetScroll(options, container);
};
/**
* Scroll animation to the specified position.
*/
SweetScroll.prototype.to = function (distance, options) {
if (!canUseDOM) {
return;
}
var _a = this, $el = _a.$el, ctx = _a.ctx, currentOptions = _a.opts;
var $trigger = ctx.$trigger;
var opts = __assign({}, currentOptions, (options || {}));
var optOffset = opts.offset, vertical = opts.vertical, horizontal = opts.horizontal;
var $header = isElement(opts.header) ? opts.header : $(opts.header);
var reg = /^#/;
var hash = isString(distance) && reg.test(distance) ? distance : null;
ctx.opts = opts; // Temporary options
ctx.cancel = false; // Disable the call flag of `cancel`
ctx.hash = hash;
// Stop current animation
this.stop();
// Does not move if the container is not found
if (!$el) {
return;
}
// Get scroll offset
var offset = parseCoordinate(optOffset, vertical);
var coordinate = parseCoordinate(distance, vertical);
var scroll = { top: 0, left: 0 };
if (coordinate) {
if (coordinate.relative) {
var current = getScroll($el, vertical ? 'y' : 'x');
scroll.top = vertical ? current + coordinate.top : coordinate.top;
scroll.left = !vertical ? current + coordinate.left : coordinate.left;
}
else {
scroll = coordinate;
}
}
else if (isString(distance) && distance !== '#') {
var $target = $(distance);
if (!$target) {
return;
}
scroll = getOffset($target, $el);
}
if (offset) {
scroll.top += offset.top;
scroll.left += offset.left;
}
if ($header) {
scroll.top = Math.max(0, scroll.top - getSize($header).height);
}
// Normalize scroll offset
var _b = getViewportAndElementSizes($el), viewport = _b.viewport, size = _b.size;
scroll.top = vertical
? Math.max(0, Math.min(size.height - viewport.height, scroll.top))
: getScroll($el, 'y');
scroll.left = horizontal
? Math.max(0, Math.min(size.width - viewport.width, scroll.left))
: getScroll($el, 'x');
// Call `before`
// Stop scrolling when it returns false
if (this.hook(opts, 'before', scroll, $trigger) === false) {
ctx.opts = null;
return;
}
// Set offset
ctx.pos = scroll;
// Run animation!!
this.start(opts);
// Bind stop events
this.bind(false, true);
};
/**
* Scroll animation to specified left position.
*/
SweetScroll.prototype.toTop = function (distance, options) {
this.to(distance, __assign({}, (options || {}), { vertical: true, horizontal: false }));
};
/**
* Scroll animation to specified top position.
*/
SweetScroll.prototype.toLeft = function (distance, options) {
this.to(distance, __assign({}, (options || {}), { vertical: false, horizontal: true }));
};
/**
* Scroll animation to specified element.
*/
SweetScroll.prototype.toElement = function ($element, options) {
var $el = this.$el;
if (!canUseDOM || !$el) {
return;
}
this.to(getOffset($element, $el), options || {});
};
/**
* Stop the current scroll animation.
*/
SweetScroll.prototype.stop = function (gotoEnd) {
if (gotoEnd === void 0) { gotoEnd = false; }
var _a = this, $el = _a.$el, ctx = _a.ctx;
var pos = ctx.pos;
if (!$el || !ctx.progress) {
return;
}
SweetScroll.caf(ctx.id);
ctx.progress = false;
ctx.start = 0;
ctx.id = 0;
if (gotoEnd && pos) {
setScroll($el, pos.left, 'x');
setScroll($el, pos.top, 'y');
}
this.complete();
};
/**
* Update options.
*/
SweetScroll.prototype.update = function (options) {
if (this.$el) {
var opts = __assign({}, this.opts, options);
this.stop();
this.unbind(true, true);
this.opts = opts;
this.bind(true, false);
}
};
/**
* Destroy instance.
*/
SweetScroll.prototype.destroy = function () {
if (this.$el) {
this.stop();
this.unbind(true, true);
this.$el = null;
}
};
/**
* Callback methods.
*/
/* tslint:disable:no-empty */
SweetScroll.prototype.onBefore = function (_, __) {
return true;
};
SweetScroll.prototype.onStep = function (_) { };
SweetScroll.prototype.onAfter = function (_, __) { };
SweetScroll.prototype.onCancel = function () { };
SweetScroll.prototype.onComplete = function (_) { };
/* tslint:enable */
/**
* Start scrolling animation.
*/
SweetScroll.prototype.start = function (opts) {
var ctx = this.ctx;
ctx.opts = opts;
ctx.progress = true;
ctx.easing = isFunction(opts.easing)
? opts.easing
: easings[opts.easing];
// Update start offset.
var $container = this.$el;
var start = {
top: getScroll($container, 'y'),
left: getScroll($container, 'x'),
};
ctx.startPos = start;
// Loop
ctx.id = SweetScroll.raf(this.loop);
};
/**
* Handle the completion of scrolling animation.
*/
SweetScroll.prototype.complete = function () {
var _a = this, $el = _a.$el, ctx = _a.ctx;
var hash = ctx.hash, cancel = ctx.cancel, opts = ctx.opts, pos = ctx.pos, $trigger = ctx.$trigger;
if (!$el || !opts) {
return;
}
if (hash != null && hash !== window.location.hash) {
var updateURL = opts.updateURL;
if (canUseDOM && canUseHistory && updateURL !== false) {
window.history[updateURL === 'replace' ? 'replaceState' : 'pushState'](null, '', hash);
}
}
this.unbind(false, true);
ctx.opts = null;
ctx.$trigger = null;
if (cancel) {
this.hook(opts, 'cancel');
}
else {
this.hook(opts, 'after', pos, $trigger);
}
this.hook(opts, 'complete', cancel);
};
/**
* Callback function and method call.
*/
SweetScroll.prototype.hook = function (options, type) {
var args = [];
for (var _i = 2; _i < arguments.length; _i++) {
args[_i - 2] = arguments[_i];
}
var _a;
var callback = options[type];
var callbackResult;
var methodResult;
// callback
if (isFunction(callback)) {
callbackResult = callback.apply(this, args.concat([this]));
}
// method
methodResult = (_a = this)["on" + (type[0].toUpperCase() + type.slice(1))].apply(_a, args);
return callbackResult !== undefined ? callbackResult : methodResult;
};
/**
* Bind events of container element.
*/
SweetScroll.prototype.bind = function (click, stop) {
var _a = this, $el = _a.$el, opts = _a.ctx.opts;
if ($el) {
if (click) {
addEvent($el, CONTAINER_CLICK_EVENT, this.handleClick, false);
}
if (stop) {
addEvent($el, CONTAINER_STOP_EVENT, this.handleStop, opts ? opts.cancellable : true);
}
}
};
/**
* Unbind events of container element.
*/
SweetScroll.prototype.unbind = function (click, stop) {
var _a = this, $el = _a.$el, opts = _a.ctx.opts;
if ($el) {
if (click) {
removeEvent($el, CONTAINER_CLICK_EVENT, this.handleClick, false);
}
if (stop) {
removeEvent($el, CONTAINER_STOP_EVENT, this.handleStop, opts ? opts.cancellable : true);
}
}
};
/**
* You can set Polyfill (or Ponyfill) for browsers that do not support requestAnimationFrame.
*/
SweetScroll.raf = raf;
SweetScroll.caf = caf;
return SweetScroll;
}());
return SweetScroll;
}));