├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __test__
└── index.js
├── docs.sh
├── package.json
├── postcss.config.js
├── src
├── js
│ ├── app.js
│ ├── footer.js
│ ├── lib
│ │ ├── scrollspy.js
│ │ └── throttle.js
│ ├── nav.js
│ └── props-table.js
├── scss
│ └── app.scss
└── templates
│ └── index.ejs
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env", {
5 | "targets": {
6 | "browsers": ["last 3 versions"],
7 | },
8 | }
9 | ],
10 | "@babel/preset-react"
11 | ],
12 | "plugins": [
13 | "@babel/plugin-transform-object-assign",
14 | "@babel/plugin-transform-runtime"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | [*.html]
9 | indent_style = tab
10 | indent_size = 4
11 | [*.hbs]
12 | indent_style = tab
13 | indent_size = 4
14 | [*.js]
15 | indent_style = space
16 | indent_size = 2
17 | [*.jsx]
18 | indent_style = space
19 | indent_size = 2
20 | [*.json]
21 | indent_style = space
22 | indent_size = 2
23 | [*.css]
24 | indent_style = space
25 | indent_size = 2
26 | [*.scss]
27 | indent_style = space
28 | indent_size = 2
29 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "plugins": [
5 | "react"
6 | ],
7 |
8 | "env": {
9 | "browser": true,
10 | "node": true,
11 | "amd": true,
12 | "mocha": true,
13 | "es6": true,
14 | "worker": true
15 | },
16 |
17 | "globals": {
18 | },
19 |
20 | "rules": {
21 | "quotes": [1, "single"],
22 | "strict": [0],
23 | "max-params": [1, 4],
24 | "semi": [1, "never"],
25 | "no-undef": [2],
26 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
27 | "no-alert": [1],
28 | "valid-jsdoc": [1],
29 | "brace-style": [2, "1tbs"],
30 | "no-mixed-spaces-and-tabs": [2, true],
31 | "no-trailing-spaces": [2],
32 | "keyword-spacing": [2, {"before": true, "after": true}],
33 | "max-nested-callbacks": [1, 3],
34 | "space-infix-ops": [2],
35 | "radix": [2],
36 | "space-unary-ops": [2],
37 | "wrap-regex": [2],
38 | "no-mixed-requires": [1, true],
39 | "eol-last": [0],
40 | "no-bitwise": [1],
41 | "dot-notation": [1],
42 | "yoda": [2, "never"],
43 | "no-nested-ternary": [1],
44 | "no-delete-var": [1],
45 | "no-shadow": [2],
46 | "no-octal": [2],
47 | "no-extra-bind": [2],
48 | "no-extend-native": [2],
49 | "no-extra-parens": [0],
50 | "no-underscore-dangle": [0],
51 | "consistent-return": [1],
52 | "no-path-concat": [1],
53 | "no-process-exit": [1],
54 | "react/jsx-uses-vars": [2],
55 | "react/jsx-uses-react": [1]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/8646d7f7d7a49a08f7112b01be8bb547070aef6d/Global/Vim.gitignore
2 |
3 | [._]*.s[a-w][a-z]
4 | [._]s[a-w][a-z]
5 | *.un~
6 | Session.vim
7 | .netrwhist
8 | *~
9 |
10 |
11 | ### https://raw.github.com/github/gitignore/8646d7f7d7a49a08f7112b01be8bb547070aef6d/Node.gitignore
12 |
13 | # Logs
14 | logs
15 | *.log
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (http://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directory
38 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
39 | node_modules
40 |
41 |
42 | build/
43 | release/
44 | *.zip
45 | *.tar.gz
46 | .DS_Store
47 | .idea
48 | dist/
49 | /lib/
50 | /scrollspy.js
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/makotot/react-scrollspy/0be9e8acb1b822f29d2c14221e1832ed2938695f/.npmignore
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | env:
3 | global:
4 | - GH_REPO="makotot/react-scrollspy"
5 | - GIT_COMMITTER_NAME=makotot
6 | - GIT_COMMITTER_EMAIL=makoto.ttn@gmail.com
7 | - GIT_AUTHOR_NAME=makotot
8 | - GIT_AUTHOR_EMAIL=makoto.ttn@gmail.com
9 | - secure: G8ee2qd/yrzGBxlafPPq85WG7ii/dS9ORiNcHSIIzHxD9j6jsXwOCx8S9i3zs04r/pxHtkeIAwhGDDJbZHdSR+yaEg78ZM7PFTJc4zqrwsQiYmsNlSczdPxn1YYLGq6w1f6gb0BeqtNq+f/NJpynoR2EjRgIxkUO9bOsKyivhbPiqCxnKPOdBvqRa4lgNraVyLSG5sldE+IQeeE0et0BKiXFWuad4JCYdjVGMmenuZB06CCwdvGT3gOueoZLLCTvWOcIV0jyeG78RTUubMAjBHHrvT4EY8RkJihmkhJgm4JAh6Q+7RXX73Xf2Q5SzTG4nzTeLWp8X60wU2FKqA2/yy43sxAo/vBfm0xJKUrgUNjRNd5oiWE5BTqWTxuls2c8oKCoAbTjastPwnJ+vHN0zntz+ZysyKPn4TuDVOLpLIyZMmdfE9R2nDK5eKJvE+W7gKGS1t6zDiYP9LfCbbRhKlM1xx/B4QRJT/T35YP+I1OV/a00ZoE+uVq2jYw0HQlDehFPjTyr050Snu1dmVoko+qchZIV0RqsUjhljcy1wqAgcHvaqKGWpESDk7+U8MjUYHg2QIm7j+kD+kZot7HzcugBE/9VskEXdSDb1d49a4JKVrDIyAioBDNlMGuyPIxCGobBMXlK1buhYmtShc9bPPYkxk/3nvrGYRmMFhbzsmg=
10 | node_js:
11 | - '6'
12 | - '8'
13 | - '10'
14 | script: yarn run test
15 | cache:
16 | yarn: true
17 | directories:
18 | - node_modules
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 3.3.1 / 2017-08-16
2 | * 3.3.1
3 | * Merge pull request https://github.com/makotot/react-scrollspy/issues/71 from
4 | makotot/onupdate
5 | pass active el
6 | * pass active el
7 | * update changelog
8 |
9 | 3.3.0 / 2017-08-16
10 | * 3.3.0
11 | * Merge pull request https://github.com/makotot/react-scrollspy/issues/70 from
12 | makotot/onupdate
13 | add onupdate
14 | * mv
15 | * add onupdate
16 |
17 | 3.1.0 / 2017-05-05
18 | * 3.1.0
19 | * Merge pull request https://github.com/makotot/react-scrollspy/issues/59 from
20 | makotot/issue-48
21 | add roolEl props
22 | * add roolEl props
23 |
24 |
25 | 3.0.0 / 2017-04-22
26 | ==================
27 |
28 | * 3.0.0
29 | * Merge pull request [#55](https://github.com/makotot/react-scrollspy/issues/55) from makotot/export
30 | export as default
31 | * fix html
32 | * rm script tag
33 | * fix test
34 | * export as default
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015-Present Makoto Tateno
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 | # DEPRECATED - no longer actively maintained.
2 |
3 | Please use [GhostUI](https://github.com/makotot/GhostUI) instead.
4 |
5 | # react-scrollspy
6 |
7 | [](https://www.npmjs.com/package/react-scrollspy)
8 | [](https://travis-ci.org/makotot/react-scrollspy)
9 | [](https://github.com/makotot/react-scrollspy)
10 | [](https://github.com/makotot/react-scrollspy)
11 | [](https://github.com/makotot/react-scrollspy)
12 | [](https://www.npmjs.com/package/react-scrollspy)
13 |
14 | > Scrollspy component
15 |
16 | [Demo](http://makotot.github.io/react-scrollspy/)
17 |
18 | ## Install
19 |
20 | ```sh
21 | $ npm i react-scrollspy
22 | ```
23 |
24 | ## Usage
25 |
26 | ```js
27 | import Scrollspy from 'react-scrollspy'
28 |
29 | ...
30 |
31 |
46 | ```
47 |
48 | ## Props
49 |
50 | ### `items={ Array }`
51 |
52 | Id list of target contents.
53 |
54 | ### `currentClassName={ String }`
55 |
56 | Class name that apply to the navigation element paired with the content element in viewport.
57 |
58 | ### `scrolledPastClassName={ String }`
59 |
60 | Class name that apply to the navigation elements that have been scrolled past [optional].
61 |
62 | ### `componentTag={ String | React element type }`
63 |
64 | HTML tag or React Component type for Scrollspy component if you want to use something other than `ul` [optional].
65 |
66 | ### `style={ Object }`
67 |
68 | Style attribute to be passed to the generated <ul/> element [optional].
69 |
70 | ### `offset={ Number }`
71 |
72 | Offset value that adjusts to determine the elements are in the viewport [optional].
73 |
74 | ### `rootEl={ String }`
75 |
76 | Name of the element of scrollable container that can be used with querySelector [optional].
77 |
78 | ### `onUpdate={ Function }`
79 |
80 | Function to be executed when the active item has been updated [optional].
81 |
82 | ## Methods
83 |
84 | ### `offEvent`
85 |
86 | Remove event listener of scrollspy.
87 |
88 | ### `onEvent`
89 |
90 | Add event listener of scrollspy.
91 |
92 | ## Development
93 |
94 | ```sh
95 | $ git clone https://github.com/makotot/react-scrollspy.git
96 | $ cd react-scrollspy
97 | $ npm i
98 | $ npm run start
99 | ```
100 |
101 | ## Contributing
102 |
103 | Pull requests and [reporting an issue](https://github.com/makotot/react-scrollspy/issues/new) are always welcome :)
104 |
105 | ## License
106 |
107 | MIT
108 |
--------------------------------------------------------------------------------
/__test__/index.js:
--------------------------------------------------------------------------------
1 | import test from 'ava'
2 | import React from 'react'
3 | import { shallow, mount, render } from 'enzyme'
4 | import Scrollspy from '../src/js/lib/scrollspy'
5 |
6 | test('renders correct children length', (t) => {
7 | const wrapper = shallow(
8 |
9 | section 1
10 | section 2
11 | section 3
12 |
13 | )
14 | t.is(wrapper.find('li').length, 3)
15 | })
16 |
17 | test('renders children with correct props', (t) => {
18 | const wrapper = shallow(
19 |
20 | section 1
21 |
22 | )
23 | t.is(wrapper.find('li').prop('randomProp'), 'someText')
24 | })
25 |
26 | test('renders expected html tag', (t) => {
27 | const defaultTag = shallow( )
28 | const customTag = shallow( )
29 |
30 | t.is(defaultTag.type(), 'ul')
31 | t.is(customTag.type(), 'div')
32 | })
33 |
34 | test('renders expected React Component', (t) => {
35 | const MyComponent = () => {}
36 | const customTag = shallow( )
37 |
38 | t.is(customTag.type(), MyComponent)
39 | })
40 |
--------------------------------------------------------------------------------
/docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | rm -rf gh_pages || exit 0;
4 | mkdir gh_pages
5 | ls -la
6 | cd gh_pages
7 |
8 | cp -r ../dist/* .
9 |
10 | git init
11 | git config user.name "${GIT_COMITTER_NAME}"
12 | git config user.email "${GIT_COMITTER_EMAIL}"
13 |
14 | ls -la
15 | git add .
16 | git commit -m "Deploy to gh-pages"
17 | git push -fq "https://${GH_TOKEN}@github.com/${GH_REPO}.git" master:gh-pages > /dev/null 2>&1
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-scrollspy",
3 | "version": "3.4.3",
4 | "description": "react scrollspy component",
5 | "main": "./lib/scrollspy.js",
6 | "files": [
7 | "README.md",
8 | "lib/scrollspy.js",
9 | "lib/throttle.js",
10 | "package.json",
11 | "webpack.config.js",
12 | ".eslintrc"
13 | ],
14 | "scripts": {
15 | "serve": "NODE_ENV=development webpack-dev-server --inline --hot --color",
16 | "docs": "NODE_ENV=production webpack",
17 | "build": "NODE_ENV=production ./node_modules/.bin/babel ./src/js/lib --out-dir ./lib",
18 | "test": "NODE_ENV=test ./node_modules/.bin/ava __test__/*.js --verbose",
19 | "prepublish": "yarn run build",
20 | "deploy": "yarn run docs && gh-pages -d ./dist",
21 | "release": "standard-version"
22 | },
23 | "ava": {
24 | "require": [
25 | "@babel/register"
26 | ]
27 | },
28 | "standard-version": {
29 | "skip": {
30 | "tag": true,
31 | "bump": true
32 | }
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/makotot/react-scrollspy.git"
37 | },
38 | "author": "makotot",
39 | "license": "MIT",
40 | "devDependencies": {
41 | "@babel/cli": "^7.2.3",
42 | "@babel/core": "^7.3.4",
43 | "@babel/plugin-transform-object-assign": "^7.2.0",
44 | "@babel/plugin-transform-runtime": "^7.3.4",
45 | "@babel/preset-env": "^7.3.4",
46 | "@babel/preset-react": "^7.0.0",
47 | "@babel/register": "^7.0.0",
48 | "@babel/runtime": "^7.3.4",
49 | "autoprefixer": "^9.4.10",
50 | "ava": "^1.3.1",
51 | "babel-eslint": "^8.0.0",
52 | "babel-loader": "^8.0.5",
53 | "css-loader": "^2.1.1",
54 | "cssnano": "^4.1.10",
55 | "enzyme": "^2.9.1",
56 | "eslint": "^4.6.1",
57 | "eslint-loader": "^2.1.2",
58 | "eslint-plugin-react": "^7.12.4",
59 | "gh-pages": "^2.0.1",
60 | "html-webpack-plugin": "^3.2.0",
61 | "node-sass": "^4.13.0",
62 | "normalize.css": "^8.0.1",
63 | "postcss-flexbugs-fixes": "^4.1.0",
64 | "postcss-loader": "^3.0.0",
65 | "react-addons-perf": "^15.4.2",
66 | "react-highlight": "^0.10.0",
67 | "react-test-renderer": "^15.6.1",
68 | "sass-loader": "^7.1.0",
69 | "standard-version": "^5.0.1",
70 | "style-loader": "^0.23.1",
71 | "webpack": "^4.29.6",
72 | "webpack-cli": "^3.2.3",
73 | "webpack-dev-server": "^3.2.1"
74 | },
75 | "peerDependencies": {
76 | "react": ">=0.14.0",
77 | "react-dom": ">=0.14.0"
78 | },
79 | "dependencies": {
80 | "babel-runtime": "^6.26.0",
81 | "classnames": "^2.2.5",
82 | "prop-types": "^15.5.10"
83 | },
84 | "keywords": [
85 | "react",
86 | "react-component",
87 | "component",
88 | "scrollspy",
89 | "scroll"
90 | ]
91 | }
92 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Highlight from 'react-highlight'
4 | import Nav from './nav'
5 | import Footer from './footer'
6 | import PropTable from './props-table'
7 |
8 | import '../scss/app.scss'
9 |
10 | const version = require('../../package.json').version
11 |
12 | class App extends React.Component {
13 | render () {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
React Scrollspy
21 |
22 | Scrollspy Component for React
23 | v { version }
24 |
25 |
Github
26 |
27 |
28 |
29 |
30 |
31 |
Getting Started
32 |
33 |
34 |
Install it from npm or yarn.
35 |
36 | {'$ npm install react-scrollspy'}
37 |
38 |
39 |
40 | {'$ yarn add react-scrollspy'}
41 |
42 |
Then, import this library in your JS.
43 |
44 | {'import Scrollspy from \'react-scrollspy\''}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Example
53 |
54 |
55 |
56 |
57 | {``}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
Props
78 |
79 |
82 |
83 |
84 |
85 |
86 |
87 | )
88 | }
89 | }
90 |
91 | ReactDOM.render( , document.getElementById('js-app'))
92 |
--------------------------------------------------------------------------------
/src/js/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Footer = () => (
4 |
5 | Licenced under MIT
6 |
7 | )
8 |
9 | export default Footer
--------------------------------------------------------------------------------
/src/js/lib/scrollspy.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React from 'react'
3 | import classNames from 'classnames'
4 | import throttle from './throttle'
5 |
6 | function isEqualArray(a, b) {
7 | return (
8 | a.length === b.length
9 | &&
10 | a.every((item, index) => {
11 | return item === b[index]
12 | })
13 | )
14 | }
15 |
16 | export default class Scrollspy extends React.Component {
17 |
18 | static get propTypes () {
19 | return {
20 | items: PropTypes.arrayOf(PropTypes.string).isRequired,
21 | currentClassName: PropTypes.string.isRequired,
22 | scrolledPastClassName: PropTypes.string,
23 | style: PropTypes.object,
24 | componentTag: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
25 | offset: PropTypes.number,
26 | rootEl: PropTypes.string,
27 | onUpdate: PropTypes.func,
28 | }
29 | }
30 |
31 | static get defaultProps () {
32 | return {
33 | items: [],
34 | currentClassName: '',
35 | style: {},
36 | componentTag: 'ul',
37 | offset: 0,
38 | onUpdate() {},
39 | }
40 | }
41 |
42 | constructor (props) {
43 | super(props)
44 |
45 | this.state = {
46 | targetItems: [],
47 | inViewState: [],
48 | isScrolledPast: []
49 | }
50 |
51 | // manually bind as ES6 does not apply this
52 | // auto binding as React.createClass does
53 | this._handleSpy = this._handleSpy.bind(this)
54 | }
55 |
56 | _initSpyTarget (items) {
57 | const targetItems = items.map((item) => {
58 |
59 | return document.getElementById(item)
60 | })
61 |
62 | return targetItems
63 | }
64 |
65 | // https://github.com/makotot/react-scrollspy/pull/45
66 | _fillArray (array, val) {
67 | let newArray = []
68 |
69 | for (let i = 0, max = array.length; i < max; i++) {
70 | newArray[i] = val
71 | }
72 |
73 | return newArray
74 | }
75 |
76 | _isScrolled () {
77 | return this._getScrollDimension().scrollTop > 0
78 | }
79 |
80 | _getScrollDimension () {
81 | const doc = document
82 | const { rootEl } = this.props
83 | const scrollTop = rootEl ? doc.querySelector(rootEl).scrollTop : (doc.documentElement.scrollTop || doc.body.parentNode.scrollTop || doc.body.scrollTop)
84 | const scrollHeight = rootEl ? doc.querySelector(rootEl).scrollHeight : (doc.documentElement.scrollHeight || doc.body.parentNode.scrollHeight || doc.body.scrollHeight)
85 |
86 | return {
87 | scrollTop,
88 | scrollHeight,
89 | }
90 | }
91 |
92 | _getElemsViewState (targets) {
93 | let elemsInView = []
94 | let elemsOutView = []
95 | let viewStatusList = []
96 |
97 | const targetItems = targets ? targets : this.state.targetItems
98 |
99 | let hasInViewAlready = false
100 |
101 | for (let i = 0, max = targetItems.length; i < max; i++) {
102 | let currentContent = targetItems[i]
103 | let isInView = hasInViewAlready ? false : this._isInView(currentContent)
104 |
105 | if (isInView) {
106 | hasInViewAlready = true
107 | elemsInView.push(currentContent)
108 | } else {
109 | elemsOutView.push(currentContent)
110 | }
111 |
112 | const isLastItem = i === max - 1
113 | const isScrolled = this._isScrolled()
114 |
115 | // https://github.com/makotot/react-scrollspy/pull/26#issue-167413769
116 | const isLastShortItemAtBottom = this._isAtBottom() && this._isInView(currentContent) && !isInView && isLastItem && isScrolled
117 |
118 | if (isLastShortItemAtBottom) {
119 | elemsOutView.pop()
120 | elemsOutView.push(...elemsInView)
121 | elemsInView = [currentContent]
122 | viewStatusList = this._fillArray(viewStatusList, false)
123 | isInView = true
124 | }
125 |
126 | viewStatusList.push(isInView)
127 | }
128 |
129 | return {
130 | inView: elemsInView,
131 | outView: elemsOutView,
132 | viewStatusList,
133 | scrolledPast: this.props.scrolledPastClassName && this._getScrolledPast(viewStatusList),
134 | }
135 | }
136 |
137 | _isInView (el) {
138 | if (!el) {
139 | return false
140 | }
141 |
142 | const { rootEl, offset } = this.props
143 | let rootRect
144 |
145 | if (rootEl) {
146 | rootRect = document.querySelector(rootEl).getBoundingClientRect()
147 | }
148 |
149 | const rect = el.getBoundingClientRect()
150 | const winH = rootEl ? rootRect.height : window.innerHeight
151 | const { scrollTop } = this._getScrollDimension()
152 | const scrollBottom = scrollTop + winH
153 | const elTop = rootEl ?
154 | rect.top + scrollTop - rootRect.top + offset
155 | :
156 | rect.top + scrollTop + offset
157 | const elBottom = elTop + el.offsetHeight
158 |
159 | return (elTop < scrollBottom) && (elBottom > scrollTop)
160 | }
161 |
162 | _isAtBottom () {
163 | const { rootEl } = this.props
164 | const { scrollTop, scrollHeight } = this._getScrollDimension()
165 | const winH = rootEl ? document.querySelector(rootEl).getBoundingClientRect().height : window.innerHeight
166 | const scrolledToBottom = (scrollTop + winH) >= scrollHeight
167 |
168 | return scrolledToBottom
169 | }
170 |
171 | _getScrolledPast (viewStatusList) {
172 | if (!viewStatusList.some((item) => item)) {
173 | return viewStatusList
174 | }
175 |
176 | let hasFoundInView = false
177 |
178 | const scrolledPastItems = viewStatusList.map((item) => {
179 | if (item && !hasFoundInView) {
180 | hasFoundInView = true
181 |
182 | return false
183 | }
184 |
185 | return !hasFoundInView
186 | })
187 |
188 | return scrolledPastItems
189 | }
190 |
191 | _spy (targets) {
192 | const elemensViewState = this._getElemsViewState(targets)
193 | const currentStatuses = this.state.inViewState
194 |
195 | this.setState({
196 | inViewState: elemensViewState.viewStatusList,
197 | isScrolledPast: elemensViewState.scrolledPast
198 | }, () => {
199 | this._update(currentStatuses)
200 | })
201 | }
202 |
203 | _update (prevStatuses) {
204 | if (isEqualArray(this.state.inViewState, prevStatuses)) {
205 | return
206 | }
207 |
208 | this.props.onUpdate(this.state.targetItems[this.state.inViewState.indexOf(true)])
209 | }
210 |
211 | _handleSpy () {
212 | throttle(this._spy(), 100)
213 | }
214 |
215 | _initFromProps () {
216 | const targetItems = this._initSpyTarget(this.props.items)
217 |
218 | this.setState({
219 | targetItems,
220 | })
221 |
222 | this._spy(targetItems)
223 | }
224 |
225 | offEvent() {
226 | const el = this.props.rootEl ? document.querySelector(this.props.rootEl) : window
227 |
228 | el.removeEventListener('scroll', this._handleSpy)
229 | }
230 |
231 | onEvent() {
232 | const el = this.props.rootEl ? document.querySelector(this.props.rootEl) : window
233 |
234 | el.addEventListener('scroll', this._handleSpy)
235 | }
236 |
237 | componentDidMount () {
238 | this._initFromProps()
239 | this.onEvent()
240 | }
241 |
242 | componentWillUnmount () {
243 | this.offEvent()
244 | }
245 |
246 | UNSAFE_componentWillReceiveProps () {
247 | this._initFromProps()
248 | }
249 |
250 | render () {
251 | const Tag = this.props.componentTag
252 | const {
253 | children,
254 | className,
255 | scrolledPastClassName,
256 | style,
257 | } = this.props
258 | let counter = 0
259 | const items = React.Children.map(children, (child, idx) => {
260 | if (!child) {
261 | return null
262 | }
263 |
264 | const ChildTag = child.type
265 | const isScrolledPast = scrolledPastClassName && this.state.isScrolledPast[idx]
266 | const childClass = classNames({
267 | [`${ child.props.className }`]: child.props.className,
268 | [`${ this.props.currentClassName }`]: this.state.inViewState[idx],
269 | [`${ this.props.scrolledPastClassName }`]: isScrolledPast,
270 | })
271 |
272 | return (
273 |
274 | { child.props.children }
275 |
276 | )
277 | })
278 |
279 | const itemClass = classNames({
280 | [`${ className }`]: className,
281 | })
282 |
283 | return (
284 |
285 | { items }
286 |
287 | )
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/src/js/lib/throttle.js:
--------------------------------------------------------------------------------
1 | const throttle = (fn, threshold = 100) => {
2 | let last
3 | let timer
4 |
5 | return () => {
6 | const now = +new Date()
7 | const timePassed = !!last && (now < last + threshold)
8 |
9 | if (timePassed) {
10 | clearTimeout(timer)
11 |
12 | timer = setTimeout(() => {
13 | last = now
14 | fn()
15 | }, threshold)
16 | } else {
17 | last = now
18 | fn()
19 | }
20 | }
21 | }
22 |
23 | export default throttle
24 |
--------------------------------------------------------------------------------
/src/js/nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Scrollspy from './lib/scrollspy'
3 |
4 | const Nav = () => (
5 |
6 |
9 | {
19 | console.log(el)
20 | }
21 | }
22 | >
23 | Getting Started
24 | Example
25 | Props
26 |
27 |
28 | )
29 |
30 | export default Nav
--------------------------------------------------------------------------------
/src/js/props-table.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const PropTable = () => (
4 |
5 |
6 |
7 | Props
8 | Summary
9 |
10 |
11 |
12 |
13 | items
14 | Id list of target contents.
15 |
16 |
17 | currentClassName
18 | Class name that apply to the navigation element paired with the content element in viewport.
19 |
20 |
21 | scrolledPastClassName
22 | Class name that apply to the navigation elements that have been scrolled past [optional].
23 |
24 |
25 | componentTag
26 | HTML tag or React Component type for Scrollspy component if you want to use something other than {''}
[optional].
27 |
28 |
29 | style
30 | Style attribute to be passed to the generated {''}
element [optional].
31 |
32 |
33 | offset
34 | Offset value that adjusts to determine the elements are in the viewport [optional].
35 |
36 |
37 | rootEl
38 | Name of the element of scrollable container that can be used with {'querySelector'}
[optional].
39 |
40 |
41 | onUpdate
42 | Function to be executed when the active item has been updated [optional].
43 |
44 |
45 |
46 | )
47 |
48 | export default PropTable
49 |
--------------------------------------------------------------------------------
/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | @import "../../node_modules/normalize.css/normalize.css";
4 |
5 | html {
6 | font-size: 16px;
7 | -webkit-font-smoothing: antialiased;
8 | }
9 |
10 | body {
11 | font-family: --apple-system, BlinkMacSystemFont, Heivetica, Arial, sans-serif;
12 | font-weight: 300;
13 | }
14 |
15 | h1, h2, h3, h4, h5, h6 {
16 | margin: 0;
17 | }
18 |
19 | ul, ol {
20 | margin: 0;
21 | padding: 0;
22 | list-style: none;
23 | }
24 |
25 | .wrapper {
26 | background: #222;
27 | color: #fff;
28 | }
29 |
30 | .main {
31 | }
32 |
33 | .title {
34 | letter-spacing: 0.2rem;
35 | text-transform: uppercase;
36 | font-size: 2rem;
37 | font-style: italic;
38 | font-weight: 300;
39 | }
40 |
41 | .title + .description {
42 | margin-top: 1rem;
43 | }
44 |
45 | .description {
46 | display: flex;
47 | flex-direction: column;
48 | }
49 |
50 | .nav {
51 | position: sticky;
52 | display: flex;
53 | flex-direction: column;
54 | top: 0;
55 | background: #fff;
56 | overflow: auto;
57 | white-space: nowrap;
58 |
59 | @media (min-width: 480px) {
60 | flex-direction: row;
61 | }
62 |
63 | &__title {
64 | letter-spacing: 0.2rem;
65 | text-transform: uppercase;
66 | font-style: italic;
67 | font-weight: 300;
68 | }
69 |
70 | &__inner {
71 | display: flex;
72 | margin: 0;
73 | padding: 0;
74 | }
75 |
76 | &__item {
77 | list-style-type: none;
78 | border-bottom: 2px solid #fff;
79 | }
80 |
81 | &__item--inverse {
82 | background: linear-gradient(45deg, #5500f0, #3f5bff);
83 | border-bottom: 2px solid #5500f0;
84 | }
85 |
86 | &__item--active {
87 | font-weight: 700;
88 | border-bottom: 2px solid #5500f0;
89 | }
90 |
91 | &__link {
92 | display: flex;
93 | padding: 1rem;
94 | color: #5500f0;
95 | text-decoration: none;
96 | transition: background 0.3s ease-in;
97 |
98 | &:hover {
99 | background: #5500f0;
100 | color: #fff;
101 | }
102 | }
103 |
104 | &__item--inverse &__link {
105 | color: #fff;
106 | }
107 | }
108 |
109 | .section {
110 | min-height: 90vh;
111 | padding: 1rem;
112 | }
113 |
114 | .section:nth-child(even) {
115 | background: #333;
116 | }
117 |
118 | .section-title {
119 | padding: 2rem 0;
120 | display: flex;
121 | justify-content: center;
122 | letter-spacing: 0.2rem;
123 | text-transform: uppercase;
124 | font-weight: 300;
125 | }
126 |
127 | .section-title__inner {
128 | display: inline;
129 | border-bottom: 1px solid #fff;
130 | font-size: 1.4rem;
131 | line-height: 2;
132 | text-transform: uppercase;
133 | }
134 |
135 | .section-title + .section-body {
136 | margin-top: 2rem;
137 | }
138 |
139 | .section-body {
140 | max-width: 720px;
141 | margin: 0 auto;
142 | }
143 |
144 | .hero {
145 | display: flex;
146 | flex-direction: column;
147 | align-items: center;
148 | justify-content: center;
149 | height: 100vh;
150 | text-align: center;
151 | line-height: 2;
152 | }
153 |
154 | .link {
155 | color: #fff;
156 |
157 | &:hover {
158 | background: #5500f0;
159 | }
160 | }
161 |
162 | .footer {
163 | padding: 2rem;
164 | text-align: center;
165 | }
166 |
167 | .code {
168 | padding: 1rem;
169 | margin: 0;
170 | border-left: 0.4rem solid #5500f0;
171 | background-color: #f7f7f7;
172 | color: #5d5d5d;
173 | white-space: pre;
174 | overflow-x: auto;
175 | }
176 |
177 | .github-link {
178 | margin-top: 1rem;
179 | padding: 1rem;
180 | line-height: 1;
181 | border: 1px solid #fff;
182 | color: #fff;
183 | text-decoration: none;
184 | transition: background 0.3s ease-in;
185 |
186 | &:hover {
187 | background: #5500f0;
188 | border: 1px solid #5500f0;
189 | }
190 | }
191 |
192 | .table {
193 | table-layout: fixed;
194 | width: 100%;
195 | border-collapse: collapse;
196 |
197 | &__head {
198 | text-align: left;
199 | font-weight: 400;
200 | padding: 0.6rem;
201 | border-bottom: 0.4rem solid #5500f0;
202 | }
203 |
204 | &__data {
205 | padding: 0.6rem;
206 | font-weight: 300;
207 | }
208 |
209 | tr:nth-child(odd) > &__data {
210 | background-color: #222;
211 | }
212 |
213 | tr:nth-child(even) > &__data {
214 | background-color: #333;
215 | }
216 | }
--------------------------------------------------------------------------------
/src/templates/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Scrollspy
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const HtmlWebpackPlguin = require('html-webpack-plugin')
4 |
5 | module.exports = {
6 | mode: 'production',
7 | entry: {
8 | 'app.min': [
9 | './src/js/app.js',
10 | ],
11 | },
12 | resolve: {
13 | extensions: ['*', '.js'],
14 | modules: [
15 | 'node_modules',
16 | path.join(__dirname, 'src/js'),
17 | ],
18 | },
19 | output: {
20 | path: path.join(__dirname, 'dist'),
21 | filename: 'js/[name].js',
22 | publicPath: './',
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.scss$/,
28 | exclude: /(node_modules)/,
29 | include: [
30 | path.resolve(__dirname, 'src/scss'),
31 | ],
32 | use: [
33 | {
34 | loader: 'style-loader',
35 | },
36 | {
37 | loader: 'css-loader',
38 | },
39 | {
40 | loader: 'postcss-loader',
41 | options: {
42 | plugins: (loader) => [
43 | require('postcss-flexbugs-fixes')(),
44 | require('autoprefixer')(),
45 | require('cssnano')(),
46 | ],
47 | },
48 | },
49 | {
50 | loader: 'sass-loader',
51 | },
52 | ],
53 | },
54 | {
55 | test: /\.(js|jsx)$/,
56 | exclude: /(node_modules)/,
57 | include: [
58 | path.resolve(__dirname, 'src/js'),
59 | ],
60 | use: [
61 | {
62 | loader: 'babel-loader',
63 | options: {
64 | cacheDirectory: false,
65 | },
66 | },
67 | ],
68 | },
69 | {
70 | test: /\.(js|jsx)$/,
71 | exclude: /(node_modules)/,
72 | include: [
73 | path.resolve(__dirname, 'src/js'),
74 | ],
75 | use: [
76 | {
77 | loader: 'eslint-loader',
78 | options: {
79 | },
80 | },
81 | ],
82 | },
83 | ],
84 | },
85 | devServer: {
86 | contentBase: './dist',
87 | publicPath: '/',
88 | port: 8080,
89 | },
90 | plugins: [
91 | new HtmlWebpackPlguin({
92 | filename: 'index.html',
93 | template: './src/templates/index.ejs',
94 | }),
95 | ],
96 | }
97 |
--------------------------------------------------------------------------------