├── .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 |
27 |
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 | };
--------------------------------------------------------------------------------