├── .gitignore ├── src ├── app.js └── index.js ├── package.json ├── webpack.config.js ├── LICENSE.md ├── demo └── index.html ├── README.md └── dist └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import {BeautifyScroll} from './index' 2 | 3 | const appendItems = (container, nbItems) => { 4 | const array = Array(nbItems).fill('item') 5 | const nodes = [] 6 | 7 | array.forEach(item => { 8 | const div = document.createElement('div') 9 | div.classList.add(item) 10 | // div.dataset.delay = Math.floor(Math.random() * 100).toString() 11 | nodes.push(div) 12 | }) 13 | 14 | container.append(...nodes) 15 | } 16 | 17 | const render = () => { 18 | const container = document.getElementById('container') 19 | 20 | appendItems(container, 50) 21 | 22 | const bs = BeautifyScroll.init({ 23 | selector: '.item' 24 | }) 25 | 26 | bs.on('update', () => bs.reset()) 27 | 28 | setInterval(() => appendItems(container, 10), 1000) 29 | } 30 | 31 | window.addEventListener('load', render) 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beautify-scroll", 3 | "version": "1.0.3", 4 | "description": "A simple library that allows you to animate elements when they enter the viewport", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "webpack --entry ./src/index.js", 8 | "watch": "webpack serve --watch-content-base --entry ./src/app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Durlecode/beautify-scroll.git" 13 | }, 14 | "author": "Arthur Laplace", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@babel/cli": "^7.12.13", 18 | "@babel/core": "^7.12.13", 19 | "@babel/plugin-proposal-class-properties": "^7.12.13", 20 | "@babel/plugin-proposal-private-methods": "^7.12.13", 21 | "@babel/preset-env": "^7.12.13", 22 | "@types/webpack": "^4.41.26", 23 | "babel-loader": "^8.2.2", 24 | "webpack": "^5.21.2", 25 | "webpack-cli": "^4.5.0", 26 | "webpack-dev-server": "^3.11.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: "production", 5 | devServer: { 6 | contentBase: path.join(__dirname, ''), 7 | watchContentBase: true 8 | }, 9 | output: { 10 | filename: "index.js", 11 | globalObject: 'this', 12 | library: 'BeautifyScroll', 13 | libraryTarget: 'umd' 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.m?js$/, 19 | exclude: /(node_modules|bower_components)/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: [ 24 | '@babel/preset-env' 25 | ], 26 | plugins: [ 27 | '@babel/plugin-proposal-private-methods', 28 | '@babel/plugin-proposal-class-properties' 29 | ] 30 | } 31 | } 32 | } 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arthur Laplace 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Beautify Scroll Demo 6 | 7 | 90 | 91 | 92 | 93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | 3 | # Beautify Scroll - Animate elements on scroll 4 | 5 | A simple library that allows you to animate elements when they enter the viewport 6 | 7 | ![alt text](https://www.zupimages.net/up/21/06/huhl.gif) 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm i beautify-scroll --save-dev 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Basic 18 | 19 | ```js 20 | BeautifyScroll.init({ 21 | selector: '.items' 22 | }) 23 | ``` 24 | 25 | By default, this will wrap items in a `` and then watch 26 | viewport for intersecting elements. 27 | 28 | When an element leaves the viewport a class is added to it, 29 | it's this one that will be used for our transition, by default it's `.scroll-animated`. 30 | 31 | You are free to define the transition used, here is the demo transformations : 32 | 33 | ```css 34 | .scroll-animated { 35 | opacity: 0; 36 | transform: translate3d(0, 50%, 0); 37 | } 38 | ``` 39 | 40 | Don't forget to add a transition for your items, here is the demo transition : 41 | 42 | ```css 43 | .items { 44 | transition: all .5s linear; 45 | } 46 | ``` 47 | 48 | ### Advanced 49 | 50 | ```js 51 | const bs = BeautifyScroll.init({ 52 | selector: '.items' 53 | }) 54 | ``` 55 | 56 | Listen to ready event : 57 | 58 | ```js 59 | bs.on('ready', () => { 60 | doSomething() 61 | }) 62 | ``` 63 | 64 | Reset the observer when adding new items to the container, useful to refresh transitions : 65 | 66 | ```js 67 | bs.on('update', () => bs.reset()) 68 | ``` 69 | 70 | ## Options 71 | 72 | Option | Type | Defaut | Informations 73 | --- | --- | --- | --- 74 | `selector` | `CSS Class` | `null` | ex: `.items` 75 | `margin` | `integer` | `0` | Observer margin 76 | `wrapItems` | `boolean` | `true` | `true` due to an issue during some transitions 77 | `callback` | `fn` | `null` | Callback when items are intersecting viewport 78 | `threshold` | `integer` | `0` | [IntersectionObserver threshold](https://developer.mozilla.org/fr/docs/Web/API/Intersection_Observer_API) 79 | `animationClassName` | `string` | `scroll-animated` | Transition class name 80 | 81 | ### Methods 82 | 83 | `init({opts})` Initialize an instance with options 84 | 85 | `reset()` Reset observer to refresh transitions 86 | 87 | ### Events 88 | 89 | `ready` instance is ready 90 | 91 | `update` items added to parent container 92 | 93 | ## Live 94 | 95 | I use this package on [one of my websites](https://gamehypes.com/), if you want to see a live version. 96 | 97 | A demo is also available in the package : `npm run watch` at `http://localhost:8080/demo/` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | 3 | class BeautifyScrollInstance { 4 | constructor(props) { 5 | this._eventBus = new EventEmitter() 6 | this._selector = props.selector 7 | this._callback = props.callback ?? this.#intersectionCallback 8 | this._wrapItems = props.wrapItems 9 | this._animationClassName = props.animationClassName 10 | this._targets = document.querySelectorAll(this._selector) 11 | this._container = this._targets[0].parentNode 12 | this._blocks = this._wrapItems ? this.#wrap(this._container, this._targets) : Array.prototype.slice.call(this._targets) 13 | this._opts = {rootMargin: props.margin + 'px', threshold: props.threshold} 14 | this.#observeMutations() 15 | this.#observeTargets() 16 | this.#ready() 17 | } 18 | 19 | #addClass(entry) { 20 | const item = this._wrapItems ? entry.target.children[0] : entry.target 21 | setTimeout(() => item.classList.add(this._animationClassName), item.dataset.delay ?? 0) 22 | } 23 | 24 | #removeClass(entry) { 25 | const item = this._wrapItems ? entry.target.children[0] : entry.target 26 | setTimeout(() => { 27 | item.classList.remove(this._animationClassName); 28 | }, item.dataset.delay ?? 0) 29 | } 30 | 31 | #intersectionCallback = entries => { 32 | entries.forEach(entry => !entry.isIntersecting ? this.#addClass(entry) : this.#removeClass(entry)) 33 | } 34 | 35 | #mutationCallback = (mutationsList, observer) => { 36 | observer.disconnect() 37 | for (const mutation of mutationsList) { 38 | if (!mutation.addedNodes.length) continue; 39 | if (mutation.type === 'childList') { 40 | this._blocks = this._blocks.concat( 41 | this._wrapItems ? 42 | this.#wrap(this._container, mutation.addedNodes) : Array.prototype.slice.call(mutation.addedNodes) 43 | ) 44 | } 45 | } 46 | 47 | this._eventBus.emit('update') 48 | } 49 | 50 | #observeMutations() { 51 | this._mutationsObserver = new MutationObserver(this.#mutationCallback) 52 | this._mutationsObserver.observe(this._container, {childList: true}) 53 | } 54 | 55 | #observeTargets() { 56 | if (this._targetsObserver) this._targetsObserver.disconnect() 57 | this._targetsObserver = new IntersectionObserver(this._callback, this._opts) 58 | this._blocks.forEach(target => this._targetsObserver.observe(target)) 59 | } 60 | 61 | #ready() { 62 | setTimeout(() => this._eventBus.emit('ready')) 63 | } 64 | 65 | #wrap = (container, targets) => { 66 | const blocks = [] 67 | 68 | targets.forEach(item => { 69 | const wrapper = document.createElement('span') 70 | 71 | wrapper.classList.add('scroll-item-wrapper') 72 | 73 | wrapper.append(item) 74 | 75 | container.append(wrapper) 76 | 77 | blocks.push(wrapper) 78 | }) 79 | 80 | return blocks 81 | } 82 | 83 | on(event, callback) { 84 | return this._eventBus.on(event, callback) 85 | } 86 | 87 | reset() { 88 | this._targets = document.querySelectorAll(this._selector) 89 | this.#observeMutations() 90 | this.#observeTargets() 91 | } 92 | } 93 | 94 | 95 | export const BeautifyScroll = { 96 | init({ 97 | selector = null, 98 | margin = 0, 99 | wrapItems = true, 100 | callback = null, 101 | threshold = 0, 102 | animationClassName = 'scroll-animated' 103 | }) { 104 | 105 | if (!selector) throw 'I need items ! Put a "selector" key in options.' 106 | if (!document.querySelectorAll(selector).length) throw 'No items for ' + selector + ' selector' 107 | 108 | return new BeautifyScrollInstance({ 109 | selector, 110 | margin, 111 | callback, 112 | wrapItems, 113 | threshold, 114 | animationClassName 115 | }) 116 | } 117 | } -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.BeautifyScroll=t():e.BeautifyScroll=t()}(this,(function(){return(()=>{"use strict";var e={352:(e,t,n)=>{n.r(t),n.d(t,{BeautifyScroll:()=>O});var r=n(187),i=n.n(r);function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,a=!0,l=!1;return{s:function(){n=e[Symbol.iterator]()},n:function(){var e=n.next();return a=e.done,e},e:function(e){l=!0,s=e},f:function(){try{a||null==n.return||n.return()}finally{if(l)throw s}}}}(e);try{for(i.s();!(n=i.n()).done;){var s=n.value;s.addedNodes.length&&"childList"===s.type&&(r._blocks=r._blocks.concat(r._wrapItems?l(r,m).call(r,r._container,s.addedNodes):Array.prototype.slice.call(s.addedNodes)))}}catch(e){i.e(e)}finally{i.f()}r._eventBus.emit("update")}}),m.set(this,{writable:!0,value:function(e,t){var n=[];return t.forEach((function(t){var r=document.createElement("span");r.classList.add("scroll-item-wrapper"),r.append(t),e.append(r),n.push(r)})),n}}),this._eventBus=new(i()),this._selector=t.selector,this._callback=null!==(n=t.callback)&&void 0!==n?n:l(this,f),this._wrapItems=t.wrapItems,this._animationClassName=t.animationClassName,this._targets=document.querySelectorAll(this._selector),this._container=this._targets[0].parentNode,this._blocks=this._wrapItems?l(this,m).call(this,this._container,this._targets):Array.prototype.slice.call(this._targets),this._opts={rootMargin:t.margin+"px",threshold:t.threshold},a(this,v,_).call(this),a(this,p,w).call(this),a(this,d,L).call(this)}var t,n;return t=e,(n=[{key:"on",value:function(e,t){return this._eventBus.on(e,t)}},{key:"reset",value:function(){this._targets=document.querySelectorAll(this._selector),a(this,v,_).call(this),a(this,p,w).call(this)}}])&&s(t.prototype,n),e}(),g=function(e){var t,n=this,r=this._wrapItems?e.target.children[0]:e.target;setTimeout((function(){return r.classList.add(n._animationClassName)}),null!==(t=r.dataset.delay)&&void 0!==t?t:0)},b=function(e){var t,n=this,r=this._wrapItems?e.target.children[0]:e.target;setTimeout((function(){r.classList.remove(n._animationClassName)}),null!==(t=r.dataset.delay)&&void 0!==t?t:0)},_=function(){this._mutationsObserver=new MutationObserver(l(this,h)),this._mutationsObserver.observe(this._container,{childList:!0})},w=function(){var e=this;this._targetsObserver&&this._targetsObserver.disconnect(),this._targetsObserver=new IntersectionObserver(this._callback,this._opts),this._blocks.forEach((function(t){return e._targetsObserver.observe(t)}))},L=function(){var e=this;setTimeout((function(){return e._eventBus.emit("ready")}))},O={init:function(e){var t=e.selector,n=void 0===t?null:t,r=e.margin,i=void 0===r?0:r,o=e.wrapItems,s=void 0===o||o,a=e.callback,l=void 0===a?null:a,u=e.threshold,c=void 0===u?0:u,f=e.animationClassName,h=void 0===f?"scroll-animated":f;if(!n)throw'I need items ! Put a "selector" key in options.';if(!document.querySelectorAll(n).length)throw"No items for "+n+" selector";return new y({selector:n,margin:i,callback:l,wrapItems:s,threshold:c,animationClassName:h})}}},187:e=>{var t,n="object"==typeof Reflect?Reflect:null,r=n&&"function"==typeof n.apply?n.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};t=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function o(){o.init.call(this)}e.exports=o,e.exports.once=function(e,t){return new Promise((function(n,r){function i(){void 0!==o&&e.removeListener("error",o),n([].slice.call(arguments))}var o;"error"!==t&&(o=function(n){e.removeListener(t,i),r(n)},e.once("error",o)),e.once(t,i)}))},o.EventEmitter=o,o.prototype._events=void 0,o.prototype._eventsCount=0,o.prototype._maxListeners=void 0;var s=10;function a(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function l(e){return void 0===e._maxListeners?o.defaultMaxListeners:e._maxListeners}function u(e,t,n,r){var i,o,s,u;if(a(n),void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),o=e._events),s=o[t]),void 0===s)s=o[t]=n,++e._eventsCount;else if("function"==typeof s?s=o[t]=r?[n,s]:[s,n]:r?s.unshift(n):s.push(n),(i=l(e))>0&&s.length>i&&!s.warned){s.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=s.length,u=c,console&&console.warn&&console.warn(u)}return e}function c(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},i=c.bind(r);return i.listener=n,r.wrapFn=i,i}function h(e,t,n){var r=e._events;if(void 0===r)return[];var i=r[t];return void 0===i?[]:"function"==typeof i?n?[i.listener||i]:[i]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(s=t[0]),s instanceof Error)throw s;var a=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw a.context=s,a}var l=o[e];if(void 0===l)return!1;if("function"==typeof l)r(l,this,t);else{var u=l.length,c=p(l,u);for(n=0;n=0;o--)if(n[o]===t||n[o].listener===t){s=n[o].listener,i=o;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},o.prototype.listeners=function(e){return h(this,e,!0)},o.prototype.rawListeners=function(e){return h(this,e,!1)},o.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):v.call(e,t)},o.prototype.listenerCount=v,o.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}}},t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}return n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n(352)})()})); --------------------------------------------------------------------------------