UNPKG

reeller

Version:

Flexible, powerful and modern library for creating the running horizontal blocks effect, also known as ticker or the «marquee effect».

798 lines (680 loc) 21.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.Reeller = {})); })(this, (function (exports) { 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; }));