├── .gitignore ├── vue-scroll-stop-off.gif ├── vue-scroll-stop-on.gif ├── .babelrc ├── src ├── plugin.js └── vue-scroll-stop.js ├── package.json ├── dist ├── vue-scroll-stop.min.js └── vue-scroll-stop.js ├── webpack.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /vue-scroll-stop-off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxtobox/vue-scroll-stop/HEAD/vue-scroll-stop-off.gif -------------------------------------------------------------------------------- /vue-scroll-stop-on.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxtobox/vue-scroll-stop/HEAD/vue-scroll-stop-on.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import VueScrollStop from './vue-scroll-stop.js'; 2 | 3 | module.exports = { 4 | install: function (Vue, options) { 5 | Vue.directive('scroll-stop', VueScrollStop); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-scroll-stop", 3 | "version": "0.1.5", 4 | "description": "A tiny Vue directive that stop propagation scroll when edge reached", 5 | "main": "dist/vue-scroll-stop.js", 6 | "scripts": { 7 | "build": "rimraf ./dist && webpack --config ./webpack.config.js" 8 | }, 9 | "author": "Volodymyr Antoniuk", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/voxtobox/vue-scroll-stop" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.10.4", 17 | "babel-loader": "^6.2.4", 18 | "babel-plugin-transform-runtime": "^6.9.0", 19 | "babel-preset-es2015": "^6.9.0", 20 | "babel-preset-stage-2": "^6.11.0", 21 | "babel-runtime": "^6.9.2", 22 | "rimraf": "^2.6.1", 23 | "vue": "^2.2.1", 24 | "vue-html-loader": "^1.2.3", 25 | "vue-loader": "^11.1.4", 26 | "vue-style-loader": "^2.0.3", 27 | "vue-template-compiler": "^2.2.1", 28 | "webpack": "1.13.1", 29 | "webpack-merge": "^4.1.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/vue-scroll-stop.min.js: -------------------------------------------------------------------------------- 1 | window.VueScrollStop=function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={exports:{},id:o,loaded:!1};return e[o].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),l=o(i);e.exports={install:function(e,t){e.directive("scroll-stop",l["default"])}}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=void 0,o=void 0,i={},l=function(e){var t=e.deltaY,n=void 0===t?0:t,i=e.deltaX,l=void 0===i?0:i;if("touchmove"===e.type){var c=e.touches&&e.touches.length&&e.touches[0]||{clientY:0,clientX:0};n=o.clientY-c.clientY,l=o.clientX-c.clientX,o=c}return{deltaX:l,deltaY:n}},c=function(e){var t=n,o=t.clientHeight,c=t.clientWidth,r=t.scrollHeight,u=t.scrollWidth,d=t.scrollTop,a=t.scrollLeft,s=l(e),v=s.deltaY,f=void 0===v?0:v,h=s.deltaX,p=void 0===h?0:h;Math.abs(f)>Math.abs(p)?!i.v&&i.h||!(f>=0&&r-d===o||f<=0&&0===d)||e.preventDefault():!i.h&&i.v||!(p>=0&&u-a===c||p<=0&&0===a)||e.preventDefault()},r=function(e){var t=e.touches&&e.touches.length&&e.touches[0]||{clientX:0,clientY:0},n=t.clientX,i=t.clientY;o={clientX:n,clientY:i}},u=function(){n.addEventListener("wheel",c),n.addEventListener("touchmove",c),n.addEventListener("touchstart",r)},d=function(){n.removeEventListener("wheel",c),n.removeEventListener("touchmove",c),n.removeEventListener("touchstart",r)};t["default"]={bind:function(e,t){n=e,i=t.modifiers,t.value!==!1&&u()},update:function(e,t){t.value!==!1?u():d()},unbind:function(e,t){d()}}}]); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const path = require('path'); 4 | 5 | var config = { 6 | output: { 7 | path: path.resolve(__dirname + '/dist/'), 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loader: 'babel', 14 | include: __dirname, 15 | exclude: /node_modules/, 16 | }, 17 | { 18 | test: /\.vue$/, 19 | loader: 'vue', 20 | }, 21 | { 22 | test: /\.css$/, 23 | loader: 'style!less!css', 24 | }, 25 | ], 26 | }, 27 | plugins: [ 28 | new webpack.optimize.UglifyJsPlugin({ 29 | minimize: true, 30 | sourceMap: false, 31 | mangle: true, 32 | compress: { 33 | warnings: false, 34 | }, 35 | }), 36 | ], 37 | }; 38 | 39 | module.exports = [ 40 | merge(config, { 41 | entry: path.resolve(__dirname + '/src/plugin.js'), 42 | output: { 43 | filename: 'vue-scroll-stop.min.js', 44 | libraryTarget: 'window', 45 | library: 'VueScrollStop', 46 | }, 47 | }), 48 | merge(config, { 49 | entry: path.resolve(__dirname + '/src/plugin.js'), 50 | output: { 51 | filename: 'vue-scroll-stop.js', 52 | libraryTarget: 'umd', 53 | library: 'vue-scroll-stop', 54 | umdNamedDefine: true, 55 | }, 56 | }), 57 | ]; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-scroll-stop 2 | 3 | A tiny Vue directive that stop propagation scroll when edge reached. 4 | Works with desktop `mousewheel` and mobile `touchmove` events 5 | 6 | :white_check_mark: On | :x: Off 7 | ------------- | ------------- 8 |  |  9 | 10 | ## Sandbox 11 | 12 | Want to try? [Here's a link](https://jsfiddle.net/voxtobox/9ghLbek8/). 13 | 14 | ## Installation 15 | 16 | ```javascript 17 | npm i --save vue-scroll-stop 18 | ``` 19 | 20 | ### Import 21 | In main.js: 22 | ```javascript 23 | import Vue from 'vue' 24 | import VueScrollStop from 'vue-scroll-stop' 25 | import App from './App.vue' 26 | 27 | Vue.use(VueScrollStop) 28 | 29 | new Vue({ 30 | el: '#app', 31 | render: h => h(App) 32 | }) 33 | ``` 34 | 35 | ### Browser 36 | 37 | Include the script file, then install the component with `Vue.use(VueScrollStop);` e.g.: 38 | 39 | ```html 40 | 41 | 42 | 45 | ``` 46 | 47 | ## Usage 48 | 49 | Once installed, it can be used in a template as simply: 50 | ```html 51 |
52 | ``` 53 | By default directive works on both direction but you can strict it by using modifier v (vertical) or h (horizontal) 54 | ```html 55 | 56 | ``` 57 | 58 | You can pass `false` as value to disable directive reactive 59 | ```html 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /dist/vue-scroll-stop.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("vue-scroll-stop",[],t):"object"==typeof exports?exports["vue-scroll-stop"]=t():e["vue-scroll-stop"]=t()}(this,function(){return function(e){function t(o){if(n[o])return n[o].exports;var i=n[o]={exports:{},id:o,loaded:!1};return e[o].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),c=o(i);e.exports={install:function(e,t){e.directive("scroll-stop",c["default"])}}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=void 0,o=void 0,i={},c=function(e){var t=e.deltaY,n=void 0===t?0:t,i=e.deltaX,c=void 0===i?0:i;if("touchmove"===e.type){var l=e.touches&&e.touches.length&&e.touches[0]||{clientY:0,clientX:0};n=o.clientY-l.clientY,c=o.clientX-l.clientX,o=l}return{deltaX:c,deltaY:n}},l=function(e){var t=n,o=t.clientHeight,l=t.clientWidth,r=t.scrollHeight,u=t.scrollWidth,s=t.scrollTop,d=t.scrollLeft,a=c(e),v=a.deltaY,f=void 0===v?0:v,p=a.deltaX,h=void 0===p?0:p;Math.abs(f)>Math.abs(h)?!i.v&&i.h||!(f>=0&&r-s===o||f<=0&&0===s)||e.preventDefault():!i.h&&i.v||!(h>=0&&u-d===l||h<=0&&0===d)||e.preventDefault()},r=function(e){var t=e.touches&&e.touches.length&&e.touches[0]||{clientX:0,clientY:0},n=t.clientX,i=t.clientY;o={clientX:n,clientY:i}},u=function(){n.addEventListener("wheel",l),n.addEventListener("touchmove",l),n.addEventListener("touchstart",r)},s=function(){n.removeEventListener("wheel",l),n.removeEventListener("touchmove",l),n.removeEventListener("touchstart",r)};t["default"]={bind:function(e,t){n=e,i=t.modifiers,t.value!==!1&&u()},update:function(e,t){t.value!==!1?u():s()},unbind:function(e,t){s()}}}])}); -------------------------------------------------------------------------------- /src/vue-scroll-stop.js: -------------------------------------------------------------------------------- 1 | // Init variables 2 | let elem, lastTouchMove, modifiers = {}; 3 | 4 | /** 5 | * Get scroll delta for x and y axios 6 | * @param e 7 | * @returns {{deltaX: number, deltaY: number}} 8 | */ 9 | const getDeltas = function(e) { 10 | let { deltaY = 0 , deltaX = 0 } = e; 11 | 12 | if (e.type === 'touchmove') { 13 | const touch = ( 14 | e.touches && e.touches.length && e.touches[0] || 15 | { clientY: 0, clientX: 0 } 16 | ); 17 | 18 | deltaY = lastTouchMove.clientY - touch.clientY; 19 | deltaX = lastTouchMove.clientX - touch.clientX; 20 | lastTouchMove = touch; 21 | } 22 | 23 | return { deltaX, deltaY }; 24 | }; 25 | 26 | /** 27 | * Call on mousewheel or touchmove events 28 | * @param e 29 | */ 30 | const onScrolling = function(e) { 31 | const { 32 | clientHeight: height, 33 | clientWidth: width, 34 | scrollHeight, 35 | scrollWidth, 36 | scrollTop, 37 | scrollLeft 38 | } = elem; 39 | let { deltaY = 0 , deltaX = 0 } = getDeltas(e); 40 | 41 | 42 | if (Math.abs(deltaY) > Math.abs(deltaX)) { 43 | if ( 44 | (modifiers.v || !modifiers.h) && 45 | ((deltaY >= 0 && (scrollHeight - scrollTop) === height) || 46 | (deltaY <= 0 && scrollTop === 0)) 47 | ) { 48 | e.preventDefault(); 49 | } 50 | } else { 51 | if ( 52 | (modifiers.h || !modifiers.v) && 53 | ((deltaX >= 0 && (scrollWidth - scrollLeft) === width) || 54 | (deltaX <= 0 && scrollLeft === 0)) 55 | ) { 56 | e.preventDefault(); 57 | } 58 | } 59 | }; 60 | 61 | /** 62 | * Save client position on start touch 63 | * 64 | * @param e 65 | */ 66 | const onTouchStart = function(e) { 67 | const { clientX, clientY } = ( 68 | (e.touches && e.touches.length && e.touches[0]) || 69 | {clientX: 0, clientY: 0} 70 | ); 71 | lastTouchMove = { clientX, clientY }; 72 | }; 73 | 74 | const _addEventListeners = function() { 75 | elem.addEventListener('wheel', onScrolling); 76 | elem.addEventListener('touchmove', onScrolling); 77 | elem.addEventListener('touchstart', onTouchStart); 78 | }; 79 | 80 | const _removeEventListeners = function() { 81 | elem.removeEventListener('wheel', onScrolling); 82 | elem.removeEventListener('touchmove', onScrolling); 83 | elem.removeEventListener('touchstart', onTouchStart); 84 | }; 85 | 86 | // Directive 87 | export default { 88 | bind(el, binding) { 89 | elem = el; 90 | modifiers = binding.modifiers; 91 | 92 | if (binding.value !== false) { 93 | _addEventListeners(); 94 | } 95 | }, 96 | update(el, binding) { 97 | if (binding.value !== false) { 98 | _addEventListeners(); 99 | } else { 100 | _removeEventListeners(); 101 | } 102 | }, 103 | unbind(el, binding) { 104 | _removeEventListeners(); 105 | }, 106 | }; 107 | --------------------------------------------------------------------------------