├── .gitignore ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | raf-scroll 2 | === 3 | A scroll event that fires only once per frame. 4 | 5 | ## Installation 6 | 7 | `npm i raf-scroll --save` 8 | 9 | ## Usage 10 | 11 | Every event call is sync on a `requestAnimationFrame` (raf) loop. 12 | The triggered event contains 2 properties, `scrollY` and `deltaY`. 13 | 14 | `rafScroll.init()` 15 | Setup the singleton (allows to teardown/recreate at will). Needs to be called before using the lib. 16 | 17 | `rafScroll.add(callback)` 18 | Will call `callback` maximum once per frame, if the scroll value has changed. 19 | An `event` will be passed containing the current `scrollY` as well as the `deltaY`. 20 | 21 | `rafScroll.addOnce(callback)` 22 | Same than `add` but only trigger the event once, then automatically unbinds itself. Useful for triggering behavior when user scrolls. 23 | 24 | `rafScroll.remove(callback)` 25 | Unbind the listener. Passing no `callback` will unbind all previous callbacks. 26 | 27 | `rafScroll.getCurrent()` 28 | Manually get the `event` (containing last `scrollY`/`deltaY`). Note that you need to wait at least 1 frame to have valid values since the scroll data is read in a requestAnimationFrame. 29 | 30 | `rafScroll.destroy()` 31 | Clean the singleton. It will auto-restart if you call `rafScroll.init`. 32 | 33 | ## Example 34 | 35 | ```js 36 | 37 | var rafScroll = require('raf-scroll'); 38 | 39 | rafScroll.add(function onScroll(event) { 40 | if (window.innerHeight + event.scrollY >= document.offsetHeight - window.innerHeight * 0.5) { 41 | triggerInfiniteScroll(); 42 | } 43 | }) 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var raf = require('component-raf'); 4 | var scrollTop = require('scrolltop'); 5 | var Emitter = require('tiny-emitter'); 6 | var emitter = new Emitter(); 7 | var rafId = -1; 8 | var scrollY = 0; 9 | var deltaY = 0; 10 | var ticking = false; 11 | 12 | module.exports = { 13 | add: function(fn) { 14 | emitter.on('scroll', fn); 15 | 16 | // Start raf on first callback 17 | if (emitter.e.scroll.length === 1) { 18 | rafId = raf(update); 19 | } 20 | }, 21 | 22 | addOnce: function(fn) { 23 | emitter.once('scroll', fn); 24 | 25 | // Start raf on first callback 26 | if (emitter.e.scroll.length === 1) { 27 | rafId = raf(update); 28 | } 29 | }, 30 | 31 | remove: function(fn) { 32 | emitter.off('scroll', fn); 33 | 34 | // Stop raf if there is no more callbacks 35 | if (!emitter.e.scroll || emitter.e.scroll.length < 1) { 36 | raf.cancel(rafId); 37 | } 38 | }, 39 | 40 | getCurrent: function() { 41 | return getEvent(); 42 | }, 43 | 44 | destroy: function() { 45 | raf.cancel(rafId); 46 | emitter = new Emitter(); 47 | scrollY = 0; 48 | deltaY = 0; 49 | } 50 | }; 51 | 52 | function getEvent() { 53 | var scroll = scrollTop(); 54 | 55 | if (ticking) { 56 | deltaY = scroll - scrollY; 57 | } 58 | 59 | scrollY = scroll; 60 | 61 | return { 62 | scrollY: scrollY, 63 | deltaY: deltaY 64 | }; 65 | } 66 | 67 | function update() { 68 | rafId = raf(update); 69 | 70 | ticking = true; 71 | emitter.emit('scroll', getEvent()); 72 | ticking = false; 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raf-scroll", 3 | "version": "1.4.0", 4 | "description": "A scroll event that fires only once per frame.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "prova -b ./test/**/*.js -l phantom -q", 11 | "test-browser": "prova -b ./test/**/*.js -l chrome" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ayamflow/raf-scroll" 16 | }, 17 | "keywords": [ 18 | "raf", 19 | "requestanimationframe", 20 | "frame", 21 | "scroll", 22 | "event" 23 | ], 24 | "author": "= <=>", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ayamflow/raf-scroll/issues" 28 | }, 29 | "homepage": "https://github.com/ayamflow/raf-scroll", 30 | "dependencies": { 31 | "component-raf": "^1.2.0", 32 | "scrolltop": "0.0.1", 33 | "tiny-emitter": "^1.0.0" 34 | }, 35 | "devDependencies": { 36 | "prova": "^2.1.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('prova'); 4 | var rafScroll = require('../index.js'); 5 | var raf = require('component-raf'); 6 | 7 | // Ensure we have space enough to scroll 8 | document.body.style.height = window.innerHeight + 500 + 'px'; 9 | 10 | test('rAF call', function(assert) { 11 | rafScroll.addOnce(function(event) { 12 | assert.notDeepEqual(event.deltaY, 0, 'Event shouldn\'t be called if scroll hasn\'t changed.'); 13 | assert.deepEqual(event.deltaY, 50, 'deltaY should reflect the latest change.'); 14 | rafScroll.destroy(); 15 | assert.end(); 16 | }); 17 | 18 | window.scrollTo(0, 50); 19 | }); 20 | 21 | test('Manual call', function(assert) { 22 | window.scrollTo(0, 50); 23 | 24 | raf(function() { 25 | var ev = rafScroll.getCurrent(); 26 | assert.notDeepEqual(ev.deltaY, 0, 'Calling getCurrent shouldn\'t affect/reset the deltaY value.'); 27 | rafScroll.destroy(); 28 | assert.end(); 29 | }); 30 | }); 31 | 32 | test('.destroy()', function(assert) { 33 | rafScroll.add(function() {}); 34 | rafScroll.destroy(); 35 | rafScroll.add(function(e) { 36 | rafScroll.destroy(); 37 | assert.equal(e !== undefined, true, 'the callback should have been called'); 38 | assert.end(); 39 | }); 40 | }); 41 | --------------------------------------------------------------------------------