scrollbooster
Advanced tools
Comparing version 1.1.0 to 2.0.0
@@ -1,1 +0,2 @@ | ||
!function(t,i){"object"==typeof exports&&"object"==typeof module?module.exports=i():"function"==typeof define&&define.amd?define("ScrollBooster",[],i):"object"==typeof exports?exports.ScrollBooster=i():t.ScrollBooster=i()}("undefined"!=typeof self?self:this,function(){return function(t){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}var e={};return i.m=t,i.c=e,i.d=function(t,e,o){i.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get: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,i){return Object.prototype.hasOwnProperty.call(t,i)},i.p="/dist/",i(i.s=0)}([function(t,i,e){"use strict";function o(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}function n(t){return Math.max(t.offsetWidth,t.scrollWidth)}function s(t){return Math.max(t.offsetHeight,t.scrollHeight)}function r(t,i,e){for(var o=void 0,n=t.childNodes,s=document.createRange(),r=0;o=n[r],r<n.length;r++)if(3===o.nodeType){s.selectNodeContents(o);var h=s.getBoundingClientRect();if(i>=h.left&&e>=h.top&&i<=h.right&&e<=h.bottom)return o}return!1}function h(){var t=window.getSelection?window.getSelection():document.selection;t&&(t.removeAllRanges?t.removeAllRanges():t.empty&&t.empty())}Object.defineProperty(i,"__esModule",{value:!0});var c=Object.assign||function(t){for(var i=1;i<arguments.length;i++){var e=arguments[i];for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o])}return t},l=function(){function t(t,i){for(var e=0;e<i.length;e++){var o=i[e];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}return function(i,e,o){return e&&t(i.prototype,e),o&&t(i,o),i}}(),p=function(){function t(){var i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(o(this,t),!(i.viewport&&i.viewport instanceof Element))return void console.error('"viewport" config property must be present and must be Element');var e={handle:i.viewport,content:i.viewport.children[0],bounce:!0,friction:.05,bounceForce:.1,textSelection:!1,onClick:function(){},shouldScroll:function(){return!0},onUpdate:function(){}};if(this.props=c({},e,i),!this.props.content)return void console.error("Viewport does not have any content");this.viewport={width:this.props.viewport.clientWidth,height:this.props.viewport.clientHeight},this.content={width:n(this.props.content),height:s(this.props.content)},this.position={x:0,y:0},this.velocity={x:0,y:0},this.friction=1-this.props.friction,this.bounceForce=this.props.bounceForce,this.isDragging=!1,this.dragStartPosition={x:0,y:0},this.dragOffsetPosition=c({},this.dragStartPosition),this.dragPosition=c({},this.position),this.isScrollEnabled=!!this.props.emulateScroll,this.isScrolling=!1,this.scrollOffset={x:0,y:0},this.bounce=this.props.bounce,this.textSelection=this.props.textSelection,this.boundX={from:Math.min(-this.content.width+this.viewport.width,0),to:0},this.boundY={from:Math.min(-this.content.height+this.viewport.height,0),to:0},this.mode={x:"x"==this.props.mode,y:"y"==this.props.mode,xy:"x"!==this.props.mode&&"y"!==this.props.mode},this.isRunning=!1,this.rafID=null,this.events={},this.animate(),this.handleEvents()}return l(t,[{key:"run",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;this.isRunning&&(this.update(),this.notify(),this.rafID=requestAnimationFrame(function(){return t.animate()}))}},{key:"update",value:function(){this.applyBoundForce(),this.applyDragForce(),this.applyScrollForce(),this.velocity.x*=this.friction,this.velocity.y*=this.friction,this.mode.y||(this.position.x+=this.velocity.x),this.mode.x||(this.position.y+=this.velocity.y),this.bounce&&!this.isScrolling||(this.position.x=Math.max(Math.min(this.position.x,this.boundX.to),this.boundX.from),this.position.y=Math.max(Math.min(this.position.y,this.boundY.to),this.boundY.from)),!this.isDragging&&!this.isScrolling&&Math.abs(this.velocity.x)<.1&&Math.abs(this.velocity.y)<.1&&(this.isRunning=!1)}},{key:"applyForce",value:function(t){this.velocity.x+=t.x,this.velocity.y+=t.y}},{key:"applyBoundForce",value:function(){if(this.bounce&&!this.isDragging){var t=this.position.x<this.boundX.from,i=this.position.x>this.boundX.to,e=this.position.y<this.boundY.from,o=this.position.y>this.boundY.to,n={x:0,y:0};if(t||i){var s=t?this.boundX.from:this.boundX.to,r=s-this.position.x,h=r*this.bounceForce,c=this.position.x+(this.velocity.x+h)/(1-this.friction);t&&c<this.boundX.from||i&&c>this.boundX.to||(h=r*this.bounceForce-this.velocity.x),n.x=h}if(e||o){var l=e?this.boundY.from:this.boundY.to,p=l-this.position.y,a=p*this.bounceForce,u=this.position.y+(this.velocity.y+a)/(1-this.friction);e&&u<this.boundY.from||o&&u>this.boundY.to||(a=p*this.bounceForce-this.velocity.y),n.y=a}this.applyForce(n)}}},{key:"applyDragForce",value:function(){if(this.isDragging){var t={x:this.dragPosition.x-this.position.x,y:this.dragPosition.y-this.position.y},i={x:t.x-this.velocity.x,y:t.y-this.velocity.y};this.applyForce(i)}}},{key:"applyScrollForce",value:function(){if(this.isScrolling){var t={x:this.scrollOffset.x-this.velocity.x,y:this.scrollOffset.y-this.velocity.y};this.scrollOffset.x=0,this.scrollOffset.y=0,this.applyForce(t)}}},{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.run()}},{key:"getUpdate",value:function(){return{isRunning:this.isRunning,isDragging:this.isDragging,isScrolling:this.isScrolling,position:{x:-this.position.x,y:-this.position.y},dragOffsetPosition:this.dragOffsetPosition,viewport:c({},this.viewport),content:c({},this.content)}}},{key:"notify",value:function(){this.props.onUpdate(this.getUpdate())}},{key:"updateMetrics",value:function(){this.viewport.width=this.props.viewport.clientWidth,this.viewport.height=this.props.viewport.clientHeight,this.content.width=n(this.props.content),this.content.height=s(this.props.content),this.boundX.from=Math.min(-this.content.width+this.viewport.width,0),this.boundY.from=Math.min(-this.content.height+this.viewport.height,0),this.run()}},{key:"handleEvents",value:function(){var t=this,i=this.props.viewport,e={x:0,y:0},o={x:0,y:0},n=!1,s=function(i){var e=void 0,s=void 0;n?(e=i.touches[0].pageX,s=i.touches[0].pageY):(e=i.pageX,s=i.pageY),t.dragOffsetPosition.x=e-o.x,t.dragOffsetPosition.y=s-o.y,t.dragPosition.x=t.dragStartPosition.x+t.dragOffsetPosition.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffsetPosition.y,n||i.preventDefault()};this.events.pointerdown=function(c){var l=void 0,p=void 0,a=void 0,u=void 0;n=!(!c.touches||!c.touches[0]),n?(l=c.touches[0].pageX,p=c.touches[0].pageY,a=c.touches[0].clientX,u=c.touches[0].clientY):(l=c.pageX,p=c.pageY,a=c.clientX,u=c.clientY);var d=i.getBoundingClientRect();if(!(a-d.left>=i.clientLeft+i.clientWidth)&&!(u-d.top>=i.clientTop+i.clientHeight)&&t.props.shouldScroll(t.getUpdate(),c)){if(t.textSelection){if(r(c.target,a,u))return;h()}t.isDragging=!0,(e.x||e.y)&&(t.position.x=e.x,t.position.y=e.y,e.x=0,e.y=0),o.x=l,o.y=p,t.dragStartPosition.x=t.position.x,t.dragStartPosition.y=t.position.y,s(c),t.run();var v=void 0,f=void 0;f=function(i){t.isDragging=!1,n?(window.removeEventListener("touchmove",s),window.removeEventListener("touchend",v)):(window.removeEventListener("mousemove",s),window.removeEventListener("mouseup",v))},n?(v=window.addEventListener("touchend",f),window.addEventListener("touchmove",s)):(v=window.addEventListener("mouseup",f),window.addEventListener("mousemove",s))}};var c=null;this.events.wheel=function(i){t.velocity.x=0,t.isScrollEnabled&&(t.isScrolling=!0,t.scrollOffset.x=-i.deltaX,t.scrollOffset.y=-i.deltaY,t.run(),clearTimeout(c),c=setTimeout(function(){return t.isScrolling=!1},80),i.preventDefault())},this.events.scroll=function(i){var o=t.props.viewport.scrollLeft,n=t.props.viewport.scrollTop;Math.abs(t.position.x+o)>3&&(t.position.x=-o,t.velocity.x=0),Math.abs(t.position.y+n)>3&&(t.position.y=-n,t.velocity.y=0),e.x=-t.props.viewport.scrollLeft,e.y=-t.props.viewport.scrollTop},this.events.click=function(i){t.props.onClick(t.getUpdate(),i)},this.events.resize=this.updateMetrics.bind(this),this.props.handle.addEventListener("mousedown",this.events.pointerdown),this.props.handle.addEventListener("touchstart",this.events.pointerdown),this.props.handle.addEventListener("click",this.events.click),this.props.viewport.addEventListener("wheel",this.events.wheel),this.props.viewport.addEventListener("scroll",this.events.scroll),window.addEventListener("resize",this.events.resize)}},{key:"destroy",value:function(){this.props.handle.removeEventListener("mousedown",this.events.pointerdown),this.props.handle.removeEventListener("touchstart",this.events.pointerdown),this.props.handle.removeEventListener("click",this.events.click),this.props.viewport.removeEventListener("wheel",this.events.wheel),this.props.viewport.removeEventListener("scroll",this.events.scroll),window.removeEventListener("resize",this.events.resize)}}]),t}();i.default=p,t.exports=i.default}])}); | ||
!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()}(window,(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;e<arguments.length;e++){var i=null!=arguments[e]?arguments[e]:{};e%2?o(Object(i),!0).forEach((function(e){s(t,e,i[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):o(Object(i)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))}))}return t}function s(t,e,i){return e in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}function r(t,e){for(var i=0;i<e.length;i++){var o=e[i];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}i.r(e),i.d(e,"default",(function(){return p}));var h=function(t){return Math.max(t.offsetWidth,t.scrollWidth)},c=function(t){return Math.max(t.offsetHeight,t.scrollHeight)},p=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t);var i={content:e.viewport.children[0],direction:"all",pointerMode:"all",bounce:!0,friction:.05,bounceForce:.1,textSelection:!1,inputsFocus:!0,emulateScroll:!1,onClick:function(){},onUpdate:function(){},shouldScroll:function(){return!0}};this.props=n({},i,{},e),this.props.viewport&&this.props.viewport instanceof Element?this.props.content?(this.viewport={width:this.props.viewport.clientWidth,height:this.props.viewport.clientHeight},this.content={width:h(this.props.content),height:c(this.props.content)},this.position={x:0,y:0},this.velocity={x:0,y:0},this.friction=1-this.props.friction,this.bounceForce=this.props.bounceForce,this.isDragging=!1,this.dragStartPosition={x:0,y:0},this.dragOffset=n({},this.dragStartPosition),this.dragPosition=n({},this.position),this.isTargetScroll=!1,this.targetPosition={x:0,y:0},this.isScrolling=!1,this.scrollOffset={x:0,y:0},this.bounce=this.props.bounce,this.textSelection=this.props.textSelection,this.boundX={from:Math.min(-this.content.width+this.viewport.width,0),to:0},this.boundY={from:Math.min(-this.content.height+this.viewport.height,0),to:0},this.isRunning=!1,this.rafID=null,this.events={},this.animate(),this.handleEvents()):console.error("ScrollBooster init error: Viewport does not have any content"):console.error('ScrollBooster init error: "viewport" config property must be present and must be Element')}var e,i,o;return e=t,(i=[{key:"run",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;this.isRunning&&(this.update(),this.props.onUpdate(this.getState()),this.rafID=requestAnimationFrame((function(){return t.animate()})))}},{key:"isMoving",value:function(){return this.isDragging||this.isScrolling||Math.abs(this.velocity.x)>=.1||Math.abs(this.velocity.y)>=.1}},{key:"update",value:function(){this.applyBoundForce(),this.applyDragForce(),this.applyScrollForce(),this.applyTargetForce(),this.velocity.x*=this.friction,this.velocity.y*=this.friction,"vertical"!==this.props.direction&&(this.position.x+=this.velocity.x),"horizontal"!==this.props.direction&&(this.position.y+=this.velocity.y),this.bounce&&!this.isScrolling||this.isTargetScroll||(this.position.x=Math.max(Math.min(this.position.x,this.boundX.to),this.boundX.from),this.position.y=Math.max(Math.min(this.position.y,this.boundY.to),this.boundY.from)),this.isMoving()||(this.isRunning=!1,this.isTargetScroll=!1)}},{key:"applyForce",value:function(t){this.velocity.x+=t.x,this.velocity.y+=t.y}},{key:"applyBoundForce",value:function(){if(this.bounce&&!this.isDragging){var t=this.position.x<this.boundX.from,e=this.position.x>this.boundX.to,i=this.position.y<this.boundY.from,o=this.position.y>this.boundY.to,n=t||e,s=i||o;if(n||s){var r=t?this.boundX.from:this.boundX.to,h=i?this.boundY.from:this.boundY.to,c=r-this.position.x,p=h-this.position.y,l={x:c*this.bounceForce,y:p*this.bounceForce},a=this.position.x+(this.velocity.x+l.x)/(1-this.friction),u=this.position.y+(this.velocity.y+l.y)/(1-this.friction);(t&&a>=this.boundX.from||e&&a<=this.boundX.to)&&(l.x=c*this.bounceForce-this.velocity.x),(i&&u>=this.boundY.from||o&&u<=this.boundY.to)&&(l.y=p*this.bounceForce-this.velocity.y),this.applyForce({x:n?l.x:0,y:s?l.y:0})}}}},{key:"applyDragForce",value:function(){if(this.isDragging){var t=this.dragPosition.x-this.position.x,e=this.dragPosition.y-this.position.y,i={x:t-this.velocity.x,y:e-this.velocity.y};this.applyForce(i)}}},{key:"applyScrollForce",value:function(){if(this.isScrolling){var t={x:this.scrollOffset.x-this.velocity.x,y:this.scrollOffset.y-this.velocity.y};this.scrollOffset.x=0,this.scrollOffset.y=0,this.applyForce(t)}}},{key:"applyTargetForce",value:function(){if(this.isTargetScroll){var t={x:.08*(this.targetPosition.x-this.position.x)-this.velocity.x,y:.08*(this.targetPosition.y-this.position.y)-this.velocity.y};this.applyForce(t)}}},{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.run()}},{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.run()}},{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}}},{key:"updateMetrics",value:function(){this.viewport.width=this.props.viewport.clientWidth,this.viewport.height=this.props.viewport.clientHeight,this.content.width=h(this.props.content),this.content.height=c(this.props.content),this.boundX.from=Math.min(-this.content.width+this.viewport.width,0),this.boundY.from=Math.min(-this.content.height+this.viewport.height,0),this.run()}},{key:"handleEvents",value:function(){var t=this,e={x:0,y:0},i=null,o=!1,n=function(i){if(t.isDragging){var n=o?i.touches[0].pageX:i.pageX,s=o?i.touches[0].pageY:i.pageY;t.dragOffset.x=n-e.x,t.dragOffset.y=s-e.y,t.dragPosition.x=t.dragStartPosition.x+t.dragOffset.x,t.dragPosition.y=t.dragStartPosition.y+t.dragOffset.y}};this.events.pointerdown=function(i){var s=(o=!(!i.touches||!i.touches[0]))?i.touches[0]:i,r=s.pageX,h=s.pageY,c=s.clientX,p=s.clientY,l=t.props.viewport,a=l.getBoundingClientRect();if(!(c-a.left>=l.clientLeft+l.clientWidth)&&!(p-a.top>=l.clientTop+l.clientHeight)&&t.props.shouldScroll(t.getState(),i)&&("mouse"!==t.props.pointerMode||!o)&&("touch"!==t.props.pointerMode||o)&&!(t.props.inputsFocus&&["input","textarea","button","select","label"].indexOf(i.target.nodeName.toLowerCase())>-1)){if(t.textSelection){if(function(t,e,i){for(var o=t.childNodes,n=document.createRange(),s=0;s<o.length;s++){var r=o[s];if(3===r.nodeType){n.selectNodeContents(r);var h=n.getBoundingClientRect();if(e>=h.left&&i>=h.top&&e<=h.right&&i<=h.bottom)return r}}return!1}(i.target,c,p))return;(u=window.getSelection?window.getSelection():document.selection)&&(u.removeAllRanges?u.removeAllRanges():u.empty&&u.empty())}var u;t.isDragging=!0,e.x=r,e.y=h,t.dragStartPosition.x=t.position.x,t.dragStartPosition.y=t.position.y,n(i),t.run(),i.preventDefault()}},this.events.pointermove=function(t){n(t)},this.events.pointerup=function(){t.isDragging=!1},this.events.wheel=function(e){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.run(),clearTimeout(i),i=setTimeout((function(){return t.isScrolling=!1}),80),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){return t.props.onClick(t.getState(),e)},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),this.props.viewport.addEventListener("click",this.events.click),this.props.viewport.addEventListener("wheel",this.events.wheel),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),window.addEventListener("mouseup",this.events.pointerup),window.addEventListener("touchend",this.events.pointerup),window.addEventListener("resize",this.events.resize)}},{key:"updateOptions",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.props=n({},this.props,{},t),this.props.onUpdate(this.getState()),this.run()}},{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)}}])&&r(e.prototype,i),o&&r(e,o),t}()}]).default})); | ||
//# sourceMappingURL=scrollbooster.min.js.map |
{ | ||
"name": "scrollbooster", | ||
"description": "Enjoyable content drag-to-scroll library", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"author": "Ilya Shubin <pixelwake@gmail.com>", | ||
"license": "MIT", | ||
"main": "dist/scrollbooster.min.js", | ||
"module": "scr/index.js", | ||
"module": "src/index.js", | ||
"scripts": { | ||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", | ||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules" | ||
"dev": "webpack --mode development --watch", | ||
"build": "webpack --mode production", | ||
"start": "webpack-dev-server --host 0.0.0.0" | ||
}, | ||
"browserslist": [ | ||
"> 1%", | ||
"last 2 versions", | ||
"not ie <= 8" | ||
], | ||
"browserslist": ["> 0.25%", "not dead"], | ||
"repository": { | ||
@@ -32,11 +29,11 @@ "type": "git", | ||
"devDependencies": { | ||
"babel-core": "^6.26.0", | ||
"babel-loader": "^7.1.2", | ||
"babel-plugin-add-module-exports": "^0.2.1", | ||
"babel-preset-env": "^1.6.0", | ||
"babel-preset-stage-3": "^6.24.1", | ||
"cross-env": "^5.0.5", | ||
"webpack": "^3.12.0", | ||
"webpack-dev-server": "^2.9.1" | ||
"@babel/core": "^7.7.7", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.7.7", | ||
"@babel/preset-env": "^7.7.7", | ||
"babel-loader": "^8.0.6", | ||
"babel-plugin-add-module-exports": "^1.0.2", | ||
"webpack": "^4.41.5", | ||
"webpack-cli": "^3.3.10", | ||
"webpack-dev-server": "^3.10.1" | ||
} | ||
} |
118
README.md
# ScrollBooster | ||
Enjoyable content drag-to-scroll micro (~2KB gzipped) library. | ||
Enjoyable drag-to-scroll micro library (~2KB gzipped). Supports smooth content scroll via mouse/touch dragging, trackpad or mouse wheel. | ||
### Installation | ||
You can install it via `npm` or `yarn` package manager or just drop a `script` tag: | ||
You can install it via `npm` or `yarn` package manager or via `script` tag: | ||
@@ -18,3 +18,3 @@ ``` bash | ||
``` html | ||
<script src="https://unpkg.com/scrollbooster@1.1.0/dist/scrollbooster.min.js"></script> | ||
<script src="https://unpkg.com/scrollbooster@2.0.0/dist/scrollbooster.min.js"></script> | ||
``` | ||
@@ -24,11 +24,25 @@ | ||
The most simple setup with default settings: | ||
``` js | ||
import ScrollBooster from 'scrollbooster' | ||
import ScrollBooster from 'scrollbooster'; | ||
let sb = new ScrollBooster({ | ||
viewport: document.querySelector('.viewport') // required | ||
// ...other options | ||
}) | ||
const viewport = document.querySelector('.viewport'); | ||
const content = document.querySelector('.scrollable-content'); | ||
new ScrollBooster({ | ||
viewport, | ||
content, | ||
onUpdate: (data) => { | ||
content.style.transform = `translate( | ||
${-data.position.x}px, | ||
${-data.position.y}px | ||
)`; | ||
}, | ||
// other options (see below) | ||
}); | ||
``` | ||
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. | ||
### Options | ||
@@ -38,15 +52,17 @@ | ||
------ | ---- | ------- | ----------- | ||
viewport | element | null | Viewport - outer element | ||
content | element | First child of viewport element | Scrollable content inside viewport | ||
handle | element | Viewport element | Element that respond to drag event | ||
bounce | boolean | true | Inertia bounce effect (scroll past viewport borders) | ||
textSelection | boolean | false | Ability to select text content | ||
friction | float | 0.05 | Scroll friction factor (scroll inertia after pointer release) | ||
bounceForce | float | 0.1 | Bounce effect factor | ||
emulateScroll | boolean | false | Emulate viewport mouse wheel events (for cases when scrolling with `transform` property) | ||
onUpdate | function | noop | User function that updates element properties according to received coordinates (see demo examples). Receives object with properties: `position`, `viewport` and `content`. Each property contains metrics to perform an actual scrolling | ||
onClick | function | noop | Function that receives object with scrolling metrics and event object. Calls after each `click` in scrollable area. Here you can, for example, prevent default event for click on links | ||
shouldScroll | function | noop | Function that receives object with scrolling metrics and event object. Calls on `pointerdown` (mousedown, touchstart) in scrollable area. Here you can return `true` or `false` to start actual scrolling or not | ||
viewport | DOM Node | null | Content viewport element (required) | ||
content | DOM Node | viewport child element | Scrollable content element inside viewport | ||
direction | String | 'all' | Scroll direction. Could be 'horizontal', 'vertical' or 'all' | ||
bounce | Boolean | true | Enables elastic bounce effect when hitting viewport borders | ||
textSelection | Boolean | false | Enables text selection inside viewport | ||
inputsFocus | Boolean | true | Enables focus for elements: 'input', 'textarea', 'button', 'select' and 'label' | ||
pointerMode | String | 'all' | Specify pointer type. Supported values - 'touch' (scroll only on touch devices), 'mouse' (scroll only on desktop), 'all' (mobile and desktop) | ||
friction | Number | 0.05 | Scroll friction factor - how fast scrolling stops after pointer release | ||
bounceForce | Number | 0.1 | Elastic bounce effect factor | ||
emulateScroll | Boolean | false | Enables mouse wheel/trackpad emulation inside viewport | ||
onUpdate | Function | noop | Handler function to perform actual scrolling. Receives scrolling state object with coordinates | ||
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 | ||
shouldScroll | Function | noop | Handler function to permit or disable scrolling. Function that receives object with scrolling metrics and event object. Calls on `pointerdown` (mousedown, touchstart) in scrollable area. You can return `true` or `false` to enable or disable scrolling | ||
### Methods to perform custom logic | ||
### List of methods | ||
@@ -56,5 +72,7 @@ Method | Description | ||
setPosition | Sets new scroll position in viewport. Receives an object with properties `x` and `y` | ||
updateMetrics | Updates element sizes. Useful for images loading or other dynamic content | ||
getUpdate | Returns current metrics and coordinates in a same format as `onUpdate` | ||
destroy | Destroys all instance's event listeners | ||
scrollTo | Smooth scroll to position in viewport. Receives an object with properties `x` and `y` | ||
updateMetrics | Forces to recalculate elments metrics. Useful for cases when content in scrollable area change its size dynamically | ||
updateOptions | Updates ScrollBooster options. All properties from `Options` config object are supported | ||
getState | Returns current scroll state in a same format as `onUpdate` | ||
destroy | Removes all instance's event listeners | ||
@@ -64,40 +82,36 @@ ### Full Example | ||
``` js | ||
let viewport = document.querySelector('.viewport') | ||
let content = document.querySelector('.viewport-content') | ||
const viewport = document.querySelector('.viewport'); | ||
const content = document.querySelector('.scrollable-content'); | ||
let sb = new ScrollBooster({ | ||
viewport: viewport, | ||
content: content, | ||
handle: document.querySelector('.viewport-scroller'), | ||
const sb = new ScrollBooster({ | ||
viewport, | ||
content, | ||
bounce: true, | ||
textSelection: false, | ||
emulateScroll: false, | ||
onUpdate: (data)=> { | ||
emulateScroll: true, | ||
onUpdate: (state) => { | ||
// state contains useful metrics: position, dragOffset, isDragging, isMoving | ||
content.style.transform = `translate( | ||
${-data.position.x}px, | ||
${-data.position.y}px | ||
)` | ||
// and also metrics: data.viewport['width'|'height'] and data.cotent['width'|'height'] | ||
${-state.position.x}px, | ||
${-state.position.y}px | ||
)`; | ||
}, | ||
shouldScroll: (data, event) => { | ||
if (event.target.classList.contains('button')) { | ||
return false | ||
} else { | ||
return true | ||
} | ||
shouldScroll: (state, event) => { | ||
// disable scroll if clicked on button | ||
const isButton = event.taget.nodeName.toLowerCase() === 'button'; | ||
return !isButton; | ||
}, | ||
onClick: (data, event) => { | ||
if (event.target.classList.contains('link')) { | ||
event.preventDefault() | ||
// prevent default link event | ||
const isLink = event.taget.nodeName.toLowerCase() === 'link'; | ||
if (isLink) { | ||
event.preventDefault(); | ||
} | ||
} | ||
}) | ||
}); | ||
// methods example: | ||
sb.updateMetrics() | ||
sb.setPosition({ | ||
x: 100, | ||
y: 100 | ||
}) | ||
sb.destroy() | ||
// methods usage examples: | ||
sb.updateMetrics(); | ||
sb.setPosition({ x: 100, y: 100 }); | ||
sb.destroy(); | ||
``` | ||
@@ -115,2 +129,2 @@ | ||
MIT License (c) Ilya Shubin | ||
MIT License (c) Ilya Shubin |
617
src/index.js
@@ -1,12 +0,35 @@ | ||
export default class ScrollBooster { | ||
constructor(props = {}) { | ||
const getFullWidth = (elem) => Math.max(elem.offsetWidth, elem.scrollWidth); | ||
const getFullHeight = (elem) => Math.max(elem.offsetHeight, elem.scrollHeight); | ||
if (!props.viewport || !(props.viewport instanceof Element)) { | ||
console.error('"viewport" config property must be present and must be Element') | ||
return | ||
const textNodeFromPoint = (element, x, y) => { | ||
const nodes = element.childNodes; | ||
const range = document.createRange(); | ||
for (let i = 0; i < nodes.length; i++) { | ||
const node = nodes[i]; | ||
if (node.nodeType !== 3) { continue; } | ||
range.selectNodeContents(node); | ||
const rect = range.getBoundingClientRect(); | ||
if (x >= rect.left && y >= rect.top && x <= rect.right && y <= rect.bottom) { | ||
return node; | ||
} | ||
} | ||
return false; | ||
}; | ||
let defaults = { | ||
handle: props.viewport, | ||
content: props.viewport.children[0], | ||
const clearTextSelection = () => { | ||
const selection = window.getSelection ? window.getSelection() : document.selection; | ||
if (!selection) { return; } | ||
if (selection.removeAllRanges) { | ||
selection.removeAllRanges(); | ||
} else if (selection.empty) { | ||
selection.empty(); | ||
} | ||
}; | ||
export default class ScrollBooster { | ||
constructor(options = {}) { | ||
const defaults = { | ||
content: options.viewport.children[0], | ||
direction: 'all', // 'vertical', 'horizontal' | ||
pointerMode: 'all', // 'touch', 'mouse' | ||
bounce: true, | ||
@@ -16,12 +39,19 @@ friction: 0.05, | ||
textSelection: false, | ||
onClick: function () {}, | ||
shouldScroll: function () { return true }, | ||
onUpdate: function () {} | ||
inputsFocus: true, | ||
emulateScroll: false, | ||
onClick() {}, | ||
onUpdate() {}, | ||
shouldScroll() { return true } | ||
}; | ||
this.props = { ...defaults, ...options }; | ||
if (!this.props.viewport || !(this.props.viewport instanceof Element)) { | ||
console.error(`ScrollBooster init error: "viewport" config property must be present and must be Element`); | ||
return; | ||
} | ||
this.props = { ...defaults, ...props } | ||
if (!this.props.content) { | ||
console.error('Viewport does not have any content') | ||
return | ||
console.error(`ScrollBooster init error: Viewport does not have any content`); | ||
return; | ||
} | ||
@@ -32,47 +62,43 @@ | ||
height: this.props.viewport.clientHeight | ||
} | ||
}; | ||
this.content = { | ||
width: getFullWidth(this.props.content), | ||
height: getFullHeight(this.props.content) | ||
} | ||
}; | ||
this.position = { x: 0, y: 0 } | ||
this.velocity = { x: 0, y: 0 } | ||
this.friction = 1 - this.props.friction | ||
this.bounceForce = this.props.bounceForce | ||
this.position = { x: 0, y: 0 }; | ||
this.velocity = { x: 0, y: 0 }; | ||
this.friction = 1 - this.props.friction; | ||
this.bounceForce = this.props.bounceForce; | ||
this.isDragging = false | ||
this.dragStartPosition = { x: 0, y: 0 } | ||
this.dragOffsetPosition = { ...this.dragStartPosition } | ||
this.dragPosition = { ...this.position } | ||
this.isDragging = false; | ||
this.dragStartPosition = { x: 0, y: 0 }; | ||
this.dragOffset = { ...this.dragStartPosition }; | ||
this.dragPosition = { ...this.position }; | ||
this.isScrollEnabled = !!this.props.emulateScroll | ||
this.isScrolling = false | ||
this.scrollOffset = { x: 0, y: 0 } | ||
this.isTargetScroll = false; | ||
this.targetPosition = { x: 0, y: 0 }; | ||
this.bounce = this.props.bounce | ||
this.textSelection = this.props.textSelection | ||
this.isScrolling = false; | ||
this.scrollOffset = { x: 0, y: 0 }; | ||
this.bounce = this.props.bounce; | ||
this.textSelection = this.props.textSelection; | ||
this.boundX = { | ||
from: Math.min(-this.content.width + this.viewport.width, 0), | ||
to: 0 | ||
} | ||
}; | ||
this.boundY = { | ||
from: Math.min(-this.content.height + this.viewport.height, 0), | ||
to: 0 | ||
} | ||
}; | ||
this.mode = { | ||
x: this.props.mode == 'x', | ||
y: this.props.mode == 'y', | ||
xy: this.props.mode !== 'x' && this.props.mode !== 'y' | ||
} | ||
this.isRunning = false; | ||
this.rafID = null; | ||
this.isRunning = false | ||
this.rafID = null | ||
this.events = {}; | ||
this.events = {} | ||
this.animate() | ||
this.handleEvents() | ||
this.animate(); | ||
this.handleEvents(); | ||
} | ||
@@ -84,43 +110,46 @@ | ||
run() { | ||
this.isRunning = true | ||
cancelAnimationFrame(this.rafID) | ||
this.rafID = requestAnimationFrame(() => this.animate()) | ||
this.isRunning = true; | ||
cancelAnimationFrame(this.rafID); | ||
this.rafID = requestAnimationFrame(() => this.animate()); | ||
} | ||
animate() { | ||
if (!this.isRunning) { return } | ||
this.update() | ||
this.notify() | ||
this.rafID = requestAnimationFrame(() => this.animate()) | ||
if (!this.isRunning) { | ||
return; | ||
} | ||
this.update(); | ||
this.props.onUpdate(this.getState()); | ||
this.rafID = requestAnimationFrame(() => this.animate()); | ||
} | ||
isMoving() { | ||
return this.isDragging || this.isScrolling || Math.abs(this.velocity.x) >= 0.1 || Math.abs(this.velocity.y) >= 0.1; | ||
} | ||
update() { | ||
this.applyBoundForce() | ||
this.applyDragForce() | ||
this.applyScrollForce() | ||
this.applyBoundForce(); | ||
this.applyDragForce(); | ||
this.applyScrollForce(); | ||
this.applyTargetForce(); | ||
this.velocity.x *= this.friction | ||
this.velocity.y *= this.friction | ||
this.velocity.x *= this.friction; | ||
this.velocity.y *= this.friction; | ||
if (!this.mode.y) { | ||
this.position.x += this.velocity.x | ||
if (this.props.direction !== 'vertical') { | ||
this.position.x += this.velocity.x; | ||
} | ||
if (!this.mode.x) { | ||
this.position.y += this.velocity.y | ||
if (this.props.direction !== 'horizontal') { | ||
this.position.y += this.velocity.y; | ||
} | ||
// if bounce effect is disabled | ||
if (!this.bounce || this.isScrolling) { | ||
this.position.x = Math.max(Math.min(this.position.x, this.boundX.to), this.boundX.from) | ||
this.position.y = Math.max(Math.min(this.position.y, this.boundY.to), this.boundY.from) | ||
// disable bounce effect | ||
if ((!this.bounce || this.isScrolling) && !this.isTargetScroll) { | ||
this.position.x = Math.max(Math.min(this.position.x, this.boundX.to), this.boundX.from); | ||
this.position.y = Math.max(Math.min(this.position.y, this.boundY.to), this.boundY.from); | ||
} | ||
// stop update loop if nothing moves | ||
if ( | ||
!this.isDragging && | ||
!this.isScrolling && | ||
Math.abs(this.velocity.x) < 0.1 && | ||
Math.abs(this.velocity.y) < 0.1 | ||
) { | ||
this.isRunning = false | ||
if (!this.isMoving()) { | ||
this.isRunning = false; | ||
this.isTargetScroll = false; | ||
} | ||
@@ -130,4 +159,4 @@ } | ||
applyForce(force) { | ||
this.velocity.x += force.x | ||
this.velocity.y += force.y | ||
this.velocity.x += force.x; | ||
this.velocity.y += force.y; | ||
} | ||
@@ -139,47 +168,50 @@ | ||
applyBoundForce() { | ||
if (!this.bounce) { return } | ||
if (this.isDragging) { return } | ||
if (!this.bounce || this.isDragging) { | ||
return; | ||
} | ||
let pastLeft = this.position.x < this.boundX.from | ||
let pastRight = this.position.x > this.boundX.to | ||
let pastTop = this.position.y < this.boundY.from | ||
let pastBottom = this.position.y > this.boundY.to | ||
// scrolled past viewport boundaries | ||
const pastLeft = this.position.x < this.boundX.from; | ||
const pastRight = this.position.x > this.boundX.to; | ||
const pastTop = this.position.y < this.boundY.from; | ||
const pastBottom = this.position.y > this.boundY.to; | ||
const pastX = pastLeft || pastRight; | ||
const pastY = pastTop || pastBottom; | ||
let resultForce = { x: 0, y: 0 } | ||
if (!pastX && !pastY) { | ||
return; | ||
} | ||
// scrolled past left of right viewport boundaries | ||
if (pastLeft || pastRight) { | ||
let bound = pastLeft ? this.boundX.from : this.boundX.to | ||
let distance = bound - this.position.x | ||
const bound = { | ||
x: pastLeft ? this.boundX.from : this.boundX.to, | ||
y: pastTop ? this.boundY.from : this.boundY.to, | ||
}; | ||
let force = distance * this.bounceForce | ||
var restX = this.position.x + (this.velocity.x + force) / (1 - this.friction) | ||
const distanceToBound = { | ||
x: bound.x - this.position.x, | ||
y: bound.y - this.position.y, | ||
}; | ||
if ( | ||
!((pastLeft && restX < this.boundX.from) || (pastRight && restX > this.boundX.to)) | ||
) { | ||
force = distance * this.bounceForce - this.velocity.x | ||
} | ||
const force = { | ||
x: distanceToBound.x * this.bounceForce, | ||
y: distanceToBound.y * this.bounceForce, | ||
}; | ||
resultForce.x = force | ||
const restPosition = { | ||
x: this.position.x + (this.velocity.x + force.x) / (1 - this.friction), | ||
y: this.position.y + (this.velocity.y + force.y) / (1 - this.friction), | ||
}; | ||
if ((pastLeft && restPosition.x >= this.boundX.from) || (pastRight && restPosition.x <= this.boundX.to)) { | ||
force.x = distanceToBound.x * this.bounceForce - this.velocity.x; | ||
} | ||
// scrolled past top of bottom viewport boundaries | ||
if (pastTop || pastBottom) { | ||
let bound = pastTop ? this.boundY.from : this.boundY.to | ||
let distance = bound - this.position.y | ||
let force = distance * this.bounceForce | ||
var restY = this.position.y + (this.velocity.y + force) / (1 - this.friction) | ||
if ( | ||
!((pastTop && restY < this.boundY.from) || (pastBottom && restY > this.boundY.to)) | ||
) { | ||
force = distance * this.bounceForce - this.velocity.y | ||
} | ||
resultForce.y = force | ||
if ((pastTop && restPosition.y >= this.boundY.from) || (pastBottom && restPosition.y <= this.boundY.to)) { | ||
force.y = distanceToBound.y * this.bounceForce - this.velocity.y; | ||
} | ||
this.applyForce(resultForce) | ||
this.applyForce({ | ||
x: pastX ? force.x : 0, | ||
y: pastY ? force.y : 0, | ||
}); | ||
} | ||
@@ -191,43 +223,70 @@ | ||
applyDragForce() { | ||
if (!this.isDragging) { return } | ||
let dragVelocity = { | ||
if (!this.isDragging) { | ||
return; | ||
} | ||
const dragVelocity = { | ||
x: this.dragPosition.x - this.position.x, | ||
y: this.dragPosition.y - this.position.y | ||
} | ||
let dragForce = { | ||
}; | ||
const force = { | ||
x: dragVelocity.x - this.velocity.x, | ||
y: dragVelocity.y - this.velocity.y | ||
} | ||
}; | ||
this.applyForce(dragForce) | ||
this.applyForce(force); | ||
} | ||
/** | ||
* Apply force to emulate mouse wheel | ||
* Apply force to emulate mouse wheel or trackpad | ||
*/ | ||
applyScrollForce() { | ||
if (!this.isScrolling) { return } | ||
if (!this.isScrolling) { | ||
return; | ||
} | ||
let scrollForce = { | ||
const force = { | ||
x: this.scrollOffset.x - this.velocity.x, | ||
y: this.scrollOffset.y - this.velocity.y | ||
}; | ||
this.scrollOffset.x = 0; | ||
this.scrollOffset.y = 0; | ||
this.applyForce(force); | ||
} | ||
applyTargetForce() { | ||
if (!this.isTargetScroll) { | ||
return | ||
} | ||
this.scrollOffset.x = 0 | ||
this.scrollOffset.y = 0 | ||
const force = { | ||
x: (this.targetPosition.x - this.position.x) * 0.08 - this.velocity.x, | ||
y: (this.targetPosition.y - this.position.y) * 0.08 - this.velocity.y, | ||
}; | ||
this.applyForce(scrollForce) | ||
this.applyForce(force); | ||
} | ||
/** | ||
* Smooth scroll to target position | ||
*/ | ||
scrollTo(position = {}) { | ||
this.isTargetScroll = true; | ||
this.targetPosition.x = -position.x || 0; | ||
this.targetPosition.y = -position.y || 0; | ||
this.run(); | ||
} | ||
/** | ||
* Manual position setting | ||
*/ | ||
setPosition(newPosition = {}) { | ||
this.velocity.x = 0 | ||
this.velocity.y = 0 | ||
this.position.x = -newPosition.x || 0 | ||
this.position.y = -newPosition.y || 0 | ||
this.run() | ||
setPosition(position = {}) { | ||
this.velocity.x = 0; | ||
this.velocity.y = 0; | ||
this.position.x = -position.x || 0; | ||
this.position.y = -position.y || 0; | ||
this.run(); | ||
} | ||
@@ -238,235 +297,179 @@ | ||
*/ | ||
getUpdate() { | ||
getState() { | ||
return { | ||
isRunning: this.isRunning, | ||
isDragging: this.isDragging, | ||
isScrolling: this.isScrolling, | ||
position: { | ||
x: -this.position.x, | ||
y: -this.position.y | ||
}, | ||
dragOffsetPosition: this.dragOffsetPosition, | ||
viewport: { ...this.viewport }, | ||
content: { ...this.content } | ||
} | ||
isMoving: this.isMoving(), | ||
isDragging: !!(this.dragOffset.x || this.dragOffset.y), | ||
position: { x: -this.position.x, y: -this.position.y }, | ||
dragOffset: this.dragOffset, | ||
}; | ||
} | ||
notify() { | ||
this.props.onUpdate(this.getUpdate()) | ||
} | ||
updateMetrics() { | ||
this.viewport.width = this.props.viewport.clientWidth | ||
this.viewport.height = this.props.viewport.clientHeight | ||
this.viewport.width = this.props.viewport.clientWidth; | ||
this.viewport.height = this.props.viewport.clientHeight; | ||
this.content.width = getFullWidth(this.props.content) | ||
this.content.height = getFullHeight(this.props.content) | ||
this.content.width = getFullWidth(this.props.content); | ||
this.content.height = getFullHeight(this.props.content); | ||
this.boundX.from = Math.min(-this.content.width + this.viewport.width, 0) | ||
this.boundY.from = Math.min(-this.content.height + this.viewport.height, 0) | ||
this.boundX.from = Math.min(-this.content.width + this.viewport.width, 0); | ||
this.boundY.from = Math.min(-this.content.height + this.viewport.height, 0); | ||
this.run() | ||
this.run(); | ||
} | ||
handleEvents() { | ||
let vp = this.props.viewport | ||
let scroll = { x: 0, y: 0 } | ||
let mousedown = { x: 0, y: 0 } | ||
const dragOrigin = { x: 0, y: 0 }; | ||
let wheelTimer = null; | ||
let isTouch = false; | ||
let isTouch = false | ||
let setDragPosition = (event) => { | ||
let pageX, pageY | ||
if (isTouch) { | ||
pageX = event.touches[0].pageX | ||
pageY = event.touches[0].pageY | ||
} else { | ||
pageX = event.pageX | ||
pageY = event.pageY | ||
const setDragPosition = (event) => { | ||
if (!this.isDragging) { | ||
return | ||
} | ||
this.dragOffsetPosition.x = pageX - mousedown.x | ||
this.dragOffsetPosition.y = pageY - mousedown.y | ||
const pageX = isTouch ? event.touches[0].pageX : event.pageX; | ||
const pageY = isTouch ? event.touches[0].pageY: event.pageY; | ||
this.dragPosition.x = this.dragStartPosition.x + this.dragOffsetPosition.x | ||
this.dragPosition.y = this.dragStartPosition.y + this.dragOffsetPosition.y | ||
this.dragOffset.x = pageX - dragOrigin.x; | ||
this.dragOffset.y = pageY - dragOrigin.y; | ||
if (!isTouch) { | ||
event.preventDefault() | ||
} | ||
} | ||
this.dragPosition.x = this.dragStartPosition.x + this.dragOffset.x; | ||
this.dragPosition.y = this.dragStartPosition.y + this.dragOffset.y; | ||
}; | ||
this.events.pointerdown = (event) => { | ||
let pageX, pageY, clientX, clientY | ||
isTouch = !!(event.touches && event.touches[0]); | ||
isTouch = !!(event.touches && event.touches[0]) | ||
const eventData = isTouch ? event.touches[0] : event; | ||
const { pageX, pageY, clientX, clientY } = eventData | ||
if (isTouch) { | ||
pageX = event.touches[0].pageX | ||
pageY = event.touches[0].pageY | ||
clientX = event.touches[0].clientX | ||
clientY = event.touches[0].clientY | ||
} else { | ||
pageX = event.pageX | ||
pageY = event.pageY | ||
clientX = event.clientX | ||
clientY = event.clientY | ||
} | ||
const { viewport } = this.props; | ||
const rect = viewport.getBoundingClientRect(); | ||
let rect = vp.getBoundingClientRect() | ||
// click on vertical scrollbar | ||
if (clientX - rect.left >= vp.clientLeft + vp.clientWidth) { | ||
return | ||
if (clientX - rect.left >= viewport.clientLeft + viewport.clientWidth) { | ||
return; | ||
} | ||
// click on horizontal scrollbar | ||
if (clientY - rect.top >= vp.clientTop + vp.clientHeight) { | ||
return | ||
if (clientY - rect.top >= viewport.clientTop + viewport.clientHeight) { | ||
return; | ||
} | ||
if (!this.props.shouldScroll(this.getUpdate(), event)) { | ||
return | ||
// interaction disabled by user | ||
if (!this.props.shouldScroll(this.getState(), event)) { | ||
return; | ||
} | ||
// text selection enabled | ||
if (this.textSelection) { | ||
let clickedNode = textNodeFromPoint(event.target, clientX, clientY) | ||
if (clickedNode) { | ||
return | ||
} else { | ||
clearTextSelection() | ||
} | ||
// disable on mobile | ||
if (this.props.pointerMode === 'mouse' && isTouch) { | ||
return; | ||
} | ||
this.isDragging = true | ||
// disable on desktop | ||
if (this.props.pointerMode === 'touch' && !isTouch) { | ||
return; | ||
} | ||
if (scroll.x || scroll.y) { | ||
this.position.x = scroll.x | ||
this.position.y = scroll.y | ||
scroll.x = 0 | ||
scroll.y = 0 | ||
// focus on form input elements | ||
const formNodes = ['input', 'textarea', 'button', 'select', 'label']; | ||
if (this.props.inputsFocus && formNodes.indexOf(event.target.nodeName.toLowerCase()) > -1) { | ||
return; | ||
} | ||
mousedown.x = pageX | ||
mousedown.y = pageY | ||
this.dragStartPosition.x = this.position.x | ||
this.dragStartPosition.y = this.position.y | ||
setDragPosition(event) | ||
// handle text selection | ||
if (this.textSelection) { | ||
const textNode = textNodeFromPoint(event.target, clientX, clientY); | ||
if (textNode) { | ||
return; | ||
} | ||
clearTextSelection(); | ||
} | ||
this.run() | ||
this.isDragging = true; | ||
let pointerUp, removeEvents | ||
dragOrigin.x = pageX; | ||
dragOrigin.y = pageY; | ||
this.dragStartPosition.x = this.position.x; | ||
this.dragStartPosition.y = this.position.y; | ||
removeEvents = (event) => { | ||
this.isDragging = false | ||
setDragPosition(event); | ||
this.run(); | ||
event.preventDefault() | ||
}; | ||
if (isTouch) { | ||
window.removeEventListener('touchmove', setDragPosition) | ||
window.removeEventListener('touchend', pointerUp) | ||
} else { | ||
window.removeEventListener('mousemove', setDragPosition) | ||
window.removeEventListener('mouseup', pointerUp) | ||
} | ||
} | ||
this.events.pointermove = (event) => { | ||
setDragPosition(event); | ||
}; | ||
if (isTouch) { | ||
pointerUp = window.addEventListener('touchend', removeEvents) | ||
window.addEventListener('touchmove', setDragPosition) | ||
} else { | ||
pointerUp = window.addEventListener('mouseup', removeEvents) | ||
window.addEventListener('mousemove', setDragPosition) | ||
} | ||
} | ||
this.events.pointerup = () => { | ||
this.isDragging = false; | ||
}; | ||
let scrollTimer = null | ||
this.events.wheel = (event) => { | ||
this.velocity.x = 0 | ||
if (!this.props.emulateScroll) { | ||
return; | ||
} | ||
this.velocity.x = 0; | ||
this.velocity.y = 0; | ||
this.isScrolling = true; | ||
if (!this.isScrollEnabled) { return } | ||
this.isScrolling = true | ||
this.scrollOffset.x = -event.deltaX; | ||
this.scrollOffset.y = -event.deltaY; | ||
this.scrollOffset.x = -event.deltaX | ||
this.scrollOffset.y = -event.deltaY | ||
this.run(); | ||
this.run() | ||
clearTimeout(wheelTimer); | ||
wheelTimer = setTimeout(() => (this.isScrolling = false), 80); | ||
event.preventDefault(); | ||
}; | ||
clearTimeout(scrollTimer) | ||
scrollTimer = setTimeout(() => this.isScrolling = false, 80) | ||
event.preventDefault() | ||
} | ||
this.events.scroll = (event) => { | ||
let sl = this.props.viewport.scrollLeft | ||
let st = this.props.viewport.scrollTop | ||
if (Math.abs(this.position.x + sl) > 3) { | ||
this.position.x = -sl | ||
this.velocity.x = 0 | ||
this.events.scroll = () => { | ||
const { scrollLeft, scrollTop } = this.props.viewport; | ||
if (Math.abs(this.position.x + scrollLeft) > 3) { | ||
this.position.x = -scrollLeft; | ||
this.velocity.x = 0; | ||
} | ||
if (Math.abs(this.position.y + st) > 3) { | ||
this.position.y = -st | ||
this.velocity.y = 0 | ||
if (Math.abs(this.position.y + scrollTop) > 3) { | ||
this.position.y = -scrollTop; | ||
this.velocity.y = 0; | ||
} | ||
scroll.x = -this.props.viewport.scrollLeft | ||
scroll.y = -this.props.viewport.scrollTop | ||
} | ||
}; | ||
this.events.click = (event) => { | ||
this.props.onClick(this.getUpdate(), event) | ||
} | ||
this.events.click = (event) => this.props.onClick(this.getState(), event); | ||
this.events.contentLoad = () => this.updateMetrics(); | ||
this.events.resize = () => this.updateMetrics(); | ||
this.events.resize = this.updateMetrics.bind(this) | ||
this.props.viewport.addEventListener('mousedown', this.events.pointerdown); | ||
this.props.viewport.addEventListener('touchstart', this.events.pointerdown); | ||
this.props.viewport.addEventListener('click', this.events.click); | ||
this.props.viewport.addEventListener('wheel', this.events.wheel); | ||
this.props.viewport.addEventListener('scroll', this.events.scroll); | ||
this.props.content.addEventListener('load', this.events.contentLoad, true); | ||
window.addEventListener('mousemove', this.events.pointermove); | ||
window.addEventListener('touchmove', this.events.pointermove); | ||
window.addEventListener('mouseup', this.events.pointerup); | ||
window.addEventListener('touchend', this.events.pointerup); | ||
window.addEventListener('resize', this.events.resize); | ||
} | ||
this.props.handle.addEventListener('mousedown', this.events.pointerdown) | ||
this.props.handle.addEventListener('touchstart', this.events.pointerdown) | ||
this.props.handle.addEventListener('click', this.events.click) | ||
this.props.viewport.addEventListener('wheel', this.events.wheel) | ||
this.props.viewport.addEventListener('scroll', this.events.scroll) | ||
window.addEventListener('resize', this.events.resize) | ||
updateOptions(options = {}) { | ||
this.props = { ...this.props, ...options }; | ||
this.props.onUpdate(this.getState()); | ||
this.run(); | ||
} | ||
destroy() { | ||
this.props.handle.removeEventListener('mousedown', this.events.pointerdown) | ||
this.props.handle.removeEventListener('touchstart', this.events.pointerdown) | ||
this.props.handle.removeEventListener('click', this.events.click) | ||
this.props.viewport.removeEventListener('wheel', this.events.wheel) | ||
this.props.viewport.removeEventListener('scroll', this.events.scroll) | ||
window.removeEventListener('resize', this.events.resize) | ||
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); | ||
} | ||
} | ||
function getFullWidth (elem) { | ||
return Math.max(elem.offsetWidth, elem.scrollWidth) | ||
} | ||
function getFullHeight (elem) { | ||
return Math.max(elem.offsetHeight, elem.scrollHeight) | ||
} | ||
function textNodeFromPoint (element, x, y) { | ||
let node | ||
let nodes = element.childNodes | ||
let range = document.createRange() | ||
for (let i = 0; node = nodes[i], i < nodes.length; i++) { | ||
if (node.nodeType !== 3) continue | ||
range.selectNodeContents(node) | ||
let rect = range.getBoundingClientRect() | ||
if (x >= rect.left && y >= rect.top && x <= rect.right && y <= rect.bottom) { | ||
return node | ||
} | ||
} | ||
return false | ||
} | ||
function clearTextSelection () { | ||
let sel = window.getSelection ? window.getSelection() : document.selection | ||
if (sel) { | ||
if (sel.removeAllRanges) { | ||
sel.removeAllRanges() | ||
} else if (sel.empty) { | ||
sel.empty() | ||
} | ||
} | ||
} |
@@ -1,38 +0,32 @@ | ||
(function() { | ||
var scrollEl = document.querySelector('#init .inner') | ||
var scr | ||
var scrollEl = document.querySelector("#init .inner"); | ||
var scr; | ||
beforeEach(function (done) { | ||
beforeEach(function(done) { | ||
scr = new ScrollBooster({ | ||
viewport: document.querySelector('#init .wrapper'), | ||
viewport: document.querySelector("#init .wrapper"), | ||
content: scrollEl, | ||
onUpdate: function (data) { | ||
scrollEl.style.transform = 'translate(' + -data.position.x + 'px, ' + -data.position.y + 'px)' | ||
onUpdate: function(data) { | ||
scrollEl.style.transform = | ||
"translate(" + -data.position.x + "px, " + -data.position.y + "px)"; | ||
} | ||
}) | ||
setTimeout(done, 300) | ||
}) | ||
}); | ||
setTimeout(done, 300); | ||
}); | ||
describe('Init', function () { | ||
describe("Init", function() { | ||
it("Init properties", function() { | ||
chai.expect(scr.position.x).to.equal(0); | ||
chai.expect(scr.position.y).to.equal(0); | ||
it('Init properties', function () { | ||
chai.expect(scr.position.x).to.equal(0) | ||
chai.expect(scr.position.y).to.equal(0) | ||
chai.expect(scr.props.viewport).to.be.an.instanceof(Element); | ||
chai.expect(scr.props.content).to.be.an.instanceof(Element); | ||
chai.expect(scr.props.viewport).to.be.an.instanceof(Element) | ||
chai.expect(scr.props.content).to.be.an.instanceof(Element) | ||
chai.expect(scr.viewport.width).to.equal(300); | ||
chai.expect(scr.viewport.height).to.equal(300); | ||
chai.expect(scr.viewport.width).to.equal(300) | ||
chai.expect(scr.viewport.height).to.equal(300) | ||
chai.expect(scr.content.width).to.gt(300) | ||
chai.expect(scr.content.height).to.gt(300) | ||
chai.expect(scr.mode.xy).to.equal(true) | ||
chai.expect(scr.mode.x).to.equal(false) | ||
chai.expect(scr.mode.y).to.equal(false) | ||
}) | ||
}) | ||
})() | ||
chai.expect(scr.content.width).to.gt(300); | ||
chai.expect(scr.content.height).to.gt(300); | ||
}); | ||
}); | ||
})(); |
@@ -1,12 +0,12 @@ | ||
(function () { | ||
var scrollEl = document.querySelector('#nobounce .inner') | ||
(function() { | ||
var scrollEl = document.querySelector("#nobounce .inner"); | ||
var scr = new ScrollBooster({ | ||
viewport: document.querySelector('#nobounce .wrapper'), | ||
viewport: document.querySelector("#nobounce .wrapper"), | ||
emulateScroll: true, | ||
bounce: false, | ||
onUpdate: function (data) { | ||
scrollEl.style.transform = 'translate(' + -data.position.x + 'px, ' + -data.position.y + 'px)' | ||
onUpdate: function(data) { | ||
scrollEl.style.transform = | ||
"translate(" + -data.position.x + "px, " + -data.position.y + "px)"; | ||
} | ||
}) | ||
})() | ||
}); | ||
})(); |
@@ -1,28 +0,27 @@ | ||
(function () { | ||
var viewportEl = document.querySelector('#scroll .wrapper') | ||
var scr | ||
(function() { | ||
var viewportEl = document.querySelector("#scroll .wrapper"); | ||
var scr; | ||
beforeEach(function (done) { | ||
beforeEach(function(done) { | ||
scr = new ScrollBooster({ | ||
viewport: viewportEl, | ||
emulateScroll: true, | ||
onUpdate: function (data) { | ||
viewportEl.scrollTop = data.position.y | ||
viewportEl.scrollLeft = data.position.x | ||
onUpdate: function(data) { | ||
viewportEl.scrollTop = data.position.y; | ||
viewportEl.scrollLeft = data.position.x; | ||
} | ||
}) | ||
scr.setPosition({ x: 100, y: 100 }) | ||
setTimeout(done, 300) | ||
}) | ||
}); | ||
scr.setPosition({ x: 100, y: 100 }); | ||
setTimeout(done, 300); | ||
}); | ||
describe('Scroll', function () { | ||
it('Scroll test', function () { | ||
chai.expect(scr.position.x).to.equal(-100) | ||
chai.expect(scr.position.y).to.equal(-100) | ||
describe("Scroll", function() { | ||
it("Scroll test", function() { | ||
chai.expect(scr.position.x).to.equal(-100); | ||
chai.expect(scr.position.y).to.equal(-100); | ||
let st = viewportEl.scrollTop | ||
chai.expect(st).to.equal(100) | ||
}) | ||
}) | ||
})() | ||
let st = viewportEl.scrollTop; | ||
chai.expect(st).to.equal(100); | ||
}); | ||
}); | ||
})(); |
@@ -1,16 +0,15 @@ | ||
(function () { | ||
var scrollEl = document.querySelector('#xonly .inner') | ||
(function() { | ||
var scrollEl = document.querySelector("#xonly .inner"); | ||
var scr = new ScrollBooster({ | ||
viewport: document.querySelector('#xonly .wrapper'), | ||
viewport: document.querySelector("#xonly .wrapper"), | ||
emulateScroll: true, | ||
mode: 'x', | ||
onUpdate: function (data) { | ||
scrollEl.style.transform = 'translate(' + -data.position.x + 'px, ' + -data.position.y + 'px)' | ||
direction: "horizontal", | ||
onUpdate: function(data) { | ||
scrollEl.style.transform = | ||
"translate(" + -data.position.x + "px, " + -data.position.y + "px)"; | ||
} | ||
}) | ||
}); | ||
scr.setPosition({ | ||
x: 100 | ||
}) | ||
})() | ||
}); | ||
})(); |
@@ -1,16 +0,15 @@ | ||
(function () { | ||
var scrollEl = document.querySelector('#yonly .inner') | ||
(function() { | ||
var scrollEl = document.querySelector("#yonly .inner"); | ||
var scr = new ScrollBooster({ | ||
viewport: document.querySelector('#yonly .wrapper'), | ||
mode: 'y', | ||
viewport: document.querySelector("#yonly .wrapper"), | ||
direction: "vertical", | ||
emulateScroll: true, | ||
onUpdate: function (data) { | ||
scrollEl.style.transform = 'translate(' + -data.position.x + 'px, ' + -data.position.y + 'px)' | ||
onUpdate: function(data) { | ||
scrollEl.style.transform = | ||
"translate(" + -data.position.x + "px, " + -data.position.y + "px)"; | ||
} | ||
}) | ||
}); | ||
scr.setPosition({ | ||
y: 100 | ||
}) | ||
})() | ||
}); | ||
})(); |
@@ -1,16 +0,12 @@ | ||
let path = require('path') | ||
let webpack = require('webpack') | ||
let path = require('path'); | ||
module.exports = { | ||
entry: { | ||
"scrollbooster": "./src/index.js", | ||
"scrollbooster.min": "./src/index.js", | ||
}, | ||
entry: './src/index.js', | ||
output: { | ||
path: path.resolve(__dirname, './dist'), | ||
publicPath: '/dist/', | ||
filename: '[name].js', | ||
path: path.resolve(__dirname, 'dist'), | ||
filename: 'scrollbooster.min.js', | ||
library: 'ScrollBooster', | ||
libraryTarget: 'umd', | ||
umdNamedDefine: true | ||
libraryExport: 'default', | ||
umdNamedDefine: true, | ||
}, | ||
@@ -21,4 +17,9 @@ module: { | ||
test: /\.js$/, | ||
loader: 'babel-loader', | ||
exclude: /node_modules/ | ||
exclude: /node_modules/, | ||
use: { | ||
loader: 'babel-loader', | ||
options: { | ||
presets: ['@babel/preset-env'], | ||
} | ||
} | ||
} | ||
@@ -28,18 +29,10 @@ ] | ||
devServer: { | ||
historyApiFallback: true, | ||
noInfo: true, | ||
overlay: true, | ||
clientLogLevel: "none" | ||
contentBase: path.join(__dirname), | ||
clientLogLevel: 'none', | ||
open: true, | ||
}, | ||
devtool: '#source-map', | ||
plugins: [ | ||
new webpack.optimize.UglifyJsPlugin({ | ||
include: /\.min\.js$/, | ||
minimize: true | ||
}), | ||
new webpack.LoaderOptionsPlugin({ | ||
include: /\.min\.js$/, | ||
minimize: true | ||
}) | ||
] | ||
} | ||
devtool: 'source-map', | ||
optimization: { | ||
minimize: true, | ||
}, | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
125
88486
591
1