├── .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 | [](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 | 
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)})()}));
--------------------------------------------------------------------------------