├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── karma.conf.babel.js ├── karma.conf.js ├── package.json ├── src ├── __tests__ │ ├── .eslintrc │ └── use-scroll-bahavior-test.js ├── index.js └── util │ ├── canUseStorage.js │ └── scroll.js └── tests.webpack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 1, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rackt" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | lib 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - stable 6 | 7 | env: 8 | - BROWSER=ChromeCi 9 | - BROWSER=Firefox 10 | 11 | cache: 12 | directories: 13 | - node_modules 14 | 15 | before_install: 16 | - export CHROME_BIN=chromium-browser 17 | - export DISPLAY=:99.0 18 | - sh -e /etc/init.d/xvfb start 19 | 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bingo Yang 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-scroll-behavior [![npm package][npm-badge]][npm] [![Travis][build-badge]][build] [![Coveralls][coveralls-badge]][coveralls] 2 | 3 | Scroll behaviors for use with [`history`](https://github.com/reactjs/history). Inspired by [`scroll-behavior`](https://github.com/taion/scroll-behavior) and simplify the behavior. 4 | 5 | ## Usage 6 | 7 | Enhance your history object with this library to get standard scroll behavior after history changed. 8 | 9 | ```jsx 10 | import { browserHistory } from 'react-router'; 11 | import useScroll from 'use-scroll-behavior'; 12 | const history = useScroll(browserHistory); 13 | // ... 14 | export default class App extends Component { 15 | render() { 16 | return ( 17 | 18 | //..your routes 19 | 20 | ); 21 | } 22 | } 23 | ``` 24 | 25 | ## Guide 26 | 27 | ### Installation 28 | 29 | ``` 30 | $ npm install history use-scroll-behavior 31 | ``` 32 | 33 | # Config 34 | ### excludePath: regular Expression Array 35 | set it if you do not want to set scroll position for some path. 36 | ```js 37 | const history = scrollBehavior(browserHistory, { 38 | excludePath: [/news\/id/], 39 | }); 40 | ``` 41 | ## TODO 42 | * add x position? 43 | 44 | [npm-badge]: https://img.shields.io/npm/v/use-scroll-behavior.svg?style=flat-square 45 | [npm]: https://www.npmjs.com/package/use-scroll-behavior 46 | 47 | [build-badge]: https://img.shields.io/travis/blackbing/use-scroll-behavior/master.svg?style=flat-square 48 | [build]: https://travis-ci.org/blackbing/use-scroll-behavior 49 | 50 | [coveralls-badge]: https://img.shields.io/coveralls/blackbing/use-scroll-behavior/master.svg?style=flat-square 51 | [coveralls]: https://coveralls.io/github/blackbing/use-scroll-behavior?branch=master 52 | -------------------------------------------------------------------------------- /karma.conf.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | 4 | export default config => { 5 | const { env } = process 6 | 7 | const isCi = env.CONTINUOUS_INTEGRATION === 'true' 8 | const runCoverage = env.COVERAGE === 'true' || isCi 9 | 10 | const coverageLoaders = [] 11 | const coverageReporters = [] 12 | 13 | if (runCoverage) { 14 | coverageLoaders.push({ 15 | test: /\.js$/, 16 | include: path.resolve('src/'), 17 | exclude: /__tests__/, 18 | loader: 'isparta' 19 | }) 20 | 21 | coverageReporters.push('coverage') 22 | 23 | if (isCi) { 24 | coverageReporters.push('coveralls') 25 | } 26 | } 27 | 28 | config.set({ 29 | frameworks: [ 'mocha' ], 30 | 31 | files: [ 'tests.webpack.js' ], 32 | 33 | preprocessors: { 34 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 35 | }, 36 | 37 | webpack: { 38 | module: { 39 | loaders: [ 40 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 41 | ...coverageLoaders 42 | ] 43 | }, 44 | plugins: [ 45 | new webpack.DefinePlugin({ 46 | 'process.env.NODE_ENV': JSON.stringify('test') 47 | }) 48 | ], 49 | devtool: 'inline-source-map' 50 | }, 51 | 52 | webpackMiddleware: { 53 | noInfo: true 54 | }, 55 | 56 | reporters: [ 'mocha', ...coverageReporters ], 57 | 58 | coverageReporter: { 59 | type: 'lcov', 60 | dir: 'coverage' 61 | }, 62 | 63 | customLaunchers: { 64 | ChromeCi: { 65 | base: 'Chrome', 66 | flags: [ '--no-sandbox' ] 67 | } 68 | }, 69 | 70 | browsers: isCi ? [ env.BROWSER ] : [ 'Chrome', 'Firefox' ], 71 | 72 | singleRun: isCi 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') 2 | module.exports = require('./karma.conf.babel.js') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-scroll-behavior", 3 | "version": "0.1.5", 4 | "description": "Scroll behaviors for use with history, inspired by scroll-behavior", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "keywords": [ 10 | "history", 11 | "location", 12 | "scroll" 13 | ], 14 | "scripts": { 15 | "build": "npm run build-cjs", 16 | "build-cjs": "rimraf lib && babel ./src -d lib --ignore '__tests__'", 17 | "lint": "eslint src *.js", 18 | "prepublish": "npm run build", 19 | "test": "npm run lint && karma start" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/blackbing/use-scroll-behavior.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/blackbing/use-scroll-behavior/issues" 27 | }, 28 | "author": "blackbing", 29 | "homepage": "https://github.com/blackbing/use-scroll-behavior#readme", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel": "5.x", 33 | "babel-core": "5.x", 34 | "babel-eslint": "4.x", 35 | "babel-loader": "5.x", 36 | "chai": "^3.4.0", 37 | "expect": "^1.12.2", 38 | "isparta-loader": "1.x", 39 | "eslint": "^1.8.0", 40 | "eslint-config-rackt": "^1.1.1", 41 | "history": "^2.0.0", 42 | "karma": "^0.13.15", 43 | "karma-chrome-launcher": "^0.2.1", 44 | "karma-coverage": "^0.5.3", 45 | "karma-coveralls": "^1.1.2", 46 | "karma-firefox-launcher": "^0.1.6", 47 | "karma-mocha": "^0.2.0", 48 | "karma-mocha-reporter": "^1.1.1", 49 | "karma-sourcemap-loader": "^0.3.6", 50 | "karma-webpack": "^1.7.0", 51 | "mocha": "^2.3.3", 52 | "rimraf": "^2.4.3", 53 | "webpack": "^1.12.13" 54 | }, 55 | "peerDependencies": { 56 | "history": "^1.12.1 || ^2.0.0" 57 | }, 58 | "dependencies": { 59 | "dom-helpers": "^2.4.0", 60 | "exenv": "^1.2.0", 61 | "lodash.debounce": "^4.0.8", 62 | "lodash.isarray": "^4.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/use-scroll-bahavior-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import canUseStorage from '../util/canUseStorage' 3 | import { 4 | createKey, 5 | getState, 6 | saveState, 7 | scrollToHash, 8 | scrollToState 9 | } from '../util/scroll' 10 | 11 | 12 | describe('use-scroll-bahavior', () => { 13 | beforeEach(() => { 14 | }) 15 | 16 | afterEach(() => { 17 | }) 18 | it('canUseStorage have to be bool', () => { 19 | expect(canUseStorage).toBe(true) 20 | }) 21 | it('should be defined', () => { 22 | expect(createKey).toExist() 23 | expect(saveState).toExist() 24 | expect(scrollToHash).toExist() 25 | expect(scrollToState).toExist() 26 | }) 27 | it('should saveState and getState', () => { 28 | const key = 'test' 29 | const val = '100' 30 | saveState(key, val) 31 | expect(getState(key)).toBe(val) 32 | }) 33 | it('should createKey', () => { 34 | const loc = { 35 | pathname: '/news/1234567', 36 | query: { 37 | a: 'a', 38 | b: 'b' 39 | } 40 | } 41 | expect(createKey(loc)).toBe('/news/1234567?a=a&b=b') 42 | }) 43 | it('should createKey from createBrowserHistory()', () => { 44 | const loc = { 45 | pathname: '/news/1234567', 46 | search: '?a=a&b=b' 47 | } 48 | expect(createKey(loc)).toBe('/news/1234567?a=a&b=b') 49 | }) 50 | it('should createKey with config excludePath', () => { 51 | const loc = { 52 | pathname: '/news/1234567', 53 | query: { 54 | a: 'a', 55 | b: 'b' 56 | } 57 | } 58 | const config = { 59 | excludePath: /news\// 60 | } 61 | expect(createKey(loc, config)).toBe(null) 62 | }) 63 | it('should createKey with config excludePath(array)', () => { 64 | const loc = { 65 | pathname: '/news/1234567', 66 | query: { 67 | a: 'a', 68 | b: 'b' 69 | } 70 | } 71 | const config = { 72 | excludePath: [ /news\// ] 73 | } 74 | expect(createKey(loc, config)).toBe(null) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint func-names: 0*/ 2 | /* eslint no-unused-expressions: 0*/ 3 | 4 | import scrollTop from 'dom-helpers/query/scrollTop' 5 | import on from 'dom-helpers/events/on' 6 | import debounce from 'lodash.debounce' 7 | import { canUseDOM } from 'exenv' 8 | import { 9 | createKey, 10 | saveState, 11 | scrollToHash, 12 | scrollToState 13 | } from './util/scroll' 14 | import canUseStorage from './util/canUseStorage' 15 | 16 | const scrollThreshold = 150 17 | 18 | export default function (history, config={}) { 19 | if (!canUseDOM) { 20 | return history 21 | } 22 | let currentKey = null 23 | history.listen( function (loc) { 24 | currentKey = createKey(loc, config) 25 | // first try to check hash 26 | // second try to scrollTo State 27 | // finally scrollToTop 28 | !scrollToHash(loc) && !scrollToState(currentKey) && window.scrollTo(0, 0) 29 | }) 30 | const onScroll = () => { 31 | if (canUseStorage) { 32 | const y = scrollTop(window) 33 | if (y && currentKey) { 34 | saveState(currentKey, y) 35 | } 36 | } 37 | } 38 | 39 | on(window, 'scroll', debounce(onScroll, scrollThreshold)) 40 | return history 41 | } 42 | -------------------------------------------------------------------------------- /src/util/canUseStorage.js: -------------------------------------------------------------------------------- 1 | 2 | let canUseStorage = true 3 | const canUseStorageKey = 'test' 4 | 5 | try { 6 | sessionStorage.setItem(canUseStorageKey, 'test') 7 | sessionStorage.getItem(canUseStorageKey) 8 | sessionStorage.removeItem(canUseStorageKey) 9 | } catch (e) { 10 | canUseStorage = false 11 | } 12 | 13 | export default canUseStorage 14 | -------------------------------------------------------------------------------- /src/util/scroll.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash.isarray' 2 | import canUseStorage from './canUseStorage' 3 | 4 | const prefix = '@@POS' 5 | 6 | export function saveState(key, y) { 7 | if (canUseStorage) { 8 | sessionStorage.setItem(`${prefix}${key}`, `${y}`) 9 | } 10 | } 11 | export function getState(key) { 12 | if (!canUseStorage) { 13 | return null 14 | } else { 15 | return sessionStorage.getItem(`${prefix}${key}`) 16 | } 17 | } 18 | export function createKey(loc, config={}) { 19 | let queryString = '' 20 | if (config.excludePath) { 21 | const excludePath = config.excludePath 22 | let excluded = false 23 | if (isArray(excludePath)) { 24 | excludePath.forEach( (pathReg) => { 25 | if (pathReg.test(loc.pathname)) { 26 | excluded = true 27 | return false 28 | } 29 | }) 30 | } else { 31 | if (excludePath.test(loc.pathname)) { 32 | excluded = true 33 | } 34 | } 35 | if (excluded) { 36 | return null 37 | } 38 | } 39 | if (loc.query && typeof loc.query === 'object') { 40 | const query = loc.query 41 | queryString = '?' + Object.keys(query).map(k => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`).join('&') 42 | } else if (loc.search) { 43 | queryString = loc.search 44 | } 45 | 46 | return `${loc.pathname}${queryString}` 47 | } 48 | 49 | export function scrollToHash(loc) { 50 | // 51 | if (!loc.hash) { 52 | return false 53 | } 54 | const hash = decodeURIComponent(loc.hash) 55 | const hashParts = hash.split('#') 56 | 57 | if (hashParts.length >= 2) { 58 | const _hash = hashParts[1] 59 | try { 60 | const element = document.querySelector(`#${_hash}`) 61 | if (element) { 62 | element.scrollIntoView() 63 | return true 64 | } 65 | } catch (e) { 66 | return false 67 | } 68 | } 69 | return false 70 | } 71 | 72 | export function scrollToState(key) { 73 | const y = getState(key) 74 | if (!y) { 75 | return false 76 | } 77 | // setTimeout 0 for avoid page rendering 78 | setTimeout( () => { 79 | window.scrollTo(0, y) 80 | }, 0) 81 | return true 82 | } 83 | 84 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | const context = require.context('./src', true, /-test\.js$/) 2 | context.keys().forEach(context) 3 | --------------------------------------------------------------------------------