├── CHANGELOG.md
├── demo
├── default.gif
├── autoFrame.gif
├── autoScroll.gif
├── index.js
├── Demo.test.js
├── index.html
├── index.css
└── Demo.js
├── .travis.yml
├── .gitignore
├── .babelrc
├── src
├── index.js
├── scrollInitalState.js
├── references.md
├── nodeToScrollState.js
├── nodeChildrenToScrollState.js
└── Scroller.js
├── test
└── test.js
├── LICENSE
├── webpack.config.js
├── package.json
└── README.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## CHANGELOG
2 |
3 | ### 0.1.0
4 | First commit
5 |
--------------------------------------------------------------------------------
/demo/default.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/du5rte/react-skroll/HEAD/demo/default.gif
--------------------------------------------------------------------------------
/demo/autoFrame.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/du5rte/react-skroll/HEAD/demo/autoFrame.gif
--------------------------------------------------------------------------------
/demo/autoScroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/du5rte/react-skroll/HEAD/demo/autoScroll.gif
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | cache:
6 | directories:
7 | - node_modules
8 |
9 | node_js:
10 | - "6"
11 |
12 | script:
13 | - npm test
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OSX Files
2 | .DS_Store
3 | .Trashes
4 | .Spotlight-V100
5 | .AppleDouble
6 | .LSOverride
7 | .icloud
8 |
9 | # NPM
10 | /node_modules/
11 | npm-debug.log
12 | /dist/
13 | /lib/
14 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import Demo from './Demo'
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('render')
9 | )
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-export-default-from",
8 | "@babel/plugin-proposal-class-properties"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/demo/Demo.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Demo from './Demo';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'core-js/fn/object/entries.js'
2 |
3 | export Scroller from './Scroller'
4 |
5 | export scrollInitalState from './scrollInitalState'
6 | export nodeToScrollState from './nodeToScrollState'
7 | export nodeChildrenToScrollState from './nodeChildrenToScrollState'
8 |
9 | export default module.exports
10 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Scroll
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 |
3 | import ReactSkroll, { Scroller, ScrollLink, ScrollProvider, } from '../src'
4 |
5 | describe('Libary', () => {
6 | describe('modules', () => {
7 | it('should export default module', () => {
8 | assert.ok(ReactSkroll)
9 | })
10 |
11 | it('should export modules', () => {
12 | assert.ok(Scroller)
13 | })
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/scrollInitalState.js:
--------------------------------------------------------------------------------
1 | export default {
2 | position: 0,
3 | positionRatio: 0,
4 | start: 0,
5 | end: 0,
6 | viewHeight: 0,
7 | scrollHeight: 0,
8 | ready: false,
9 | onStart: true,
10 | onMiddle: false,
11 | onEnd: false,
12 | children: [],
13 | // autoFrame: props.autoFrame,
14 | // autoScroll: props.autoScroll,
15 | originalPosition: null,
16 | changedPosition: null,
17 | timeStamp: null,
18 | scrolling: false,
19 | wheeling: false,
20 | touching: false,
21 | moving: false,
22 | resting: true,
23 | touches: [],
24 | deltaY: 0,
25 | }
26 |
--------------------------------------------------------------------------------
/src/references.md:
--------------------------------------------------------------------------------
1 | ## References:
2 | - https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.2cnfo15to
3 | - https://github.com/souporserious/react-measure/blob/master/src/Measure.jsx
4 | - https://github.com/ReactTraining/react-router/blob/master/modules/Link.js
5 | - https://facebook.github.io/react/blog/2016/07/13/mixins-considered-harmful.html#context
6 | - https://css-tricks.com/snippets/jquery/smooth-scrolling/
7 | - https://github.com/callmecavs/jump.js
8 | - https://github.com/jlmakes/scrollreveal
9 | - https://www.youtube.com/watch?v=rNsC1VI9388
10 |
11 | ## Decay
12 | - https://stackoverflow.com/questions/35656031/react-native-continue-animation-with-last-velocity-of-touch-gesture
13 | - http://fooo.fr/~vjeux/fb/animated-docs/
14 |
--------------------------------------------------------------------------------
/src/nodeToScrollState.js:
--------------------------------------------------------------------------------
1 | export default function nodeToScrollState({
2 | scrollTop,
3 | scrollHeight,
4 | offsetHeight,
5 | children
6 | }) {
7 | // Interpreting native values
8 | let start = 0
9 | let viewHeight = offsetHeight
10 | let end = scrollHeight - viewHeight
11 |
12 | // current position
13 | let position = scrollTop
14 | let positionRatio = scrollTop / end
15 |
16 | // Conditionals
17 | let onStart = position <= start
18 | let onEnd = position >= end
19 | let onMiddle = !onStart && !onEnd
20 |
21 | // let scrolling = true / false
22 |
23 | let positionRelativeRatio = Math.abs(start - scrollTop / offsetHeight)
24 |
25 | return {
26 | position,
27 | positionRatio,
28 | // positionIndex,
29 | positionRelativeRatio,
30 | start,
31 | end,
32 | viewHeight,
33 | scrollHeight,
34 | onStart,
35 | onMiddle,
36 | onEnd
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Duarte Monteiro
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 |
--------------------------------------------------------------------------------
/src/nodeChildrenToScrollState.js:
--------------------------------------------------------------------------------
1 | export default function nodeChildrenToScrollState({ children, scrollTop }) {
2 | let list = []
3 |
4 | // used to increment children view heights
5 | let start = 0
6 |
7 | // Fix: default props
8 | // let { theshold } = this.props
9 | let theshold = 0.5
10 |
11 | // TODO: experiment a map
12 | for (let i = 0; i < children.length; i++) {
13 | let { offsetHeight, attributes } = children[i]
14 |
15 | // interpreting native values
16 | let viewHeight = offsetHeight
17 | let end = start + viewHeight
18 |
19 | // current position values
20 | let position = start - scrollTop
21 | let positionRatio = position / offsetHeight
22 | let positionRatioRemainer = positionRatio <= -1 ? 1 : positionRatio >= 1 ? 1 : Math.abs(positionRatio % 1)
23 |
24 | /* Used for creating navigations and to links to
25 | *
26 | */
27 |
28 | // Conditionals
29 | // FIX: use exact values
30 | let onView = positionRatio <= theshold && positionRatio >= -theshold
31 | let onFrame = position === scrollTop
32 | // TODO: review active
33 | // TODO: addfunction to run on activate()
34 | let active = onView
35 |
36 | list.push({ position, positionRatio, positionRatioRemainer, start, end, viewHeight, onView, active, onFrame })
37 |
38 | // increament based on stacked item's height
39 | start += offsetHeight
40 | }
41 |
42 | return { children: list }
43 | }
44 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin')
4 | var HtmlWebpackPlugin = require('html-webpack-plugin')
5 |
6 | var env = process.env.NODE_ENV
7 |
8 | var config = {
9 | output: {
10 | library: 'ReactSkroll',
11 | libraryTarget: 'umd',
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(js)$/,
17 | exclude: /node_modules/,
18 | use: { loader: 'babel-loader' }
19 | },
20 | ]
21 | },
22 | resolve: {
23 | extensions: ['.js']
24 | },
25 | plugins: [
26 | new webpack.DefinePlugin({
27 | 'process.env.NODE_ENV': JSON.stringify(env)
28 | })
29 | ]
30 | }
31 |
32 | if (process.env.NODE_ENV !== 'production') {
33 | config.mode = 'development'
34 | config.devtool = 'inline-source-map'
35 |
36 | config.plugins.push(
37 | new HtmlWebpackPlugin({
38 | template: 'demo/index.html'
39 | })
40 | )
41 | }
42 |
43 | if (process.env.NODE_ENV === 'production') {
44 | config.mode = 'production'
45 |
46 | config.externals = {
47 | 'react': 'React',
48 | 'react-dom': 'ReactDOM',
49 | 'react-spring': 'ReactSpring',
50 | 'prop-types': 'PropTypes',
51 | }
52 |
53 | if (process.env.TARGET === 'minify') {
54 | config.optimization = {
55 | minimizer: [
56 | new UglifyJsPlugin()
57 | ]
58 | }
59 | }
60 | }
61 |
62 | module.exports = config
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-skroll",
3 | "description": "Reactive Scrolling",
4 | "version": "0.7.3",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rm -rf dist lib && mkdir dist lib",
8 | "prebuild": "npm run clean",
9 | "build:lib": "NODE_ENV=production babel src --out-dir lib",
10 | "build:umd": "NODE_ENV=production webpack src/index.js -o dist/react-skroll.js",
11 | "build:umd:min": "NODE_ENV=production TARGET=minify webpack src/index.js -o dist/react-skroll.min.js",
12 | "build": "npm run build:lib && npm run build:umd && npm run build:umd:min",
13 | "build:dev": "npm run build:lib -- --watch & npm run test -- --watch",
14 | "dev": "webpack-dev-server demo/index.js --content-base demo/",
15 | "prepublish": "npm run clean && npm run build",
16 | "test": "jest"
17 | },
18 | "license": "MIT",
19 | "homepage": "https://github.com/du5rte/react-skroll#readme",
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/du5rte/react-skroll.git"
23 | },
24 | "author": "Duarte Monteiro (http://du5rte.com)",
25 | "bugs": {
26 | "url": "https://github.com/du5rte/react-skroll/issues"
27 | },
28 | "keywords": [
29 | "react",
30 | "react-spring",
31 | "scroll",
32 | "scroller",
33 | "skroll",
34 | "react-scroll",
35 | "react-skroll"
36 | ],
37 | "files": [
38 | "dist",
39 | "lib"
40 | ],
41 | "dependencies": {
42 | "lodash": ">=4.0.0",
43 | "resize-observer-polyfill": "^1.3.2",
44 | "throttle-debounce": "^1.0.1"
45 | },
46 | "peerDependencies": {
47 | "core-js": ">=2.6.0",
48 | "prop-types": ">=15.0.0",
49 | "react": ">=16.6.0",
50 | "react-dom": ">=16.6.0",
51 | "react-spring": ">=7.0.0"
52 | },
53 | "devDependencies": {
54 | "@babel/cli": "^7.2.0",
55 | "@babel/core": "^7.2.2",
56 | "@babel/plugin-proposal-class-properties": "^7.2.1",
57 | "@babel/plugin-proposal-export-default-from": "^7.2.0",
58 | "@babel/preset-env": "^7.2.0",
59 | "@babel/preset-react": "^7.0.0",
60 | "babel-core": "^7.0.0-bridge.0",
61 | "babel-jest": "^23.6.0",
62 | "babel-loader": "^8.0.4",
63 | "html-webpack-plugin": "^3.2.0",
64 | "jest": "^23.6.0",
65 | "prop-types": "^15.6.2",
66 | "react": "^16.6.3",
67 | "react-dom": "^16.6.3",
68 | "react-spring": "^7.2.1",
69 | "uglifyjs-webpack-plugin": "^2.0.1",
70 | "webpack": "^4.28.0",
71 | "webpack-cli": "^3.1.2",
72 | "webpack-dev-server": "^3.1.10"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/demo/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | html,
7 | body {
8 | position: relative;
9 | width: 100%;
10 | height: 100%;
11 | font: 1em Helvetica, sans-serif;
12 | lineHeight: 1;
13 | color: white;
14 | }
15 |
16 | h1 {
17 | font-size: 20vw;
18 | margin: 0;
19 | }
20 |
21 | nav {
22 | font-size: 1.5vw;
23 | position: absolute;
24 | display: -webkit-box;
25 | display: -webkit-flex;
26 | display: -ms-flexbox;
27 | display: flex;
28 | -webkit-flex-flow: row wrap;
29 | -ms-flex-flow: row wrap;
30 | flex-flow: row wrap;
31 | -webkit-box-align: center;
32 | -webkit-align-items: center;
33 | -ms-flex-align: center;
34 | align-items: center;
35 | -webkit-box-pack: center;
36 | -webkit-justify-content: center;
37 | -ms-flex-pack: center;
38 | justify-content: center;
39 | box-sizing: border-box;
40 | top: 0;
41 | left: 0;
42 | width: 100%;
43 | padding: 0.5em;
44 | }
45 |
46 | h1 {
47 | text-align: middle;
48 | }
49 |
50 | ul {
51 | font-size: 2vw;
52 | margin: 0;
53 | left: 0;
54 | list-style: none;
55 | }
56 |
57 | li {
58 | padding-left: 2vw;
59 | }
60 |
61 | span {
62 | opacity: 0.3;
63 | }
64 | span.inactive {
65 | opacity: 0.5;
66 | }
67 | span.active {
68 | opacity: 1;
69 | }
70 |
71 | section {
72 | display: -webkit-box;
73 | display: -webkit-flex;
74 | display: -ms-flexbox;
75 | display: flex;
76 | height: 100%;
77 | -webkit-box-orient: vertical;
78 | -webkit-box-direction: normal;
79 | -webkit-flex-direction: row;
80 | -ms-flex-direction: row;
81 | flex-direction: row;
82 | -webkit-box-align: center;
83 | -webkit-align-items: center;
84 | -ms-flex-align: center;
85 | align-items: center;
86 | -webkit-box-pack: center;
87 | -webkit-justify-content: center;
88 | -ms-flex-pack: center;
89 | justify-content: center;
90 | }
91 |
92 | .wrapper {
93 | position: fixed;
94 | height: 100%;
95 | width: 100%;
96 | }
97 |
98 | .flex {
99 | display: flex;
100 | flex-direction: column;
101 | }
102 |
103 | .half-width {
104 | width: 50%;
105 | }
106 |
107 | .center-center {
108 | justify-content: center;
109 | align-items: center;
110 | }
111 |
112 | .left-center {
113 | justify-content: center;
114 | align-items: flex-start;
115 | }
116 |
117 | a, button {
118 | font-size: 1em;
119 | color: rgba(0, 0, 0, 0.25);
120 | padding: 0.25em 0.75em;
121 | margin: 0.25em;
122 | background: rgba(0, 0, 0, 0.1);
123 | border-radius: 9999px;
124 | outline: none;
125 | border: none;
126 | cursor: pointer;
127 | user-select: none;
128 | }
129 |
130 | a.active, button.active {
131 | color: rgba(0, 0, 0, 0.75);
132 | background: rgba(0, 0, 0, 0.25);
133 | }
134 |
--------------------------------------------------------------------------------
/demo/Demo.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import { Scroller, scrollInitalState } from '../src'
4 |
5 | function round(val) {
6 | return (Math.round(val * 100) / 100).toFixed(2);
7 | }
8 |
9 | const colors = [
10 | {name: "Blue", color: "#215cf4" },
11 | {name: "Cyan", color: "#0ccabf" },
12 | {name: "Green", color: "#4ac36c" },
13 | {name: "Yellow", color: "#e0be18" },
14 | {name: "Red", color: "#e91e4f" },
15 | {name: "Magenta", color: "#ca28e4" },
16 | ]
17 |
18 | export default class Demo extends Component {
19 | constructor() {
20 | super()
21 |
22 | this.state = {
23 | scroll: scrollInitalState
24 | }
25 | }
26 |
27 | render() {
28 | const { scroll } = this.state
29 |
30 | return (
31 |
96 | )
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-skroll
2 | Uses `react-spring` for butter smooth enhanced scrolling experience
3 |
4 | [](https://travis-ci.org/du5rte/react-skroll)
5 | [](https://github.com/du5rte/react-skroll)
6 | [](CONTRIBUTING.md#pull-requests)
7 | [](CONTRIBUTING.md#pull-requests)
8 |
9 | ## Install
10 | ```
11 | npm install react-skroll --save
12 | ```
13 |
14 | ## UMD
15 | ```
16 |
17 |
18 | ```
19 | (Module exposed as `ReactSkroll`)
20 |
21 | ## Demo
22 | [Codepen Demo](http://codepen.io/du5rte/pen/KrGjEm)
23 |
24 | ## Usage
25 |
26 | ### Functional Children Pattern
27 | Most useful for simple scenarios when you only need the `scroll` inside the `Scroller` scope.
28 |
29 | ```javascript
30 | import { Scroller } from 'react-skroll'
31 |
32 | const Demo = () => (
33 | this.scroll = ref}
35 | autoScroll={true}
36 | autoFrame={true}
37 | >
38 | {scroll =>
39 |
40 |
45 |
46 |
51 | }
52 |
53 | )
54 | ```
55 |
56 | ### Callback Pattern
57 | Most useful for when you only need to read the `scroll` information
58 |
59 | ```javascript
60 | import { Scroller, scrollInitalState } from 'react-skroll'
61 |
62 | class Demo extends Component {
63 | constructor() {
64 | super()
65 |
66 | this.state = {
67 | // recommend to use for first render
68 | scroll: scrollInitalState
69 | }
70 | }
71 |
72 | render() {
73 | return (
74 |
75 | {this.State.scroll.position}
76 |
77 | this.setState({ scroll })}
79 | >
80 |
81 | ...
82 |
83 |
84 | ...
85 |
86 |
87 |
88 | )
89 | }
90 | }
91 | ```
92 |
93 | ### Reference Pattern
94 | Most useful for when you need `scroll` outside the `Scroller` scope, for example in a navigation bar.
95 |
96 | ```javascript
97 | import { Scroller } from 'react-skroll'
98 |
99 | class Demo extends Component {
100 | constructor() {
101 | super()
102 |
103 | this.scroll = null
104 | }
105 |
106 | render() {
107 | return (
108 |
109 |
130 | )
131 | }
132 | }
133 | ```
134 |
135 | ## Props
136 |
137 | ### default
138 | Default scrolling with scrollTo and scroll stats features
139 |
140 | 
141 |
142 |
143 | ### autoFrame
144 | Default scrolling with scrolling reframe the view to the current item
145 |
146 | 
147 |
148 | ### autoScroll
149 | Prevents default scrolling and automatically scroll to next item
150 |
151 | 
152 |
153 | ### this.props.scroll
154 |
155 | Types:
156 | - position: `number`
157 | - positionRatio: `float`
158 | - start: `number`
159 | - end: `number`
160 | - viewHeight: `number`
161 | - scrollHeight: `number`
162 | - ready: `boolean`
163 | - onStart: `boolean`
164 | - onMiddle: `boolean`
165 | - onEnd: `boolean`
166 | - children: `[childScroll]`,
167 | - scrolling: `boolean`
168 | - wheeling: `boolean`
169 | - touching: `boolean`
170 | - moving: `boolean`
171 | - resting: `boolean`
172 | - scrollTo(`position: number` || `name: string` || `node: DOM Element`)
173 | - scrollToPosition(`position`)
174 | - scrollToByIndex(`number`)
175 | - scrollToTop()
176 | - scrollToBottom()
177 | - scrollToElement()
178 | - scrollToActive()
179 |
180 | ### this.props.scroll.children
181 | - name: `string`
182 | - position: `number`
183 | - positionRatio: `float`
184 | - positionRatioRemainer: `float`
185 | - start: `number`
186 | - end: `number`
187 | - viewHeight: `number`
188 | - onView: `boolean`
189 | - active: `boolean`
190 | - onFrame: `boolean`
191 |
192 |
193 | ## More on props
194 | Check out source code:
195 | - [Scroller.js](https://github.com/du5rte/react-skroll/blob/master/src/Scroller.js)
196 | - [contextProviderShape.js](https://github.com/du5rte/react-skroll/blob/master/src/contextProviderShape.js)
197 | - [nodeToScrollState.js](https://github.com/du5rte/react-skroll/blob/master/src/nodeToScrollState.js#L18)
198 | - [nodeChildrenToScrollState.js](https://github.com/du5rte/react-skroll/blob/master/src/nodeChildrenToScrollState.js#L37)
199 |
200 | ## TODO
201 | - [ ] Document
202 | - [ ] Test
203 |
--------------------------------------------------------------------------------
/src/Scroller.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PureComponent } from 'react'
2 | import { Controller, config, Globals, animated } from 'react-spring'
3 | import PropTypes from 'prop-types'
4 |
5 | import isFunction from 'lodash/isFunction'
6 | import { throttle, debounce } from 'throttle-debounce'
7 | import ResizeObserver from 'resize-observer-polyfill'
8 |
9 | import scrollInitalState from './scrollInitalState'
10 | import nodeToScrollState from './nodeToScrollState'
11 | import nodeChildrenToScrollState from './nodeChildrenToScrollState'
12 |
13 | const View = Globals.defaultElement
14 |
15 | export default class Scroller extends Component {
16 | static defaultProps = {
17 | autoFrame: false,
18 | autoScroll: false,
19 | ScrollerNavigation: () => null
20 | }
21 |
22 | constructor(props) {
23 | super(props)
24 |
25 | this.state = {
26 | scroll: scrollInitalState
27 | }
28 |
29 | // debounce is used to mimiques start, move and end events that don't have this functions
30 | this.handleScrollStart = debounce(500, true, this.handleScrollStart)
31 | this.handleResizeMove = throttle(50, this.handleResizeMove)
32 | this.handleScrollEnd = debounce(500, this.handleScrollEnd)
33 | this.handleWheelStart = debounce(100, true, this.handleWheelStart)
34 | this.handleWheelEnd = debounce(100, this.handleWheelEnd)
35 | this.handleResizeStart = debounce(250, true, this.handleResizeStart)
36 | this.handleResizeEnd = debounce(250, this.handleResizeEnd)
37 |
38 | this.scrollToPrevDebounced = debounce(250, true, this.scrollToPrev)
39 | this.scrollToNextDebounced = debounce(250, true, this.scrollToNext)
40 |
41 | this.controller = new Controller({ scroll: 0 })
42 | }
43 |
44 | componentWillUnmount() {
45 | this.deleteRef()
46 | }
47 |
48 | createRef = (ref) => {
49 | this.target = ref
50 |
51 | // add component to resize observer to detect changes on resize
52 | this.resizeObserver = new ResizeObserver((entries, observer) => {
53 | if (this.state.ready) {
54 | this.handleResize()
55 | } else {
56 | this.setStateScroll({
57 | ready: true
58 | })
59 | }
60 | })
61 |
62 | if(this.target){
63 | this.resizeObserver.observe(this.target)
64 | }
65 |
66 | this.props.scrollRef(this.connection)
67 | }
68 |
69 | deleteRef = () => {
70 | if (this.target) {
71 | this.resizeObserver.disconnect(this.target)
72 | }
73 |
74 | this.setStateScroll({
75 | ready: false
76 | })
77 | }
78 |
79 | get connection() {
80 | return {
81 | ...this.state.scroll,
82 | target: this.target,
83 | autoFrame: this.props.autoFrame,
84 | autoScroll: this.props.autoScroll,
85 | scrollToPosition: this.scrollToPosition,
86 | scrollToByIndex: this.scrollToByIndex,
87 | scrollToTop: this.scrollToTop,
88 | scrollToBottom: this.scrollToBottom,
89 | scrollToPrev: this.scrollToPrev,
90 | scrollToNext: this.scrollToNext,
91 | scrollToElement: this.scrollToElement,
92 | scrollToActive: this.scrollToActive,
93 | }
94 | }
95 |
96 | setStateScroll = (additionalStates) => {
97 | const { onScrollChange } = this.props;
98 |
99 | const newScroll = {
100 | ...this.state.scroll,
101 | ...nodeToScrollState(this.target),
102 | ...nodeChildrenToScrollState(this.target),
103 | ...additionalStates
104 | }
105 |
106 | this.setState({ scroll: newScroll })
107 |
108 | if (onScrollChange) {
109 | onScrollChange(newScroll)
110 | }
111 | }
112 |
113 | setStateScrollStart = (additionalStates) => {
114 | const { position } = this.state.scroll
115 |
116 | this.setStateScroll({
117 | originalPosition: position,
118 | timeStamp: Date.now(),
119 | ...additionalStates
120 | })
121 | }
122 |
123 | setStateScrollMove = (additionalStates) => {
124 | this.setStateScroll({
125 | moving: true,
126 | resting: false,
127 | ...additionalStates
128 | })
129 | }
130 |
131 | setStateScrollRest = (additionalStates) => {
132 | this.setStateScroll({
133 | moving: false,
134 | resting: true,
135 | ...additionalStates
136 | })
137 | }
138 |
139 | setStateScrollEnd = (additionalStates) => {
140 | this.setStateScroll({
141 | originalPosition: null,
142 | changedPosition: null,
143 | timeStamp: null,
144 | ...additionalStates
145 | })
146 | }
147 |
148 | findChildOnView = () => {
149 | const { children } = this.state.scroll
150 |
151 | return children.find((child) => child.onView)
152 | }
153 |
154 | findChildIndexOnView = () => {
155 | const { children } = this.state.scroll
156 |
157 | return children.findIndex((child) => child.onView)
158 | }
159 |
160 | scrollToPosition = (position) => {
161 | this.controller.update({
162 | scroll: position,
163 | onFrame: ({ scroll }) => (this.target.scrollTop = scroll),
164 | })
165 | }
166 |
167 | scrollToByIndex = (index) => {
168 | const { children } = this.state.scroll
169 |
170 | this.scrollToPosition(children[index].start)
171 | }
172 |
173 | scrollToTop = () => {
174 | const { start } = this.state.scroll
175 |
176 | this.scrollToPosition(start)
177 | }
178 |
179 | scrollToBottom = () => {
180 | const { end } = this.state.scroll
181 |
182 | this.scrollToPosition(end)
183 | }
184 |
185 | previousOfIndex = (
186 | i=this.findChildIndexOnView(),
187 | arr=this.state.scroll.children
188 | ) => {
189 | return arr[i > 0 ? i - 1 : i]
190 | }
191 |
192 | nextOfIndex = (
193 | i=this.findChildIndexOnView(),
194 | arr=this.state.scroll.children
195 | ) => {
196 | return arr[i < arr.length - 1 ? i + 1 : i]
197 | }
198 |
199 | scrollToPrev = () => {
200 | const prevPosition = this.previousOfIndex().start
201 |
202 | this.scrollToPosition(prevPosition)
203 | }
204 |
205 | scrollToNext = () => {
206 | const nextPosition = this.nextOfIndex().start
207 |
208 | this.scrollToPosition(nextPosition)
209 | }
210 |
211 | scrollToElement = (element, options) => {
212 | const start = element.scrollTop
213 |
214 | this.scrollToPosition(start)
215 | }
216 |
217 | scrollToActive = () => {
218 | let newPosition = this.findChildOnView().start
219 |
220 | this.scrollToPosition(newPosition)
221 | }
222 |
223 | handleScroll = () => {
224 | this.handleScrollStart()
225 | this.handleScrollMove()
226 | this.handleScrollEnd()
227 | }
228 |
229 | handleScrollStart = () => {
230 | this.setStateScrollMove()
231 | }
232 |
233 | handleScrollMove = () => {
234 | this.setStateScroll()
235 | }
236 |
237 | handleScrollEnd = () => {
238 | this.setStateScrollRest()
239 | }
240 |
241 | handleResize = () => {
242 | this.handleResizeStart()
243 | this.handleResizeMove()
244 | this.handleResizeEnd()
245 | }
246 |
247 | handleResizeStart = () => {
248 | this.setStateScrollMove()
249 | }
250 |
251 | handleResizeMove = () => {
252 | this.handleScroll()
253 | }
254 |
255 | handleResizeEnd = () => {
256 | const { autoFrame } = this.props
257 |
258 | if (autoFrame) {
259 | this.scrollToActive()
260 | }
261 | }
262 |
263 | handleWheel = (e) => {
264 | const { autoScroll } = this.props
265 |
266 | if (autoScroll) {
267 | e.preventDefault()
268 | }
269 |
270 | this.handleWheelStart(e)
271 | this.handleWheelMove(e)
272 | this.handleWheelEnd(e)
273 | }
274 |
275 | handleWheelStart = (e) => {
276 | const { autoScroll } = this.props
277 | const { changedPosition } = this.state.scroll
278 |
279 | this.setStateScrollStart({
280 | wheeling: true,
281 | changedPosition: !autoScroll ? null : changedPosition
282 | })
283 |
284 | if (autoScroll) {
285 | const movingUpwards = e.deltaY > 0
286 | const movingDownwards = e.deltaY < 0
287 |
288 | if (movingDownwards) this.scrollToPrevDebounced()
289 | if (movingUpwards) this.scrollToNextDebounced()
290 | }
291 | }
292 |
293 | handleWheelMove = (e) => {
294 | const { autoScroll } = this.props
295 |
296 | if (autoScroll) {
297 | const prev = this.state.deltaY
298 | const next = e.deltaY
299 |
300 | const changed = Math.abs(next) > Math.abs(prev)
301 |
302 | if (changed) {
303 | const movingUpwards = next > 0
304 | const movingDownwards = next < 0
305 |
306 | if (movingDownwards) {
307 | this.scrollToPrevDebounced()
308 | }
309 | if (movingUpwards) {
310 | this.scrollToNextDebounced()
311 | }
312 | }
313 | }
314 |
315 | this.setState({ deltaY: e.deltaY })
316 | }
317 |
318 | handleWheelEnd = (e) => {
319 | const { autoFrame } = this.state.scroll
320 |
321 | this.setStateScrollEnd({
322 | wheeling: false,
323 | deltaY: null
324 | })
325 |
326 | if (autoFrame) this.scrollToActive()
327 | }
328 |
329 | handleTouchStart = (e) => {
330 | this.setStateScrollStart({
331 | touching: true,
332 | touches: e.touches,
333 | })
334 | }
335 |
336 | handleTouchMove = (e) => {
337 | const { touches, originalPosition } = this.state.scroll
338 |
339 | let distanceFromTouchStart = e.changedTouches[0].clientY - touches[0].clientY
340 | let touchPosition = originalPosition - distanceFromTouchStart
341 |
342 | this.scrollToPosition(touchPosition)
343 | }
344 |
345 | handleTouchEnd = (e) => {
346 | const { timeStamp, touches } = this.state.scroll
347 |
348 | const timeLapse = Date.now() - timeStamp
349 |
350 | if (timeLapse < 200) {
351 | const movingUpwards = e.changedTouches[0].clientY < touches[0].clientY
352 | const movingDownwards = e.changedTouches[0].clientY > touches[0].clientY
353 |
354 | if (movingDownwards) this.scrollToPrev()
355 | if (movingUpwards) this.scrollToNext()
356 | } else {
357 | this.scrollToActive()
358 | }
359 |
360 | this.setStateScroll({
361 | touching: false,
362 | })
363 | }
364 |
365 | render() {
366 | const {
367 | children,
368 | autoFrame,
369 | autoScroll,
370 | ScrollerNavigation
371 | } = this.props
372 |
373 | const scroll = this.connection
374 |
375 | return (
376 |
377 |
380 |
389 | {isFunction(children) ? children(scroll) : children}
390 |
391 |
392 | )
393 | }
394 | }
395 |
396 | const containerStyle = {
397 | height: '100%',
398 | width: '100%',
399 | }
400 |
401 | class ScrollerContainer extends PureComponent {
402 | render() {
403 | return
404 | }
405 | }
406 |
407 | class ScrollerContent extends PureComponent {
408 | render() {
409 | const {
410 | scroll,
411 | scrollRef,
412 | autoFrame,
413 | autoScroll,
414 | ...props
415 | } = this.props;
416 |
417 | const style = {
418 | height: '100%',
419 | width: '100%',
420 | overflowY: autoScroll || scroll.touching ? 'hidden' : 'auto',
421 | // TODO: investigar glich on touchScroll with overFlow
422 | // overflowScrolling: 'touch',
423 | // WebkitOverflowScrolling: 'touch',
424 | // overflowY: !autoScroll && !touching ? 'auto' : 'hidden',
425 | }
426 |
427 | return (
428 |
433 | )
434 | }
435 | }
436 |
--------------------------------------------------------------------------------