├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json └── src ├── Dispatcher.js └── ScrollSpy.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | dist 3 | lib 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | dist 3 | public 4 | site 5 | src 6 | gulpfile.js 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jed Watson 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-scroll-spy 2 | 3 | React.js mixin for updating state based on the window's scroll position 4 | 5 | Useful for things like triggering animation when content is scrolled into view, or UI updates when elements are within the viewport. 6 | 7 | Can be seen in action on [admyt.com](http://www.admyt.com) 8 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | var taskConfig = { 5 | component: { 6 | name: 'ScrollSpy', 7 | lib: 'lib', 8 | dependencies: [ 9 | 'react' 10 | ] 11 | } 12 | }; 13 | 14 | initGulpTasks(gulp, taskConfig); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-spy", 3 | "version": "1.0.0", 4 | "description": "React.js mixin for updating state based on the window's scroll position", 5 | "main": "lib/ScrollSpy.js", 6 | "author": "Jed Watson", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JedWatson/react-scroll-spy.git" 11 | }, 12 | "devDependencies": { 13 | "babel-eslint": "^4.1.3", 14 | "eslint": "^1.4.3", 15 | "eslint-plugin-react": "^3.4.1", 16 | "gulp": "^3.9.0", 17 | "react": "^0.13.3", 18 | "react-component-gulp-tasks": "^0.7.1" 19 | }, 20 | "peerDependencies": { 21 | "react": "^0.13.0 || ^0.14.0-beta" 22 | }, 23 | "browserify-shim": { 24 | "react": "global:React" 25 | }, 26 | "scripts": { 27 | "build": "gulp build", 28 | "build:lib": "gulp build:lib", 29 | "bump": "gulp bump", 30 | "lint": "eslint ./", 31 | "prepublish": "npm run build:lib", 32 | "publish:npm": "gulp publish:npm", 33 | "publish:tag": "gulp publish:tag", 34 | "release": "gulp release", 35 | "watch": "gulp watch:lib" 36 | }, 37 | "keywords": [ 38 | "react", 39 | "react-component", 40 | "ui", 41 | "scroll", 42 | "spy", 43 | "scrollspy", 44 | "mixin" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/Dispatcher.js: -------------------------------------------------------------------------------- 1 | const Dispatcher = { 2 | bound: false, 3 | listeners: [], 4 | register (fn) { 5 | this.listeners.push(fn); 6 | if (!this.bound) this.bindEvent(); 7 | }, 8 | unregister (fn) { 9 | var i = this.listeners.indexOf(fn); 10 | if (i < 0) return; 11 | this.listeners.splice(i, 1); 12 | if (!this.listeners.length) this.unbindEvent(); 13 | }, 14 | onScroll (e) { 15 | this.listeners.forEach(fn => fn(e)); 16 | }, 17 | bindEvent () { 18 | this.bound = true; 19 | window.addEventListener('scroll', this.onScroll); 20 | }, 21 | unbindEvent () { 22 | this.bound = false; 23 | window.addEventListener('scroll', this.onScroll); 24 | } 25 | }; 26 | 27 | Dispatcher.onScroll = Dispatcher.onScroll.bind(Dispatcher); 28 | 29 | export default Dispatcher; 30 | -------------------------------------------------------------------------------- /src/ScrollSpy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dispatcher from './Dispatcher'; 3 | 4 | function elementInViewport (el) { 5 | 6 | var rect = el.getBoundingClientRect(); 7 | 8 | // Hack to initiate transitions at the right time - Needs a rethink 9 | return ( 10 | rect.left >= 0 && 11 | rect.top < (window.innerHeight * 0.75 || document.documentElement.clientHeight * 0.75) && 12 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 13 | ); 14 | } 15 | 16 | function ScrollTransitionMixin (ref, isInViewKey, wasInViewKey) { 17 | 18 | ref = ref || 'scrollspy'; 19 | isInViewKey = isInViewKey || 'currentlyInView'; 20 | wasInViewKey = wasInViewKey || 'wasInView'; 21 | 22 | var mixin = { 23 | getInitialState () { 24 | let initialState = {}; 25 | initialState[isInViewKey] = false; 26 | initialState[wasInViewKey] = false; 27 | return initialState; 28 | }, 29 | componentDidMount () { 30 | Dispatcher.register(this[ref + '__handleScroll']); 31 | }, 32 | componentWillUnmount () { 33 | Dispatcher.unregister(this[ref + '__handleScroll']); 34 | } 35 | }; 36 | mixin[ref + '__handleScroll'] = function () { 37 | let newState = {}; 38 | let node = React.findDOMNode(this.refs[ref]) 39 | 40 | if (elementInViewport(node)) { 41 | if (!this.state[isInViewKey]) { 42 | console.log('element ' + ref + ' came into view'); 43 | newState[isInViewKey] = true; 44 | newState[wasInViewKey] = true; 45 | this.setState(newState); 46 | } 47 | } else if (this.state[isInViewKey]) { 48 | console.log('element ' + ref + ' left view'); 49 | newState[isInViewKey] = false; 50 | this.setState(newState); 51 | } 52 | }; 53 | 54 | return mixin; 55 | } 56 | 57 | export default ScrollTransitionMixin; 58 | --------------------------------------------------------------------------------