├── .babelrc
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
└── src
├── .npmignore
├── js
├── AutoAdjust.jsx
├── HorizontalWrapper.jsx
├── LightweightRow.jsx
├── Row.jsx
├── Scrollable.jsx
├── constants.js
├── index.jsx
├── models.js
├── propTypes.js
└── utils.js
└── less
└── scrollable.less
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["transform-object-rest-spread"],
3 | "presets": ["react", "es2015"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "ecmaFeatures": {
4 | "jsx": true
5 | },
6 | "env": {
7 | "es6": true,
8 | "node": true
9 | },
10 | "extends": "airbnb",
11 | "plugins": [
12 | "react"
13 | ],
14 | "globals": {
15 | "window": true,
16 | "document": true
17 | },
18 | "rules": {
19 | "camelcase": [
20 | 2,
21 | {
22 | "properties": "always"
23 | }
24 | ],
25 | "array-bracket-spacing": 0,
26 | "comma-dangle": [2, "never"],
27 | "func-names": 0,
28 | "import/prefer-default-export": 0,
29 | "indent": ["error", 2, { "SwitchCase": 1 }],
30 | "jsx-quotes": [2, "prefer-single"],
31 | "max-len": [2, 120],
32 | "new-cap": 0,
33 | "no-confusing-arrow": 0,
34 | "no-continue": 0,
35 | "no-mixed-spaces-and-tabs": 2,
36 | "no-multi-spaces": 2,
37 | "no-new": 2,
38 | "no-param-reassign": ["error", { props: false }],
39 | "no-trailing-spaces": 2,
40 | "no-undef": 2,
41 | "no-underscore-dangle": 0,
42 | "no-unused-vars": 2,
43 | "object-curly-spacing": 0,
44 | "object-shorthand": [2, "always"],
45 | "react/display-name": 2,
46 | "react/jsx-indent": "off",
47 | "react/jsx-no-undef": 2,
48 | "react/jsx-quotes": 0,
49 | "react/jsx-sort-props": 2,
50 | "react/prefer-stateless-function": 1,
51 | "react/sort-prop-types": 2,
52 | "sort-vars": 2,
53 | "space-before-function-paren": [2, "never"],
54 | "space-in-parens": [2, "never"],
55 | "space-unary-ops": [2, { "words": true, "nonwords": false }],
56 | "vars-on-top": 0
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Tyler Wanek
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rickscroll
2 | **This package is deprecated, please use [https://www.npmjs.com/package/react-window](react-window) or [https://www.npmjs.com/package/react-virtualized](react-virtualized).**
3 |
4 | A high performance scrolling utility for React. Featured in [GitKraken](http://www.gitkraken.com/)
5 |
6 | Basic Usage
7 | ------
8 | Install with npm: `npm install rickscroll`
9 |
10 | ```javascript
11 | import { Rickscroll } from 'rickscroll';
12 |
13 | const list = new Array(10000).fill({}).map(() => ({
14 | contentComponent() { return Hello world!; },
15 | height: 20
16 | }));
17 |
18 | function renderList() {
19 | return ;
20 | }
21 | ```
22 |
23 | *Example written in ES6*
24 |
25 | Features
26 | ------
27 | Rickscroll offloads scrolling onto the GPU by using CSS 3d translations. Rickscroll partitions display rows in small groups that are easy for a browser to paint. This module tries to minimize repaints to at most 1 partition at a time during a scroll event. Not only does this work well, it is incredibly fast, and it can support millions of nodes.
28 |
29 | Rickscroll does not use an infinite loader. When you scroll, your scrollbar will not scale in response to adding more partitions to the DOM. Rickscroll will never grow the DOM to more than 2 + the number of partitions that would reasonably fit in a given display area.
30 |
31 | Rickscroll supports horizontal scrolling. There are other implementations of large, performant scrollers, but none that support decent horizontal scrolling. Rickscroll will horizontally scroll your content component for you but can optionally pass through scroll offsets to your content component, instead. For situations like the graph in GitKraken, it's necessary to have more fine grain control over horizontal scrolling.
32 |
33 | Rickscroll provides gutter functionality. Gutters are an option provided by Rickscroll to provide advanced horizontal scrolling. Often when building large display lists, we want to lock static content to the left or right of our main content. Rickscroll addresses this by providing gutters that work with horizontal scrolling and also provides resizable gutter capabilities.
34 |
35 | Rickscroll provides scrollTo methods for navigating to any location in Rickscroll through props.
36 |
37 | Rickscroll also works well with multi-lists. Multi-lists can specify headers for each list where these headers gain special properties in Rickscroll. Headers are collapsible. Headers are lockable. Headers are stackable. There are methods provided to scrollTo the exact location of a header. No calculations on your part.
38 |
39 | Most importantly, Rickscroll will:
40 | - always gonna scroll you up
41 | - always gonna scroll you down
42 | - never gonna run around and desert you
43 | - never gonna make you cry
44 | - never gonna say goodbye
45 | - never gonna tell a lie and hurt you
46 |
47 | Public Methods
48 | ------
49 | - scrollTo({ x: **integer**, y: **integer** }): Scrolls to the provided pixel value position.
50 | - scrollRowToMiddle(rowIndex: **integer**, x: **integer**): Scrolls the given row index into view with a preference toward the middle of the viewable content. Does not include headers in index calculations.
51 | - scrollToHeader(headerIndex: **integer**, x: **integer**): Scrolls to the header at headerIndex.
52 | - toggleSection(sectionIndex: **integer**): Toggles the visibility of the rows under headerIndex.
53 | - updateDimensions(): Calling this will internally recalculate the dimensions of rickscroll. Rick scroll is already listening to the window resize event, but if you wrap rickscroll with a resizable container, you may need to call updateDimensions when the resizable container is resized.
54 |
55 | PropTypes
56 | ------
57 | - className: **string** *(optional)*
58 | - disableBidirectionalScrolling: **bool** *(optional)* - [default: false]
59 | - dynamicColumn: **string** *(optional)* - [default: 'middle']
60 | - guttersConfig: **object** *(optional)*
61 | - left: **object** *(optional)*
62 | - className: **string** *(optional)*
63 | - handleClassName: **string** *(optional)*
64 | - handleWidth: **number** *(optional)*
65 | - maxPosition: **number** *(optional)*
66 | - minPosition: **number** *(optional)*
67 | - onResize: **function(number)** *(optional)*
68 | - onResizeEnd: **function(number)** *(optional)*
69 | - position: **number**
70 | - right: **object** *(optional)*
71 | - className: **string** *(optional)*
72 | - handleClassName: **string** *(optional)*
73 | - handleWidth: **number** *(optional)*
74 | - maxPosition: **number** *(optional)*
75 | - minPosition: **number** *(optional)*
76 | - onResize: **function(number)** *(optional)*
77 | - onResizeEnd: **function(number)** *(optional)*
78 | - position: **number**
79 | - headerType: **string** *(optional)* - [default: 'default']
80 | - heightAdjust: **string** *(optional)*
81 | - horizontalScrollConfig: **object** *(optional)*
82 | - className: **string** *(optional)*
83 | - contentWidth: **number**
84 | - passthroughOffsets: **bool** *(optional)* - [default: false]
85 | - scaleWithCenterContent: **bool** *(optional)* - [default: false]
86 | - scrollbarHeight: **number** *(optional)* - [default: 15]
87 | - onScroll: **function(number)** *(optional)*
88 | - light: **bool** *(optional)*
89 | - list \*: **array** of **objects** containing
90 | - className: **string** *(optional)*
91 | - contentClassName: **string** *(optional)*
92 | - contentComponent: **React.Component**
93 | - gutters: **object** *(optional)*
94 | - left: **object** *(optional)*
95 | - className: **string** *(optional)*
96 | - contentComponent: **React.Component**
97 | - handleClassName: **string** *(optional)*
98 | - props: **object** *(optional)*
99 | - right: **object** *(optional)*
100 | - className: **string** *(optional)*
101 | - contentComponent: **React.Component**
102 | - handleClassName: **string** *(optional)*
103 | - props: **object** *(optional)*
104 | - height: **number**
105 | - key: **string** *(optional)*
106 | - props: **object** *(optional)*
107 | - lists \*: **array** of **objects** containing
108 | - headerClassName: **string** *(optional)*
109 | - headerComponent: **React.Component**
110 | - headerKey: **string** *(optional)*
111 | - headerProps: **object** *(optional)*
112 | - height: **number**
113 | - initCollapsed: **boolean** *(optional)*
114 | - rows: *(see definition of **list** prop type above)*
115 | - onRegisteredScrollTo: **function** *(optional)*
116 | - scrollTo: **object** *(optional)*
117 | - location: **object** *(optional)*
118 | - x: **number** *(optional)* - [default: 0]
119 | - y: **number** *(optional)* - [default: 0]
120 | - preserveHorizontal: **booelan** *(optional)* - [default: false]
121 | - preserveVertical: **booelan** *(optional)* - [default: false]
122 | - type: **string** *(optional)* - [default: 'row']
123 | - verticalScrollConfig: **object** *(optional)*
124 | - className: **string** *(optional)*
125 | - scrollbarWidth: **number** *(optional)* - [default: 15]
126 | - onScroll: **function(number)** *(optional)*
127 | - widthAdjust: **string** *(optional)*
128 |
129 |
130 | \* rickscroll requires only one of list/lists. One must be set and no more than one should be set.
131 |
132 | Keying Rows
133 | ------
134 | If you choose to use the optional keys on the headers and rows, it's all-or-none.
135 | You must supply completely unique keys to all rows + headers, which are pooled together.
136 | If some keys are undefined, an error will occur. If some keys are duplicated, an error will occur.
137 | It's very important that these keys are unique, because if keys collide, you will see missing rows in viewable area.
138 |
139 | Positioning the Gutters
140 | ------
141 | There are 3 columns in Rickscroll, the left gutter, content area, and right gutter.
142 | The columns are partitioned by a handle, which is specified in the guttersconfig.
143 | The size of the columns are determined by the size position of the handles.
144 | There is now a configuration property on Rickscroll for which column inherits dynamic scaling, dynamicColumn.
145 |
146 | Dynamic column can be one of
147 | - left
148 | - middle
149 | - right
150 |
151 | When dynamic column is right the handle positions are 0 based with respect to the left border of Rickscroll, such that:
152 |
153 | ```
154 | | | | |
155 | 0 l r w
156 | ```
157 |
158 | With the left column being dynamic, the positions are 0 indexed from the right border, such that:
159 | ```
160 | | | | |
161 | w l r 0
162 | ```
163 |
164 | With the middle column being dynamic, the left gutter is indexed off of 0 from the left border and the right gutter is indexed off of 0 from the right border, such that:
165 | ```
166 | | | | |
167 | 0 l r 0
168 | ```
169 |
170 | Supplying a Static Width to Rickscroll
171 | ------
172 | If you have a situation where you need to control the width/height of rickscroll explicitly,
173 | on the Rickscroll import pull out the Static class:
174 |
175 | ```javascript
176 | import { Rickscroll } from 'rickscroll';
177 |
178 | const list = new Array(10000).fill({}).map(() => ({
179 | contentComponent() { return Hello world!; },
180 | height: 20
181 | }));
182 |
183 | function renderList() {
184 | return ;
185 | }
186 | ```
187 |
188 | *Example written in ES6*
189 |
190 | Triggering a ScrollTo Event through Props
191 | ------
192 | There are 3 types of scroll to events that can be triggered, row, header, and position.
193 | The location property of the scrollTo prop has an x and a y field, when the scroll type is set to header or row, y refers to the index of that header or row.
194 | X always refers to a pixel value location in the horizontal space.
195 | If a onRegisteredScrollTo callback is passed as a prop, Rickscroll will call that when it starts a scrollTo operation.
196 |
197 | Listening for Scroll Events
198 | ------
199 | Rickscroll will pass an additional prop to every row/gutter component describing scroll operations.
200 | The isScrolling prop will be true during scrolling.
201 | The isFastScrolling prop represents whether the velocity of a given scroll event is considered fast.
202 | If a scroll event has a sufficient velocity, every row will receive the true as isFastScrolling.
203 | If you are rendering heavy content in every row,
204 | you can use this prop to render a lighter weight representation of your rows.
205 |
206 | Lighter Render Steps
207 | ------
208 | Specify light on Rickscroll to bypass all gutter configuration at the row level.
209 |
210 | Dependencies
211 | ------
212 | Rickscroll is written using es6 via babel. So we ask that you provide `babel-polyfill@^0.6.13`
213 |
214 | Rickscroll is written for `react@^0.14.0`
215 |
216 | License
217 | ------
218 | Copyright (c) 2016 Tyler Wanek
219 |
220 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
221 |
222 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
223 |
224 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
225 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rickscroll",
3 | "version": "0.9.3",
4 | "description": "Optimized scrollable view for react",
5 | "main": "lib/js/index.js",
6 | "scripts": {
7 | "compile": "lessc src/less/scrollable.less lib/css/scrollable.css && babel -d ./lib/js ./src/js",
8 | "lint": "eslint --ext .jsx --ext .js src/js --quiet",
9 | "prepublish": "npm run compile"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/Axosoft/rickscroll.git"
14 | },
15 | "files": [
16 | "lib"
17 | ],
18 | "author": "Axosoft, LLC",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/Axosoft/rickscroll/issues"
22 | },
23 | "homepage": "https://github.com/Axosoft/rickscroll",
24 | "dependencies": {
25 | "animation-timer": "^1.0.12",
26 | "classnames": "^2.2.5",
27 | "functional-easing": "^1.0.8",
28 | "lodash": "^4.14.2",
29 | "resize-observer-polyfill": "^1.2.1"
30 | },
31 | "devDependencies": {
32 | "babel-cli": "^6.11.4",
33 | "babel-core": "^6.9.1",
34 | "babel-eslint": "^6.1.2",
35 | "babel-plugin-transform-object-rest-spread": "^6.19.0",
36 | "babel-preset-es2015": "^6.9.0",
37 | "babel-preset-react": "^6.5.0",
38 | "eslint": "^3.3.1",
39 | "eslint-config-airbnb": "^10.0.1",
40 | "eslint-plugin-import": "^1.13.0",
41 | "eslint-plugin-jsx-a11y": "^2.1.0",
42 | "eslint-plugin-react": "^6.1.2",
43 | "less": "^2.7.1",
44 | "react": "^15.0.0"
45 | },
46 | "peerDependencies": {
47 | "babel-polyfill": "^6.13.0",
48 | "react": "^0.14.0 || ^15.0.0"
49 | },
50 | "keywords": [
51 | "scrolling",
52 | "scrollable",
53 | "scroll",
54 | "performance",
55 | "optimized",
56 | "gpu",
57 | "translate3d",
58 | "fast"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/src/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .eslintrc
3 | src
4 |
--------------------------------------------------------------------------------
/src/js/AutoAdjust.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes as types } from 'react';
2 | import ResizeObserver from 'resize-observer-polyfill';
3 |
4 | import Rickscroll from './Scrollable';
5 |
6 | export default class AutoAdjust extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | height: 0,
11 | width: 0
12 | };
13 | this._autoAdjustDiv = null;
14 | this._getRef = this._getRef.bind(this);
15 | this._getRickscroll = this._getRickscroll.bind(this);
16 | this._updateDimensions = this._updateDimensions.bind(this);
17 | this._resizeObserver = new ResizeObserver(this._updateDimensions);
18 | this.scrollRowToMiddle = this.scrollRowToMiddle.bind(this);
19 | this.scrollTo = this.scrollTo.bind(this);
20 | this.scrollToHeader = this.scrollToHeader.bind(this);
21 | this.toggleSection = this.toggleSection.bind(this);
22 | }
23 |
24 | componentDidMount() {
25 | window.addEventListener('resize', this._updateDimensions);
26 | this._resizeObserver.observe(this._autoAdjustDiv);
27 | this._updateDimensions();
28 | }
29 |
30 | componentDidUpdate() {
31 | this._updateDimensions();
32 | }
33 |
34 | componentWillUnmount() {
35 | window.removeEventListener('resize', this._updateDimensions);
36 | this._resizeObserver.unobserve(this._autoAdjustDiv);
37 | this.setState({ height: 0, width: 0 });
38 | this.autoAdjustDiv = null;
39 | }
40 |
41 | _getRef(ref) {
42 | this._autoAdjustDiv = ref;
43 | }
44 |
45 | _getRickscroll(ref) {
46 | this._rickscroll = ref;
47 | }
48 |
49 | _updateDimensions() {
50 | const {
51 | _autoAdjustDiv,
52 | _autoAdjustDiv: {
53 | clientHeight: height,
54 | clientWidth: width
55 | },
56 | state: {
57 | height: prevHeight,
58 | width: prevWidth
59 | }
60 | } = this;
61 |
62 | if (!_autoAdjustDiv || (prevHeight === height && prevWidth === width)) {
63 | return;
64 | }
65 |
66 | this.setState({ height, width });
67 | }
68 |
69 | // forward rickscroll public functions
70 | scrollRowToMiddle(...args) {
71 | return this._rickscroll && this._rickscroll.scrollRowToMiddle(...args);
72 | }
73 |
74 | scrollTo(...args) {
75 | return this._rickscroll && this._rickscroll.scrollTo(...args);
76 | }
77 |
78 | scrollToHeader(...args) {
79 | return this._rickscroll && this._rickscroll.scrollToHeader(...args);
80 | }
81 |
82 | toggleSection(...args) {
83 | return this._rickscroll && this._rickscroll.toggleSection(...args);
84 | }
85 |
86 | render() {
87 | const {
88 | props: {
89 | heightAdjust,
90 | widthAdjust,
91 | ...props
92 | },
93 | state: {
94 | height,
95 | width
96 | },
97 | _autoAdjustDiv
98 | } = this;
99 |
100 | const style = {
101 | height: heightAdjust
102 | ? `calc(100% - ${heightAdjust})`
103 | : '100%',
104 | width: widthAdjust
105 | ? `calc(100% - ${widthAdjust})`
106 | : '100%'
107 | };
108 |
109 | const child = _autoAdjustDiv
110 | ?
111 | : null;
112 |
113 | return (
114 |
115 | {child}
116 |
117 | );
118 | }
119 | }
120 |
121 | AutoAdjust.propTypes = {
122 | heightAdjust: types.oneOfType([types.string, types.number]),
123 | widthAdjust: types.oneOfType([types.string, types.number])
124 | };
125 |
--------------------------------------------------------------------------------
/src/js/HorizontalWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes as types } from 'react';
2 | import _ from 'lodash';
3 |
4 | export default class HorizontalWrapper extends React.Component {
5 | shouldComponentUpdate(nextProps, nextState) {
6 | return !_.isEqual(nextProps, this.props) || !_.isEqual(nextState, this.state);
7 | }
8 |
9 | render() {
10 | const { children, offset } = this.props;
11 | const horizontalWrapperStyle = {
12 | transform: `translate3d(-${offset}px, -0px, 0px)`
13 | };
14 | return {children}
;
15 | }
16 | }
17 |
18 | HorizontalWrapper.propTypes = {
19 | children: types.node.isRequired,
20 | offset: types.number.isRequired
21 | };
22 |
--------------------------------------------------------------------------------
/src/js/LightweightRow.jsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { PropTypes as types } from 'react';
3 |
4 | import HorizontalWrapper from './HorizontalWrapper';
5 | import * as customTypes from './propTypes';
6 |
7 | export default class LightweightRow extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this._buildContentComponent = this._buildContentComponent.bind(this);
12 | this._cacheMiss = this._cacheMiss.bind(this);
13 | this._getContentComponentWithShallowCaching = this._getContentComponentWithShallowCaching.bind(this);
14 | this._shouldWrapComponent = this._shouldWrapComponent.bind(this);
15 |
16 | this._cache = {
17 | props: {},
18 | renderedRow: null
19 | };
20 | }
21 |
22 | _buildContentComponent() {
23 | const {
24 | contentComponent: ContentComponent,
25 | horizontalTransform,
26 | isFastScrolling,
27 | isScrolling,
28 | passthroughOffsets,
29 | rowProps,
30 | scrollsHorizontally
31 | } = this.props;
32 |
33 | // save from last render
34 | this._cache.props = {
35 | horizontalTransform,
36 | isFastScrolling,
37 | isScrolling,
38 | passthroughOffsets,
39 | rowProps,
40 | scrollsHorizontally
41 | };
42 |
43 | if (scrollsHorizontally && !passthroughOffsets) {
44 | this._cache.renderedRow = (
45 |
52 | );
53 | } else {
54 | this._cache.renderedRow = (
55 |
62 | );
63 | }
64 |
65 | return this._cache.renderedRow;
66 | }
67 |
68 | _cacheMiss() {
69 | return !this._cache.renderedRow
70 | || this._cache.props.horizontalTransform !== this.props.horizontalTransform
71 | || this._cache.props.isFastScrolling !== this.props.isFastScrolling
72 | || this._cache.props.isScrolling !== this.props.isScrolling
73 | || this._cache.props.passthroughOffsets !== this.props.passthroughOffsets
74 | || this._cache.props.rowProps !== this.props.rowProps
75 | || this._cache.props.scrollsHorizontally !== this.props.scrollsHorizontally;
76 | }
77 |
78 | _getContentComponentWithShallowCaching() {
79 | return this._cacheMiss()
80 | ? this._buildContentComponent()
81 | : this._cache.renderedRow;
82 | }
83 |
84 | _shouldWrapComponent() {
85 | return this.props.scrollsHorizontally && !this.props.passthroughOffsets;
86 | }
87 |
88 | render() {
89 | const {
90 | className: thisRowClassName,
91 | contentClassName: thisContentClassName,
92 | horizontalTransform,
93 | onClick,
94 | rowHeight,
95 | width
96 | } = this.props;
97 | const rowStyle = {
98 | height: `${rowHeight}px`,
99 | width: `${width}px`
100 | };
101 |
102 | const contentComponent = this._getContentComponentWithShallowCaching();
103 |
104 | const horizontallyWrappedContentComponent =
105 | this._shouldWrapComponent()
106 | ? (
107 |
108 | {contentComponent}
109 |
110 | )
111 | : contentComponent;
112 |
113 | const rowClassName = classnames('rickscroll__row', thisRowClassName);
114 | const contentClassName = classnames('rickscroll__content', thisContentClassName);
115 |
116 | return (
117 |
118 |
119 | {horizontallyWrappedContentComponent}
120 |
121 |
122 | );
123 | }
124 | }
125 |
126 | LightweightRow.propTypes = {
127 | className: types.string,
128 | contentClassName: types.string,
129 | contentComponent: customTypes.renderableComponent,
130 | horizontalTransform: types.number,
131 | isFastScrolling: types.bool,
132 | isScrolling: types.bool,
133 | onClick: types.func,
134 | passthroughOffsets: types.bool,
135 | rowHeight: types.number.isRequired,
136 | rowProps: types.object,
137 | scrollsHorizontally: types.bool.isRequired,
138 | width: types.number.isRequired
139 | };
140 |
--------------------------------------------------------------------------------
/src/js/Row.jsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { PropTypes as types } from 'react';
3 | import _ from 'lodash';
4 |
5 | import * as constants from './constants';
6 | import HorizontalWrapper from './HorizontalWrapper';
7 | import * as customTypes from './propTypes';
8 | import { getGutterWidths, getWidthStyle } from './utils';
9 |
10 | export default class Row extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | this._getRenderableHandle = this._getRenderableHandle.bind(this);
14 | }
15 |
16 | shouldComponentUpdate(nextProps, nextState) {
17 | return !_.isEqual(nextProps, this.props) || !_.isEqual(nextState, this.state);
18 | }
19 |
20 | _getRenderableGutter(side, index, {
21 | className: thisGutterClassName,
22 | contentComponent: ContentComponent,
23 | props = {}
24 | } = {}, width, gutterClassName) {
25 | const {
26 | isFastScrolling = false,
27 | isScrolling = false
28 | } = this.props;
29 | const gutterStyle = getWidthStyle(width);
30 | const className = classnames('rickscroll__gutter', gutterClassName, thisGutterClassName);
31 | return ContentComponent && gutterStyle ? (
32 |
33 |
39 |
40 | ) : undefined;
41 | }
42 |
43 | _getRenderableHandle(side, { contentComponent, handleClassName: thisHandleClassName } = {}, width, handleClassName) {
44 | const {
45 | guttersConfig: {
46 | [side]: {
47 | onResize
48 | } = {}
49 | } = {},
50 | onStartResize = (() => () => {})
51 | } = this.props;
52 | const handleStyle = getWidthStyle(width);
53 | const className = classnames(
54 | handleClassName,
55 | thisHandleClassName,
56 | 'rickscroll__handle',
57 | { 'rickscroll__handle--grabbable': !!onResize },
58 | `rickscroll__handle--${side}`
59 | );
60 | return contentComponent && handleStyle ? (
61 |
67 | ) : undefined;
68 | }
69 |
70 | render() {
71 | const {
72 | className: thisRowClassName,
73 | contentComponent: ContentComponent,
74 | contentClassName: thisContentClassName,
75 | dynamicColumn = constants.columns.MIDDLE,
76 | guttersConfig,
77 | guttersConfig: {
78 | left: leftCanExist,
79 | left: {
80 | className: leftGutterClassName,
81 | handleClassName: leftHandleClassName,
82 | handleWidth: leftHandleWidth,
83 | position: leftHandlePosition
84 | } = {},
85 | right: rightCanExist,
86 | right: {
87 | className: rightGutterClassName,
88 | handleClassName: rightHandleClassName,
89 | handleWidth: rightHandleWidth = constants.RIGHT_HANDLE_WIDTH,
90 | position: rightHandlePosition
91 | } = {}
92 | } = {},
93 | gutters = {},
94 | horizontalTransform,
95 | index,
96 | isFastScrolling = false,
97 | isScrolling = false,
98 | onClick,
99 | passthroughOffsets,
100 | rowHeight,
101 | rowProps = {},
102 | scrollsHorizontally = false,
103 | width
104 | } = this.props;
105 | const rowStyle = {
106 | height: `${rowHeight}px`,
107 | width: `${width}px`
108 | };
109 |
110 | const { leftGutterWidth, rightGutterWidth } = getGutterWidths({
111 | dynamicColumn: guttersConfig
112 | ? dynamicColumn
113 | : constants.columns.MIDDLE,
114 | leftHandlePosition,
115 | leftHandleWidth,
116 | rightHandlePosition,
117 | rightHandleWidth,
118 | width
119 | });
120 |
121 | const leftComponent = leftCanExist
122 | ? this._getRenderableGutter(
123 | constants.columns.LEFT,
124 | index,
125 | gutters.left,
126 | leftGutterWidth,
127 | leftGutterClassName
128 | )
129 | : null;
130 | const leftHandleComponent = leftCanExist
131 | ? this._getRenderableHandle(
132 | constants.columns.LEFT,
133 | gutters.left,
134 | leftHandleWidth,
135 | leftHandleClassName
136 | )
137 | : null;
138 | const rightComponent = rightCanExist
139 | ? this._getRenderableGutter(
140 | constants.columns.RIGHT,
141 | index,
142 | gutters.right,
143 | rightGutterWidth,
144 | rightGutterClassName
145 | )
146 | : null;
147 | const rightHandleComponent = rightCanExist
148 | ? this._getRenderableHandle(
149 | constants.columns.RIGHT,
150 | gutters.right,
151 | rightHandleWidth,
152 | rightHandleClassName
153 | )
154 | : null;
155 |
156 | let contentComponent;
157 | if (scrollsHorizontally) {
158 | contentComponent = passthroughOffsets
159 | ? (
160 |
167 | )
168 | : (
169 |
170 |
171 |
172 | );
173 | } else {
174 | contentComponent = (
175 |
176 | );
177 | }
178 |
179 | const rowClassName = classnames('rickscroll__row', thisRowClassName);
180 | const contentClassName = classnames('rickscroll__content', thisContentClassName);
181 |
182 | return (
183 |
184 | {leftComponent}
185 | {leftHandleComponent}
186 | {contentComponent}
187 | {rightHandleComponent}
188 | {rightComponent}
189 |
190 | );
191 | }
192 | }
193 |
194 | Row.propTypes = {
195 | className: types.string,
196 | contentClassName: types.string,
197 | contentComponent: customTypes.renderableComponent,
198 | dynamicColumn: customTypes.column,
199 | gutters: customTypes.gutters,
200 | guttersConfig: customTypes.guttersConfig,
201 | horizontalTransform: types.number,
202 | index: types.number.isRequired,
203 | isFastScrolling: types.bool,
204 | isScrolling: types.bool,
205 | onClick: types.func,
206 | onStartResize: types.func,
207 | passthroughOffsets: types.bool,
208 | rowHeight: types.number.isRequired,
209 | rowProps: types.object,
210 | scrollsHorizontally: types.bool.isRequired,
211 | width: types.number.isRequired
212 | };
213 |
--------------------------------------------------------------------------------
/src/js/Scrollable.jsx:
--------------------------------------------------------------------------------
1 | import { AnimationTimer } from 'animation-timer';
2 | import classnames from 'classnames';
3 | import { Easer } from 'functional-easing';
4 | import React, { PropTypes as types } from 'react';
5 | import _ from 'lodash';
6 |
7 | import * as constants from './constants';
8 | import * as customTypes from './propTypes';
9 | import LightweightRow from './LightweightRow';
10 | import { Point } from './models';
11 | import Row from './Row';
12 | import {
13 | buildRowConfig,
14 | getGutterWidths,
15 | getMaxHeight,
16 | getResizeValues,
17 | getVelocityInfo,
18 | getVerticalScrollValues,
19 | returnWidthIfComponentExists
20 | } from './utils';
21 |
22 | const easer = new Easer()
23 | .using('out-cubic');
24 |
25 | export default class Scrollable extends React.Component {
26 | constructor(props) {
27 | super(props);
28 | [
29 | '_applyScrollChange',
30 | '_getBottomGutterHeight',
31 | '_getContentWidth',
32 | '_getDimensions',
33 | '_getThrottledHorizontalAnimationFrameFn',
34 | '_getThrottledVerticalAnimationFrameFn',
35 | '_getTopGutterHeight',
36 | '_getWeightedWidth',
37 | '_onHorizontalScroll',
38 | '_onResize',
39 | '_onMouseWheel',
40 | '_onVerticalScroll',
41 | '_renderContents',
42 | '_renderCorner',
43 | '_renderHorizontalScrollbar',
44 | '_renderHeader',
45 | '_renderVerticalScrollbar',
46 | '_shouldRenderScrollbars',
47 | '_startResize',
48 | '_stopResize',
49 | '_switchScrollProp',
50 | 'scrollRowToMiddle',
51 | 'scrollToHeader',
52 | 'toggleSection'
53 | ].forEach(method => { this[method] = this[method].bind(this); });
54 | this._debouncedStartHorizontalScroll = _.debounce(this._startHorizontalScroll, 100);
55 | this._endScroll = _.debounce(() => this.setState({
56 | isFastScrolling: false,
57 | isScrolling: false,
58 | velocityQueue: [[0, 0]]
59 | }), 50, { trailing: true });
60 | this._endVerticalScroll = _.debounce(() => this.setState({
61 | isFastScrolling: false,
62 | isScrolling: false,
63 | velocityQueue: [[0, 0]]
64 | }), 200, { trailing: true });
65 | this._onThrottledMouseWheel = _.throttle(this._applyScrollChange, constants.ANIMATION_FPS_120, { trailing: true });
66 | const {
67 | headerType = constants.headerType.DEFAULT,
68 | height,
69 | list,
70 | lists,
71 | width
72 | } = props;
73 | const stackingHeaders = headerType === constants.headerType.STACKING;
74 | const listContainer = list || lists;
75 | const {
76 | avgRowHeight,
77 | collapsedSections,
78 | contentHeight,
79 | headers,
80 | partitions,
81 | rowOffsets,
82 | rows
83 | } = buildRowConfig(listContainer, stackingHeaders);
84 | const {
85 | displayBuffer,
86 | shouldRender
87 | } = this._getDimensions(avgRowHeight, contentHeight, height, width);
88 |
89 | this.state = {
90 | animation: null,
91 | avgRowHeight,
92 | collapsedSections,
93 | contentHeight,
94 | displayBuffer,
95 | isFastScrolling: false,
96 | headers,
97 | horizontalTransform: 0,
98 | partitions,
99 | resize: {
100 | basePosition: 0,
101 | currentPosition: 0,
102 | performing: false,
103 | side: '',
104 | startingPosition: 0
105 | },
106 | rowOffsets,
107 | rows,
108 | isScrolling: false,
109 | scrollingToPosition: new Point(0, 0),
110 | shouldRender,
111 | topPartitionIndex: 0,
112 | velocityQueue: [[0, 0]],
113 | verticalTransform: 0
114 | };
115 | }
116 |
117 | componentDidMount() {
118 | const {
119 | scrollTo: scrollToWithoutDefaults
120 | } = this.props;
121 |
122 | if (!scrollToWithoutDefaults) {
123 | return;
124 | }
125 |
126 | this._switchScrollProp(_.merge(
127 | _.cloneDeep(constants.defaultScrollTo),
128 | _.cloneDeep(scrollToWithoutDefaults)
129 | ));
130 | }
131 |
132 | componentWillReceiveProps({
133 | guttersConfig,
134 | headerType = constants.headerType.DEFAULT,
135 | height,
136 | horizontalScrollConfig,
137 | list: nextList,
138 | lists: nextLists,
139 | scrollTo: scrollToWithoutDefaults = {},
140 | verticalScrollConfig,
141 | width
142 | }) {
143 | const {
144 | props: {
145 | guttersConfig: prevGuttersConfig,
146 | height: prevHeight,
147 | horizontalScrollConfig: prevHorizontalScrollConfig,
148 | list: prevList,
149 | lists: prevLists,
150 | scrollTo: prevScrollToWithoutDefaults = {},
151 | verticalScrollConfig: prevVerticalScrollConfig,
152 | width: prevWidth
153 | },
154 | state: {
155 | avgRowHeight: prevAvgRowHeight,
156 | collapsedSections: oldCollapsedSections,
157 | contentHeight: prevContentHeight
158 | }
159 | } = this;
160 |
161 | const stackingHeaders = headerType === constants.headerType.STACKING;
162 | const prevListContainer = prevList || prevLists;
163 | const nextListContainer = nextList || nextLists;
164 |
165 | if (prevListContainer !== nextListContainer || !_.isEqual(prevListContainer, nextListContainer)) {
166 | const {
167 | avgRowHeight,
168 | collapsedSections,
169 | contentHeight,
170 | headers,
171 | partitions,
172 | rowOffsets,
173 | rows
174 | } = buildRowConfig(nextListContainer, stackingHeaders, oldCollapsedSections);
175 | this.setState({
176 | avgRowHeight,
177 | collapsedSections,
178 | contentHeight,
179 | headers,
180 | partitions,
181 | rowOffsets,
182 | rows,
183 | ...this._getDimensions(avgRowHeight, contentHeight, height, width)
184 | });
185 | } else if (
186 | height !== prevHeight
187 | || width !== prevWidth
188 | || !_.isEqual(prevGuttersConfig, guttersConfig)
189 | || !_.isEqual(prevHorizontalScrollConfig, horizontalScrollConfig)
190 | || !_.isEqual(prevVerticalScrollConfig, verticalScrollConfig)
191 | ) {
192 | this.setState(this._getDimensions(prevAvgRowHeight, prevContentHeight, height, width));
193 | }
194 |
195 | const prevScrollTo = _.merge(
196 | _.cloneDeep(constants.defaultScrollTo),
197 | _.cloneDeep(prevScrollToWithoutDefaults)
198 | );
199 | const scrollTo = _.merge(
200 | _.cloneDeep(constants.defaultScrollTo),
201 | _.cloneDeep(scrollToWithoutDefaults)
202 | );
203 |
204 | if (!_.isEqual(prevScrollTo, scrollTo)) {
205 | this._switchScrollProp(scrollTo);
206 | }
207 | }
208 |
209 | shouldComponentUpdate(nextProps, nextState) {
210 | return this.state.verticalTransform !== nextState.verticalTransform
211 | || this.state.horizontalTransform !== nextState.horizontalTransform
212 | || this.props.height !== nextProps.height
213 | || this.props.width !== nextProps.width
214 | || !_.isEqual(this.props, nextProps)
215 | || !_.isEqual(this.state, nextState);
216 | }
217 |
218 | // private
219 |
220 | _applyScrollChange({ deltaX: _deltaX, deltaY: _deltaY }) {
221 | const {
222 | props: {
223 | disableBidirectionalScrolling = false,
224 | height,
225 | horizontalScrollConfig,
226 | horizontalScrollConfig: {
227 | onScroll: onHorizontalScroll = () => {}
228 | } = {},
229 | verticalScrollConfig: {
230 | onScroll: onVerticalScroll = () => {}
231 | } = {}
232 | },
233 | state: {
234 | contentHeight,
235 | isFastScrolling: oldFastScrollTrip,
236 | partitions,
237 | shouldRender,
238 | velocityQueue: oldVelocityQueue
239 | },
240 | _horizontalScrollbar,
241 | _verticalScrollbar
242 | } = this;
243 | const withHorizontalScrolling = !!horizontalScrollConfig && shouldRender.horizontalScrollbar;
244 |
245 | let deltaX = _deltaX;
246 | let deltaY = _deltaY;
247 |
248 | if (disableBidirectionalScrolling) {
249 | if (Math.abs(deltaX) > Math.abs(deltaY)) {
250 | deltaY = 0;
251 | } else {
252 | deltaX = 0;
253 | }
254 | }
255 |
256 | const scrollChanges = {
257 | isScrolling: true
258 | };
259 |
260 | // vertical
261 | if (shouldRender.verticalScrollbar) {
262 | const maxHeight = getMaxHeight(contentHeight, _verticalScrollbar.offsetHeight);
263 | const verticalTransform = this.state.verticalTransform + deltaY;
264 | const { averageVelocity, velocityQueue } = getVelocityInfo(deltaY, oldVelocityQueue);
265 |
266 | const isFastScrolling = oldFastScrollTrip
267 | || Math.abs(averageVelocity) > (height * constants.USER_INITIATED_FAST_SCROLL_FACTOR);
268 |
269 | _.assign(
270 | scrollChanges,
271 | getVerticalScrollValues(verticalTransform, maxHeight, partitions),
272 | { isFastScrolling, velocityQueue }
273 | );
274 | }
275 |
276 | // horizontal scrolling
277 | if (withHorizontalScrolling) {
278 | scrollChanges.horizontalTransform = _.clamp(
279 | this.state.horizontalTransform + deltaX,
280 | 0,
281 | _horizontalScrollbar.scrollWidth - _horizontalScrollbar.offsetWidth
282 | );
283 | }
284 |
285 | this.setState(scrollChanges, () => {
286 | if (shouldRender.verticalScrollbar) {
287 | if (_verticalScrollbar.scrollTop !== scrollChanges.verticalTransform) {
288 | onVerticalScroll(scrollChanges.verticalTransform);
289 | }
290 | _verticalScrollbar.scrollTop = scrollChanges.verticalTransform;
291 | }
292 |
293 | if (withHorizontalScrolling) {
294 | if (_horizontalScrollbar.scrollLeft !== scrollChanges.horizontalTransform) {
295 | onHorizontalScroll(scrollChanges.horizontalTransform);
296 | }
297 | _horizontalScrollbar.scrollLeft = scrollChanges.horizontalTransform;
298 | }
299 |
300 | this._endScroll();
301 | });
302 | }
303 |
304 | _getBottomGutterHeight() {
305 | const {
306 | props: {
307 | height,
308 | headerType = constants.headerType.DEFAULT,
309 | horizontalScrollConfig: {
310 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
311 | } = {}
312 | },
313 | state: {
314 | headers,
315 | shouldRender,
316 | verticalTransform
317 | }
318 | } = this;
319 |
320 | if (headerType !== constants.headerType.STACKING) {
321 | return 0;
322 | }
323 |
324 | const adjustedHeight = shouldRender.horizontalScrollbar
325 | ? height - scrollbarHeight
326 | : height;
327 |
328 | let slidingWindowOfGutterHeight = 0;
329 | const indexOfFirstFreeGutter = headers.length - _.findIndex(
330 | _.reverse(_.clone(headers)),
331 | ({ height: headerHeight, realOffset }) => {
332 | const adjustedTransform = (verticalTransform + adjustedHeight) - slidingWindowOfGutterHeight;
333 | const gutterIsLocked = adjustedTransform < realOffset;
334 | if (gutterIsLocked) {
335 | slidingWindowOfGutterHeight = headerHeight;
336 | }
337 | return !gutterIsLocked;
338 | }
339 | );
340 |
341 | return _(headers)
342 | .slice(indexOfFirstFreeGutter)
343 | .reduce((prevHeight, { height: headerHeight }) => prevHeight + headerHeight, 0);
344 | }
345 |
346 | _getTopGutterHeight() {
347 | const {
348 | props: {
349 | headerType = constants.headerType.DEFAULT
350 | },
351 | state: {
352 | headers,
353 | verticalTransform
354 | }
355 | } = this;
356 |
357 | if (headerType === constants.headerType.DEFAULT) {
358 | return 0;
359 | }
360 |
361 | const findNextHeaderIndex = _.findIndex(headers, ({ lockPosition }) => lockPosition > verticalTransform);
362 | const nextHeaderIndex = findNextHeaderIndex === -1
363 | ? headers.length
364 | : findNextHeaderIndex;
365 |
366 | if (headerType === constants.headerType.LOCKING) {
367 | const header = headers[nextHeaderIndex - 1];
368 | return header.height;
369 | }
370 |
371 | return _(headers)
372 | .slice(0, nextHeaderIndex)
373 | .reduce((prevHeight, { height }) => prevHeight + height, 0);
374 | }
375 |
376 | _getContentWidth() {
377 | const {
378 | props: {
379 | dynamicColumn = constants.columns.MIDDLE,
380 | guttersConfig,
381 | guttersConfig: {
382 | left,
383 | left: {
384 | handleWidth: leftHandleWidth = constants.LEFT_HANDLE_WIDTH,
385 | position: leftGutterPosition = 0
386 | } = {},
387 | right,
388 | right: {
389 | handleWidth: rightHandleWidth = constants.RIGHT_HANDLE_WIDTH,
390 | position: rightGutterPosition = 0
391 | } = {}
392 | } = {},
393 | horizontalScrollConfig: {
394 | contentWidth = 0
395 | } = {},
396 | width
397 | }
398 | } = this;
399 |
400 | let leftGutterWidth;
401 | let rightGutterWidth;
402 | switch (guttersConfig ? dynamicColumn : constants.columns.MIDDLE) {
403 | case constants.columns.LEFT:
404 | leftGutterWidth = width - leftGutterPosition - leftHandleWidth;
405 | rightGutterWidth = rightGutterPosition;
406 | break;
407 | case constants.columns.RIGHT:
408 | leftGutterWidth = leftGutterPosition;
409 | rightGutterWidth = width - rightGutterPosition - rightHandleWidth;
410 | break;
411 | default:
412 | leftGutterWidth = leftGutterPosition;
413 | rightGutterWidth = rightGutterPosition;
414 | }
415 |
416 | return _.sum([
417 | contentWidth,
418 | leftGutterWidth,
419 | left ? leftHandleWidth : 0,
420 | right ? rightHandleWidth : 0,
421 | rightGutterWidth
422 | ]);
423 | }
424 |
425 | _getDimensions(avgRowHeight, contentHeight, height, width) {
426 | const {
427 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
428 | } = this.props.horizontalScrollConfig || {};
429 | const shouldRender = this._shouldRenderScrollbars(contentHeight, height, width);
430 | const contentsDivHeight = height - (shouldRender.horizontalScrollbar ? scrollbarHeight : 0);
431 | const numRowsInContents = _.ceil(contentsDivHeight / avgRowHeight);
432 |
433 | let displayBuffer = numRowsInContents + (2 * constants.OFFSET_BUFFER);
434 | displayBuffer += constants.OFFSET_BUFFER - (displayBuffer % constants.OFFSET_BUFFER);
435 |
436 | const newState = {
437 | displayBuffer,
438 | shouldRender
439 | };
440 |
441 | if (!shouldRender.verticalScrollbar) {
442 | newState.verticalTransform = 0;
443 | newState.topPartitionIndex = 0;
444 | if (this._verticalScrollbar) {
445 | this._verticalScrollbar.scrollTop = 0;
446 | }
447 | }
448 |
449 | if (!shouldRender.horizontalScrollbar) {
450 | newState.horizontalTransform = 0;
451 | if (this._horizontalScrollbar) {
452 | this._horizontalScrollbar.scrollLeft = 0;
453 | }
454 | }
455 |
456 | return newState;
457 | }
458 |
459 | _getThrottledHorizontalAnimationFrameFn(scrollTo) {
460 | const {
461 | props: {
462 | horizontalScrollConfig: {
463 | onScroll: onHorizontalScroll = () => {}
464 | } = {}
465 | },
466 | state: {
467 | horizontalTransform,
468 | verticalTransform
469 | }
470 | } = this;
471 |
472 | const delta = new Point(scrollTo.x, 0)
473 | .sub(new Point(horizontalTransform, 0));
474 |
475 | this.setState({
476 | scrolling: true
477 | });
478 |
479 | return _.throttle(easer(easedElapsedTime => {
480 | const {
481 | props: {
482 | horizontalScrollConfig
483 | },
484 | state: {
485 | scrollingToPosition: latestScrollingToPosition,
486 | shouldRender
487 | },
488 | _horizontalScrollbar
489 | } = this;
490 | if (!_.isEqual(scrollTo, latestScrollingToPosition)) {
491 | return;
492 | }
493 |
494 | const withHorizontalScrolling = !!horizontalScrollConfig && shouldRender.horizontalScrollbar;
495 | const elapsedTime = easedElapsedTime > 0.999 ? 1 : easedElapsedTime;
496 | const deltaScrolled = new Point(delta.x, delta.y)
497 | .scale(elapsedTime);
498 | const newTransform = new Point(horizontalTransform, verticalTransform)
499 | .add(deltaScrolled);
500 |
501 | const scrollChanges = {
502 | isScrolling: true
503 | };
504 |
505 | if (withHorizontalScrolling) {
506 | scrollChanges.horizontalTransform = _.clamp(
507 | newTransform.x,
508 | 0,
509 | _horizontalScrollbar.scrollWidth - _horizontalScrollbar.offsetWidth
510 | );
511 | }
512 |
513 | this.setState(scrollChanges, () => {
514 | if (withHorizontalScrolling) {
515 | if (_horizontalScrollbar.scrollLeft !== scrollChanges.horizontalTransform) {
516 | onHorizontalScroll(scrollChanges.horizontalTransform);
517 | }
518 | _horizontalScrollbar.scrollLeft = scrollChanges.horizontalTransform;
519 | }
520 |
521 | this._endScroll();
522 | });
523 | }), constants.ANIMATION_FPS_120, { leading: true });
524 | }
525 |
526 | _getThrottledVerticalAnimationFrameFn(scrollTo) {
527 | const {
528 | props: {
529 | height,
530 | verticalScrollConfig: {
531 | onScroll: onVerticalScroll = () => {}
532 | } = {}
533 | },
534 | state: {
535 | horizontalTransform,
536 | verticalTransform
537 | }
538 | } = this;
539 | const delta = new Point(0, scrollTo.y)
540 | .sub(new Point(0, verticalTransform));
541 |
542 | const initStateUpdate = {
543 | scrolling: true
544 | };
545 |
546 | if (Math.abs(delta.y) > height * constants.SCROLL_TO_FAST_SCROLL_FACTOR) {
547 | initStateUpdate.isFastScrolling = true;
548 | }
549 |
550 | this.setState(initStateUpdate);
551 |
552 | return _.throttle(easer(easedElapsedTime => {
553 | const {
554 | state: {
555 | contentHeight,
556 | partitions,
557 | scrollingToPosition: latestScrollingToPosition,
558 | shouldRender
559 | },
560 | _verticalScrollbar
561 | } = this;
562 | if (!_.isEqual(scrollTo, latestScrollingToPosition)) {
563 | return;
564 | }
565 |
566 | const elapsedTime = easedElapsedTime > 0.999 ? 1 : easedElapsedTime;
567 | const deltaScrolled = new Point(delta.x, delta.y)
568 | .scale(elapsedTime);
569 | const newTransform = new Point(horizontalTransform, verticalTransform)
570 | .add(deltaScrolled);
571 |
572 | const scrollChanges = {
573 | isScrolling: true
574 | };
575 |
576 | if (shouldRender.verticalScrollbar) {
577 | const maxHeight = getMaxHeight(contentHeight, _verticalScrollbar.offsetHeight);
578 | _.assign(scrollChanges, getVerticalScrollValues(newTransform.y, maxHeight, partitions));
579 | }
580 |
581 | this.setState(scrollChanges, () => {
582 | if (shouldRender.verticalScrollbar) {
583 | if (_verticalScrollbar.scrollTop !== scrollChanges.verticalTransform) {
584 | onVerticalScroll(scrollChanges.verticalTransform);
585 | }
586 | _verticalScrollbar.scrollTop = scrollChanges.verticalTransform;
587 | }
588 |
589 | this._endScroll();
590 | });
591 | }), constants.ANIMATION_FPS_120, { leading: true });
592 | }
593 |
594 | _getWeightedWidth() {
595 | const {
596 | props: {
597 | verticalScrollConfig: {
598 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
599 | } = {},
600 | width
601 | },
602 | state: { shouldRender }
603 | } = this;
604 |
605 | return width - (shouldRender.verticalScrollbar ? scrollbarWidth : 0);
606 | }
607 |
608 | _onHorizontalScroll() {
609 | const {
610 | props: {
611 | horizontalScrollConfig: {
612 | onScroll = () => {}
613 | } = {}
614 | },
615 | state: {
616 | horizontalTransform
617 | },
618 | _horizontalScrollbar
619 | } = this;
620 |
621 | if (!_horizontalScrollbar || _horizontalScrollbar.scrollLeft === horizontalTransform) {
622 | return;
623 | }
624 |
625 | const { scrollLeft = 0 } = _horizontalScrollbar || {};
626 | this.setState({ horizontalTransform: scrollLeft });
627 | onScroll(scrollLeft);
628 | }
629 |
630 | _onMouseWheel({ deltaX, deltaY }) {
631 | this._onThrottledMouseWheel({ deltaX, deltaY });
632 | }
633 |
634 | /**
635 | * Performs a calculation to determine the size difference between each movement of the mouse cursor. Only occurs when
636 | * a resize is active. Will call the onResize handler for the gutter that is being resized with the new width of the
637 | * gutter.
638 | * @param {number} clientX the position of the mouse cursor horizontally
639 | */
640 | _onResize({ clientX }) {
641 | const { basePosition, performing, side, startingPosition } = this.state.resize;
642 | const width = this._getWeightedWidth();
643 | const {
644 | dynamicColumn = constants.columns.MIDDLE,
645 | guttersConfig,
646 | guttersConfig: {
647 | left,
648 | left: {
649 | handleWidth: leftHandleWidth = constants.LEFT_HANDLE_WIDTH,
650 | position: leftHandlePosition
651 | } = {},
652 | right,
653 | right: {
654 | handleWidth: rightHandleWidth = constants.RIGHT_HANDLE_WIDTH,
655 | position: rightHandlePosition
656 | } = {},
657 | [side]: {
658 | minPosition = 0,
659 | maxPosition = width,
660 | onResize = (() => {})
661 | } = {}
662 | } = {}
663 | } = this.props;
664 | if (performing) {
665 | const deltaPosition = startingPosition - clientX;
666 | const { max, min, mod } = getResizeValues({
667 | dynamicColumn: guttersConfig
668 | ? dynamicColumn
669 | : constants.columns.MIDDLE,
670 | leftExists: Boolean(left),
671 | leftHandlePosition,
672 | leftHandleWidth,
673 | rightExists: Boolean(right),
674 | rightHandlePosition,
675 | rightHandleWidth,
676 | side,
677 | width
678 | });
679 |
680 | onResize(_.clamp(
681 | basePosition + (mod * deltaPosition),
682 | Math.max(minPosition, min),
683 | Math.min(maxPosition, max)
684 | ));
685 | }
686 | }
687 |
688 | _onVerticalScroll() {
689 | const {
690 | props: {
691 | height,
692 | verticalScrollConfig: {
693 | onScroll = () => {}
694 | } = {}
695 | },
696 | state: {
697 | contentHeight,
698 | isFastScrolling: wasFastScrolling,
699 | partitions,
700 | velocityQueue: oldVelocityQueue,
701 | verticalTransform
702 | },
703 | _verticalScrollbar,
704 | _verticalScrollbar: {
705 | offsetHeight,
706 | scrollTop
707 | } = {}
708 | } = this;
709 |
710 | if (!_verticalScrollbar || scrollTop === verticalTransform) {
711 | return;
712 | }
713 |
714 | const maxHeight = getMaxHeight(contentHeight, offsetHeight);
715 |
716 | const nextScrollState = getVerticalScrollValues(scrollTop, maxHeight, partitions);
717 | const {
718 | averageVelocity,
719 | velocityQueue
720 | } = getVelocityInfo(nextScrollState.verticalTransform - verticalTransform, oldVelocityQueue);
721 | const isFastScrolling = wasFastScrolling || Math.abs(averageVelocity) > (height * 3) / 4;
722 |
723 | this.setState(_.assign(
724 | nextScrollState,
725 | {
726 | averageVelocity,
727 | isFastScrolling,
728 | isScrolling: true,
729 | velocityQueue
730 | }
731 | ), () => this._endScroll());
732 | onScroll(nextScrollState.verticalTransform);
733 | }
734 |
735 | _renderContents() {
736 | const {
737 | props: {
738 | dynamicColumn = constants.columns.MIDDLE,
739 | guttersConfig,
740 | horizontalScrollConfig,
741 | horizontalScrollConfig: {
742 | passthroughOffsets = false
743 | } = {},
744 | light = false,
745 | verticalScrollConfig: {
746 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
747 | } = {}
748 | },
749 | state: {
750 | displayBuffer,
751 | isFastScrolling,
752 | horizontalTransform,
753 | partitions,
754 | rows,
755 | isScrolling,
756 | shouldRender,
757 | topPartitionIndex,
758 | verticalTransform
759 | }
760 | } = this;
761 |
762 | const contentsStyle = shouldRender.verticalScrollbar ? {
763 | width: `calc(100% - ${scrollbarWidth}px)`
764 | } : undefined;
765 |
766 | const weightedPartitionIndex = topPartitionIndex * constants.OFFSET_BUFFER;
767 | const startingRowIndex = Math.min(weightedPartitionIndex, rows.length);
768 | const endingRowIndex = weightedPartitionIndex + displayBuffer;
769 | const weightedWidth = this._getWeightedWidth();
770 |
771 | const rowsWeWillRender = _.slice(rows, startingRowIndex, endingRowIndex);
772 | const partitionedRows = _.chunk(rowsWeWillRender, constants.OFFSET_BUFFER);
773 | const renderedPartitions = _.map(partitionedRows, (row, outerIndex) => {
774 | const partitionIndex = outerIndex + topPartitionIndex;
775 | const basePartitionOffset = partitions[partitionIndex];
776 | const partitionStyle = {
777 | transform: `translate3d(-0px, ${basePartitionOffset - verticalTransform}px, 0px)`
778 | };
779 |
780 | return (
781 |
782 | {_.map(
783 | row,
784 | ({
785 | className,
786 | contentComponent,
787 | contentClassName,
788 | gutters,
789 | height,
790 | isHeader,
791 | key,
792 | props: rowProps
793 | }, innerIndex) => light
794 | ? (
795 |
809 | )
810 | : (
811 |
830 | )
831 | )}
832 |
833 | );
834 | });
835 |
836 | const { bottomHeaderGutter, header, topHeaderGutter } = this._renderHeader();
837 |
838 | // TODO remove partitions and shift the contents of the div
839 | return (
840 |
841 | {header}
842 | {topHeaderGutter}
843 | {renderedPartitions}
844 | {bottomHeaderGutter}
845 |
846 | );
847 | }
848 |
849 | _renderCorner() {
850 | const {
851 | props: {
852 | horizontalScrollConfig,
853 | horizontalScrollConfig: {
854 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
855 | } = {},
856 | verticalScrollConfig: {
857 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
858 | } = {}
859 | },
860 | state: { shouldRender }
861 | } = this;
862 |
863 | const shouldRenderCorner = !!horizontalScrollConfig && shouldRender.verticalScrollbar;
864 |
865 | if (!shouldRenderCorner) {
866 | return null;
867 | }
868 |
869 | const cornerStyle = {
870 | height: `${scrollbarHeight}px`,
871 | width: `${scrollbarWidth}px`
872 | };
873 |
874 | return ;
875 | }
876 |
877 | _renderHeader() {
878 | const {
879 | props: {
880 | dynamicColumn = constants.columns.MIDDLE,
881 | guttersConfig,
882 | headerType = constants.headerType.DEFAULT,
883 | height,
884 | horizontalScrollConfig: {
885 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
886 | } = {},
887 | light
888 | },
889 | state: {
890 | headers,
891 | isScrolling,
892 | isFastScrolling,
893 | rows,
894 | shouldRender,
895 | verticalTransform
896 | }
897 | } = this;
898 |
899 | if (!headers || headers.length === 0) {
900 | return {};
901 | }
902 |
903 | const { lockPosition: maxLockPosition } = headers[headers.length - 1];
904 | const findNextHeaderIndex = _.findIndex(headers, ({ lockPosition }) => lockPosition > verticalTransform);
905 | const nextHeaderIndex = findNextHeaderIndex === -1 ? headers.length : findNextHeaderIndex;
906 | const weightedWidth = this._getWeightedWidth();
907 |
908 | if (headerType === constants.headerType.STACKING) {
909 | const topHeaderGutter = (
910 |
911 | {_.times(nextHeaderIndex, headerIndex => {
912 | const { index: headerRowIndex } = headers[headerIndex];
913 | const { className, contentComponent, height: rowHeight, key, props: rowProps } = rows[headerRowIndex];
914 |
915 | return light
916 | ? (
917 |
930 | )
931 | : (
932 |
948 | );
949 | })}
950 |
951 | );
952 |
953 | let bottomGutterStartIndex = nextHeaderIndex;
954 | /* We want to erase headers as they come into view in the contents view from the header gutter
955 | * We solve for the vertical transform that we need to remove a header from the bottom gutter:
956 | * height: height of the header we are transitioning
957 | * topHeight: height of all other gutters pinned to the top, not including baseHeight
958 | * realOffset: the verticalTransform that aligns the next header with the top of the rickscroll__contents
959 | * bottomHeight: the height of the bottom gutter of combined headers
960 | * adjustedBottomHeight: the total height of the headers in the bottom gutter with the baseHeight
961 | * adjustedTransform: the vertical transform that is adjusted to the scale of viewable contents
962 | * ------------------------------------------------------------------------------------------------------------
963 | * we should delete the top header from the bottom gutter if the adjusted transform is smaller than the
964 | * height of contents window
965 | */
966 | const contentsDivHeight = height - (shouldRender.horizontalScrollbar ? scrollbarHeight : 0);
967 | const { height: baseHeight } = headers[0];
968 | const {
969 | adjustHeaderOffset: topHeight,
970 | realOffset: removeFirstHeaderOffset
971 | } = headers[nextHeaderIndex] || headers[nextHeaderIndex - 1];
972 | const { adjustHeaderOffset: bottomHeight } = headers[headers.length - 1];
973 | const adjustedBottomHeight = (baseHeight + bottomHeight) - topHeight;
974 | const adjustedTransform = (removeFirstHeaderOffset - verticalTransform) + adjustedBottomHeight;
975 | if (bottomGutterStartIndex !== headers.length && adjustedTransform <= contentsDivHeight - 1) {
976 | bottomGutterStartIndex++;
977 | const skipHeadersUntil = _(headers)
978 | .slice(bottomGutterStartIndex)
979 | .findIndex(({ adjustHeaderOffset, realOffset }) => {
980 | const restHeight = bottomHeight - adjustHeaderOffset;
981 | return realOffset + topHeight >= ((contentsDivHeight + verticalTransform) - restHeight);
982 | });
983 |
984 | if (skipHeadersUntil >= 0) {
985 | bottomGutterStartIndex += skipHeadersUntil;
986 | } else {
987 | bottomGutterStartIndex = headers.length;
988 | }
989 | }
990 |
991 | const bottomHeaderGutter = (
992 |
993 | {_(headers).slice(bottomGutterStartIndex).map(({ index: headerRowIndex, lockPosition }, index) => {
994 | const headerIndex = bottomGutterStartIndex + index;
995 | const { className, contentComponent, height: rowHeight, key, props: rowProps } = rows[headerRowIndex];
996 |
997 | return light
998 | ? (
999 |
1012 | )
1013 | : (
1014 |
1030 | );
1031 | }).value()}
1032 |
1033 | );
1034 |
1035 | return { bottomHeaderGutter, topHeaderGutter };
1036 | } else if (headerType === constants.headerType.LOCKING) {
1037 | const headerIndex = nextHeaderIndex - 1;
1038 | const { lockPosition } = headers[nextHeaderIndex] || headers[headerIndex];
1039 |
1040 | const { index: headerRowIndex } = headers[headerIndex];
1041 | const { className, contentComponent, height: rowHeight, key, props: rowProps } = rows[headerRowIndex];
1042 |
1043 | const headerStyle = {
1044 | height: `${rowHeight}px`,
1045 | transform: 'translate3d(0px, 0px, 0px)'
1046 | };
1047 |
1048 | if (verticalTransform < maxLockPosition && verticalTransform >= lockPosition - rowHeight) {
1049 | const overlap = (lockPosition - verticalTransform);
1050 | const headerOffset = rowHeight - overlap;
1051 | headerStyle.transform = `translate3d(0px, -${headerOffset}px, 0px)`;
1052 | }
1053 |
1054 | const rowContent = light
1055 | ? (
1056 |
1069 | )
1070 | : (
1071 |
1087 | );
1088 |
1089 | const header = (
1090 |
1091 | {rowContent}
1092 |
1093 | );
1094 |
1095 | return { header };
1096 | }
1097 |
1098 | return {};
1099 | }
1100 |
1101 | /**
1102 | * Decides whether or not to render the horizontal scroll bar
1103 | * @return null or a container with horizontal scrollbar and maybe the corner piece
1104 | */
1105 | _renderHorizontalScrollbar() {
1106 | const {
1107 | props: {
1108 | dynamicColumn = constants.columns.MIDDLE,
1109 | guttersConfig,
1110 | guttersConfig: {
1111 | left,
1112 | left: {
1113 | handleWidth: leftHandleWidth = constants.LEFT_HANDLE_WIDTH,
1114 | position: leftHandlePosition
1115 | } = {},
1116 | right,
1117 | right: {
1118 | handleWidth: rightHandleWidth = constants.RIGHT_HANDLE_WIDTH,
1119 | position: rightHandlePosition
1120 | } = {}
1121 | } = {},
1122 | horizontalScrollConfig,
1123 | horizontalScrollConfig: {
1124 | className,
1125 | scaleWithCenterContent = false,
1126 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
1127 | } = {},
1128 | verticalScrollConfig: {
1129 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
1130 | } = {}
1131 | },
1132 | state: { shouldRender }
1133 | } = this;
1134 |
1135 | const withHorizontalScrolling = !!horizontalScrollConfig && shouldRender.horizontalScrollbar;
1136 |
1137 | if (!withHorizontalScrolling) {
1138 | return null;
1139 | }
1140 |
1141 | // TODO fix scaleWithCenterContent
1142 | const contentWidth = this._getContentWidth();
1143 | const { leftGutterWidth, rightGutterWidth } = getGutterWidths({
1144 | dynamicColumn: guttersConfig
1145 | ? dynamicColumn
1146 | : constants.columns.MIDDLE,
1147 | leftHandlePosition,
1148 | leftHandleWidth,
1149 | rightHandlePosition,
1150 | rightHandleWidth,
1151 | width: this._getWeightedWidth()
1152 | });
1153 | const shouldRenderCorner = !!horizontalScrollConfig && shouldRender.verticalScrollbar;
1154 | const cornerWidth = returnWidthIfComponentExists(scrollbarWidth, shouldRenderCorner);
1155 | let adjustedContentWidth = contentWidth - cornerWidth;
1156 | let leftWidth;
1157 | let position;
1158 | let scaledWidth;
1159 |
1160 | // If the scale with center content flag is enabled, we will adjust the scrollbar to be in the correct position
1161 | // and set up the width to be equivelant to the center content
1162 | // we will also have to adjust the size of the filler content by the gutters
1163 | if (scaleWithCenterContent) {
1164 | const rightWidth = returnWidthIfComponentExists(rightHandleWidth + rightGutterWidth, right);
1165 |
1166 | leftWidth = returnWidthIfComponentExists(leftHandleWidth + leftGutterWidth, left);
1167 | adjustedContentWidth -= leftWidth + rightWidth;
1168 | position = 'relative';
1169 | scaledWidth = `calc(100% - ${leftWidth}px - ${rightWidth}px - ${cornerWidth}px)`;
1170 | }
1171 |
1172 | const wrapperStyle = {
1173 | height: `${scrollbarHeight}px`
1174 | };
1175 |
1176 | const scrollBarDivStyle = {
1177 | height: `${scrollbarHeight}px`,
1178 | left: leftWidth,
1179 | position,
1180 | width: scaledWidth
1181 | };
1182 |
1183 | const fillerStyle = { height: '1px', width: `${adjustedContentWidth}px` };
1184 |
1185 | const getHorizontalScrollbarRef = r => { this._horizontalScrollbar = r; };
1186 | const horizontalScrollbarClassName = classnames('rickscroll__horizontal-scrollbar', className);
1187 |
1188 | return (
1189 |
1190 |
1197 |
{/* this causes the scrollbar to appear */}
1198 |
1199 | {this._renderCorner()}
1200 |
1201 | );
1202 | }
1203 |
1204 | /**
1205 | * Decides whether or not to render the vertical scroll bar
1206 | * @return null or a container with vertical scrollbar
1207 | */
1208 | _renderVerticalScrollbar() {
1209 | const {
1210 | props: {
1211 | verticalScrollConfig: {
1212 | className,
1213 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
1214 | } = {}
1215 | },
1216 | state: { contentHeight, shouldRender }
1217 | } = this;
1218 |
1219 | if (!shouldRender.verticalScrollbar) {
1220 | return null;
1221 | }
1222 |
1223 | const fillerStyle = {
1224 | height: `${contentHeight}px`,
1225 | width: '1px'
1226 | };
1227 | const verticalScrollbarStyle = {
1228 | minWidth: `${scrollbarWidth}px`
1229 | };
1230 |
1231 | const getVerticalScrollbarRef = r => { this._verticalScrollbar = r; };
1232 | const verticalScrollbarCassName = classnames('rickscroll__vertical-scrollbar', className);
1233 | return (
1234 |
1240 |
{/* this causes the scrollbar to appear */}
1241 |
1242 | );
1243 | }
1244 |
1245 | /**
1246 | * Decides which scrollbars should be showing based off of the dimensions of the content and rickscroll container.
1247 | * @return { horizontalScrollbar, verticalScrollbar } a pair of booleans that tell rickscroll whether or not to render
1248 | * the horizontal and vertical scrollbars
1249 | */
1250 | _shouldRenderScrollbars(contentHeight, height, width) {
1251 | const {
1252 | props: {
1253 | horizontalScrollConfig: {
1254 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
1255 | } = {},
1256 | verticalScrollConfig: {
1257 | scrollbarWidth = constants.VERTICAL_SCROLLBAR_WIDTH
1258 | } = {}
1259 | }
1260 | } = this;
1261 |
1262 | const clientHeightTooSmall = height < contentHeight;
1263 | const clientHeightTooSmallWithHorizontalScrollbar = height < (contentHeight + scrollbarHeight);
1264 |
1265 | const contentWidth = this._getContentWidth();
1266 | const clientWidthTooSmall = width < contentWidth;
1267 | const clientWidthTooSmallWithVerticalScrollbar = width < (contentWidth + scrollbarWidth);
1268 |
1269 | const shouldRenderVerticalScrollbar = clientHeightTooSmall || (
1270 | clientWidthTooSmall && clientHeightTooSmallWithHorizontalScrollbar
1271 | );
1272 | const shouldRenderHorizontalScrollbar = clientWidthTooSmall || (
1273 | clientHeightTooSmall && clientWidthTooSmallWithVerticalScrollbar
1274 | );
1275 |
1276 | return {
1277 | horizontalScrollbar: shouldRenderHorizontalScrollbar,
1278 | verticalScrollbar: shouldRenderVerticalScrollbar
1279 | };
1280 | }
1281 |
1282 | _startHorizontalScroll(scrollTo) {
1283 | const {
1284 | animation: oldAnimation
1285 | } = this.state;
1286 |
1287 | const animation = new AnimationTimer()
1288 | .on('tick', this._getThrottledHorizontalAnimationFrameFn(scrollTo))
1289 | .play();
1290 | this.setState({ animation }, () => {
1291 | if (oldAnimation) {
1292 | oldAnimation.stop();
1293 | }
1294 | animation.play();
1295 | });
1296 | }
1297 |
1298 | _startResize(side) {
1299 | return ({ clientX }) => {
1300 | document.addEventListener('mouseup', this._stopResize, { capture: true });
1301 | const {
1302 | guttersConfig: {
1303 | [side]: {
1304 | position: basePosition
1305 | }
1306 | } = {}
1307 | } = this.props;
1308 | this.setState({
1309 | resize: {
1310 | basePosition,
1311 | performing: true,
1312 | side,
1313 | startingPosition: clientX
1314 | }
1315 | });
1316 | };
1317 | }
1318 |
1319 | _stopResize() {
1320 | document.removeEventListener('mouseup', this._stopResize, { capture: true });
1321 | const { side } = this.state.resize;
1322 | const {
1323 | guttersConfig: {
1324 | [side]: {
1325 | onResizeEnd = (() => {}),
1326 | position
1327 | } = {}
1328 | } = {}
1329 | } = this.props;
1330 | this.setState({
1331 | resize: {
1332 | basePosition: 0,
1333 | performing: false,
1334 | side: '',
1335 | startingPosition: 0
1336 | }
1337 | });
1338 | onResizeEnd(position);
1339 | }
1340 |
1341 | _switchScrollProp(scrollTo) {
1342 | const {
1343 | horizontalTransform,
1344 | verticalTransform
1345 | } = this.state;
1346 |
1347 | if (
1348 | (scrollTo.location.x === 0 && scrollTo.location.y === 0)
1349 | || (scrollTo.preserveHorizontal && scrollTo.preserveVertical)
1350 | ) {
1351 | return;
1352 | }
1353 |
1354 | const x = scrollTo.preserveHorizontal
1355 | ? horizontalTransform
1356 | : scrollTo.location.x;
1357 | const y = scrollTo.preserveVertical
1358 | ? verticalTransform
1359 | : scrollTo.location.y;
1360 |
1361 | switch (scrollTo.type) {
1362 | case constants.scrollType.ROW:
1363 | this.scrollRowToMiddle(y, x);
1364 | break;
1365 | case constants.scrollType.HEADER:
1366 | this.scrollToHeader(y, x);
1367 | break;
1368 | default:
1369 | this.scrollTo({ x, y });
1370 | break;
1371 | }
1372 | }
1373 |
1374 | // public
1375 |
1376 | scrollRowToMiddle(rowIndex, x = 0) {
1377 | const {
1378 | props: {
1379 | height,
1380 | horizontalScrollConfig: {
1381 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
1382 | }
1383 | },
1384 | state: {
1385 | rowOffsets,
1386 | shouldRender,
1387 | verticalTransform
1388 | }
1389 | } = this;
1390 |
1391 | if (rowIndex < 0 || rowIndex >= rowOffsets.length) {
1392 | return;
1393 | }
1394 |
1395 | const {
1396 | height: rowHeight,
1397 | offset
1398 | } = rowOffsets[rowIndex];
1399 |
1400 | let adjustedHeight = shouldRender.horizontalScrollbar
1401 | ? height - scrollbarHeight
1402 | : height;
1403 | adjustedHeight -= this._getBottomGutterHeight();
1404 |
1405 | const adjustedVerticalTransform = verticalTransform + this._getTopGutterHeight();
1406 | const needsYScroll = adjustedVerticalTransform > offset
1407 | || verticalTransform + adjustedHeight < offset + rowHeight;
1408 | const y = needsYScroll
1409 | ? offset - (adjustedHeight / 2)
1410 | : verticalTransform;
1411 |
1412 | this.scrollTo({ x, y });
1413 | }
1414 |
1415 | scrollTo({ x = 0, y = 0 }) {
1416 | const {
1417 | props: {
1418 | onRegisteredScrollTo = () => {}
1419 | }, state: {
1420 | animation,
1421 | horizontalTransform,
1422 | verticalTransform
1423 | }
1424 | } = this;
1425 |
1426 | if (horizontalTransform === x && verticalTransform === y) {
1427 | return;
1428 | }
1429 |
1430 | const scrollingToPosition = new Point(x, y);
1431 | const stateTransition = {
1432 | scrollingToPosition
1433 | };
1434 |
1435 | if (horizontalTransform === x && verticalTransform !== y) {
1436 | stateTransition.animation = new AnimationTimer()
1437 | .on('tick', this._getThrottledVerticalAnimationFrameFn(scrollingToPosition));
1438 | } else if (horizontalTransform !== x && verticalTransform === y) {
1439 | this._debouncedStartHorizontalScroll(scrollingToPosition);
1440 | } else {
1441 | stateTransition.animation = new AnimationTimer()
1442 | .on('tick', this._getThrottledVerticalAnimationFrameFn(scrollingToPosition))
1443 | .on('stop', () => {
1444 | if (!_.isEqual(this.state.scrollingToPosition, scrollingToPosition)) {
1445 | return;
1446 | }
1447 |
1448 | this._debouncedStartHorizontalScroll(scrollingToPosition);
1449 | });
1450 | }
1451 |
1452 | this.setState(stateTransition, () => {
1453 | if (animation) {
1454 | animation.stop();
1455 | }
1456 | if (stateTransition.animation) {
1457 | stateTransition.animation.play();
1458 | }
1459 | });
1460 | onRegisteredScrollTo();
1461 | }
1462 |
1463 | scrollToHeader(headerIndex, x = 0) {
1464 | const {
1465 | props: { lists },
1466 | state: { headers }
1467 | } = this;
1468 |
1469 | if (!lists || headerIndex >= lists.length || headerIndex < 0) {
1470 | return;
1471 | }
1472 |
1473 | this.scrollTo({ x, y: headers[headerIndex].lockPosition });
1474 | }
1475 |
1476 | toggleSection(sectionIndex) {
1477 | const {
1478 | props: {
1479 | headerType = constants.headerType.DEFAULT,
1480 | lists
1481 | },
1482 | state: { collapsedSections: oldCollapsedSections }
1483 | } = this;
1484 | const stackHeaders = headerType === constants.headerType.STACKING;
1485 |
1486 | if (!lists || sectionIndex >= lists.length || sectionIndex < 0) {
1487 | return;
1488 | }
1489 |
1490 | const collapsedState = !oldCollapsedSections[sectionIndex];
1491 | oldCollapsedSections[sectionIndex] = collapsedState;
1492 |
1493 | const {
1494 | avgRowHeight,
1495 | collapsedSections,
1496 | contentHeight,
1497 | headers,
1498 | partitions,
1499 | rowOffsets,
1500 | rows
1501 | } = buildRowConfig(lists, stackHeaders, oldCollapsedSections);
1502 | this.setState({ avgRowHeight, collapsedSections, contentHeight, headers, partitions, rowOffsets, rows });
1503 | }
1504 |
1505 | render() {
1506 | const {
1507 | className,
1508 | height,
1509 | horizontalScrollConfig,
1510 | horizontalScrollConfig: {
1511 | scrollbarHeight = constants.HORIZONTAL_SCROLLBAR_HEIGHT
1512 | } = {},
1513 | style = {},
1514 | width,
1515 | wrappedWithAutoAdjust
1516 | } = this.props;
1517 | const {
1518 | resize: { performing },
1519 | shouldRender
1520 | } = this.state;
1521 |
1522 | const scrollableClassName = classnames('rickscroll', {
1523 | 'rickscroll--performing-resize': performing
1524 | }, className);
1525 | const topWrapperStyle = !!horizontalScrollConfig && shouldRender.horizontalScrollbar ? {
1526 | height: `calc(100% - ${scrollbarHeight}px)`
1527 | } : undefined;
1528 |
1529 | const rickscrollStyle = wrappedWithAutoAdjust
1530 | ? style
1531 | : {
1532 | ...style,
1533 | height: `${height}px`,
1534 | width: `${width}px`
1535 | };
1536 |
1537 | return (
1538 |
1539 |
1547 | {this._renderContents()}
1548 | {this._renderVerticalScrollbar()}
1549 |
1550 | {this._renderHorizontalScrollbar()}
1551 |
1552 | );
1553 | }
1554 | }
1555 |
1556 | Scrollable.propTypes = {
1557 | className: types.string,
1558 | disableBidirectionalScrolling: types.bool,
1559 | dynamicColumn: customTypes.column,
1560 | guttersConfig: customTypes.guttersConfig,
1561 | headerType: customTypes.headerType,
1562 | height: types.number.isRequired,
1563 | horizontalScrollConfig: customTypes.horizontalScrollConfig,
1564 | light: types.bool,
1565 | list: customTypes.list,
1566 | lists: customTypes.lists,
1567 | onRegisteredScrollTo: types.func,
1568 | scrollTo: customTypes.scrollTo,
1569 | style: types.object,
1570 | verticalScrollConfig: customTypes.verticalScrollConfig,
1571 | width: types.number.isRequired,
1572 | wrappedWithAutoAdjust: types.bool
1573 | };
1574 |
--------------------------------------------------------------------------------
/src/js/constants.js:
--------------------------------------------------------------------------------
1 | export const ANIMATION_FPS_120 = 1000 / 120;
2 |
3 | export const columns = {
4 | LEFT: 'left',
5 | MIDDLE: 'middle',
6 | RIGHT: 'right'
7 | };
8 |
9 | export const headerType = {
10 | DEFAULT: 'default',
11 | LOCKING: 'locking',
12 | STACKING: 'stacking'
13 | };
14 |
15 | export const scrollType = {
16 | HEADER: 'header',
17 | POSITION: 'position',
18 | ROW: 'row'
19 | };
20 |
21 | export const defaultScrollTo = {
22 | location: {
23 | x: 0,
24 | y: 0
25 | },
26 | preserveHorizontal: false,
27 | preserveVertical: false,
28 | type: scrollType.ROW
29 | };
30 |
31 |
32 | export const LEFT_GUTTER_WIDTH = 0;
33 |
34 | export const LEFT_HANDLE_WIDTH = 0;
35 |
36 | export const HORIZONTAL_SCROLLBAR_HEIGHT = 15;
37 |
38 | export const OFFSET_BUFFER = 6;
39 |
40 | export const RIGHT_GUTTER_WIDTH = 0;
41 |
42 | export const RIGHT_HANDLE_WIDTH = 0;
43 |
44 | export const SCROLL_TO_FAST_SCROLL_FACTOR = 2;
45 |
46 | export const USER_INITIATED_FAST_SCROLL_FACTOR = 3 / 4;
47 |
48 | export const VERTICAL_SCROLLBAR_WIDTH = 15;
49 |
--------------------------------------------------------------------------------
/src/js/index.jsx:
--------------------------------------------------------------------------------
1 | import AutoAdjust from './AutoAdjust';
2 | import HorizontalWrapper from './HorizontalWrapper';
3 | import Scrollable from './Scrollable';
4 |
5 | const Rickscroll = AutoAdjust;
6 | Rickscroll.Static = Scrollable;
7 |
8 | module.exports = {
9 | Rickscroll,
10 | HorizontalWrapper
11 | };
12 |
--------------------------------------------------------------------------------
/src/js/models.js:
--------------------------------------------------------------------------------
1 | export class Point {
2 | constructor(x, y) {
3 | this.x = x;
4 | this.y = y;
5 | }
6 |
7 | add(dPoint) {
8 | this.x += dPoint.x;
9 | this.y += dPoint.y;
10 | return this;
11 | }
12 |
13 | sub(dPoint) {
14 | this.x -= dPoint.x;
15 | this.y -= dPoint.y;
16 | return this;
17 | }
18 |
19 | scale(c) {
20 | this.x *= c;
21 | this.y *= c;
22 | return this;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/js/propTypes.js:
--------------------------------------------------------------------------------
1 | import {
2 | isValidElement,
3 | PropTypes as types
4 | } from 'react';
5 | import _ from 'lodash';
6 |
7 | import * as constants from './constants';
8 |
9 | export const renderableComponent = types.oneOfType([types.func, types.element]);
10 |
11 | export const gutterConfig = types.shape({
12 | className: types.string,
13 | handleClassName: types.string,
14 | handleWidth: types.number,
15 | minPosition: types.number,
16 | maxPosition: types.number,
17 | position: types.number.isRequired,
18 | onResize: types.func
19 | });
20 |
21 | export const guttersConfig = types.shape({
22 | left: gutterConfig,
23 | right: gutterConfig
24 | });
25 |
26 | export const column = types.oneOf(_.values(constants.columns));
27 |
28 | export const headerType = types.oneOf(_.values(constants.headerType));
29 |
30 | export const horizontalScrollConfig = types.shape({
31 | className: types.string,
32 | contentWidth: types.number.isRequired,
33 | onScroll: types.function,
34 | passthroughOffsets: types.bool,
35 | scaleWithCenterContent: types.bool,
36 | scrollbarHeight: types.number
37 | });
38 |
39 | export const rowGutter = types.shape({
40 | className: types.string,
41 | contentComponent: renderableComponent.isRequired,
42 | handleClassName: types.string,
43 | props: types.object
44 | });
45 |
46 | export const gutters = types.shape({
47 | left: rowGutter,
48 | right: rowGutter
49 | });
50 |
51 | export const row = types.shape({
52 | className: types.string,
53 | contentClassName: types.string,
54 | contentComponent: renderableComponent.isRequired,
55 | gutters,
56 | height: types.number.isRequired,
57 | key: types.string,
58 | props: types.object
59 | });
60 |
61 | export const scrollTo = types.shape({
62 | location: types.shape({
63 | x: types.number,
64 | y: types.number
65 | }),
66 | preserveHorizontal: types.bool,
67 | preserveVertical: types.bool,
68 | type: types.oneOf(_.values(constants.scrollType))
69 | });
70 |
71 | export const verticalScrollConfig = types.shape({
72 | className: types.string,
73 | onScroll: types.function,
74 | scrollbarWidth: types.number
75 | });
76 |
77 | function findInvalid(container, fn) {
78 | let invalid = null;
79 | _.forEach(container, (innerContainer, index) => {
80 | invalid = fn(innerContainer, index);
81 | if (invalid) {
82 | return false;
83 | }
84 | return true;
85 | });
86 |
87 | return invalid;
88 | }
89 |
90 | function validateRenderable(props, propName, location) {
91 | if (!_.isFunction(props[propName]) && !isValidElement(props[propName])) {
92 | return new Error(`Invalid ${location} \`${propName}\` supplied to \`Rickscroll\`.`);
93 | }
94 | return null;
95 | }
96 |
97 | function validateGutter(props, side, location) {
98 | const { [side]: gutterProp } = props;
99 | const fullLocation = `${location}.side`;
100 |
101 | if (_.isUndefined(gutterProp)) {
102 | return null;
103 | }
104 |
105 | if (!_.isObject(gutterProp)) {
106 | return new Error(`Invalid ${location} \`${side}\` supplied to \`Rickscroll\`.`);
107 | }
108 |
109 | if (!_.isUndefined(gutterProp.className) && !_.isString(gutterProp.className)) {
110 | return new Error(`Invalid ${fullLocation} \`className\` supplied to \`Rickscroll\`.`);
111 | }
112 |
113 | const invalid = validateRenderable(gutterProp, 'contentComponent', fullLocation);
114 | if (invalid) {
115 | return invalid;
116 | }
117 |
118 | if (!_.isUndefined(gutterProp.handleClassName) && !_.isString(gutterProp.handleClassName)) {
119 | return new Error(`Invalid ${fullLocation} \`handleClassName\` supplied to \`Rickscroll\`.`);
120 | }
121 |
122 | if (!_.isUndefined(gutterProp.props) && !_.isObject(gutterProp.props)) {
123 | return new Error(`Invalid prop at ${location} \`props\` supplied to \`Rickscroll\`.`);
124 | }
125 |
126 | return null;
127 | }
128 |
129 | function validateGutters(props, location) {
130 | const { gutters: guttersProp } = props;
131 | const fullLocation = `${location}.gutters`;
132 |
133 | if (_.isUndefined(guttersProp)) {
134 | return null;
135 | }
136 |
137 | if (!_.isObject(guttersProp)) {
138 | return new Error(`Invalid ${location} \`gutters\` supplied to \`Rickscroll\`.`);
139 | }
140 |
141 | let invalid = validateGutter(props, 'left', fullLocation);
142 | if (invalid) {
143 | return invalid;
144 | }
145 |
146 | invalid = validateGutter(props, 'right', fullLocation);
147 | if (invalid) {
148 | return invalid;
149 | }
150 |
151 | return null;
152 | }
153 |
154 | function validateRow(props, location) {
155 | if (!_.isUndefined(props.className) && !_.isString(props.className)) {
156 | return new Error(`Invalid ${location} \`className\` supplied to \`Rickscroll\`.`);
157 | }
158 |
159 | if (!_.isUndefined(props.contentClassName) && !_.isString(props.contentClassName)) {
160 | return new Error(`Invalid ${location} \`contentClassName\` supplied to \`Rickscroll\`.`);
161 | }
162 |
163 | let invalid = validateRenderable(props, 'contentComponent', location);
164 | if (invalid) {
165 | return invalid;
166 | }
167 |
168 | invalid = validateGutters(props, location);
169 | if (invalid) {
170 | return invalid;
171 | }
172 |
173 | if (!_.isNumber(props.height)) {
174 | return new Error(`Invalid prop at ${location} \`height\` supplied to \`Rickscroll\`.`);
175 | }
176 |
177 | if (!_.isUndefined(props.props) && !_.isObject(props.props)) {
178 | return new Error(`Invalid prop at ${location} \`props\` supplied to \`Rickscroll\`.`);
179 | }
180 |
181 | return null;
182 | }
183 |
184 | function validateRows(rows, locationSuffix, keySet = new Set()) {
185 | const initialKeyCount = keySet.size;
186 | const invalidRow = findInvalid(rows, (listRow, index) => {
187 | if (listRow.key === 0 || listRow.key) {
188 | // covers the case where we have a duplicate key
189 | if (keySet.has(listRow.key)) {
190 | return new Error(`Invalid props supplied to \`Rickscroll\`. Duplicate row key ${listRow.key}.`);
191 | }
192 |
193 | keySet.add(listRow.key);
194 | } else if (keySet.size) {
195 | return new Error(`Invalid props supplied to \`Rickscroll\`. Missing row key at ${index}.`);
196 | }
197 |
198 | return validateRow(listRow, `${locationSuffix}[${index}]`);
199 | });
200 |
201 | if (invalidRow) {
202 | return invalidRow;
203 | }
204 |
205 | // covers the case where some rows don't have a key
206 | if (keySet.size !== 0 && ((keySet.size - initialKeyCount) !== rows.length)) {
207 | return new Error('Invalid props supplied to `Rickscroll`. Missing keys on row declarations.');
208 | }
209 |
210 | return null;
211 | }
212 |
213 | export function list(props) {
214 | const { list: listProp, lists: listsProp } = props;
215 |
216 | if ((!listProp && !listsProp) || (listProp && listsProp)) {
217 | return new Error('Invalid props supplied to `Rickscroll`. Must supply at maximum one of list or lists.');
218 | }
219 |
220 | if (!listProp && listsProp) {
221 | return null;
222 | }
223 |
224 | if (!_.isArray(listProp)) {
225 | return new Error('Invalid prop `list` supplied to `Rickscroll`. Must be an array.');
226 | }
227 |
228 | return validateRows(listProp, 'list');
229 | }
230 |
231 | export function lists(props) {
232 | const { list: listProp, lists: listsProp } = props;
233 |
234 | if ((!listProp && !listsProp) || (listProp && listsProp)) {
235 | return new Error('Invalid props supplied to `Rickscroll`. Must supply at maximum one of list or lists.');
236 | }
237 |
238 | if (listProp && !listsProp) {
239 | return null;
240 | }
241 |
242 | if (!_.isArray(listsProp)) {
243 | return new Error('Invalid prop `lists` supplied to `Rickscroll`. Must be an array.');
244 | }
245 |
246 | // used to catch duplicate or missing keys
247 | const keySet = new Set();
248 | let numberOfNonHeaderRows = 0;
249 |
250 | const listsAreInvalid = findInvalid(listsProp, (listContainer, containerIndex) => {
251 | if (!_.isUndefined(props.headerClassName) && !_.isString(props.headerClassName)) {
252 | return new Error(`Invalid lists[${containerIndex}] \`headerClassName\` supplied to \`Rickscroll\`.`);
253 | }
254 |
255 | const invalidRenderable = validateRenderable(listContainer, 'headerComponent', `lists[${containerIndex}]`);
256 | if (invalidRenderable) {
257 | return invalidRenderable;
258 | }
259 |
260 | if (listContainer.headerKey === 0 || listContainer.headerKey) {
261 | if (keySet.has(listContainer.headerKey)) {
262 | return new Error(`Duplicate \`headerKey\` at lists[${containerIndex}] supplied to \`Rickscroll\`.`);
263 | }
264 | keySet.add(listContainer.headerKey);
265 | } else if (keySet.size) {
266 | return new Error(`Invalid props supplied to \`Rickscroll\`. Missing headerKey at lists[${containerIndex}].`);
267 | }
268 |
269 | if (!_.isUndefined(listContainer.headerProps) && !_.isObject(listContainer.headerProps)) {
270 | return new Error(`Invalid prop at lists[${containerIndex}] \`headerProps\` supplied to \`Rickscroll\`.`);
271 | }
272 |
273 | if (!_.isNumber(listContainer.height)) {
274 | return new Error(`Invalid prop at lists[${containerIndex}] \`height\` supplied to \`Rickscroll\`.`);
275 | }
276 |
277 | if (!_.isUndefined(props.initCollapsed) && !_.isBoolean(props.initCollapsed)) {
278 | return new Error(`Invalid lists[${containerIndex}] \`initCollapsed\` supplied to \`Rickscroll\`.`);
279 | }
280 |
281 | const initalKeyCount = keySet.size;
282 | const rowsInvalid = validateRows(listContainer.rows, 'lists.rows', keySet);
283 | if (rowsInvalid) {
284 | return rowsInvalid;
285 | }
286 |
287 | if (listContainer.headerKey && listContainer.rows.length && keySet.size === initalKeyCount) {
288 | return new Error('Invalid props supplied to `Rickscroll`. headerKey supplied, but no keys defined for ' +
289 | `lists[${containerIndex}] \`rows\`.`);
290 | }
291 |
292 | if (!listContainer.headerKey && keySet.size !== initalKeyCount) {
293 | return new Error(`Invalid props supplied to \`Rickscroll\`. No headerKey supplied to lists[${containerIndex}]`);
294 | }
295 |
296 |
297 | numberOfNonHeaderRows += listContainer.rows.length;
298 |
299 | return null;
300 | });
301 |
302 | if (listsAreInvalid) {
303 | return listsAreInvalid;
304 | }
305 |
306 | if (keySet.size !== 0 && (numberOfNonHeaderRows + listsProp.length) !== keySet.size) {
307 | return new Error('Invalid props supplied to `Rickscroll`. Missing keys on header declarations.');
308 | }
309 |
310 | return null;
311 | }
312 |
--------------------------------------------------------------------------------
/src/js/utils.js:
--------------------------------------------------------------------------------
1 | import fp from 'lodash/fp';
2 |
3 | import * as constants from './constants';
4 |
5 | export function avgRowHeight(config) {
6 | const {
7 | contentHeight,
8 | rows: { length }
9 | } = config;
10 | return fp.assign(config, {
11 | avgRowHeight: length && fp.ceil(contentHeight / length)
12 | });
13 | }
14 |
15 | /**
16 | * Initializes the reduce call to reduceRowsIntoRowConfig. The step where we convert a list prop into lists is done here
17 | * @param {array} list List or lists prop.
18 | * @param {boolean} stackHeaders Signifies that we should do the math to calculate stacking header offsets
19 | * @param {Array} [collapsedSections=[]] The collapsedSection state from the scrollable class
20 | * @return {object} Reduces the list/lists into consumable state for the scrollable
21 | */
22 | export function buildRowConfig(list, stackHeaders, collapsedSections = []) {
23 | let offsetCount = constants.OFFSET_BUFFER - 1;
24 |
25 | if (list.length === 0) {
26 | return { contentHeight: 0, headers: null, partitions: [], rows: [] };
27 | }
28 |
29 | const isMultiList = Boolean(list[0] && list[0].headerComponent);
30 |
31 | const lists = isMultiList ? list : [{ rows: list }];
32 |
33 | let numRowsWithoutHeaders = 0;
34 | let listIterator = lists.length;
35 | while (listIterator--) {
36 | numRowsWithoutHeaders += lists[listIterator].rows.length;
37 | }
38 |
39 | const numRows = isMultiList
40 | ? numRowsWithoutHeaders + lists.length
41 | : numRowsWithoutHeaders;
42 |
43 | const rows = new Array(numRows);
44 | const rowOffsets = new Array(numRowsWithoutHeaders);
45 | const headers = [];
46 | const partitions = [];
47 |
48 | let controlIterator = numRows;
49 | let rowIterator = -1;
50 | let insertionIterator = 0;
51 | let offsetInsertionIterator = 0;
52 | let adjustHeaderOffset = 0;
53 | let contentHeight = 0;
54 | let listItem;
55 |
56 | listIterator = 0;
57 |
58 | while (controlIterator--) {
59 | if (rowIterator === -1) {
60 | listItem = lists[listIterator];
61 | rowIterator++;
62 |
63 | if (listItem.headerComponent) {
64 | offsetCount++;
65 | offsetCount %= constants.OFFSET_BUFFER;
66 |
67 | if (offsetCount === 0) {
68 | partitions.push(contentHeight);
69 | }
70 |
71 | headers.push({
72 | adjustHeaderOffset,
73 | index: insertionIterator,
74 | height: listItem.height,
75 | lockPosition: contentHeight - adjustHeaderOffset,
76 | realOffset: contentHeight
77 | });
78 |
79 | if (stackHeaders) {
80 | adjustHeaderOffset += listItem.height;
81 | }
82 |
83 | if (collapsedSections[listIterator] === undefined) {
84 | collapsedSections[listIterator] = Boolean(listItem.initCollapsed);
85 | }
86 |
87 | rows[insertionIterator++] = {
88 | className: listItem.headerClassName,
89 | contentComponent: listItem.headerComponent,
90 | height: listItem.height,
91 | isHeader: true,
92 | key: listItem.headerKey,
93 | props: listItem.headerProps
94 | };
95 |
96 | contentHeight += listItem.height;
97 | }
98 |
99 | if (isMultiList) {
100 | if (!listItem.rows.length) {
101 | listIterator++;
102 | rowIterator = -1;
103 | }
104 | continue;
105 | }
106 | }
107 |
108 | if (collapsedSections[listIterator]) {
109 | controlIterator -= listItem.rows.length - 1;
110 | continue;
111 | }
112 |
113 | const row = listItem.rows[rowIterator++];
114 |
115 | offsetCount++;
116 | offsetCount %= constants.OFFSET_BUFFER;
117 |
118 | if (offsetCount === 0) {
119 | partitions.push(contentHeight);
120 | }
121 |
122 | rows[insertionIterator++] = row;
123 | rowOffsets[offsetInsertionIterator++] = {
124 | height: row.height,
125 | offset: contentHeight
126 | };
127 |
128 | contentHeight += row.height;
129 |
130 | if (listItem.rows.length === rowIterator) {
131 | listIterator++;
132 | rowIterator = -1;
133 | }
134 | }
135 |
136 | return avgRowHeight({
137 | adjustHeaderOffset,
138 | collapsedSections,
139 | contentHeight,
140 | headers,
141 | stackHeaders,
142 | offsetCount,
143 | partitions,
144 | rowOffsets,
145 | rows
146 | });
147 | }
148 |
149 | /**
150 | * assign meaning to math
151 | * @param {number} contentHeight the height of the content
152 | * @param {number} offsetHeight the offsetHeight from the vertical scrollbar
153 | * @return {number} the difference between contentHeight and offsetHeight
154 | */
155 | export function getMaxHeight(contentHeight, offsetHeight) {
156 | return contentHeight - offsetHeight;
157 | }
158 |
159 | export const getResizeValues = fp.cond([
160 | // Dynamic column : LEFT ---------------------------------------------------------------------------------------------
161 | [ // resizing LEFT side
162 | fp.isMatch({
163 | dynamicColumn: constants.columns.LEFT,
164 | side: constants.columns.LEFT
165 | }),
166 | ({ leftHandleWidth, rightExists, rightHandlePosition, rightHandleWidth, width }) => ({
167 | max: width - leftHandleWidth,
168 | mod: 1,
169 | min: rightExists
170 | ? rightHandlePosition + rightHandleWidth
171 | : 0
172 | })
173 | ],
174 | [ // resizing RIGHT side
175 | fp.isMatch({
176 | dynamicColumn: constants.columns.LEFT,
177 | side: constants.columns.RIGHT
178 | }),
179 | ({ leftExists, leftHandlePosition, rightHandleWidth, width }) => ({
180 | max: leftExists
181 | ? leftHandlePosition - rightHandleWidth
182 | : width - rightHandleWidth,
183 | mod: 1,
184 | min: 0
185 | })
186 | ],
187 | // dynamic column : RIGHT --------------------------------------------------------------------------------------------
188 | [ // resizing LEFT side
189 | fp.isMatch({
190 | dynamicColumn: constants.columns.RIGHT,
191 | side: constants.columns.LEFT
192 | }),
193 | ({ leftHandleWidth, rightExists, rightHandlePosition, width }) => ({
194 | max: rightExists
195 | ? rightHandlePosition - leftHandleWidth
196 | : width - leftHandleWidth,
197 | mod: -1,
198 | min: 0
199 | })
200 | ],
201 | [ // resizing RIGHT side
202 | fp.isMatch({
203 | dynamicColumn: constants.columns.RIGHT,
204 | side: constants.columns.RIGHT
205 | }),
206 | ({ leftExists, leftHandlePosition, leftHandleWidth, rightHandleWidth, width }) => ({
207 | max: width - rightHandleWidth,
208 | mod: -1,
209 | min: leftExists
210 | ? leftHandlePosition + leftHandleWidth
211 | : 0
212 | })
213 | ],
214 | // dynamic column : MIDDLE -------------------------------------------------------------------------------------------
215 | [ // resizing LEFT side
216 | fp.isMatch({
217 | dynamicColumn: constants.columns.MIDDLE,
218 | side: constants.columns.LEFT
219 | }),
220 | ({ leftHandleWidth, rightExists, rightHandlePosition, rightHandleWidth, width }) => ({
221 | max: rightExists
222 | ? width - (rightHandlePosition + rightHandleWidth) - leftHandleWidth
223 | : width - leftHandleWidth,
224 | mod: -1,
225 | min: 0
226 | })
227 | ],
228 | [ // resizing RIGHT side
229 | fp.isMatch({
230 | dynamicColumn: constants.columns.MIDDLE,
231 | side: constants.columns.RIGHT
232 | }),
233 | ({ leftExists, leftHandlePosition, leftHandleWidth, rightHandleWidth, width }) => ({
234 | max: leftExists
235 | ? width - (leftHandlePosition + leftHandleWidth) - rightHandleWidth
236 | : width - rightHandleWidth,
237 | mod: 1,
238 | min: 0
239 | })
240 | ]
241 | ]);
242 |
243 | const getMiddleColumnWidths = ({ leftHandlePosition, rightHandlePosition }) => ({
244 | leftGutterWidth: leftHandlePosition,
245 | rightGutterWidth: rightHandlePosition
246 | });
247 | export const getGutterWidths = fp.cond([
248 | [
249 | fp.isMatch({ dynamicColumn: constants.columns.LEFT }),
250 | ({ leftHandlePosition, leftHandleWidth, rightHandlePosition, width }) => ({
251 | leftGutterWidth: width - leftHandlePosition - leftHandleWidth,
252 | rightGutterWidth: rightHandlePosition
253 | })
254 | ],
255 | [
256 | fp.isMatch({ dynamicColumn: constants.columns.RIGHT }),
257 | ({ leftHandlePosition, rightHandlePosition, rightHandleWidth, width }) => ({
258 | leftGutterWidth: leftHandlePosition,
259 | rightGutterWidth: width - rightHandlePosition - rightHandleWidth
260 | })
261 | ],
262 | [fp.isMatch({ dynamicColumn: constants.columns.MIDDLE }), getMiddleColumnWidths],
263 | [fp.stubTrue, getMiddleColumnWidths]
264 | ]);
265 |
266 | /**
267 | * calculates which partition is the top of our renderable content and clamps verticalTransform to within min/max values
268 | * @param {number} preClampTransform vertical transform before we clamp it between min / max values
269 | * @param {number} maxHeight maximum height our vertical transform can reach
270 | * @param {array} partitions list of numbers which represent the starting vertical transformation of partitions
271 | * by index (partitions[5] is the starting vertical transform of the 6th partition)
272 | * @return {object} topPartionIndex and the clamped verticalTransform
273 | */
274 | export function getVerticalScrollValues(preClampTransform, maxHeight, partitions) {
275 | const verticalTransform = fp.clamp(0, maxHeight, preClampTransform);
276 | const topPartitionIndex = fp.findIndex(
277 | partitionStartingIndex => partitionStartingIndex > verticalTransform,
278 | partitions
279 | ) - 1;
280 | return { topPartitionIndex, verticalTransform };
281 | }
282 |
283 | /**
284 | * helper function that returns a very strict width object for gutters
285 | * @param {Number} [width=0] the width we wish to express as a style object
286 | * @return {object} a style object representing a the given width
287 | */
288 | export function getWidthStyle(width = 0) {
289 | return fp.isNumber(width) && width > 0 ? {
290 | minWidth: `${width}px`,
291 | width: `${width}px`
292 | } : undefined;
293 | }
294 |
295 | export const returnWidthIfComponentExists = (width, component) =>
296 | component ? width : 0;
297 |
298 | export function getVelocityInfo(velocity, queue) {
299 | const now = window.performance.now();
300 | const velocityQueue = fp.reject(([timestamp]) => timestamp < now - 10, queue);
301 |
302 | if (velocityQueue.length === 20) {
303 | velocityQueue.shift();
304 | }
305 |
306 | velocityQueue.push([window.performance.now(), velocity]);
307 |
308 | const _averageVelocity = fp.flow([
309 | fp.map(fp.last),
310 | fp.mean
311 | ])(velocityQueue);
312 |
313 | const averageVelocity = fp.isNaN(_averageVelocity)
314 | ? Number.POSITIVE_INFINITY
315 | : _averageVelocity;
316 |
317 | return { averageVelocity, velocityQueue};
318 | }
319 |
--------------------------------------------------------------------------------
/src/less/scrollable.less:
--------------------------------------------------------------------------------
1 | .rickscroll {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | overflow: hidden;
7 |
8 | &--performing-resize {
9 | -webkit-touch-callout: none;
10 | -webkit-user-select: none;
11 | -khtml-user-select: none;
12 | -moz-user-select: none;
13 | -ms-user-select: none;
14 | user-select: none;
15 | }
16 |
17 | &__bottom-wrapper {
18 | display: flex;
19 | flex-direction: row;
20 | justify-content: space-between;
21 | z-index: 2;
22 | }
23 |
24 | &__top-wrapper {
25 | width: 100%;
26 | height: 100%;
27 | display: flex;
28 | flex-direction: row;
29 | }
30 |
31 | &__contents {
32 | position: relative;
33 | top: 0;
34 | bottom: 0;
35 | right: 0;
36 | left: 0;
37 | width: 100%;
38 | }
39 |
40 | &__vertical-scrollbar {
41 | overflow-x: hidden;
42 | overflow-y: scroll;
43 | z-index: 2;
44 | }
45 |
46 | &__horizontal-scrollbar {
47 | width: 100%;
48 | overflow-x: scroll;
49 | overflow-y: hidden;
50 | }
51 |
52 | &__row {
53 | display: flex;
54 | flex-direction: row;
55 | justify-content: space-between;
56 | }
57 |
58 | &__content {
59 | overflow: hidden;
60 | flex-grow: 1;
61 | z-index: 1;
62 | }
63 |
64 | &__horizontal-wrapper {
65 | z-index: 1;
66 | width: 100%;
67 | height: 100%;
68 | }
69 |
70 | &__gutter {
71 | z-index: 2;
72 | }
73 |
74 | &__handle {
75 | background-color: #000000;
76 | z-index: 2;
77 |
78 | &--grabbable {
79 | cursor: col-resize;
80 | }
81 | }
82 |
83 | &__header {
84 | position: absolute;
85 | width: 100%;
86 | z-index: 2;
87 | }
88 |
89 | &__header-gutter {
90 | position: absolute;
91 | width: 100%;
92 | z-index: 2;
93 | &--top {
94 | top: 0;
95 | }
96 | &--bottom {
97 | bottom: 0;
98 | }
99 | }
100 |
101 | &__partition {
102 | position: absolute;
103 | left: 0;
104 | right: 0;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------