├── .babelrc ├── .editorconfig ├── .gitignore ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dist ├── scrollbooster.min.js └── scrollbooster.min.js.map ├── index.html ├── package.json ├── src └── index.js ├── test ├── bread.jpg ├── index.html ├── init.test.js ├── nobounce.test.js ├── scroll.test.js ├── scrollto.test.js ├── test.css ├── xonly.test.js └── yonly.test.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-proposal-object-rest-spread", "add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Editor directories and files 7 | .idea 8 | *.suo 9 | *.ntvs* 10 | *.njsproj 11 | *.sln 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | arrowParens: 'always', 5 | printWidth: 120, 6 | tabWidth: 4, 7 | }; 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! 4 | 5 | 1. Fork this repository and clone it 6 | 2. Run `yarn` to install all dependencies 7 | 3. Make your changes. Do not change `dist` directory files manually 8 | 4. Quick way to test it is to run `yarn start` and check it in your browser 9 | 5. Run `yarn build` to build minified files 10 | 6. Commit your changes and make PR 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ilya Shubin 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 | # ScrollBooster 2 | 3 | Enjoyable drag-to-scroll micro library (~2KB gzipped). Supports smooth content scroll via mouse/touch dragging, trackpad or mouse wheel. Zero dependencies. 4 | 5 | Easy to setup yet flexible enough to support any custom scrolling logic. 6 | 7 | ### Installation 8 | 9 | You can install it via `npm` or `yarn` package manager or via `script` tag: 10 | 11 | ``` bash 12 | npm i scrollbooster 13 | ``` 14 | 15 | ``` bash 16 | yarn add scrollbooster 17 | ``` 18 | 19 | ``` html 20 | 21 | ``` 22 | 23 | ### Usage 24 | 25 | The most simple setup with default settings: 26 | 27 | ``` js 28 | import ScrollBooster from 'scrollbooster'; 29 | 30 | new ScrollBooster({ 31 | viewport: document.querySelector('.viewport'), 32 | scrollMode: 'transform' 33 | }); 34 | ``` 35 | 36 | Please note that in order to support IE11 you should replace arrow functions and string templates from code examples to supported equivalents or just use Babel. 37 | 38 | ### Options 39 | 40 | Option | Type | Default | Description 41 | ------ | ---- | ------- | ----------- 42 | viewport | DOM Node | null | Content viewport element (required) 43 | content | DOM Node | viewport child element | Scrollable content element inside viewport 44 | scrollMode | String | undefined | Scroll technique - via CSS transform or natively. Could be 'transform' or 'native' 45 | direction | String | 'all' | Scroll direction. Could be 'horizontal', 'vertical' or 'all' 46 | bounce | Boolean | true | Enables elastic bounce effect when hitting viewport borders 47 | textSelection | Boolean | false | Enables text selection inside viewport 48 | inputsFocus | Boolean | true | Enables focus for elements: 'input', 'textarea', 'button', 'select' and 'label' 49 | pointerMode | String | 'all' | Specify pointer type. Supported values - 'touch' (scroll only on touch devices), 'mouse' (scroll only on desktop), 'all' (mobile and desktop) 50 | friction | Number | 0.05 | Scroll friction factor - how fast scrolling stops after pointer release 51 | bounceForce | Number | 0.1 | Elastic bounce effect factor 52 | emulateScroll | Boolean | false | Enables mouse wheel/trackpad emulation inside viewport 53 | preventDefaultOnEmulateScroll | String | false | Prevents horizontal or vertical default when `emulateScroll` is enabled (eg. useful to prevent horizontal trackpad gestures while enabling vertical scrolling). Could be 'horizontal' or 'vertical'. 54 | lockScrollOnDragDirection | String | false | Detect drag direction and either prevent default `mousedown`/`touchstart` event or lock content scroll. Could be 'horizontal', 'vertical' or 'all' 55 | dragDirectionTolerance | Number | 40 | Tolerance for horizontal or vertical drag detection 56 | onUpdate | Function | noop | Handler function to perform actual scrolling. Receives scrolling state object with coordinates 57 | onClick | Function | noop | Click handler function. Here you can, for example, prevent default event for click on links. Receives object with scrolling metrics and event object. Calls after each `click` in scrollable area 58 | onPointerDown | Function | noop | `mousedown`/`touchstart` events handler 59 | onPointerUp | Function | noop | `mouseup`/`touchend` events handler 60 | onPointerMove | Function | noop | `mousemove`/`touchmove` events handler 61 | onWheel | Function | noop | `wheel` event handler 62 | shouldScroll | Function | noop | Function to permit or disable scrolling. Receives object with scrolling state and event object. Calls on `pointerdown` (mousedown, touchstart) in scrollable area. You can return `true` or `false` to enable or disable scrolling 63 | 64 | ### List of methods 65 | 66 | Method | Description 67 | ------ | ----------- 68 | setPosition | Sets new scroll position in viewport. Receives an object with properties `x` and `y` 69 | scrollTo | Smooth scroll to position in viewport. Receives an object with properties `x` and `y` 70 | updateMetrics | Forces to recalculate elements metrics. Useful for cases when content in scrollable area change its size dynamically 71 | updateOptions | Sets option value. All properties from `Options` config object are supported 72 | getState | Returns current scroll state in a same format as `onUpdate` 73 | destroy | Removes all instance's event listeners 74 | 75 | ### Full Example 76 | 77 | ``` js 78 | const viewport = document.querySelector('.viewport'); 79 | const content = document.querySelector('.scrollable-content'); 80 | 81 | const sb = new ScrollBooster({ 82 | viewport, 83 | content, 84 | bounce: true, 85 | textSelection: false, 86 | emulateScroll: true, 87 | onUpdate: (state) => { 88 | // state contains useful metrics: position, dragOffset, dragAngle, isDragging, isMoving, borderCollision 89 | // you can control scroll rendering manually without 'scrollMethod' option: 90 | content.style.transform = `translate( 91 | ${-state.position.x}px, 92 | ${-state.position.y}px 93 | )`; 94 | }, 95 | shouldScroll: (state, event) => { 96 | // disable scroll if clicked on button 97 | const isButton = event.target.nodeName.toLowerCase() === 'button'; 98 | return !isButton; 99 | }, 100 | onClick: (state, event, isTouchDevice) => { 101 | // prevent default link event 102 | const isLink = event.target.nodeName.toLowerCase() === 'link'; 103 | if (isLink) { 104 | event.preventDefault(); 105 | } 106 | } 107 | }); 108 | 109 | // methods usage examples: 110 | sb.updateMetrics(); 111 | sb.scrollTo({ x: 100, y: 100 }); 112 | sb.updateOptions({ emulateScroll: false }); 113 | sb.destroy(); 114 | ``` 115 | 116 | ### [Live ScrollBooster Examples On CodeSandbox](https://codesandbox.io/s/scrollbooster-examples-3g00p) 117 | 118 | ### Browser support 119 | 120 | ScrollBooster has been tested in IE 11, Edge and other modern browsers (Chrome, Firefox, Safari). 121 | 122 | ### Special thanks 123 | 124 | David DeSandro for his talk ["Practical UI Physics"](https://www.youtube.com/watch?v=90oMnMFozEE). 125 | 126 | ### License 127 | 128 | MIT License (c) Ilya Shubin 129 | -------------------------------------------------------------------------------- /dist/scrollbooster.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("ScrollBooster",[],e):"object"==typeof exports?exports.ScrollBooster=e():t.ScrollBooster=e()}(this,(function(){return function(t){var e={};function i(o){if(e[o])return e[o].exports;var n=e[o]={i:o,l:!1,exports:{}};return t[o].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=t,i.c=e,i.d=function(t,e,o){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)i.d(o,n,function(e){return t[e]}.bind(null,n));return o},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=0)}([function(t,e,i){"use strict";function o(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,o)}return i}function n(t){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:{};s(this,t);var i={content:e.viewport.children[0],direction:"all",pointerMode:"all",scrollMode:void 0,bounce:!0,bounceForce:.1,friction:.05,textSelection:!1,inputsFocus:!0,emulateScroll:!1,preventDefaultOnEmulateScroll:!1,preventPointerMoveDefault:!0,lockScrollOnDragDirection:!1,pointerDownPreventDefault:!0,dragDirectionTolerance:40,onPointerDown:function(){},onPointerUp:function(){},onPointerMove:function(){},onClick:function(){},onUpdate:function(){},onWheel:function(){},shouldScroll:function(){return!0}};if(this.props=n(n({},i),e),this.props.viewport&&this.props.viewport instanceof Element)if(this.props.content){this.isDragging=!1,this.isTargetScroll=!1,this.isScrolling=!1,this.isRunning=!1;var o={x:0,y:0};this.position=n({},o),this.velocity=n({},o),this.dragStartPosition=n({},o),this.dragOffset=n({},o),this.clientOffset=n({},o),this.dragPosition=n({},o),this.targetPosition=n({},o),this.scrollOffset=n({},o),this.rafID=null,this.events={},this.updateMetrics(),this.handleEvents()}else console.error("ScrollBooster init error: Viewport does not have any content");else console.error('ScrollBooster init error: "viewport" config property must be present and must be Element')}var e,i,o;return e=t,(i=[{key:"updateOptions",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.props=n(n({},this.props),t),this.props.onUpdate(this.getState()),this.startAnimationLoop()}},{key:"updateMetrics",value:function(){var t;this.viewport={width:this.props.viewport.clientWidth,height:this.props.viewport.clientHeight},this.content={width:(t=this.props.content,Math.max(t.offsetWidth,t.scrollWidth)),height:l(this.props.content)},this.edgeX={from:Math.min(-this.content.width+this.viewport.width,0),to:0},this.edgeY={from:Math.min(-this.content.height+this.viewport.height,0),to:0},this.props.onUpdate(this.getState()),this.startAnimationLoop()}},{key:"startAnimationLoop",value:function(){var t=this;this.isRunning=!0,cancelAnimationFrame(this.rafID),this.rafID=requestAnimationFrame((function(){return t.animate()}))}},{key:"animate",value:function(){var t=this;if(this.isRunning){this.updateScrollPosition(),this.isMoving()||(this.isRunning=!1,this.isTargetScroll=!1);var e=this.getState();this.setContentPosition(e),this.props.onUpdate(e),this.rafID=requestAnimationFrame((function(){return t.animate()}))}}},{key:"updateScrollPosition",value:function(){this.applyEdgeForce(),this.applyDragForce(),this.applyScrollForce(),this.applyTargetForce();var t=1-this.props.friction;this.velocity.x*=t,this.velocity.y*=t,"vertical"!==this.props.direction&&(this.position.x+=this.velocity.x),"horizontal"!==this.props.direction&&(this.position.y+=this.velocity.y),this.props.bounce&&!this.isScrolling||this.isTargetScroll||(this.position.x=Math.max(Math.min(this.position.x,this.edgeX.to),this.edgeX.from),this.position.y=Math.max(Math.min(this.position.y,this.edgeY.to),this.edgeY.from))}},{key:"applyForce",value:function(t){this.velocity.x+=t.x,this.velocity.y+=t.y}},{key:"applyEdgeForce",value:function(){if(this.props.bounce&&!this.isDragging){var t=this.position.xthis.edgeX.to,i=this.position.ythis.edgeY.to,n=t||e,r=i||o;if(n||r){var s=t?this.edgeX.from:this.edgeX.to,a=i?this.edgeY.from:this.edgeY.to,l=s-this.position.x,p=a-this.position.y,c={x:l*this.props.bounceForce,y:p*this.props.bounceForce},h=this.position.x+(this.velocity.x+c.x)/this.props.friction,u=this.position.y+(this.velocity.y+c.y)/this.props.friction;(t&&h>=this.edgeX.from||e&&h<=this.edgeX.to)&&(c.x=l*this.props.bounceForce-this.velocity.x),(i&&u>=this.edgeY.from||o&&u<=this.edgeY.to)&&(c.y=p*this.props.bounceForce-this.velocity.y),this.applyForce({x:n?c.x:0,y:r?c.y:0})}}}},{key:"applyDragForce",value:function(){if(this.isDragging){var t=this.dragPosition.x-this.position.x,e=this.dragPosition.y-this.position.y;this.applyForce({x:t-this.velocity.x,y:e-this.velocity.y})}}},{key:"applyScrollForce",value:function(){this.isScrolling&&(this.applyForce({x:this.scrollOffset.x-this.velocity.x,y:this.scrollOffset.y-this.velocity.y}),this.scrollOffset.x=0,this.scrollOffset.y=0)}},{key:"applyTargetForce",value:function(){this.isTargetScroll&&this.applyForce({x:.08*(this.targetPosition.x-this.position.x)-this.velocity.x,y:.08*(this.targetPosition.y-this.position.y)-this.velocity.y})}},{key:"isMoving",value:function(){return this.isDragging||this.isScrolling||Math.abs(this.velocity.x)>=.01||Math.abs(this.velocity.y)>=.01}},{key:"scrollTo",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.isTargetScroll=!0,this.targetPosition.x=-t.x||0,this.targetPosition.y=-t.y||0,this.startAnimationLoop()}},{key:"setPosition",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.velocity.x=0,this.velocity.y=0,this.position.x=-t.x||0,this.position.y=-t.y||0,this.startAnimationLoop()}},{key:"getState",value:function(){return{isMoving:this.isMoving(),isDragging:!(!this.dragOffset.x&&!this.dragOffset.y),position:{x:-this.position.x,y:-this.position.y},dragOffset:this.dragOffset,dragAngle:this.getDragAngle(this.clientOffset.x,this.clientOffset.y),borderCollision:{left:this.position.x>=this.edgeX.to,right:this.position.x<=this.edgeX.from,top:this.position.y>=this.edgeY.to,bottom:this.position.y<=this.edgeY.from}}}},{key:"getDragAngle",value:function(t,e){return Math.round(Math.atan2(t,e)*(180/Math.PI))}},{key:"getDragDirection",value:function(t,e){return Math.abs(90-Math.abs(t))<=90-e?"horizontal":"vertical"}},{key:"setContentPosition",value:function(t){"transform"===this.props.scrollMode&&(this.props.content.style.transform="translate(".concat(-t.position.x,"px, ").concat(-t.position.y,"px)")),"native"===this.props.scrollMode&&(this.props.viewport.scrollTop=t.position.y,this.props.viewport.scrollLeft=t.position.x)}},{key:"handleEvents",value:function(){var t=this,e={x:0,y:0},i={x:0,y:0},o=null,n=null,r=!1,s=function(n){if(t.isDragging){var s=r?n.touches[0]:n,a=s.pageX,l=s.pageY,p=s.clientX,c=s.clientY;t.dragOffset.x=a-e.x,t.dragOffset.y=l-e.y,t.clientOffset.x=p-i.x,t.clientOffset.y=c-i.y,(Math.abs(t.clientOffset.x)>5&&!o||Math.abs(t.clientOffset.y)>5&&!o)&&(o=t.getDragDirection(t.getDragAngle(t.clientOffset.x,t.clientOffset.y),t.props.dragDirectionTolerance)),t.props.lockScrollOnDragDirection&&"all"!==t.props.lockScrollOnDragDirection?o===t.props.lockScrollOnDragDirection&&r?(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y):r?(t.dragPosition.x=t.dragStartPosition.x,t.dragPosition.y=t.dragStartPosition.y):(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y):(t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y)}};this.events.pointerdown=function(o){r=!(!o.touches||!o.touches[0]),t.props.onPointerDown(t.getState(),o,r);var n=r?o.touches[0]:o,a=n.pageX,l=n.pageY,p=n.clientX,c=n.clientY,h=t.props.viewport,u=h.getBoundingClientRect();if(!(p-u.left>=h.clientLeft+h.clientWidth)&&!(c-u.top>=h.clientTop+h.clientHeight)&&t.props.shouldScroll(t.getState(),o)&&2!==o.button&&("mouse"!==t.props.pointerMode||!r)&&("touch"!==t.props.pointerMode||r)&&!(t.props.inputsFocus&&["input","textarea","button","select","label"].indexOf(o.target.nodeName.toLowerCase())>-1)){if(t.props.textSelection){if(function(t,e,i){for(var o=t.childNodes,n=document.createRange(),r=0;r=a.left&&i>=a.top&&e<=a.right&&i<=a.bottom)return s}}return!1}(o.target,p,c))return;(f=window.getSelection?window.getSelection():document.selection)&&(f.removeAllRanges?f.removeAllRanges():f.empty&&f.empty())}var f;t.isDragging=!0,e.x=a,e.y=l,i.x=p,i.y=c,t.dragStartPosition.x=t.position.x,t.dragStartPosition.y=t.position.y,s(o),t.startAnimationLoop(),!r&&t.props.pointerDownPreventDefault&&o.preventDefault()}},this.events.pointermove=function(e){!e.cancelable||"all"!==t.props.lockScrollOnDragDirection&&t.props.lockScrollOnDragDirection!==o||e.preventDefault(),s(e),t.props.onPointerMove(t.getState(),e,r)},this.events.pointerup=function(e){t.isDragging=!1,o=null,t.props.onPointerUp(t.getState(),e,r)},this.events.wheel=function(e){var i=t.getState();t.props.emulateScroll&&(t.velocity.x=0,t.velocity.y=0,t.isScrolling=!0,t.scrollOffset.x=-e.deltaX,t.scrollOffset.y=-e.deltaY,t.props.onWheel(i,e),t.startAnimationLoop(),clearTimeout(n),n=setTimeout((function(){return t.isScrolling=!1}),80),t.props.preventDefaultOnEmulateScroll&&t.getDragDirection(t.getDragAngle(-e.deltaX,-e.deltaY),t.props.dragDirectionTolerance)===t.props.preventDefaultOnEmulateScroll&&e.preventDefault())},this.events.scroll=function(){var e=t.props.viewport,i=e.scrollLeft,o=e.scrollTop;Math.abs(t.position.x+i)>3&&(t.position.x=-i,t.velocity.x=0),Math.abs(t.position.y+o)>3&&(t.position.y=-o,t.velocity.y=0)},this.events.click=function(e){var i=t.getState(),o="vertical"!==t.props.direction?i.dragOffset.x:0,n="horizontal"!==t.props.direction?i.dragOffset.y:0;Math.max(Math.abs(o),Math.abs(n))>5&&(e.preventDefault(),e.stopPropagation()),t.props.onClick(i,e,r)},this.events.contentLoad=function(){return t.updateMetrics()},this.events.resize=function(){return t.updateMetrics()},this.props.viewport.addEventListener("mousedown",this.events.pointerdown),this.props.viewport.addEventListener("touchstart",this.events.pointerdown,{passive:!1}),this.props.viewport.addEventListener("click",this.events.click),this.props.viewport.addEventListener("wheel",this.events.wheel,{passive:!1}),this.props.viewport.addEventListener("scroll",this.events.scroll),this.props.content.addEventListener("load",this.events.contentLoad,!0),window.addEventListener("mousemove",this.events.pointermove),window.addEventListener("touchmove",this.events.pointermove,{passive:!1}),window.addEventListener("mouseup",this.events.pointerup),window.addEventListener("touchend",this.events.pointerup),window.addEventListener("resize",this.events.resize)}},{key:"destroy",value:function(){this.props.viewport.removeEventListener("mousedown",this.events.pointerdown),this.props.viewport.removeEventListener("touchstart",this.events.pointerdown),this.props.viewport.removeEventListener("click",this.events.click),this.props.viewport.removeEventListener("wheel",this.events.wheel),this.props.viewport.removeEventListener("scroll",this.events.scroll),this.props.content.removeEventListener("load",this.events.contentLoad),window.removeEventListener("mousemove",this.events.pointermove),window.removeEventListener("touchmove",this.events.pointermove),window.removeEventListener("mouseup",this.events.pointerup),window.removeEventListener("touchend",this.events.pointerup),window.removeEventListener("resize",this.events.resize)}}])&&a(e.prototype,i),o&&a(e,o),t}()}]).default})); 2 | //# sourceMappingURL=scrollbooster.min.js.map -------------------------------------------------------------------------------- /dist/scrollbooster.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://ScrollBooster/webpack/universalModuleDefinition","webpack://ScrollBooster/webpack/bootstrap","webpack://ScrollBooster/./src/index.js"],"names":["root","factory","exports","module","define","amd","this","installedModules","__webpack_require__","moduleId","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","getFullHeight","elem","Math","max","offsetHeight","scrollHeight","ScrollBooster","options","defaults","content","viewport","children","direction","pointerMode","scrollMode","undefined","bounce","bounceForce","friction","textSelection","inputsFocus","emulateScroll","preventDefaultOnEmulateScroll","preventPointerMoveDefault","lockScrollOnDragDirection","pointerDownPreventDefault","dragDirectionTolerance","onPointerDown","onPointerUp","onPointerMove","onClick","onUpdate","onWheel","shouldScroll","props","Element","isDragging","isTargetScroll","isScrolling","isRunning","START_COORDINATES","x","y","position","velocity","dragStartPosition","dragOffset","clientOffset","dragPosition","targetPosition","scrollOffset","rafID","events","updateMetrics","handleEvents","console","error","getState","startAnimationLoop","width","clientWidth","height","clientHeight","offsetWidth","scrollWidth","edgeX","from","min","to","edgeY","cancelAnimationFrame","requestAnimationFrame","animate","updateScrollPosition","isMoving","state","setContentPosition","applyEdgeForce","applyDragForce","applyScrollForce","applyTargetForce","inverseFriction","force","beyondXFrom","beyondXTo","beyondYFrom","beyondYTo","beyondX","beyondY","edge","distanceToEdge","restPosition","applyForce","dragVelocity","abs","dragAngle","getDragAngle","borderCollision","left","right","top","bottom","round","atan2","PI","angle","tolerance","style","transform","scrollTop","scrollLeft","dragOrigin","clientOrigin","dragDirection","wheelTimer","isTouch","setDragPosition","event","eventData","touches","pageX","pageY","clientX","clientY","getDragDirection","pointerdown","rect","getBoundingClientRect","clientLeft","clientTop","button","indexOf","target","nodeName","toLowerCase","element","nodes","childNodes","range","document","createRange","length","node","nodeType","selectNodeContents","textNodeFromPoint","selection","window","getSelection","removeAllRanges","empty","preventDefault","pointermove","cancelable","pointerup","wheel","deltaX","deltaY","clearTimeout","setTimeout","scroll","click","dragOffsetX","dragOffsetY","stopPropagation","contentLoad","resize","addEventListener","passive","removeEventListener"],"mappings":"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","file":"scrollbooster.min.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"ScrollBooster\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ScrollBooster\"] = factory();\n\telse\n\t\troot[\"ScrollBooster\"] = factory();\n})(this, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","const getFullWidth = (elem) => Math.max(elem.offsetWidth, elem.scrollWidth);\nconst getFullHeight = (elem) => Math.max(elem.offsetHeight, elem.scrollHeight);\n\nconst textNodeFromPoint = (element, x, y) => {\n const nodes = element.childNodes;\n const range = document.createRange();\n for (let i = 0; i < nodes.length; i++) {\n const node = nodes[i];\n if (node.nodeType !== 3) {\n continue;\n }\n range.selectNodeContents(node);\n const rect = range.getBoundingClientRect();\n if (x >= rect.left && y >= rect.top && x <= rect.right && y <= rect.bottom) {\n return node;\n }\n }\n return false;\n};\n\nconst clearTextSelection = () => {\n const selection = window.getSelection ? window.getSelection() : document.selection;\n if (!selection) {\n return;\n }\n if (selection.removeAllRanges) {\n selection.removeAllRanges();\n } else if (selection.empty) {\n selection.empty();\n }\n};\n\nconst CLICK_EVENT_THRESHOLD_PX = 5;\n\nexport default class ScrollBooster {\n /**\n * Create ScrollBooster instance\n * @param {Object} options - options object\n * @param {Element} options.viewport - container element\n * @param {Element} options.content - scrollable content element\n * @param {String} options.direction - scroll direction\n * @param {String} options.pointerMode - mouse or touch support\n * @param {String} options.scrollMode - predefined scrolling technique\n * @param {Boolean} options.bounce - bounce effect\n * @param {Number} options.bounceForce - bounce effect factor\n * @param {Number} options.friction - scroll friction factor\n * @param {Boolean} options.textSelection - enables text selection\n * @param {Boolean} options.inputsFocus - enables focus on input elements\n * @param {Boolean} options.emulateScroll - enables mousewheel emulation\n * @param {Function} options.onClick - click handler\n * @param {Function} options.onUpdate - state update handler\n * @param {Function} options.onWheel - wheel handler\n * @param {Function} options.shouldScroll - predicate to allow or disable scroll\n */\n constructor(options = {}) {\n const defaults = {\n content: options.viewport.children[0],\n direction: 'all', // 'vertical', 'horizontal'\n pointerMode: 'all', // 'touch', 'mouse'\n scrollMode: undefined, // 'transform', 'native'\n bounce: true,\n bounceForce: 0.1,\n friction: 0.05,\n textSelection: false,\n inputsFocus: true,\n emulateScroll: false,\n preventDefaultOnEmulateScroll: false, // 'vertical', 'horizontal'\n preventPointerMoveDefault: true,\n lockScrollOnDragDirection: false, // 'vertical', 'horizontal', 'all'\n pointerDownPreventDefault: true,\n dragDirectionTolerance: 40,\n onPointerDown() {},\n onPointerUp() {},\n onPointerMove() {},\n onClick() {},\n onUpdate() {},\n onWheel() {},\n shouldScroll() {\n return true;\n },\n };\n\n this.props = { ...defaults, ...options };\n\n if (!this.props.viewport || !(this.props.viewport instanceof Element)) {\n console.error(`ScrollBooster init error: \"viewport\" config property must be present and must be Element`);\n return;\n }\n\n if (!this.props.content) {\n console.error(`ScrollBooster init error: Viewport does not have any content`);\n return;\n }\n\n this.isDragging = false;\n this.isTargetScroll = false;\n this.isScrolling = false;\n this.isRunning = false;\n\n const START_COORDINATES = { x: 0, y: 0 };\n\n this.position = { ...START_COORDINATES };\n this.velocity = { ...START_COORDINATES };\n this.dragStartPosition = { ...START_COORDINATES };\n this.dragOffset = { ...START_COORDINATES };\n this.clientOffset = { ...START_COORDINATES };\n this.dragPosition = { ...START_COORDINATES };\n this.targetPosition = { ...START_COORDINATES };\n this.scrollOffset = { ...START_COORDINATES };\n\n this.rafID = null;\n this.events = {};\n\n this.updateMetrics();\n this.handleEvents();\n }\n\n /**\n * Update options object with new given values\n */\n updateOptions(options = {}) {\n this.props = { ...this.props, ...options };\n this.props.onUpdate(this.getState());\n this.startAnimationLoop();\n }\n\n /**\n * Update DOM container elements metrics (width and height)\n */\n updateMetrics() {\n this.viewport = {\n width: this.props.viewport.clientWidth,\n height: this.props.viewport.clientHeight,\n };\n this.content = {\n width: getFullWidth(this.props.content),\n height: getFullHeight(this.props.content),\n };\n this.edgeX = {\n from: Math.min(-this.content.width + this.viewport.width, 0),\n to: 0,\n };\n this.edgeY = {\n from: Math.min(-this.content.height + this.viewport.height, 0),\n to: 0,\n };\n\n this.props.onUpdate(this.getState());\n this.startAnimationLoop();\n }\n\n /**\n * Run animation loop\n */\n startAnimationLoop() {\n this.isRunning = true;\n cancelAnimationFrame(this.rafID);\n this.rafID = requestAnimationFrame(() => this.animate());\n }\n\n /**\n * Main animation loop\n */\n animate() {\n if (!this.isRunning) {\n return;\n }\n this.updateScrollPosition();\n // stop animation loop if nothing moves\n if (!this.isMoving()) {\n this.isRunning = false;\n this.isTargetScroll = false;\n }\n const state = this.getState();\n this.setContentPosition(state);\n this.props.onUpdate(state);\n this.rafID = requestAnimationFrame(() => this.animate());\n }\n\n /**\n * Calculate and set new scroll position\n */\n updateScrollPosition() {\n this.applyEdgeForce();\n this.applyDragForce();\n this.applyScrollForce();\n this.applyTargetForce();\n\n const inverseFriction = 1 - this.props.friction;\n this.velocity.x *= inverseFriction;\n this.velocity.y *= inverseFriction;\n\n if (this.props.direction !== 'vertical') {\n this.position.x += this.velocity.x;\n }\n if (this.props.direction !== 'horizontal') {\n this.position.y += this.velocity.y;\n }\n\n // disable bounce effect\n if ((!this.props.bounce || this.isScrolling) && !this.isTargetScroll) {\n this.position.x = Math.max(Math.min(this.position.x, this.edgeX.to), this.edgeX.from);\n this.position.y = Math.max(Math.min(this.position.y, this.edgeY.to), this.edgeY.from);\n }\n }\n\n /**\n * Increase general scroll velocity by given force amount\n */\n applyForce(force) {\n this.velocity.x += force.x;\n this.velocity.y += force.y;\n }\n\n /**\n * Apply force for bounce effect\n */\n applyEdgeForce() {\n if (!this.props.bounce || this.isDragging) {\n return;\n }\n\n // scrolled past viewport edges\n const beyondXFrom = this.position.x < this.edgeX.from;\n const beyondXTo = this.position.x > this.edgeX.to;\n const beyondYFrom = this.position.y < this.edgeY.from;\n const beyondYTo = this.position.y > this.edgeY.to;\n const beyondX = beyondXFrom || beyondXTo;\n const beyondY = beyondYFrom || beyondYTo;\n\n if (!beyondX && !beyondY) {\n return;\n }\n\n const edge = {\n x: beyondXFrom ? this.edgeX.from : this.edgeX.to,\n y: beyondYFrom ? this.edgeY.from : this.edgeY.to,\n };\n\n const distanceToEdge = {\n x: edge.x - this.position.x,\n y: edge.y - this.position.y,\n };\n\n const force = {\n x: distanceToEdge.x * this.props.bounceForce,\n y: distanceToEdge.y * this.props.bounceForce,\n };\n\n const restPosition = {\n x: this.position.x + (this.velocity.x + force.x) / this.props.friction,\n y: this.position.y + (this.velocity.y + force.y) / this.props.friction,\n };\n\n if ((beyondXFrom && restPosition.x >= this.edgeX.from) || (beyondXTo && restPosition.x <= this.edgeX.to)) {\n force.x = distanceToEdge.x * this.props.bounceForce - this.velocity.x;\n }\n\n if ((beyondYFrom && restPosition.y >= this.edgeY.from) || (beyondYTo && restPosition.y <= this.edgeY.to)) {\n force.y = distanceToEdge.y * this.props.bounceForce - this.velocity.y;\n }\n\n this.applyForce({\n x: beyondX ? force.x : 0,\n y: beyondY ? force.y : 0,\n });\n }\n\n /**\n * Apply force to move content while dragging with mouse/touch\n */\n applyDragForce() {\n if (!this.isDragging) {\n return;\n }\n\n const dragVelocity = {\n x: this.dragPosition.x - this.position.x,\n y: this.dragPosition.y - this.position.y,\n };\n\n this.applyForce({\n x: dragVelocity.x - this.velocity.x,\n y: dragVelocity.y - this.velocity.y,\n });\n }\n\n /**\n * Apply force to emulate mouse wheel or trackpad\n */\n applyScrollForce() {\n if (!this.isScrolling) {\n return;\n }\n\n this.applyForce({\n x: this.scrollOffset.x - this.velocity.x,\n y: this.scrollOffset.y - this.velocity.y,\n });\n\n this.scrollOffset.x = 0;\n this.scrollOffset.y = 0;\n }\n\n /**\n * Apply force to scroll to given target coordinate\n */\n applyTargetForce() {\n if (!this.isTargetScroll) {\n return;\n }\n\n this.applyForce({\n x: (this.targetPosition.x - this.position.x) * 0.08 - this.velocity.x,\n y: (this.targetPosition.y - this.position.y) * 0.08 - this.velocity.y,\n });\n }\n\n /**\n * Check if scrolling happening\n */\n isMoving() {\n return (\n this.isDragging ||\n this.isScrolling ||\n Math.abs(this.velocity.x) >= 0.01 ||\n Math.abs(this.velocity.y) >= 0.01\n );\n }\n\n /**\n * Set scroll target coordinate for smooth scroll\n */\n scrollTo(position = {}) {\n this.isTargetScroll = true;\n this.targetPosition.x = -position.x || 0;\n this.targetPosition.y = -position.y || 0;\n this.startAnimationLoop();\n }\n\n /**\n * Manual position setting\n */\n setPosition(position = {}) {\n this.velocity.x = 0;\n this.velocity.y = 0;\n this.position.x = -position.x || 0;\n this.position.y = -position.y || 0;\n this.startAnimationLoop();\n }\n\n /**\n * Get latest metrics and coordinates\n */\n getState() {\n return {\n isMoving: this.isMoving(),\n isDragging: !!(this.dragOffset.x || this.dragOffset.y),\n position: { x: -this.position.x, y: -this.position.y },\n dragOffset: this.dragOffset,\n dragAngle: this.getDragAngle(this.clientOffset.x, this.clientOffset.y),\n borderCollision: {\n left: this.position.x >= this.edgeX.to,\n right: this.position.x <= this.edgeX.from,\n top: this.position.y >= this.edgeY.to,\n bottom: this.position.y <= this.edgeY.from,\n },\n };\n }\n\n /**\n * Get drag angle (up: 180, left: -90, right: 90, down: 0)\n */\n getDragAngle(x, y) {\n return Math.round(Math.atan2(x, y) * (180 / Math.PI));\n }\n\n /**\n * Get drag direction (horizontal or vertical)\n */\n getDragDirection(angle, tolerance) {\n const absAngle = Math.abs(90 - Math.abs(angle));\n\n if (absAngle <= 90 - tolerance) {\n return 'horizontal';\n } else {\n return 'vertical';\n }\n }\n\n /**\n * Update DOM container elements metrics (width and height)\n */\n setContentPosition(state) {\n if (this.props.scrollMode === 'transform') {\n this.props.content.style.transform = `translate(${-state.position.x}px, ${-state.position.y}px)`;\n }\n if (this.props.scrollMode === 'native') {\n this.props.viewport.scrollTop = state.position.y;\n this.props.viewport.scrollLeft = state.position.x;\n }\n }\n\n /**\n * Register all DOM events\n */\n handleEvents() {\n const dragOrigin = { x: 0, y: 0 };\n const clientOrigin = { x: 0, y: 0 };\n let dragDirection = null;\n let wheelTimer = null;\n let isTouch = false;\n\n const setDragPosition = (event) => {\n if (!this.isDragging) {\n return;\n }\n\n const eventData = isTouch ? event.touches[0] : event;\n const { pageX, pageY, clientX, clientY } = eventData;\n\n this.dragOffset.x = pageX - dragOrigin.x;\n this.dragOffset.y = pageY - dragOrigin.y;\n\n this.clientOffset.x = clientX - clientOrigin.x;\n this.clientOffset.y = clientY - clientOrigin.y;\n\n // get dragDirection if offset threshold is reached\n if (\n (Math.abs(this.clientOffset.x) > 5 && !dragDirection) ||\n (Math.abs(this.clientOffset.y) > 5 && !dragDirection)\n ) {\n dragDirection = this.getDragDirection(\n this.getDragAngle(this.clientOffset.x, this.clientOffset.y),\n this.props.dragDirectionTolerance\n );\n }\n\n // prevent scroll if not expected scroll direction\n if (this.props.lockScrollOnDragDirection && this.props.lockScrollOnDragDirection !== 'all') {\n if (dragDirection === this.props.lockScrollOnDragDirection && isTouch) {\n this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x;\n this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y;\n } else if (!isTouch) {\n this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x;\n this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y;\n } else {\n this.dragPosition.x = this.dragStartPosition.x;\n this.dragPosition.y = this.dragStartPosition.y;\n }\n } else {\n this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x;\n this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y;\n }\n };\n\n this.events.pointerdown = (event) => {\n isTouch = !!(event.touches && event.touches[0]);\n\n this.props.onPointerDown(this.getState(), event, isTouch);\n\n const eventData = isTouch ? event.touches[0] : event;\n const { pageX, pageY, clientX, clientY } = eventData;\n\n const { viewport } = this.props;\n const rect = viewport.getBoundingClientRect();\n\n // click on vertical scrollbar\n if (clientX - rect.left >= viewport.clientLeft + viewport.clientWidth) {\n return;\n }\n\n // click on horizontal scrollbar\n if (clientY - rect.top >= viewport.clientTop + viewport.clientHeight) {\n return;\n }\n\n // interaction disabled by user\n if (!this.props.shouldScroll(this.getState(), event)) {\n return;\n }\n\n // disable right mouse button scroll\n if (event.button === 2) {\n return;\n }\n\n // disable on mobile\n if (this.props.pointerMode === 'mouse' && isTouch) {\n return;\n }\n\n // disable on desktop\n if (this.props.pointerMode === 'touch' && !isTouch) {\n return;\n }\n\n // focus on form input elements\n const formNodes = ['input', 'textarea', 'button', 'select', 'label'];\n if (this.props.inputsFocus && formNodes.indexOf(event.target.nodeName.toLowerCase()) > -1) {\n return;\n }\n\n // handle text selection\n if (this.props.textSelection) {\n const textNode = textNodeFromPoint(event.target, clientX, clientY);\n if (textNode) {\n return;\n }\n clearTextSelection();\n }\n\n this.isDragging = true;\n\n dragOrigin.x = pageX;\n dragOrigin.y = pageY;\n\n clientOrigin.x = clientX;\n clientOrigin.y = clientY;\n\n this.dragStartPosition.x = this.position.x;\n this.dragStartPosition.y = this.position.y;\n\n setDragPosition(event);\n this.startAnimationLoop();\n\n if (!isTouch && this.props.pointerDownPreventDefault) {\n event.preventDefault();\n }\n };\n\n this.events.pointermove = (event) => {\n // prevent default scroll if scroll direction is locked\n if (event.cancelable && (this.props.lockScrollOnDragDirection === 'all' ||\n this.props.lockScrollOnDragDirection === dragDirection)) {\n event.preventDefault();\n }\n setDragPosition(event);\n this.props.onPointerMove(this.getState(), event, isTouch);\n };\n\n this.events.pointerup = (event) => {\n this.isDragging = false;\n dragDirection = null;\n this.props.onPointerUp(this.getState(), event, isTouch);\n };\n\n this.events.wheel = (event) => {\n const state = this.getState();\n if (!this.props.emulateScroll) {\n return;\n }\n this.velocity.x = 0;\n this.velocity.y = 0;\n this.isScrolling = true;\n\n this.scrollOffset.x = -event.deltaX;\n this.scrollOffset.y = -event.deltaY;\n\n this.props.onWheel(state, event);\n\n this.startAnimationLoop();\n\n clearTimeout(wheelTimer);\n wheelTimer = setTimeout(() => (this.isScrolling = false), 80);\n\n // get (trackpad) scrollDirection and prevent default events\n if (\n this.props.preventDefaultOnEmulateScroll &&\n this.getDragDirection(\n this.getDragAngle(-event.deltaX, -event.deltaY),\n this.props.dragDirectionTolerance\n ) === this.props.preventDefaultOnEmulateScroll\n ) {\n event.preventDefault();\n }\n };\n\n this.events.scroll = () => {\n const { scrollLeft, scrollTop } = this.props.viewport;\n if (Math.abs(this.position.x + scrollLeft) > 3) {\n this.position.x = -scrollLeft;\n this.velocity.x = 0;\n }\n if (Math.abs(this.position.y + scrollTop) > 3) {\n this.position.y = -scrollTop;\n this.velocity.y = 0;\n }\n };\n\n this.events.click = (event) => {\n const state = this.getState();\n const dragOffsetX = this.props.direction !== 'vertical' ? state.dragOffset.x : 0;\n const dragOffsetY = this.props.direction !== 'horizontal' ? state.dragOffset.y : 0;\n if (Math.max(Math.abs(dragOffsetX), Math.abs(dragOffsetY)) > CLICK_EVENT_THRESHOLD_PX) {\n event.preventDefault();\n event.stopPropagation();\n }\n this.props.onClick(state, event, isTouch);\n };\n\n this.events.contentLoad = () => this.updateMetrics();\n this.events.resize = () => this.updateMetrics();\n\n this.props.viewport.addEventListener('mousedown', this.events.pointerdown);\n this.props.viewport.addEventListener('touchstart', this.events.pointerdown, { passive: false });\n this.props.viewport.addEventListener('click', this.events.click);\n this.props.viewport.addEventListener('wheel', this.events.wheel, { passive: false });\n this.props.viewport.addEventListener('scroll', this.events.scroll);\n this.props.content.addEventListener('load', this.events.contentLoad, true);\n window.addEventListener('mousemove', this.events.pointermove);\n window.addEventListener('touchmove', this.events.pointermove, { passive: false });\n window.addEventListener('mouseup', this.events.pointerup);\n window.addEventListener('touchend', this.events.pointerup);\n window.addEventListener('resize', this.events.resize);\n }\n\n /**\n * Unregister all DOM events\n */\n destroy() {\n this.props.viewport.removeEventListener('mousedown', this.events.pointerdown);\n this.props.viewport.removeEventListener('touchstart', this.events.pointerdown);\n this.props.viewport.removeEventListener('click', this.events.click);\n this.props.viewport.removeEventListener('wheel', this.events.wheel);\n this.props.viewport.removeEventListener('scroll', this.events.scroll);\n this.props.content.removeEventListener('load', this.events.contentLoad);\n window.removeEventListener('mousemove', this.events.pointermove);\n window.removeEventListener('touchmove', this.events.pointermove);\n window.removeEventListener('mouseup', this.events.pointerup);\n window.removeEventListener('touchend', this.events.pointerup);\n window.removeEventListener('resize', this.events.resize);\n }\n}\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | scrollbooster mini test 7 | 29 | 30 | 31 |
32 |
33 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Exercitationem 34 | corrupti reiciendis, mollitia molestias magni quasi voluptates culpa dignissimos minima hic. 35 | Lorem ipsum dolor sit amet consectetur adipisicing elit. In praesentium odit ex officia, possimus qui omnis, 36 | facere incidunt neque ducimus suscipit! Vel, neque! Sapiente tempora veritatis voluptatem itaque! Repellendus, 37 | optio. 38 |
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Atque iusto vitae quibusdam debitis 39 | illum consequuntur iste laborum fuga laboriosam mollitia. 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam aperiam aspernatur autem consequuntur corporis deleniti dolor doloremque eligendi enim eos facere facilis fuga hic iste iusto laboriosam laborum magni maiores maxime minima minus molestiae neque nisi non, odio placeat praesentium rem sint tempore temporibus veniam voluptas, voluptatibus voluptatum! Beatae dolorem earum ipsa odit optio, quaerat sint? Amet animi aperiam blanditiis, corporis culpa excepturi facilis fugiat harum, id inventore ipsum libero magni minima natus nesciunt porro praesentium quos rem sint tenetur veritatis vitae voluptas, voluptatibus! Aut cum debitis doloremque esse eum pariatur quam reiciendis similique sit sunt! A accusamus beatae consectetur cupiditate deleniti doloremque ea eaque eligendi fugiat harum necessitatibus nihil nostrum officia officiis pariatur placeat, quas saepe sapiente similique sunt vel vero voluptatem voluptatibus. Aliquam assumenda beatae, distinctio ipsa laborum molestias obcaecati quas ratione repellendus sapiente. Architecto blanditiis deserunt, ipsa officiis placeat quasi quia quos recusandae voluptate voluptatem. A ab architecto aspernatur atque beatae corporis deleniti distinctio esse eveniet ex excepturi hic illo itaque iusto labore maiores molestias necessitatibus nemo neque nesciunt nisi non officia quaerat, quam reiciendis repellat sapiente sit tempora temporibus velit veniam vitae voluptas voluptatum? Ab at enim facere iusto libero nemo numquam quasi quibusdam quod sapiente! Alias aliquid aperiam atque, cupiditate distinctio eaque eum excepturi facere facilis fugiat harum maiores molestiae molestias odio, optio perferendis provident quam quas quasi quibusdam sint ut voluptates? Amet minima optio quae voluptate? Assumenda atque consectetur corporis debitis delectus deserunt dignissimos distinctio ducimus et excepturi exercitationem, impedit iure iusto magnam magni maxime nam nihil optio quaerat quas ratione repudiandae, sit tenetur ullam vero. Architecto commodi dicta provident. Accusantium alias aperiam aspernatur atque aut, corporis deleniti eius enim eos esse eum maiores mollitia neque non nostrum odio, optio perspiciatis provident quasi quo recusandae repellat sunt temporibus veniam voluptatum. Dolor facilis fuga, fugit iure laboriosam magnam modi molestias nemo neque nesciunt nisi non nulla numquam provident quasi quibusdam ratione sequi veniam? A alias animi beatae deserunt dolor eius eligendi, enim, eum expedita hic illo, illum impedit iste laborum libero maxime nesciunt numquam officiis perferendis quaerat quas qui similique soluta suscipit tempora? Aut nisi, voluptatem! Ab ad alias animi aspernatur consectetur debitis delectus doloremque eaque eos est fuga fugiat harum ipsa ipsam, ipsum nihil officia optio quam quia ratione recusandae soluta suscipit tenetur ut velit veniam veritatis! Ab deserunt eos illo inventore perferendis qui reiciendis? Accusamus, ad aut consectetur cum cupiditate distinctio dolor exercitationem expedita illum iste labore laboriosam libero maiores perspiciatis quos repellendus reprehenderit suscipit totam ullam, ut? Accusantium adipisci animi blanditiis consequatur debitis delectus dolor dolores et expedita id in inventore iste laboriosam laudantium libero magnam maiores pariatur placeat quia quibusdam, quo repudiandae sed soluta totam unde veritatis vero? Accusantium at dolorem dolores eveniet facere fugit iure iusto magnam, neque porro possimus quia repudiandae sed veritatis, voluptates. Ab aliquid amet assumenda blanditiis, commodi consequatur corporis esse harum, labore laudantium molestias nobis odio, omnis pariatur perspiciatis praesentium quidem quisquam rerum temporibus veritatis. Adipisci aspernatur aut cum dolorem eaque eius et excepturi explicabo illum incidunt ipsa libero minima, molestiae nam nemo nesciunt porro reiciendis rem sapiente sed sequi sint tempore unde vel veniam. Ad aut dolorem ducimus eius, eos et eum explicabo fugit in laudantium perspiciatis qui quis quo. Accusantium aspernatur culpa cum excepturi fugit illum ipsa magni necessitatibus porro praesentium! A ab at delectus ea perferendis, repudiandae sequi. Adipisci deleniti dolore praesentium quia reiciendis? A beatae dolorum inventore laboriosam mollitia quaerat ut voluptas. Ad architecto beatae distinctio explicabo necessitatibus nihil officiis quos velit? Architecto autem, commodi deleniti esse, facilis laudantium nulla obcaecati perferendis possimus quas velit vitae voluptas. Beatae blanditiis culpa cumque doloremque dolorum excepturi fuga hic maxime minima mollitia nihil nostrum numquam odio, provident repellendus repudiandae sapiente sit tenetur vero voluptatum. Dolore ea earum eos eum ipsa labore laborum molestias similique voluptatem. Amet, commodi debitis dolore, doloremque earum enim illum natus nisi pariatur quas recusandae reprehenderit sequi ullam. Ab dolorem eaque hic ipsa, laudantium libero minus nesciunt officiis, placeat repellat sint soluta. Amet delectus eius laudantium. Animi aperiam consequatur corporis, cum eaque earum eligendi eos error ex fuga impedit inventore iusto laborum magni numquam obcaecati quaerat, quo reiciendis reprehenderit repudiandae rerum sit soluta ullam. Adipisci, aliquam aliquid atque consequuntur culpa cumque ea earum eum facere fugit id laborum minima modi natus nisi obcaecati, provident quaerat quas quisquam quo rem rerum sed. Aut beatae dolor doloremque nisi voluptas. Consectetur deleniti explicabo non reprehenderit? Laudantium neque officiis pariatur repellendus voluptas? Blanditiis commodi, consequuntur delectus impedit minus natus omnis quidem repellendus suscipit ullam! Aliquam culpa cumque deserunt doloribus earum eos id incidunt iste iure modi, nesciunt omnis perferendis provident quia quibusdam quidem sit tempore veniam vero voluptates! Deleniti ducimus molestias praesentium quod repudiandae. Ab aperiam, architecto at atque consequuntur cum dolore dolorum fugit laudantium libero necessitatibus officiis perspiciatis, quia similique suscipit tenetur vel veritatis. Animi, dolorem, dolores eius error ex laborum natus nemo numquam pariatur repellat repellendus totam voluptas! Aliquam aliquid aspernatur assumenda commodi corporis delectus dolorum error est fugiat illo ipsa ipsum molestias, natus quaerat quas quasi qui quia reiciendis reprehenderit ut! Atque corporis expedita explicabo fugit id ipsum molestiae neque quam quo temporibus? Ad, autem delectus esse fugit maxime nisi numquam odio optio, quia temporibus velit, voluptatum. Ad aliquid aspernatur cupiditate, debitis deserunt dolores ea eligendi est excepturi facilis ipsum itaque iure labore laboriosam magnam magni molestiae nam nesciunt nulla, perferendis quasi, quidem reiciendis rem reprehenderit repudiandae sequi sunt totam ullam vel velit. Accusamus amet assumenda culpa dolores, ducimus illum iure laboriosam, possimus provident quam sequi sint velit? Commodi eaque ex iure nobis praesentium ratione, rerum tempore ullam? Aut iure natus quisquam reprehenderit vero? Ab aliquam, amet animi dignissimos distinctio exercitationem fugiat id labore magni maiores natus neque, nihil nisi obcaecati quibusdam, repellat sit veritatis vero. Aliquam dolor fugit iste nesciunt optio porro quis ratione voluptas? Aliquam amet beatae blanditiis consequuntur dicta dolorum earum error explicabo impedit libero optio recusandae reiciendis, saepe. Consequatur, delectus eius fuga fugiat hic nihil possimus tempore temporibus vero? Accusantium, blanditiis expedita ipsa minima natus obcaecati praesentium quidem rem. 48 |
49 | 50 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollbooster", 3 | "description": "Enjoyable content drag-to-scroll library", 4 | "version": "3.0.2", 5 | "author": "Ilya Shubin ", 6 | "license": "MIT", 7 | "main": "dist/scrollbooster.min.js", 8 | "module": "src/index.js", 9 | "scripts": { 10 | "dev": "webpack --mode development --watch", 11 | "build": "webpack --mode production", 12 | "start": "webpack-dev-server --host 0.0.0.0" 13 | }, 14 | "browserslist": [ 15 | "> 0.25%", 16 | "not dead" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ilyashubin/scrollbooster.git" 21 | }, 22 | "keywords": [ 23 | "drag", 24 | "draggable", 25 | "scroll", 26 | "scrollable", 27 | "UI", 28 | "microlibrary" 29 | ], 30 | "homepage": "https://ilyashubin.github.io/scrollbooster", 31 | "devDependencies": { 32 | "@babel/core": "^7.10.2", 33 | "@babel/plugin-proposal-object-rest-spread": "^7.10.1", 34 | "@babel/preset-env": "^7.10.2", 35 | "babel-loader": "^8.1.0", 36 | "babel-plugin-add-module-exports": "^1.0.2", 37 | "webpack": "^4.43.0", 38 | "webpack-cli": "^3.3.10", 39 | "webpack-dev-server": "^3.11.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const getFullWidth = (elem) => Math.max(elem.offsetWidth, elem.scrollWidth); 2 | const getFullHeight = (elem) => Math.max(elem.offsetHeight, elem.scrollHeight); 3 | 4 | const textNodeFromPoint = (element, x, y) => { 5 | const nodes = element.childNodes; 6 | const range = document.createRange(); 7 | for (let i = 0; i < nodes.length; i++) { 8 | const node = nodes[i]; 9 | if (node.nodeType !== 3) { 10 | continue; 11 | } 12 | range.selectNodeContents(node); 13 | const rect = range.getBoundingClientRect(); 14 | if (x >= rect.left && y >= rect.top && x <= rect.right && y <= rect.bottom) { 15 | return node; 16 | } 17 | } 18 | return false; 19 | }; 20 | 21 | const clearTextSelection = () => { 22 | const selection = window.getSelection ? window.getSelection() : document.selection; 23 | if (!selection) { 24 | return; 25 | } 26 | if (selection.removeAllRanges) { 27 | selection.removeAllRanges(); 28 | } else if (selection.empty) { 29 | selection.empty(); 30 | } 31 | }; 32 | 33 | const CLICK_EVENT_THRESHOLD_PX = 5; 34 | 35 | export default class ScrollBooster { 36 | /** 37 | * Create ScrollBooster instance 38 | * @param {Object} options - options object 39 | * @param {Element} options.viewport - container element 40 | * @param {Element} options.content - scrollable content element 41 | * @param {String} options.direction - scroll direction 42 | * @param {String} options.pointerMode - mouse or touch support 43 | * @param {String} options.scrollMode - predefined scrolling technique 44 | * @param {Boolean} options.bounce - bounce effect 45 | * @param {Number} options.bounceForce - bounce effect factor 46 | * @param {Number} options.friction - scroll friction factor 47 | * @param {Boolean} options.textSelection - enables text selection 48 | * @param {Boolean} options.inputsFocus - enables focus on input elements 49 | * @param {Boolean} options.emulateScroll - enables mousewheel emulation 50 | * @param {Function} options.onClick - click handler 51 | * @param {Function} options.onUpdate - state update handler 52 | * @param {Function} options.onWheel - wheel handler 53 | * @param {Function} options.shouldScroll - predicate to allow or disable scroll 54 | */ 55 | constructor(options = {}) { 56 | const defaults = { 57 | content: options.viewport.children[0], 58 | direction: 'all', // 'vertical', 'horizontal' 59 | pointerMode: 'all', // 'touch', 'mouse' 60 | scrollMode: undefined, // 'transform', 'native' 61 | bounce: true, 62 | bounceForce: 0.1, 63 | friction: 0.05, 64 | textSelection: false, 65 | inputsFocus: true, 66 | emulateScroll: false, 67 | preventDefaultOnEmulateScroll: false, // 'vertical', 'horizontal' 68 | preventPointerMoveDefault: true, 69 | lockScrollOnDragDirection: false, // 'vertical', 'horizontal', 'all' 70 | pointerDownPreventDefault: true, 71 | dragDirectionTolerance: 40, 72 | onPointerDown() {}, 73 | onPointerUp() {}, 74 | onPointerMove() {}, 75 | onClick() {}, 76 | onUpdate() {}, 77 | onWheel() {}, 78 | shouldScroll() { 79 | return true; 80 | }, 81 | }; 82 | 83 | this.props = { ...defaults, ...options }; 84 | 85 | if (!this.props.viewport || !(this.props.viewport instanceof Element)) { 86 | console.error(`ScrollBooster init error: "viewport" config property must be present and must be Element`); 87 | return; 88 | } 89 | 90 | if (!this.props.content) { 91 | console.error(`ScrollBooster init error: Viewport does not have any content`); 92 | return; 93 | } 94 | 95 | this.isDragging = false; 96 | this.isTargetScroll = false; 97 | this.isScrolling = false; 98 | this.isRunning = false; 99 | 100 | const START_COORDINATES = { x: 0, y: 0 }; 101 | 102 | this.position = { ...START_COORDINATES }; 103 | this.velocity = { ...START_COORDINATES }; 104 | this.dragStartPosition = { ...START_COORDINATES }; 105 | this.dragOffset = { ...START_COORDINATES }; 106 | this.clientOffset = { ...START_COORDINATES }; 107 | this.dragPosition = { ...START_COORDINATES }; 108 | this.targetPosition = { ...START_COORDINATES }; 109 | this.scrollOffset = { ...START_COORDINATES }; 110 | 111 | this.rafID = null; 112 | this.events = {}; 113 | 114 | this.updateMetrics(); 115 | this.handleEvents(); 116 | } 117 | 118 | /** 119 | * Update options object with new given values 120 | */ 121 | updateOptions(options = {}) { 122 | this.props = { ...this.props, ...options }; 123 | this.props.onUpdate(this.getState()); 124 | this.startAnimationLoop(); 125 | } 126 | 127 | /** 128 | * Update DOM container elements metrics (width and height) 129 | */ 130 | updateMetrics() { 131 | this.viewport = { 132 | width: this.props.viewport.clientWidth, 133 | height: this.props.viewport.clientHeight, 134 | }; 135 | this.content = { 136 | width: getFullWidth(this.props.content), 137 | height: getFullHeight(this.props.content), 138 | }; 139 | this.edgeX = { 140 | from: Math.min(-this.content.width + this.viewport.width, 0), 141 | to: 0, 142 | }; 143 | this.edgeY = { 144 | from: Math.min(-this.content.height + this.viewport.height, 0), 145 | to: 0, 146 | }; 147 | 148 | this.props.onUpdate(this.getState()); 149 | this.startAnimationLoop(); 150 | } 151 | 152 | /** 153 | * Run animation loop 154 | */ 155 | startAnimationLoop() { 156 | this.isRunning = true; 157 | cancelAnimationFrame(this.rafID); 158 | this.rafID = requestAnimationFrame(() => this.animate()); 159 | } 160 | 161 | /** 162 | * Main animation loop 163 | */ 164 | animate() { 165 | if (!this.isRunning) { 166 | return; 167 | } 168 | this.updateScrollPosition(); 169 | // stop animation loop if nothing moves 170 | if (!this.isMoving()) { 171 | this.isRunning = false; 172 | this.isTargetScroll = false; 173 | } 174 | const state = this.getState(); 175 | this.setContentPosition(state); 176 | this.props.onUpdate(state); 177 | this.rafID = requestAnimationFrame(() => this.animate()); 178 | } 179 | 180 | /** 181 | * Calculate and set new scroll position 182 | */ 183 | updateScrollPosition() { 184 | this.applyEdgeForce(); 185 | this.applyDragForce(); 186 | this.applyScrollForce(); 187 | this.applyTargetForce(); 188 | 189 | const inverseFriction = 1 - this.props.friction; 190 | this.velocity.x *= inverseFriction; 191 | this.velocity.y *= inverseFriction; 192 | 193 | if (this.props.direction !== 'vertical') { 194 | this.position.x += this.velocity.x; 195 | } 196 | if (this.props.direction !== 'horizontal') { 197 | this.position.y += this.velocity.y; 198 | } 199 | 200 | // disable bounce effect 201 | if ((!this.props.bounce || this.isScrolling) && !this.isTargetScroll) { 202 | this.position.x = Math.max(Math.min(this.position.x, this.edgeX.to), this.edgeX.from); 203 | this.position.y = Math.max(Math.min(this.position.y, this.edgeY.to), this.edgeY.from); 204 | } 205 | } 206 | 207 | /** 208 | * Increase general scroll velocity by given force amount 209 | */ 210 | applyForce(force) { 211 | this.velocity.x += force.x; 212 | this.velocity.y += force.y; 213 | } 214 | 215 | /** 216 | * Apply force for bounce effect 217 | */ 218 | applyEdgeForce() { 219 | if (!this.props.bounce || this.isDragging) { 220 | return; 221 | } 222 | 223 | // scrolled past viewport edges 224 | const beyondXFrom = this.position.x < this.edgeX.from; 225 | const beyondXTo = this.position.x > this.edgeX.to; 226 | const beyondYFrom = this.position.y < this.edgeY.from; 227 | const beyondYTo = this.position.y > this.edgeY.to; 228 | const beyondX = beyondXFrom || beyondXTo; 229 | const beyondY = beyondYFrom || beyondYTo; 230 | 231 | if (!beyondX && !beyondY) { 232 | return; 233 | } 234 | 235 | const edge = { 236 | x: beyondXFrom ? this.edgeX.from : this.edgeX.to, 237 | y: beyondYFrom ? this.edgeY.from : this.edgeY.to, 238 | }; 239 | 240 | const distanceToEdge = { 241 | x: edge.x - this.position.x, 242 | y: edge.y - this.position.y, 243 | }; 244 | 245 | const force = { 246 | x: distanceToEdge.x * this.props.bounceForce, 247 | y: distanceToEdge.y * this.props.bounceForce, 248 | }; 249 | 250 | const restPosition = { 251 | x: this.position.x + (this.velocity.x + force.x) / this.props.friction, 252 | y: this.position.y + (this.velocity.y + force.y) / this.props.friction, 253 | }; 254 | 255 | if ((beyondXFrom && restPosition.x >= this.edgeX.from) || (beyondXTo && restPosition.x <= this.edgeX.to)) { 256 | force.x = distanceToEdge.x * this.props.bounceForce - this.velocity.x; 257 | } 258 | 259 | if ((beyondYFrom && restPosition.y >= this.edgeY.from) || (beyondYTo && restPosition.y <= this.edgeY.to)) { 260 | force.y = distanceToEdge.y * this.props.bounceForce - this.velocity.y; 261 | } 262 | 263 | this.applyForce({ 264 | x: beyondX ? force.x : 0, 265 | y: beyondY ? force.y : 0, 266 | }); 267 | } 268 | 269 | /** 270 | * Apply force to move content while dragging with mouse/touch 271 | */ 272 | applyDragForce() { 273 | if (!this.isDragging) { 274 | return; 275 | } 276 | 277 | const dragVelocity = { 278 | x: this.dragPosition.x - this.position.x, 279 | y: this.dragPosition.y - this.position.y, 280 | }; 281 | 282 | this.applyForce({ 283 | x: dragVelocity.x - this.velocity.x, 284 | y: dragVelocity.y - this.velocity.y, 285 | }); 286 | } 287 | 288 | /** 289 | * Apply force to emulate mouse wheel or trackpad 290 | */ 291 | applyScrollForce() { 292 | if (!this.isScrolling) { 293 | return; 294 | } 295 | 296 | this.applyForce({ 297 | x: this.scrollOffset.x - this.velocity.x, 298 | y: this.scrollOffset.y - this.velocity.y, 299 | }); 300 | 301 | this.scrollOffset.x = 0; 302 | this.scrollOffset.y = 0; 303 | } 304 | 305 | /** 306 | * Apply force to scroll to given target coordinate 307 | */ 308 | applyTargetForce() { 309 | if (!this.isTargetScroll) { 310 | return; 311 | } 312 | 313 | this.applyForce({ 314 | x: (this.targetPosition.x - this.position.x) * 0.08 - this.velocity.x, 315 | y: (this.targetPosition.y - this.position.y) * 0.08 - this.velocity.y, 316 | }); 317 | } 318 | 319 | /** 320 | * Check if scrolling happening 321 | */ 322 | isMoving() { 323 | return ( 324 | this.isDragging || 325 | this.isScrolling || 326 | Math.abs(this.velocity.x) >= 0.01 || 327 | Math.abs(this.velocity.y) >= 0.01 328 | ); 329 | } 330 | 331 | /** 332 | * Set scroll target coordinate for smooth scroll 333 | */ 334 | scrollTo(position = {}) { 335 | this.isTargetScroll = true; 336 | this.targetPosition.x = -position.x || 0; 337 | this.targetPosition.y = -position.y || 0; 338 | this.startAnimationLoop(); 339 | } 340 | 341 | /** 342 | * Manual position setting 343 | */ 344 | setPosition(position = {}) { 345 | this.velocity.x = 0; 346 | this.velocity.y = 0; 347 | this.position.x = -position.x || 0; 348 | this.position.y = -position.y || 0; 349 | this.startAnimationLoop(); 350 | } 351 | 352 | /** 353 | * Get latest metrics and coordinates 354 | */ 355 | getState() { 356 | return { 357 | isMoving: this.isMoving(), 358 | isDragging: !!(this.dragOffset.x || this.dragOffset.y), 359 | position: { x: -this.position.x, y: -this.position.y }, 360 | dragOffset: this.dragOffset, 361 | dragAngle: this.getDragAngle(this.clientOffset.x, this.clientOffset.y), 362 | borderCollision: { 363 | left: this.position.x >= this.edgeX.to, 364 | right: this.position.x <= this.edgeX.from, 365 | top: this.position.y >= this.edgeY.to, 366 | bottom: this.position.y <= this.edgeY.from, 367 | }, 368 | }; 369 | } 370 | 371 | /** 372 | * Get drag angle (up: 180, left: -90, right: 90, down: 0) 373 | */ 374 | getDragAngle(x, y) { 375 | return Math.round(Math.atan2(x, y) * (180 / Math.PI)); 376 | } 377 | 378 | /** 379 | * Get drag direction (horizontal or vertical) 380 | */ 381 | getDragDirection(angle, tolerance) { 382 | const absAngle = Math.abs(90 - Math.abs(angle)); 383 | 384 | if (absAngle <= 90 - tolerance) { 385 | return 'horizontal'; 386 | } else { 387 | return 'vertical'; 388 | } 389 | } 390 | 391 | /** 392 | * Update DOM container elements metrics (width and height) 393 | */ 394 | setContentPosition(state) { 395 | if (this.props.scrollMode === 'transform') { 396 | this.props.content.style.transform = `translate(${-state.position.x}px, ${-state.position.y}px)`; 397 | } 398 | if (this.props.scrollMode === 'native') { 399 | this.props.viewport.scrollTop = state.position.y; 400 | this.props.viewport.scrollLeft = state.position.x; 401 | } 402 | } 403 | 404 | /** 405 | * Register all DOM events 406 | */ 407 | handleEvents() { 408 | const dragOrigin = { x: 0, y: 0 }; 409 | const clientOrigin = { x: 0, y: 0 }; 410 | let dragDirection = null; 411 | let wheelTimer = null; 412 | let isTouch = false; 413 | 414 | const setDragPosition = (event) => { 415 | if (!this.isDragging) { 416 | return; 417 | } 418 | 419 | const eventData = isTouch ? event.touches[0] : event; 420 | const { pageX, pageY, clientX, clientY } = eventData; 421 | 422 | this.dragOffset.x = pageX - dragOrigin.x; 423 | this.dragOffset.y = pageY - dragOrigin.y; 424 | 425 | this.clientOffset.x = clientX - clientOrigin.x; 426 | this.clientOffset.y = clientY - clientOrigin.y; 427 | 428 | // get dragDirection if offset threshold is reached 429 | if ( 430 | (Math.abs(this.clientOffset.x) > 5 && !dragDirection) || 431 | (Math.abs(this.clientOffset.y) > 5 && !dragDirection) 432 | ) { 433 | dragDirection = this.getDragDirection( 434 | this.getDragAngle(this.clientOffset.x, this.clientOffset.y), 435 | this.props.dragDirectionTolerance 436 | ); 437 | } 438 | 439 | // prevent scroll if not expected scroll direction 440 | if (this.props.lockScrollOnDragDirection && this.props.lockScrollOnDragDirection !== 'all') { 441 | if (dragDirection === this.props.lockScrollOnDragDirection && isTouch) { 442 | this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x; 443 | this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y; 444 | } else if (!isTouch) { 445 | this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x; 446 | this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y; 447 | } else { 448 | this.dragPosition.x = this.dragStartPosition.x; 449 | this.dragPosition.y = this.dragStartPosition.y; 450 | } 451 | } else { 452 | this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x; 453 | this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y; 454 | } 455 | }; 456 | 457 | this.events.pointerdown = (event) => { 458 | isTouch = !!(event.touches && event.touches[0]); 459 | 460 | this.props.onPointerDown(this.getState(), event, isTouch); 461 | 462 | const eventData = isTouch ? event.touches[0] : event; 463 | const { pageX, pageY, clientX, clientY } = eventData; 464 | 465 | const { viewport } = this.props; 466 | const rect = viewport.getBoundingClientRect(); 467 | 468 | // click on vertical scrollbar 469 | if (clientX - rect.left >= viewport.clientLeft + viewport.clientWidth) { 470 | return; 471 | } 472 | 473 | // click on horizontal scrollbar 474 | if (clientY - rect.top >= viewport.clientTop + viewport.clientHeight) { 475 | return; 476 | } 477 | 478 | // interaction disabled by user 479 | if (!this.props.shouldScroll(this.getState(), event)) { 480 | return; 481 | } 482 | 483 | // disable right mouse button scroll 484 | if (event.button === 2) { 485 | return; 486 | } 487 | 488 | // disable on mobile 489 | if (this.props.pointerMode === 'mouse' && isTouch) { 490 | return; 491 | } 492 | 493 | // disable on desktop 494 | if (this.props.pointerMode === 'touch' && !isTouch) { 495 | return; 496 | } 497 | 498 | // focus on form input elements 499 | const formNodes = ['input', 'textarea', 'button', 'select', 'label']; 500 | if (this.props.inputsFocus && formNodes.indexOf(event.target.nodeName.toLowerCase()) > -1) { 501 | return; 502 | } 503 | 504 | // handle text selection 505 | if (this.props.textSelection) { 506 | const textNode = textNodeFromPoint(event.target, clientX, clientY); 507 | if (textNode) { 508 | return; 509 | } 510 | clearTextSelection(); 511 | } 512 | 513 | this.isDragging = true; 514 | 515 | dragOrigin.x = pageX; 516 | dragOrigin.y = pageY; 517 | 518 | clientOrigin.x = clientX; 519 | clientOrigin.y = clientY; 520 | 521 | this.dragStartPosition.x = this.position.x; 522 | this.dragStartPosition.y = this.position.y; 523 | 524 | setDragPosition(event); 525 | this.startAnimationLoop(); 526 | 527 | if (!isTouch && this.props.pointerDownPreventDefault) { 528 | event.preventDefault(); 529 | } 530 | }; 531 | 532 | this.events.pointermove = (event) => { 533 | // prevent default scroll if scroll direction is locked 534 | if (event.cancelable && (this.props.lockScrollOnDragDirection === 'all' || 535 | this.props.lockScrollOnDragDirection === dragDirection)) { 536 | event.preventDefault(); 537 | } 538 | setDragPosition(event); 539 | this.props.onPointerMove(this.getState(), event, isTouch); 540 | }; 541 | 542 | this.events.pointerup = (event) => { 543 | this.isDragging = false; 544 | dragDirection = null; 545 | this.props.onPointerUp(this.getState(), event, isTouch); 546 | }; 547 | 548 | this.events.wheel = (event) => { 549 | const state = this.getState(); 550 | if (!this.props.emulateScroll) { 551 | return; 552 | } 553 | this.velocity.x = 0; 554 | this.velocity.y = 0; 555 | this.isScrolling = true; 556 | 557 | this.scrollOffset.x = -event.deltaX; 558 | this.scrollOffset.y = -event.deltaY; 559 | 560 | this.props.onWheel(state, event); 561 | 562 | this.startAnimationLoop(); 563 | 564 | clearTimeout(wheelTimer); 565 | wheelTimer = setTimeout(() => (this.isScrolling = false), 80); 566 | 567 | // get (trackpad) scrollDirection and prevent default events 568 | if ( 569 | this.props.preventDefaultOnEmulateScroll && 570 | this.getDragDirection( 571 | this.getDragAngle(-event.deltaX, -event.deltaY), 572 | this.props.dragDirectionTolerance 573 | ) === this.props.preventDefaultOnEmulateScroll 574 | ) { 575 | event.preventDefault(); 576 | } 577 | }; 578 | 579 | this.events.scroll = () => { 580 | const { scrollLeft, scrollTop } = this.props.viewport; 581 | if (Math.abs(this.position.x + scrollLeft) > 3) { 582 | this.position.x = -scrollLeft; 583 | this.velocity.x = 0; 584 | } 585 | if (Math.abs(this.position.y + scrollTop) > 3) { 586 | this.position.y = -scrollTop; 587 | this.velocity.y = 0; 588 | } 589 | }; 590 | 591 | this.events.click = (event) => { 592 | const state = this.getState(); 593 | const dragOffsetX = this.props.direction !== 'vertical' ? state.dragOffset.x : 0; 594 | const dragOffsetY = this.props.direction !== 'horizontal' ? state.dragOffset.y : 0; 595 | if (Math.max(Math.abs(dragOffsetX), Math.abs(dragOffsetY)) > CLICK_EVENT_THRESHOLD_PX) { 596 | event.preventDefault(); 597 | event.stopPropagation(); 598 | } 599 | this.props.onClick(state, event, isTouch); 600 | }; 601 | 602 | this.events.contentLoad = () => this.updateMetrics(); 603 | this.events.resize = () => this.updateMetrics(); 604 | 605 | this.props.viewport.addEventListener('mousedown', this.events.pointerdown); 606 | this.props.viewport.addEventListener('touchstart', this.events.pointerdown, { passive: false }); 607 | this.props.viewport.addEventListener('click', this.events.click); 608 | this.props.viewport.addEventListener('wheel', this.events.wheel, { passive: false }); 609 | this.props.viewport.addEventListener('scroll', this.events.scroll); 610 | this.props.content.addEventListener('load', this.events.contentLoad, true); 611 | window.addEventListener('mousemove', this.events.pointermove); 612 | window.addEventListener('touchmove', this.events.pointermove, { passive: false }); 613 | window.addEventListener('mouseup', this.events.pointerup); 614 | window.addEventListener('touchend', this.events.pointerup); 615 | window.addEventListener('resize', this.events.resize); 616 | } 617 | 618 | /** 619 | * Unregister all DOM events 620 | */ 621 | destroy() { 622 | this.props.viewport.removeEventListener('mousedown', this.events.pointerdown); 623 | this.props.viewport.removeEventListener('touchstart', this.events.pointerdown); 624 | this.props.viewport.removeEventListener('click', this.events.click); 625 | this.props.viewport.removeEventListener('wheel', this.events.wheel); 626 | this.props.viewport.removeEventListener('scroll', this.events.scroll); 627 | this.props.content.removeEventListener('load', this.events.contentLoad); 628 | window.removeEventListener('mousemove', this.events.pointermove); 629 | window.removeEventListener('touchmove', this.events.pointermove); 630 | window.removeEventListener('mouseup', this.events.pointerup); 631 | window.removeEventListener('touchend', this.events.pointerup); 632 | window.removeEventListener('resize', this.events.resize); 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /test/bread.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/scrollbooster/31ee2d16b5df5dbe7abc6475abdffc58bd6a6e6e/test/bread.jpg -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |

Init test

16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 |

Native scroll test

26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |

Horizontal only

36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |

Vertical only

46 | 47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 | 55 |

Without bounce

56 | 57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 | 65 |

Smooth scroll

66 | 67 |
68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /test/init.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var scrollEl = document.querySelector("#init .inner"); 3 | var scr; 4 | 5 | beforeEach(function(done) { 6 | scr = new ScrollBooster({ 7 | viewport: document.querySelector("#init .wrapper"), 8 | content: scrollEl, 9 | onUpdate: function(data) { 10 | scrollEl.style.transform = 11 | "translate(" + -data.position.x + "px, " + -data.position.y + "px)"; 12 | } 13 | }); 14 | setTimeout(done, 300); 15 | }); 16 | 17 | describe("Init", function() { 18 | it("Init properties", function() { 19 | chai.expect(scr.position.x).to.equal(0); 20 | chai.expect(scr.position.y).to.equal(0); 21 | 22 | chai.expect(scr.props.viewport).to.be.an.instanceof(Element); 23 | chai.expect(scr.props.content).to.be.an.instanceof(Element); 24 | 25 | chai.expect(scr.viewport.width).to.equal(300); 26 | chai.expect(scr.viewport.height).to.equal(300); 27 | 28 | chai.expect(scr.content.width).to.gt(300); 29 | chai.expect(scr.content.height).to.gt(300); 30 | }); 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /test/nobounce.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var scrollEl = document.querySelector("#nobounce .inner"); 3 | var scr = new ScrollBooster({ 4 | viewport: document.querySelector("#nobounce .wrapper"), 5 | emulateScroll: true, 6 | bounce: false, 7 | onUpdate: function(data) { 8 | scrollEl.style.transform = 9 | "translate(" + -data.position.x + "px, " + -data.position.y + "px)"; 10 | } 11 | }); 12 | })(); 13 | -------------------------------------------------------------------------------- /test/scroll.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var viewportEl = document.querySelector("#scroll .wrapper"); 3 | var scr; 4 | 5 | beforeEach(function(done) { 6 | scr = new ScrollBooster({ 7 | viewport: viewportEl, 8 | emulateScroll: true, 9 | onUpdate: function(data) { 10 | viewportEl.scrollTop = data.position.y; 11 | viewportEl.scrollLeft = data.position.x; 12 | } 13 | }); 14 | scr.setPosition({ x: 100, y: 100 }); 15 | setTimeout(done, 300); 16 | }); 17 | 18 | describe("Scroll", function() { 19 | it("Scroll test", function() { 20 | chai.expect(scr.position.x).to.equal(-100); 21 | chai.expect(scr.position.y).to.equal(-100); 22 | 23 | let st = viewportEl.scrollTop; 24 | chai.expect(st).to.equal(100); 25 | }); 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /test/scrollto.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var scrollEl = document.querySelector("#scrollto .inner"); 3 | var scr = new ScrollBooster({ 4 | viewport: document.querySelector("#scrollto .wrapper"), 5 | emulateScroll: true, 6 | onUpdate: function(data) { 7 | scrollEl.style.transform = 8 | "translate(" + -data.position.x + "px, " + -data.position.y + "px)"; 9 | } 10 | }); 11 | 12 | document.querySelector('#scrollto-button').addEventListener('click', function () { 13 | scr.scrollTo({ 14 | x: Math.random() * 150, 15 | y: Math.random() * 150 16 | }) 17 | }) 18 | })(); 19 | -------------------------------------------------------------------------------- /test/test.css: -------------------------------------------------------------------------------- 1 | 2 | #testapp { 3 | padding: 50px; 4 | text-align: center; 5 | } 6 | 7 | .wrapper { 8 | overflow: hidden; 9 | margin: auto; 10 | width: 300px; 11 | height: 300px; 12 | border: 2px solid orange; 13 | } 14 | 15 | .inner { 16 | background: skyblue; 17 | } 18 | 19 | #scroll .wrapper { 20 | overflow: scroll; 21 | } -------------------------------------------------------------------------------- /test/xonly.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var scrollEl = document.querySelector("#xonly .inner"); 3 | var scr = new ScrollBooster({ 4 | viewport: document.querySelector("#xonly .wrapper"), 5 | emulateScroll: true, 6 | direction: "horizontal", 7 | onUpdate: function(data) { 8 | scrollEl.style.transform = 9 | "translate(" + -data.position.x + "px, " + -data.position.y + "px)"; 10 | } 11 | }); 12 | scr.setPosition({ 13 | x: 100 14 | }); 15 | })(); 16 | -------------------------------------------------------------------------------- /test/yonly.test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var scrollEl = document.querySelector("#yonly .inner"); 3 | var scr = new ScrollBooster({ 4 | viewport: document.querySelector("#yonly .wrapper"), 5 | direction: "vertical", 6 | emulateScroll: true, 7 | onUpdate: function(data) { 8 | scrollEl.style.transform = 9 | "translate(" + -data.position.x + "px, " + -data.position.y + "px)"; 10 | } 11 | }); 12 | scr.setPosition({ 13 | y: 100 14 | }); 15 | })(); 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'scrollbooster.min.js', 8 | library: 'ScrollBooster', 9 | libraryTarget: 'umd', 10 | libraryExport: 'default', 11 | umdNamedDefine: true, 12 | globalObject: 'this', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env'], 23 | }, 24 | }, 25 | }, 26 | ], 27 | }, 28 | devServer: { 29 | contentBase: path.join(__dirname), 30 | clientLogLevel: 'none', 31 | open: true, 32 | }, 33 | devtool: 'source-map', 34 | optimization: { 35 | minimize: true, 36 | }, 37 | }; 38 | --------------------------------------------------------------------------------