├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.js └── index.js.LICENSE.txt ├── package.json ├── src ├── core │ ├── Scrollbar.js │ ├── Smooth.js │ ├── index.js │ ├── options.js │ └── store.js ├── index.js └── utils │ ├── Emitter.js │ ├── bindAll.js │ ├── detect.js │ ├── index.js │ └── preload.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-proposal-private-methods" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | node_modules 4 | yarn.lock 5 | index.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jesper Landberg 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JScroll 2 | 3 | Smooth scrolling sections based on VirtualScroll. 4 | 5 | ## Usage 6 | 7 | ###### Install 8 | `yarn add @twotwentytwo/jscroll` 9 | 10 | ###### Javascript 11 | ```Javascript 12 | import JScroll from '@twotwentytwo/jscroll' 13 | 14 | JScroll.init({ /* Options are optional */ }) 15 | ``` 16 | ###### Markup 17 | ```HTML 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ``` 32 | 33 | ## Options 34 | `ease`: Easing value (defaults to `0.1`) 35 | 36 | `scrollbar`: Virtual scrollbar (defaults to `false`) 37 | 38 | `disableMobile`: Disable JScroll on mobile devices (defaults to `true`) 39 | 40 | `preload`: Trigger resize after loading all images (defaults to `false`) 41 | 42 | `vs`: 43 | - `mouseMultiplier`: Defaults to 0.45 44 | - `touchMultiplier`: Defaults to 2.5 45 | - `firefoxMultiplier`: Defaults to 90 46 | 47 | ## Methods 48 | `init()`: Initialise instance 49 | 50 | `update()`: Update instance 51 | 52 | `resize()`: Trigger resize 53 | 54 | `preload()`: Preload images 55 | 56 | `tick()`: Where the magic happens 57 | 58 | `stop()`: Stop scrolling 59 | 60 | `resume()`: Resume scrolling 61 | 62 | `destroy()`: Clean instance 63 | 64 | `scrollTo(someElement.offsetTop)`: Anchor scroll 65 | 66 | ## Events 67 | 68 | `on('tick', ({ target, current }) => {})`: Raf callback. Scroll and lerped scroll params. 69 | 70 | `on('scroll', ({ delta, target }) => {})`: Scroll callback. Delta and scroll params. 71 | 72 | ## Getters 73 | `getSmooth`: Returns lerped scroll value 74 | 75 | `getScroll`: Returns scroll value 76 | 77 | ## References 78 | 79 | Some great examples of sites using JScroll. 80 | 81 | Fabio Caretti
82 | Discoverylandco 83 | 84 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see index.js.LICENSE.txt */ 2 | (()=>{var t={721:t=>{"use strict";var e=Object.prototype.toString,i=Object.prototype.hasOwnProperty;function n(t,e){return function(){return t.apply(e,arguments)}}t.exports=function(t){if(!t)return console.warn("bindAll requires at least one argument.");var s=Array.prototype.slice.call(arguments,1);if(0===s.length)for(var o in t)i.call(t,o)&&"function"==typeof t[o]&&"[object Function]"==e.call(t[o])&&s.push(o);for(var r=0;r=e;1<=e?t++:t--)i.push(null);return i}.call(this),this.lastDownDeltas=function(){var t,e,i;for(i=[],t=1,e=2*this.stability;1<=e?t<=e:t>=e;1<=e?t++:t--)i.push(null);return i}.call(this),this.deltasTimestamp=function(){var t,e,i;for(i=[],t=1,e=2*this.stability;1<=e?t<=e:t>=e;1<=e?t++:t--)i.push(null);return i}.call(this)}return t.prototype.check=function(t){var e;return null!=(t=t.originalEvent||t).wheelDelta?e=t.wheelDelta:null!=t.deltaY?e=-40*t.deltaY:null==t.detail&&0!==t.detail||(e=-40*t.detail),this.deltasTimestamp.push(Date.now()),this.deltasTimestamp.shift(),e>0?(this.lastUpDeltas.push(e),this.lastUpDeltas.shift(),this.isInertia(1)):(this.lastDownDeltas.push(e),this.lastDownDeltas.shift(),this.isInertia(-1))},t.prototype.isInertia=function(t){var e,i,n,s,o,r,a;return null===(e=-1===t?this.lastDownDeltas:this.lastUpDeltas)[0]?t:!(this.deltasTimestamp[2*this.stability-2]+this.delay>Date.now()&&e[0]===e[2*this.stability-1])&&(n=e.slice(0,this.stability),i=e.slice(this.stability,2*this.stability),a=n.reduce((function(t,e){return t+e})),o=i.reduce((function(t,e){return t+e})),r=a/n.length,s=o/i.length,Math.abs(r){var n=/^\s+|\s+$/g,s=/^[-+]0x[0-9a-f]+$/i,o=/^0b[01]+$/i,r=/^0o[0-7]+$/i,a=parseInt,l="object"==typeof i.g&&i.g&&i.g.Object===Object&&i.g,h="object"==typeof self&&self&&self.Object===Object&&self,u=l||h||Function("return this")(),c=Object.prototype.toString,f=Math.max,d=Math.min,p=function(){return u.Date.now()};function v(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function m(t){if("number"==typeof t)return t;if(function(t){return"symbol"==typeof t||function(t){return!!t&&"object"==typeof t}(t)&&"[object Symbol]"==c.call(t)}(t))return NaN;if(v(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=v(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(n,"");var i=o.test(t);return i||r.test(t)?a(t.slice(2),i?2:8):s.test(t)?NaN:+t}t.exports=function(t,e,i){var n,s,o,r,a,l,h=0,u=!1,c=!1,y=!0;if("function"!=typeof t)throw new TypeError("Expected a function");function g(e){var i=n,o=s;return n=s=void 0,h=e,r=t.apply(o,i)}function b(t){return h=t,a=setTimeout(k,e),u?g(t):r}function w(t){var i=t-l;return void 0===l||i>=e||i<0||c&&t-h>=o}function k(){var t=p();if(w(t))return _(t);a=setTimeout(k,function(t){var i=e-(t-l);return c?d(i,o-(t-h)):i}(t))}function _(t){return a=void 0,y&&n?g(t):(n=s=void 0,r)}function T(){var t=p(),i=w(t);if(n=arguments,s=this,l=t,i){if(void 0===a)return b(l);if(c)return a=setTimeout(k,e),g(l)}return void 0===a&&(a=setTimeout(k,e)),r}return e=m(e)||0,v(i)&&(u=!!i.leading,o=(c="maxWait"in i)?f(m(i.maxWait)||0,e):o,y="trailing"in i?!!i.trailing:y),T.cancel=function(){void 0!==a&&clearTimeout(a),h=0,n=l=s=a=void 0},T.flush=function(){return void 0===a?r:_(p())},T}},418:t=>{"use strict";var e=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,n=Object.prototype.propertyIsEnumerable;function s(t){if(null==t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}t.exports=function(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de","5"===Object.getOwnPropertyNames(t)[0])return!1;for(var e={},i=0;i<10;i++)e["_"+String.fromCharCode(i)]=i;if("0123456789"!==Object.getOwnPropertyNames(e).map((function(t){return e[t]})).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach((function(t){n[t]=t})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(t){return!1}}()?Object.assign:function(t,o){for(var r,a,l=s(t),h=1;h{function e(){}e.prototype={on:function(t,e,i){var n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this},once:function(t,e,i){var n=this;function s(){n.off(t,s),e.apply(i,arguments)}return s._=e,this.on(t,s,i)},emit:function(t){for(var e=[].slice.call(arguments,1),i=((this.e||(this.e={}))[t]||[]).slice(),n=0,s=i.length;n{function e(){}e.prototype={on:function(t,e,i){var n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this},once:function(t,e,i){var n=this;function s(){n.off(t,s),e.apply(i,arguments)}return s._=e,this.on(t,s,i)},emit:function(t){for(var e=[].slice.call(arguments,1),i=((this.e||(this.e={}))[t]||[]).slice(),n=0,s=i.length;n{"use strict";t.exports=function(t){return JSON.parse(JSON.stringify(t))}},809:(t,e,i)=>{"use strict";var n=i(418),s=i(334),o=i(714).Lethargy,r=i(268),a=(i(593),i(721)),l="virtualscroll";t.exports=p;var h=37,u=38,c=39,f=40,d=32;function p(t){a(this,"_onWheel","_onMouseWheel","_onTouchStart","_onTouchMove","_onKeyDown"),this.el=window,t&&t.el&&(this.el=t.el,delete t.el),this.options=n({mouseMultiplier:1,touchMultiplier:2,firefoxMultiplier:15,keyStep:120,preventTouch:!1,unpreventTouchClass:"vs-touchmove-allowed",limitInertia:!1,useKeyboard:!0,useTouch:!0},t),this.options.limitInertia&&(this._lethargy=new o),this._emitter=new s,this._event={y:0,x:0,deltaX:0,deltaY:0},this.touchStartX=null,this.touchStartY=null,this.bodyTouchAction=null,void 0!==this.options.passive&&(this.listenerOptions={passive:this.options.passive})}p.prototype._notify=function(t){var e=this._event;e.x+=e.deltaX,e.y+=e.deltaY,this._emitter.emit(l,{x:e.x,y:e.y,deltaX:e.deltaX,deltaY:e.deltaY,originalEvent:t})},p.prototype._onWheel=function(t){var e=this.options;if(!this._lethargy||!1!==this._lethargy.check(t)){var i=this._event;i.deltaX=t.wheelDeltaX||-1*t.deltaX,i.deltaY=t.wheelDeltaY||-1*t.deltaY,r.isFirefox&&1==t.deltaMode&&(i.deltaX*=e.firefoxMultiplier,i.deltaY*=e.firefoxMultiplier),i.deltaX*=e.mouseMultiplier,i.deltaY*=e.mouseMultiplier,this._notify(t)}},p.prototype._onMouseWheel=function(t){if(!this.options.limitInertia||!1!==this._lethargy.check(t)){var e=this._event;e.deltaX=t.wheelDeltaX?t.wheelDeltaX:0,e.deltaY=t.wheelDeltaY?t.wheelDeltaY:t.wheelDelta,this._notify(t)}},p.prototype._onTouchStart=function(t){var e=t.targetTouches?t.targetTouches[0]:t;this.touchStartX=e.pageX,this.touchStartY=e.pageY},p.prototype._onTouchMove=function(t){var e=this.options;e.preventTouch&&!t.target.classList.contains(e.unpreventTouchClass)&&t.preventDefault();var i=this._event,n=t.targetTouches?t.targetTouches[0]:t;i.deltaX=(n.pageX-this.touchStartX)*e.touchMultiplier,i.deltaY=(n.pageY-this.touchStartY)*e.touchMultiplier,this.touchStartX=n.pageX,this.touchStartY=n.pageY,this._notify(t)},p.prototype._onKeyDown=function(t){var e=this._event;e.deltaX=e.deltaY=0;var i=window.innerHeight-40;switch(t.keyCode){case h:case u:e.deltaY=this.options.keyStep;break;case c:case f:e.deltaY=-this.options.keyStep;break;case d&&t.shiftKey:e.deltaY=i;break;case d:e.deltaY=-i;break;default:return}this._notify(t)},p.prototype._bind=function(){r.hasWheelEvent&&this.el.addEventListener("wheel",this._onWheel,this.listenerOptions),r.hasMouseWheelEvent&&this.el.addEventListener("mousewheel",this._onMouseWheel,this.listenerOptions),r.hasTouch&&this.options.useTouch&&(this.el.addEventListener("touchstart",this._onTouchStart,this.listenerOptions),this.el.addEventListener("touchmove",this._onTouchMove,this.listenerOptions)),r.hasPointer&&r.hasTouchWin&&(this.bodyTouchAction=document.body.style.msTouchAction,document.body.style.msTouchAction="none",this.el.addEventListener("MSPointerDown",this._onTouchStart,!0),this.el.addEventListener("MSPointerMove",this._onTouchMove,!0)),r.hasKeyDown&&this.options.useKeyboard&&document.addEventListener("keydown",this._onKeyDown)},p.prototype._unbind=function(){r.hasWheelEvent&&this.el.removeEventListener("wheel",this._onWheel),r.hasMouseWheelEvent&&this.el.removeEventListener("mousewheel",this._onMouseWheel),r.hasTouch&&(this.el.removeEventListener("touchstart",this._onTouchStart),this.el.removeEventListener("touchmove",this._onTouchMove)),r.hasPointer&&r.hasTouchWin&&(document.body.style.msTouchAction=this.bodyTouchAction,this.el.removeEventListener("MSPointerDown",this._onTouchStart,!0),this.el.removeEventListener("MSPointerMove",this._onTouchMove,!0)),r.hasKeyDown&&this.options.useKeyboard&&document.removeEventListener("keydown",this._onKeyDown)},p.prototype.on=function(t,e){this._emitter.on(l,t,e);var i=this._emitter.e;i&&i[l]&&1===i[l].length&&this._bind()},p.prototype.off=function(t,e){this._emitter.off(l,t,e);var i=this._emitter.e;(!i[l]||i[l].length<=0)&&this._unbind()},p.prototype.reset=function(){var t=this._event;t.x=0,t.y=0},p.prototype.destroy=function(){this._emitter.off(),this._unbind()}},268:t=>{"use strict";t.exports={hasWheelEvent:"onwheel"in document,hasMouseWheelEvent:"onmousewheel"in document,hasTouch:"ontouchstart"in window||window.TouchEvent||window.DocumentTouch&&document instanceof DocumentTouch,hasTouchWin:navigator.msMaxTouchPoints&&navigator.msMaxTouchPoints>1,hasPointer:!!window.navigator.msPointerEnabled,hasKeyDown:"onkeydown"in document,isFirefox:navigator.userAgent.indexOf("Firefox")>-1}}},e={};function i(n){if(e[n])return e[n].exports;var s=e[n]={exports:{}};return t[n].call(s.exports,s,s.exports,i),s.exports}i.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return i.d(e,{a:e}),e},i.d=(t,e)=>{for(var n in e)i.o(e,n)&&!i.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=i(809),e=i.n(t),n=i(296),s=i.n(n);const o={body:document.body,window};var r=i(279);const a=new(i.n(r)());function l(t){return function(t){if(Array.isArray(t))return h(t)}(t)||function(t){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(t))return Array.from(t)}(t)||function(t,e){if(!t)return;if("string"==typeof t)return h(t,e);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(i);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return h(t,e)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function h(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,n=new Array(e);i0&&void 0!==arguments[0]?arguments[0]:{};Object.assign(t,f);var i=f.el,n=f.elems,s=f.threshold,o=f.vs,r=f.disableMobile;c&&r||(this.el=i||document.querySelector("[data-smooth]"),this.elems=n||document.querySelectorAll("[data-smooth-item]"),this.threshold=s,this.vs=new(e())({limitInertia:o.limitInertia,mouseMultiplier:o.mouseMultiplier,touchMultiplier:o.touchMultiplier,firefoxMultiplier:o.firefoxMultiplier,passive:o.passive}),this.setStyles(),this.setScrollLimit(),this.cacheSections(),this.addEvents(),f.preload&&this.preload(),this.state.initialised=!0,this.state.stopped=!1)}},{key:"setStyles",value:function(){Object.assign(this.el.style,{position:"fixed",top:0,left:0,width:"100%"}),o.body.style.overflow="hidden",o.body.classList.add("is-virtual-scroll")}},{key:"preload",value:function(){(function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:document.body,e=l(t.querySelectorAll("img")).map((function(t){return t.src}));return Promise.all(e.map(u))})(this.el).then(this.resize)}},{key:"setScrollLimit",value:function(){var t=this.state,e=this.el.getBoundingClientRect().height;t.scrollLimit=e>=t.wh?e-t.wh:e}},{key:"cacheSections",value:function(){var t=this;if(this.elems)for(var e=function(e){var i=t.elems[e],n=i.dataset.speed||1,s=t.getVars(i,n),o=s.top,r=s.bottom,a=s.offset,l=i.parentNode.closest("[data-smooth-item]");l&&t.sections.some((function(t){t.el===l&&(l=t)})),t.sections.push({el:i,parent:l,top:o,bottom:r,offset:a,speed:n,out:!0,transform:0}),i.style.transform="translate3d(0, 0, 0)"},i=0;i-this.threshold;return{isVisible:f,transform:u}}},{key:"clampTarget",value:function(){var t=this.state;t.target=Math.min(Math.max(t.target,-0),t.scrollLimit)}},{key:"scrollTo",value:function(t){this.state.target=t}},{key:"update",value:function(t){this.elems=this.sections=null,this.elems=t||document.querySelectorAll("[data-smooth-item]"),this.cacheSections(),this.setScrollLimit(),this.scrollbar&&this.scrollbar.update(),f.preload&&this.preload()}},{key:"removeEvents",value:function(){this.vs.off(this.scroll),this.vs.destroy(),o.window.removeEventListener("resize",s()(this.resize,200)),this.cancelRaf(),this.scrollbar&&this.scrollbar.destroy()}},{key:"destroy",value:function(){this.removeEvents(),this.state=null,this.opts=null,this.sections=null,this.elems=null,this.el=null}},{key:"getSmooth",get:function(){return this.state.currentRounded}},{key:"getScroll",get:function(){return this.state.target}}])&&m(i.prototype,n),r&&m(i,r),t}())})()})(); -------------------------------------------------------------------------------- /dist/index.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twotwentytwo/jscroll", 3 | "version": "1.0.7", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack --mode development --watch", 9 | "build": "webpack --mode production", 10 | "clean": "rm -rf dist/*" 11 | }, 12 | "author": "Jesper Landberg", 13 | "license": "MIT", 14 | "dependencies": { 15 | "lodash.debounce": "^4.0.8", 16 | "tiny-emitter": "^2.1.0", 17 | "virtual-scroll": "^1.5.1" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.7.4", 21 | "@babel/plugin-proposal-class-properties": "^7.7.4", 22 | "@babel/plugin-proposal-private-methods": "^7.7.4", 23 | "@babel/preset-env": "^7.7.4", 24 | "babel-loader": "^8.0.6", 25 | "cssnano": "^4.1.10", 26 | "sass": "^1.22.12", 27 | "webpack": "^5.0.0-beta.7", 28 | "webpack-cli": "^3.3.10" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/core/Scrollbar.js: -------------------------------------------------------------------------------- 1 | import store from './store' 2 | import { Events } from '../utils' 3 | 4 | export default class { 5 | 6 | constructor(context) { 7 | this.context = context 8 | 9 | this.el = null 10 | this.handle = null 11 | 12 | this.state = { 13 | clicked: false, 14 | scale: 0 15 | } 16 | 17 | this.init() 18 | } 19 | 20 | init() { 21 | this.create() 22 | this.setBounds() 23 | this.addEvents() 24 | } 25 | 26 | addEvents() { 27 | Events.on('tick', this.transform) 28 | Events.on('resize', this.resize) 29 | 30 | this.el.addEventListener('click', this.click) 31 | this.handle.addEventListener('mousedown', this.down) 32 | 33 | window.addEventListener('mousemove', this.move) 34 | window.addEventListener('mouseup', this.up) 35 | } 36 | 37 | setBounds() { 38 | const { scrollLimit, wh } = this.context.state 39 | 40 | this.state.scale = (scrollLimit + wh) / wh 41 | this.handle.style.height = `${wh / this.state.scale}px` 42 | } 43 | 44 | transform = ({ current }) => { 45 | this.handle.style.transform = `translate3d(0, ${current / this.state.scale}px, 0)` 46 | } 47 | 48 | click = (e) => { 49 | this.calcScroll(e) 50 | } 51 | 52 | down = () => { 53 | this.state.clicked = true 54 | store.body.classList.add('is-dragging') 55 | } 56 | 57 | move = (e) => { 58 | if (!this.state.clicked) return 59 | this.calcScroll(e) 60 | } 61 | 62 | up = () => { 63 | this.state.clicked = false 64 | store.body.classList.remove('is-dragging') 65 | } 66 | 67 | resize = () => { 68 | this.setBounds() 69 | } 70 | 71 | calcScroll(e) { 72 | const delta = e.clientY * this.state.scale 73 | 74 | this.context.state.target = delta 75 | this.context.clampTarget() 76 | } 77 | 78 | create() { 79 | this.el = document.createElement('div') 80 | this.handle = document.createElement('div') 81 | 82 | this.el.classList.add('scrollbar', 'js-scrollbar') 83 | this.handle.classList.add('scrollbar__handle', 'js-scrollbar__handle') 84 | 85 | Object.assign(this.el.style, { 86 | position: 'fixed', 87 | top: 0, right: 0, 88 | height: '100%', 89 | pointerEvents: 'all' 90 | }) 91 | 92 | Object.assign(this.handle.style, { 93 | position: 'absolute', 94 | top: 0, left: 0, 95 | width: '100%', 96 | cursor: 'pointer' 97 | }) 98 | 99 | store.body.appendChild(this.el) 100 | this.el.appendChild(this.handle) 101 | } 102 | 103 | update() { 104 | this.setBounds() 105 | } 106 | 107 | removeEvents() { 108 | this.el.removeEventListener('click', this.click) 109 | this.handle.removeEventListener('mousedown', this.down) 110 | 111 | store.window.removeEventListener('mousemove', this.move) 112 | store.window.removeEventListener('mouseup', this.up) 113 | } 114 | 115 | destroy() { 116 | this.removeEvents() 117 | } 118 | } -------------------------------------------------------------------------------- /src/core/Smooth.js: -------------------------------------------------------------------------------- 1 | import VirtualScroll from 'virtual-scroll' 2 | import debounce from 'lodash.debounce' 3 | 4 | import store from './store' 5 | import { 6 | Events, 7 | detect, 8 | preload 9 | } from '../utils' 10 | 11 | import options from './options' 12 | import Scrollbar from './Scrollbar' 13 | 14 | export default class { 15 | 16 | constructor() { 17 | this.state = { 18 | target: 0, 19 | current: 0, 20 | currentRounded: 0, 21 | scrollLimit: 0, 22 | wh: window.innerHeight, 23 | resizing: false, 24 | initialised: false, 25 | stopped: true 26 | } 27 | 28 | this.sections = [] 29 | this.raf = null 30 | } 31 | 32 | init(opts = {}) { 33 | Object.assign(opts, options) 34 | 35 | const { 36 | el, elems, 37 | threshold, 38 | vs, 39 | disableMobile 40 | } = options 41 | 42 | if (detect.device && disableMobile) return 43 | 44 | this.el = el || document.querySelector('[data-smooth]') 45 | this.elems = elems || document.querySelectorAll('[data-smooth-item]') 46 | this.threshold = threshold; 47 | 48 | // Initalise Virtual Scroll 49 | this.vs = new VirtualScroll({ 50 | limitInertia: vs.limitInertia, 51 | mouseMultiplier: vs.mouseMultiplier, 52 | touchMultiplier: vs.touchMultiplier, 53 | firefoxMultiplier:vs.firefoxMultiplier, 54 | passive: vs.passive 55 | }) 56 | 57 | this.setStyles() 58 | this.setScrollLimit() 59 | this.cacheSections() 60 | this.addEvents() 61 | 62 | if (options.preload) this.preload() 63 | 64 | this.state.initialised = true 65 | this.state.stopped = false 66 | } 67 | 68 | setStyles() { 69 | Object.assign(this.el.style, { 70 | position: 'fixed', 71 | top: 0, left: 0, 72 | width: '100%' 73 | }) 74 | 75 | store.body.style.overflow = 'hidden' 76 | store.body.classList.add('is-virtual-scroll') 77 | } 78 | 79 | preload() { 80 | preload(this.el).then(this.resize) 81 | } 82 | 83 | setScrollLimit() { 84 | const state = this.state 85 | const height = this.el.getBoundingClientRect().height 86 | state.scrollLimit = height >= state.wh ? height - state.wh : height 87 | } 88 | 89 | cacheSections() { 90 | if (!this.elems) return 91 | 92 | for (let i = 0; i < this.elems.length; i++) { 93 | const el = this.elems[i] 94 | const speed = el.dataset.speed || 1 95 | const { top, bottom, offset } = this.getVars(el, speed) 96 | 97 | let parent = el.parentNode.closest('[data-smooth-item]') 98 | if (parent) { 99 | this.sections.some(obj => { 100 | if (obj.el === parent) { 101 | parent = obj 102 | } 103 | }) 104 | } 105 | 106 | this.sections.push({ 107 | el, parent, 108 | top, bottom, 109 | offset, speed, 110 | out: true, 111 | transform: 0 112 | }) 113 | 114 | el.style.transform = 'translate3d(0, 0, 0)' 115 | } 116 | } 117 | 118 | updateSections() { 119 | if (!this.sections) return 120 | 121 | for (let i = 0; i < this.sections.length; i++) { 122 | const section = this.sections[i] 123 | 124 | section.el.style.transform = 'translate3d(0, 0, 0)' 125 | 126 | const { top, bottom, offset } = this.getVars(section.el, section.speed) 127 | 128 | Object.assign(section, { 129 | top, bottom, 130 | offset 131 | }) 132 | } 133 | 134 | this.transformSections() 135 | } 136 | 137 | getVars(el, speed) { 138 | const { wh } = this.state 139 | const rect = el.getBoundingClientRect() 140 | const centering = (wh / 2) - (rect.height / 2) 141 | const offset = rect.top < wh ? 0 : ((rect.top - centering) * speed) - (rect.top - centering) 142 | const top = rect.top + offset 143 | const bottom = rect.bottom + offset 144 | 145 | return { 146 | top, bottom, 147 | offset 148 | } 149 | } 150 | 151 | addEvents() { 152 | this.vs.on(this.scroll) 153 | 154 | store.window.addEventListener('resize', debounce(this.resize, 200)) 155 | 156 | if (options.scrollbar) (this.scrollbar = new Scrollbar(this)) 157 | 158 | // Call requestAnimationFrame first time 159 | this.requestRaf() 160 | } 161 | 162 | tick = () => { 163 | const state = this.state 164 | 165 | if (!state.stopped) { 166 | state.current += (state.target - state.current) * options.ease 167 | state.currentRounded = Math.round(state.current * 100) / 100 168 | 169 | this.transformSections() 170 | } 171 | 172 | // Emit tick event and scroll values (lerped and non-lerped) 173 | Events.emit('tick', { 174 | target: state.target, 175 | current: state.currentRounded 176 | }) 177 | 178 | this.requestRaf() 179 | } 180 | 181 | on(event, cb) { 182 | return Events.on(event, cb) 183 | } 184 | 185 | // Returns the current lerped scroll 186 | get getSmooth() { 187 | return this.state.currentRounded 188 | } 189 | 190 | // Returns the current scroll 191 | get getScroll() { 192 | return this.state.target 193 | } 194 | 195 | stop() { 196 | this.state.stopped = true 197 | } 198 | 199 | resume() { 200 | this.state.stopped = false 201 | } 202 | 203 | requestRaf() { 204 | this.raf = requestAnimationFrame(this.tick) 205 | } 206 | 207 | cancelRaf() { 208 | this.raf && cancelAnimationFrame(this.raf) 209 | } 210 | 211 | transformSections() { 212 | for (let i = 0; i < this.sections.length; i++) { 213 | const section = this.sections[i] 214 | 215 | const { 216 | isVisible, 217 | transform 218 | } = this.isVisible(section) 219 | 220 | if (isVisible || this.state.resizing || !section.out) { 221 | section.out = section.out ? true : false 222 | section.transform = transform 223 | section.el.style.transform = this.translate(transform) 224 | } 225 | } 226 | } 227 | 228 | translate(transform) { 229 | return `translate3d(0, ${-transform}px, 0)` 230 | } 231 | 232 | isVisible({ 233 | top, bottom, 234 | offset, speed, 235 | parent 236 | }) { 237 | const { currentRounded, wh } = this.state 238 | const extra = (parent && parent.transform) || 0 239 | const translate = currentRounded * speed 240 | const transform = translate - offset - extra 241 | const start = top - translate 242 | const end = bottom - translate 243 | const isVisible = start < (this.threshold + wh) && end > -this.threshold 244 | 245 | return { 246 | isVisible, 247 | transform, 248 | } 249 | } 250 | 251 | clampTarget() { 252 | const state = this.state 253 | state.target = Math.min(Math.max(state.target, -0), state.scrollLimit) 254 | } 255 | 256 | scroll = ({ deltaY }) => { 257 | const state = this.state 258 | 259 | if (state.stopped) return 260 | 261 | const delta = deltaY * -1 262 | 263 | state.target += delta 264 | this.clampTarget() 265 | 266 | // Emit scroll event 267 | Events.emit('scroll', { 268 | delta, 269 | target: state.target 270 | }) 271 | } 272 | 273 | resize = () => { 274 | const state = this.state 275 | 276 | state.resizing = true 277 | state.wh = window.innerHeight 278 | 279 | this.updateSections() 280 | this.setScrollLimit() 281 | this.clampTarget() 282 | 283 | Events.emit('resize') 284 | 285 | state.resizing = false 286 | } 287 | 288 | scrollTo(offset) { 289 | this.state.target = offset 290 | } 291 | 292 | update(elems) { 293 | this.elems = this.sections = null 294 | this.elems = elems || document.querySelectorAll('[data-smooth-item]') 295 | 296 | this.cacheSections() 297 | this.setScrollLimit() 298 | 299 | if (this.scrollbar) this.scrollbar.update() 300 | if (options.preload) this.preload() 301 | } 302 | 303 | removeEvents() { 304 | this.vs.off(this.scroll) 305 | this.vs.destroy() 306 | 307 | store.window.removeEventListener('resize', debounce(this.resize, 200)) 308 | 309 | this.cancelRaf() 310 | 311 | if (this.scrollbar) this.scrollbar.destroy() 312 | } 313 | 314 | destroy() { 315 | this.removeEvents() 316 | 317 | this.state = null 318 | this.opts = null 319 | this.sections = null 320 | this.elems = null 321 | this.el = null 322 | } 323 | } -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import Smooth from './Smooth' 2 | import Scrollbar from './Scrollbar' 3 | 4 | export { 5 | Smooth, 6 | Scrollbar 7 | } -------------------------------------------------------------------------------- /src/core/options.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ease: 0.1, 3 | scrollbar: false, 4 | preload: false, 5 | threshold: 100, 6 | disableMobile: true, 7 | raf: true, 8 | vs: { 9 | mouseMultiplier: 0.45, 10 | touchMultiplier: 2.5, 11 | firefoxMultiplier: 90, 12 | passive: true, 13 | limitInertia: false, 14 | } 15 | } -------------------------------------------------------------------------------- /src/core/store.js: -------------------------------------------------------------------------------- 1 | export default { 2 | body: document.body, 3 | window: window, 4 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Smooth } from './core' 2 | export default new Smooth() -------------------------------------------------------------------------------- /src/utils/Emitter.js: -------------------------------------------------------------------------------- 1 | import Emitter from 'tiny-emitter' 2 | 3 | export default new Emitter() 4 | -------------------------------------------------------------------------------- /src/utils/bindAll.js: -------------------------------------------------------------------------------- 1 | export default function bindAll(object) { 2 | const functions = [].slice.call(arguments, 1) 3 | for(var i = 0; i < functions.length; i++) { 4 | const f = functions[i] 5 | object[f] = bind(object[f], object) 6 | } 7 | } 8 | 9 | function bind(func, context) { 10 | return function() { 11 | return func.apply(context, arguments) 12 | } 13 | } -------------------------------------------------------------------------------- /src/utils/detect.js: -------------------------------------------------------------------------------- 1 | function isDevice() { 2 | if( navigator.userAgent.match(/Android/i) 3 | || navigator.userAgent.match(/webOS/i) 4 | || navigator.userAgent.match(/iPhone/i) 5 | || navigator.userAgent.match(/iPad/i) 6 | || navigator.userAgent.match(/iPod/i) 7 | || navigator.userAgent.match(/BlackBerry/i) 8 | || navigator.userAgent.match(/Windows Phone/i) 9 | ){ 10 | return true; 11 | } 12 | else { 13 | return false; 14 | } 15 | } 16 | 17 | 18 | export const detect = { 19 | device: isDevice() 20 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import bindAll from './bindAll' 2 | import Events from './Emitter' 3 | import preload from './preload' 4 | import { detect } from './detect' 5 | 6 | export { 7 | bindAll, 8 | Events, 9 | preload, 10 | detect 11 | } -------------------------------------------------------------------------------- /src/utils/preload.js: -------------------------------------------------------------------------------- 1 | export default function preload(el = document.body) { 2 | const paths = [...el.querySelectorAll('img')].map(image => image.src) 3 | return Promise.all(paths.map(loadImage)) 4 | } 5 | 6 | const loadImage = path => new Promise(resolve => { 7 | const img = new Image() 8 | img.onload = () => resolve({ path, status: 'ok' }) 9 | img.onerror = () => resolve({ path, status: 'error' }) 10 | 11 | img.src = path 12 | }) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | filename: 'index.js', 4 | }, 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | use: { 11 | loader: "babel-loader" 12 | } 13 | } 14 | ] 15 | } 16 | }; --------------------------------------------------------------------------------