reeller
Version:
Flexible, powerful and modern library for creating the running horizontal blocks effect, also known as ticker or the «marquee effect».
791 lines (674 loc) • 19.6 kB
JavaScript
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);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
var Base = /*#__PURE__*/function () {
/**
* Base class.
*/
function Base() {
this.events = {};
}
/**
* Attach an event handler function.
*
* @param {string} event Event name.
* @param {function} callback Callback.
*/
var _proto = Base.prototype;
_proto.on = function on(event, callback) {
if (!(this.events[event] instanceof Array)) this.events[event] = [];
this.events[event].push(callback);
}
/**
* Remove an event handler.
*
* @param {string} event Event name.
* @param {function} [callback] Callback.
*/
;
_proto.off = function off(event, callback) {
if (callback) {
this.events[event] = this.events[event].filter(function (f) {
return f !== callback;
});
} else {
this.events[event] = [];
}
}
/**
* Execute all handlers for the given event type.
*
* @param {string} event Event name.
* @param params Extra parameters.
*/
;
_proto.trigger = function trigger(event) {
var _arguments = arguments,
_this = this;
if (!this.events[event]) return;
this.events[event].forEach(function (f) {
return f.call.apply(f, [_this, _this].concat([].slice.call(_arguments, 1)));
});
};
return Base;
}();
var Filler = /*#__PURE__*/function (_Base) {
_inheritsLoose(Filler, _Base);
/**
* @typedef {Object} FillerOptions
* @property {string|HTMLElement|null} container Container element or selector.
* @property {string|HTMLElement|null} wrapper Inner element or selector.
* @property {string|null} itemSelector Items CSS selector.
* @property {string} [cloneClassName] Class name of the new clones.
* @property {boolean} [autoUpdate] Use ResizeObserver to auto update clones number.
* @property {boolean} [clonesOverflow] Create artificial overflow with clones.
* @property {boolean} [clonesFinish] Bring the cycle of clones to an end.
* @property {boolean} [clonesMin] Minimum number of clones.
*/
/**
* Default options.
*
* @type {FillerOptions}
*/
/**
* Create Filler instance.
*
* @param {FillerOptions} [options] Filler options.
*/
function Filler(options) {
var _this;
_this = _Base.call(this) || this;
/** @type {FillerOptions} **/
_this.options = _extends({}, Filler.defaultOptions, options);
_this.container = typeof _this.options.container === 'string' ? document.querySelector(_this.options.container) : _this.options.container;
_this.wrapper = typeof _this.options.wrapper === 'string' ? _this.container.querySelector(_this.options.wrapper) : _this.options.wrapper || _this.options.container;
/** @type Array.<HTMLElement> **/
_this.item = [];
_this.refresh(false);
if (_this.options.autoUpdate) {
_this.bindResizeObserver();
} else {
_this.update();
}
return _this;
}
/**
* Bind ResizeObserver to container for auto update.
*/
var _proto = Filler.prototype;
_proto.bindResizeObserver = function bindResizeObserver() {
var _this2 = this;
this.resizeObserver = new ResizeObserver(function () {
_this2.update();
});
this.resizeObserver.observe(this.container);
}
/**
* Creates and adds clones to end in the desired number from given offset.
*
* @param {number} [count] Number of clones to add.
* @param {number} [offset] Offset from start.
*/
;
_proto.addClones = function addClones(count, offset) {
var _this$wrapper;
if (offset === void 0) {
offset = 0;
}
var clones = [];
for (var i = 0; i < count; i++) {
var item = this.item[(offset + i) % this.item.length].cloneNode(true);
item.classList.add(this.options.cloneClassName);
clones.push(item);
}
(_this$wrapper = this.wrapper).append.apply(_this$wrapper, clones);
}
/**
* Removes the desired number of clones from the end.
*
* @param {number} [count] Number of clones to remove.
*/
;
_proto.removeClones = function removeClones(count) {
if (count === void 0) {
count = 0;
}
var clones = Array.from(this.wrapper.getElementsByClassName(this.options.cloneClassName));
clones.slice(-count).forEach(function (el) {
return el.remove();
});
}
/**
* Sets the desired number of clones.
*
* @param {number} [count] Number of clones.
*/
;
_proto.setClonesCount = function setClonesCount(count) {
if (this.clonesCount === count) return;
if (this.clonesCount < count) this.addClones(count - this.clonesCount, this.clonesCount);
if (this.clonesCount > count) this.removeClones(this.clonesCount - count);
this.clonesCount = count;
}
/**
* Get calculated data object.
*
* @return {Object} Calculated data.
*/
;
_proto.getCalcData = function getCalcData() {
var data = {
clonesCount: 0,
clonesWidth: 0,
containerWidth: this.container.offsetWidth,
fullWidth: 0,
itemWidth: [],
itemsWidth: 0,
lastIndex: 0
};
this.item.map(function (el) {
var style = window.getComputedStyle(el);
var width = el.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
data.itemWidth.push(width);
data.itemsWidth += width;
});
var itemLength = data.itemWidth.length;
var width = this.options.clonesOverflow ? data.containerWidth : data.containerWidth - data.itemsWidth;
while (width > data.clonesWidth || data.clonesCount < this.options.clonesMin || this.options.clonesFinish && data.clonesCount % itemLength > 0) {
data.lastIndex = data.clonesCount % itemLength;
data.clonesWidth += data.itemWidth[data.lastIndex];
data.clonesCount++;
}
data.fullWidth = data.clonesWidth + data.itemsWidth;
return data;
}
/**
* Calculates and sets the number of clones.
*/
;
_proto.update = function update() {
this.calcData = this.getCalcData();
this.setClonesCount(this.calcData.clonesCount);
this.trigger('update', this.calcData);
}
/**
* Fully refresh and update all clones.
*
* @param {boolean} [update] Update after refresh.
*/
;
_proto.refresh = function refresh(update) {
if (update === void 0) {
update = true;
}
this.removeClones();
this.item = Array.from(this.container.querySelectorAll(this.options.itemSelector));
this.calcData = {};
this.clonesCount = 0;
this.trigger('refresh');
if (update) this.update();
}
/**
* Destroy Reeller instance.
*
* @param {boolean} [removeClones] Remove clones from DOM.
*/
;
_proto.destroy = function destroy(removeClones) {
if (removeClones === void 0) {
removeClones = false;
}
if (removeClones) this.removeClones();
if (this.resizeObserver) this.resizeObserver.disconnect();
this.trigger('destroy');
};
return Filler;
}(Base);
Filler.defaultOptions = {
container: null,
wrapper: null,
itemSelector: null,
cloneClassName: '-clone',
autoUpdate: true,
clonesOverflow: false,
clonesFinish: false,
clonesMin: 0
};
var Reeller = /*#__PURE__*/function (_Base) {
_inheritsLoose(Reeller, _Base);
/**
* @typedef {Object} ReellerOptions
* @property {string|HTMLElement|null} container Container element or selector.
* @property {string|HTMLElement|null} wrapper Inner element or selector.
* @property {string|null} itemSelector Items CSS selector.
* @property {string} [cloneClassName] Class name of the new clones.
* @property {number} [speed] Movement speed.
* @property {string} [ease] Timing function.
* @property {number} [initialSeek] Initial seek of timeline.
* @property {boolean} [loop] Loop movement.
* @property {boolean} [paused] Initialize in paused mode.
* @property {boolean} [reversed] Reverse mode.
* @property {boolean} [autoStop] Use IntersectionObserver to auto stop movement.
* @property {boolean} [autoUpdate] Use ResizeObserver to auto update clones number.
* @property {boolean} [clonesOverflow] Create artificial overflow with clones.
* @property {boolean} [clonesFinish] Bring the cycle of clones to an end.
* @property {boolean} [clonesMin] Minimum number of clones.
* @property {Object|null} [plugins] Options for plugins.
*/
/**
* Default options.
*
* @type {ReellerOptions}
*/
/**
* Registered plugin storage.
*
* @type {Object}
*/
/**
* Create Reeller instance.
*
* @param {ReellerOptions} [options] Reeller options.
*/
function Reeller(options) {
var _this;
_this = _Base.call(this) || this;
/** @type {ReellerOptions} **/
_this.options = _extends({}, Reeller.defaultOptions, options);
_this.gsap = Reeller.gsap || window.gsap;
_this.paused = _this.options.paused;
_this.createFiller();
_this.createTimeline();
if (_this.options.autoStop) _this.bindIntersectionObserver();
if (_this.options.plugins) _this.initPlugins();
return _this;
}
/**
* Register GSAP animation library.
*
* @param {GSAP} gsap GSAP library.
*/
Reeller.registerGSAP = function registerGSAP(gsap) {
Reeller.gsap = gsap;
}
/**
* Register plugins.
*/
;
Reeller.use = function use() {
[].slice.call(arguments).forEach(function (plugin) {
var name = plugin.pluginName;
if (typeof name !== 'string') throw new TypeError('Invalid plugin. Name is required.');
Reeller.plugins[name] = plugin;
});
}
/**
* Create filler.
*/
;
var _proto = Reeller.prototype;
_proto.createFiller = function createFiller() {
var _this2 = this;
this.filler = new Filler(this.options);
this.filler.on('update', function (filler, calcData) {
_this2.invalidate();
_this2.trigger('update', calcData);
});
this.filler.on('refresh', function () {
_this2.trigger('refresh');
});
}
/**
* Create timeline.
*/
;
_proto.createTimeline = function createTimeline() {
var _this3 = this;
this.tl = new this.gsap.timeline({
paused: this.options.paused,
reversed: this.options.reversed,
repeat: -1,
yoyo: !this.options.loop,
onReverseComplete: function onReverseComplete() {
this.progress(1);
}
});
this.gsap.set(this.filler.container, {
overflow: 'hidden'
});
this.tl.fromTo(this.filler.wrapper, {
x: function x() {
if (!_this3.options.clonesOverflow) {
return -(_this3.filler.calcData.fullWidth - _this3.filler.calcData.containerWidth);
}
return -_this3.filler.calcData.itemsWidth;
}
}, {
x: 0,
duration: this.options.speed,
ease: this.options.ease
});
this.tl.seek(this.options.seek);
return this.tl;
}
/**
* Bind IntersectionObserver to container for autoplay.
*/
;
_proto.bindIntersectionObserver = function bindIntersectionObserver() {
var _this4 = this;
this.intersectionObserver = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
_this4.resume();
} else {
_this4.pause();
}
});
this.intersectionObserver.observe(this.filler.container);
}
/**
* Init plugins from options.
*/
;
_proto.initPlugins = function initPlugins() {
this.plugin = {};
for (var _i = 0, _Object$entries = Object.entries(this.options.plugins); _i < _Object$entries.length; _i++) {
var _Object$entries$_i = _Object$entries[_i],
name = _Object$entries$_i[0],
options = _Object$entries$_i[1];
var factory = Reeller.plugins[name];
if (factory) {
this.plugin[name] = new factory(this, options);
} else {
console.error("Plugin " + name + " not found. Make sure you register it with Reeller.use()");
}
}
}
/**
* Destroy initialized plugins.
*/
;
_proto.destroyPlugins = function destroyPlugins() {
for (var _i2 = 0, _Object$values = Object.values(this.plugin); _i2 < _Object$values.length; _i2++) {
var instance = _Object$values[_i2];
if (instance.destroy) instance.destroy();
}
}
/**
* Resume moving.
*/
;
_proto.resume = function resume() {
this.gsap.set(this.filler.container, {
z: '0'
});
this.gsap.set(this.filler.wrapper, {
willChange: 'transform'
});
this.paused = false;
this.tl.resume();
this.trigger('resume');
}
/**
* Set reversed moving.
*
* @param {boolean} [reversed] Is movement reversed?
*/
;
_proto.reverse = function reverse(reversed) {
if (reversed === void 0) {
reversed = true;
}
this.tl.reversed(reversed);
this.resume();
this.trigger('reverse', reversed);
}
/**
* Pause moving.
*/
;
_proto.pause = function pause() {
this.gsap.set(this.filler.container, {
clearProps: 'z'
});
this.gsap.set(this.filler.wrapper, {
willChange: 'auto'
});
this.paused = true;
this.tl.pause();
this.trigger('pause');
}
/**
* Refresh timeline.
*/
;
_proto.invalidate = function invalidate() {
this.tl.invalidate();
this.trigger('invalidate');
}
/**
* Recalculate data.
*/
;
_proto.update = function update() {
this.filler.update();
}
/**
* Fully refresh and update all clones and position.
*
* @param {boolean} [update] Update after refresh.
*/
;
_proto.refresh = function refresh(update) {
if (update === void 0) {
update = true;
}
this.filler.refresh(update);
}
/**
* Destroy Reeller instance.
*
* @param {boolean} [removeClones] Remove clones from DOM.
* @param {boolean} [clearProps] Remove transformations.
*/
;
_proto.destroy = function destroy(removeClones, clearProps) {
if (removeClones === void 0) {
removeClones = false;
}
if (clearProps === void 0) {
clearProps = false;
}
if (this.intersectionObserver) this.intersectionObserver.disconnect();
if (this.options.plugins) this.destroyPlugins();
this.tl.kill();
this.filler.destroy(removeClones);
if (clearProps) {
this.gsap.set(this.filler.container, {
clearProps: 'overflow'
});
this.gsap.set(this.filler.wrapper, {
clearProps: 'x,willChange'
});
}
this.trigger('destroy');
};
return Reeller;
}(Base);
Reeller.defaultOptions = {
container: null,
wrapper: null,
itemSelector: null,
cloneClassName: '-clone',
speed: 10,
ease: 'none',
initialSeek: 10,
loop: true,
paused: true,
reversed: false,
autoStop: true,
autoUpdate: true,
clonesOverflow: true,
clonesFinish: false,
clonesMin: 0,
plugins: null
};
Reeller.plugins = {};
var ScrollerPlugin = /*#__PURE__*/function () {
/**
* @typedef {Object} ScrollerPluginOptions
* @property {number} [speed] Movement and inertia speed.
* @property {number} [multiplier] Movement multiplier.
* @property {number} [threshold] Movement threshold.
* @property {string} [ease] Timing function.
* @property {boolean} [overwrite] GSAP overwrite mode.
* @property {boolean} [bothDirection] Allow movement in both directions.
* @property {boolean} [reversed] Reverse scroll movement.
* @property {boolean} [stopOnEnd] Use IntersectionObserver to auto stop movement.
* @property {function} [scrollProxy] Use ResizeObserver to auto update clones number.
*/
/**
* Plugin name.
*
* @type {string}
*/
/**
* Default options.
*
* @type {ScrollerPluginOptions}
*/
/**
* Reeller ScrollerPlugin.
*
* @param {Reeller} reeller Reeller instance.
* @param {object} options Options
*/
function ScrollerPlugin(reeller, options) {
/** @type {ScrollerPluginOptions} **/
this.options = _extends({}, ScrollerPlugin.defaultOptions, options);
this.reeller = reeller;
this.gsap = this.reeller.gsap;
this.tl = this.reeller.tl;
this.init();
}
/**
* Return scroll position.
*
* @return {number} Scroll position.
*/
var _proto = ScrollerPlugin.prototype;
_proto.getScrollPos = function getScrollPos() {
if (this.options.scrollProxy) return this.options.scrollProxy();
return window.scrollY;
}
/**
* Initialize plugin.
*/
;
_proto.init = function init() {
var _this = this;
var lastScrollPos = this.getScrollPos();
var lastDirection = 1;
var reachedEnd = true;
var isScrolled = false;
this.tickerFn = function () {
var scrollPos = _this.getScrollPos();
var velocity = scrollPos - lastScrollPos;
if (velocity) isScrolled = true;
if (!isScrolled) return;
if (!_this.options.bothDirection) {
velocity = Math.abs(velocity);
}
if (_this.options.reversed) {
velocity *= -1;
}
if (_this.reeller.paused) {
lastDirection = Math.sign(velocity);
lastScrollPos = scrollPos;
if (!reachedEnd) {
_this.gsap.killTweensOf(_this.tl);
reachedEnd = true;
}
_this.tl.timeScale(lastDirection * _this.options.threshold);
return;
}
if (velocity) {
var delta = velocity * _this.options.multiplier;
var timeScale = delta > 0 ? Math.max(_this.options.threshold, delta) : Math.min(-_this.options.threshold, delta);
_this.tween = _this.gsap.to(_this.tl, {
timeScale: timeScale,
duration: _this.options.speed,
ease: _this.options.ease,
overwrite: _this.options.overwrite
});
reachedEnd = false;
} else {
if (!reachedEnd) {
var _timeScale = _this.options.stopOnEnd ? 0 : lastDirection * _this.options.threshold;
_this.gsap.killTweensOf(_this.tl);
_this.tween = _this.gsap.to(_this.tl, {
timeScale: _timeScale,
duration: _this.options.speed,
overwrite: _this.options.overwrite,
ease: _this.options.ease
});
reachedEnd = true;
}
}
lastDirection = Math.sign(velocity);
lastScrollPos = scrollPos;
};
this.gsap.ticker.add(this.tickerFn);
}
/**
* Destroy plugin.
*/
;
_proto.destroy = function destroy() {
if (this.tickerFn) {
this.gsap.ticker.remove(this.tickerFn);
this.tickerFn = null;
}
if (this.tween) this.tween.kill();
};
return ScrollerPlugin;
}();
ScrollerPlugin.pluginName = 'scroller';
ScrollerPlugin.defaultOptions = {
speed: 1,
multiplier: 0.5,
threshold: 1,
ease: 'expo.out',
overwrite: true,
bothDirection: true,
reversed: false,
stopOnEnd: false,
scrollProxy: null
};
exports.Filler = Filler;
exports.Reeller = Reeller;
exports.ScrollerPlugin = ScrollerPlugin;
exports["default"] = Reeller;