4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-dnd-scrollzone
2 |
3 | Cross browser compatible scrolling containers for drag and drop interactions.
4 |
5 | ### [Basic Example](./examples/basic)
6 |
7 | ```js
8 | import React, { Component } from 'react';
9 | import { DragDropContextProvider } from 'react-dnd';
10 | import HTML5Backend from 'react-dnd-html5-backend';
11 | import withScrolling from 'react-dnd-scrollzone';
12 | import DragItem from './DragItem';
13 | import './App.css';
14 |
15 | const ScrollingComponent = withScrolling('div');
16 |
17 | const ITEMS = [1,2,3,4,5,6,7,8,9,10];
18 |
19 | export default class App extends Component {
20 | render() {
21 | return (
22 |
23 |
24 | {ITEMS.map(n => (
25 |
26 | ))}
27 |
28 |
29 | );
30 | }
31 | }
32 | ```
33 |
34 | Note: You should replace the original `div` you would like to make scrollable with the `ScrollingComponent`.
35 |
36 | ### Easing Example
37 |
38 | ```js
39 | import React, { Component } from 'react';
40 | import { DragDropContextProvider } from 'react-dnd';
41 | import HTML5Backend from 'react-dnd-html5-backend';
42 | import withScrolling, { createHorizontalStrength, createVerticalStrength } from 'react-dnd-scrollzone';
43 | import DragItem from './DragItem';
44 | import './App.css';
45 |
46 |
47 | const ScrollZone = withScrolling('ul');
48 | const linearHorizontalStrength = createHorizontalStrength(150);
49 | const linearVerticalStrength = createVerticalStrength(150);
50 | const ITEMS = [1,2,3,4,5,6,7,8,9,10];
51 |
52 | // this easing function is from https://gist.github.com/gre/1650294 and
53 | // expects/returns a number between [0, 1], however strength functions
54 | // expects/returns a value between [-1, 1]
55 | function ease(val) {
56 | const t = (val + 1) / 2; // [-1, 1] -> [0, 1]
57 | const easedT = t<.5 ? 2*t*t : -1+(4-2*t)*t;
58 | return easedT * 2 - 1; // [0, 1] -> [-1, 1]
59 | }
60 |
61 | function hStrength(box, point) {
62 | return ease(linearHorizontalStrength(box, point));
63 | }
64 |
65 | function vStrength(box, point) {
66 | return ease(linearVerticalStrength(box, point));
67 | }
68 |
69 | export default App(props) {
70 | return (
71 |
72 |
76 |
77 | {ITEMS.map(n => (
78 |
79 | ))}
80 |
81 |
82 | );
83 | }
84 | ```
85 | Note: You should replace the original `div` you would like to make scrollable with the `ScrollingComponent`.
86 |
87 | ### Virtualized Example
88 |
89 | Since react-dnd-scrollzone utilizes the Higher Order Components (HOC) pattern, drag and drop scrolling behaviour can easily be added to existing components. For example to speedup huge lists by using [react-virtualized](https://github.com/bvaughn/react-virtualized) for a windowed view where only the visible rows are rendered:
90 |
91 | ```js
92 | import React from 'react';
93 | import { DragDropContextProvider } from 'react-dnd';
94 | import HTML5Backend from 'react-dnd-html5-backend';
95 | import withScrolling from 'react-dnd-scrollzone';
96 | import { List } from 'react-virtualized';
97 | import DragItem from './DragItem';
98 | import './App.css';
99 |
100 | const ScrollingVirtualList = withScrolling(List);
101 |
102 | // creates array with 1000 entries
103 | const ITEMS = Array.from(Array(1000)).map((e,i)=> `Item ${i}`);
104 |
105 |
106 | export default App(props) {
107 | return (
108 |
109 | (
117 |
122 | )
123 | }
124 | />
125 |
126 | );
127 | }
128 | ```
129 |
130 |
131 | ### API
132 |
133 | #### `withScrolling`
134 |
135 | A React higher order component with the following properties:
136 |
137 | ```js
138 | const ScrollZone = withScrolling(String|Component);
139 |
140 |
145 |
146 | {children}
147 |
148 | ```
149 | Apply the withScrolling function to any html-identifier ("div", "ul" etc) or react component to add drag and drop scrolling behaviour.
150 |
151 | * `horizontalStrength` a function that returns the strength of the horizontal scroll direction
152 | * `verticalStrength` - a function that returns the strength of the vertical scroll direction
153 | * `strengthMultiplier` - strength multiplier, play around with this (default 30)
154 | * `onScrollChange` - a function that is called when `scrollLeft` or `scrollTop` of the component are changed. Called with those two arguments in that order.
155 |
156 | The strength functions are both called with two arguments. An object representing the rectangle occupied by the Scrollzone, and an object representing the coordinates of mouse.
157 |
158 | They should return a value between -1 and 1.
159 | * Negative values scroll up or left.
160 | * Positive values scroll down or right.
161 | * 0 stops all scrolling.
162 |
163 | #### `createVerticalStrength(buffer)` and `createHorizontalStrength(buffer)`
164 |
165 | These allow you to create linearly scaling strength functions with a sensitivity different than the default value of 150px.
166 |
167 | ##### Example
168 |
169 | ```js
170 | import withScrolling, { createVerticalStrength, createHorizontalStrength } from 'react-dnd-scrollzone';
171 |
172 | const Scrollzone = withScrolling('ul');
173 | const vStrength = createVerticalStrength(500);
174 | const hStrength = createHorizontalStrength(300);
175 |
176 | // zone will scroll when the cursor drags within
177 | // 500px of the top/bottom and 300px of the left/right
178 | const zone = (
179 |
180 |
181 |
182 | );
183 | ```
184 |
--------------------------------------------------------------------------------
/examples/basic/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic Example
2 |
3 | ```bash
4 | # in this directory
5 | npm install
6 | npm start
7 | ```
8 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "0.9.5"
7 | },
8 | "dependencies": {
9 | "prop-types": "^15.5.9",
10 | "react": "^15.5.4",
11 | "react-dnd": "^2.4.0",
12 | "react-dnd-html5-backend": "^2.4.1",
13 | "react-dnd-scrollzone": "^3.1.0",
14 | "react-dom": "^15.5.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/basic/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azuqua/react-dnd-scrollzone/334066a1b079aa6e4b0130008f0519942ab27723/examples/basic/public/favicon.ico
--------------------------------------------------------------------------------
/examples/basic/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | React App
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/basic/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | width: 500px;
3 | height: 500px;
4 | overflow: scroll;
5 | border: solid 2px black;
6 | }
7 |
--------------------------------------------------------------------------------
/examples/basic/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import HTML5Backend from 'react-dnd-html5-backend';
3 | import { DragDropContextProvider } from 'react-dnd';
4 | import withScrolling from 'react-dnd-scrollzone';
5 | import DragItem from './DragItem';
6 | import './App.css';
7 |
8 | const ScrollingComponent = withScrolling('div');
9 |
10 | const ITEMS = [1,2,3,4,5,6,7,8,9,10];
11 |
12 | export default class App extends Component {
13 | render() {
14 | return (
15 |
16 |
17 | {ITEMS.map(n => (
18 |
19 | ))}
20 |
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/basic/src/DragItem.css:
--------------------------------------------------------------------------------
1 | .DragItem {
2 | width: 100%;
3 | border: solid 1px black;
4 | padding: 30px;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/basic/src/DragItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { DragSource } from 'react-dnd';
4 | import './DragItem.css';
5 |
6 | class DragItem extends PureComponent {
7 |
8 | static propTypes = {
9 | label: PropTypes.string.isRequired,
10 | };
11 |
12 | render() {
13 | return this.props.dragSource(
14 |
15 | {this.props.label}
16 |
17 | );
18 | }
19 | }
20 |
21 | export default DragSource(
22 | 'foo',
23 | {
24 | beginDrag() {
25 | return {}
26 | }
27 | },
28 | (connect) => ({
29 | dragSource: connect.dragSource(),
30 | })
31 | )(DragItem);
32 |
--------------------------------------------------------------------------------
/examples/basic/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/basic/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/examples/mobile-backend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 |
--------------------------------------------------------------------------------
/examples/mobile-backend/README.md:
--------------------------------------------------------------------------------
1 | # Basic Example
2 |
3 | ```bash
4 | # in this directory
5 | npm install
6 | npm start
7 | ```
8 |
--------------------------------------------------------------------------------
/examples/mobile-backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mobile-backend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "0.9.5"
7 | },
8 | "dependencies": {
9 | "prop-types": "^15.5.9",
10 | "react": "^15.5.4",
11 | "react-dnd": "^2.4.0",
12 | "react-dnd-scrollzone": "^4.0.0",
13 | "react-dnd-touch-backend": "^0.3.10",
14 | "react-dom": "^15.5.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/mobile-backend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azuqua/react-dnd-scrollzone/334066a1b079aa6e4b0130008f0519942ab27723/examples/mobile-backend/public/favicon.ico
--------------------------------------------------------------------------------
/examples/mobile-backend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | React App
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | width: 500px;
3 | height: 500px;
4 | overflow: scroll;
5 | border: solid 2px black;
6 | }
7 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import TouchBackend from 'react-dnd-touch-backend';
3 | import { DragDropContextProvider } from 'react-dnd';
4 | import withScrolling from 'react-dnd-scrollzone';
5 | import DragItem from './DragItem';
6 | import DragPreview from './DragPreview';
7 | import './App.css';
8 |
9 | const ScrollingComponent = withScrolling('div');
10 |
11 | const ITEMS = [1,2,3,4,5,6,7,8,9,10];
12 |
13 | export default class App extends Component {
14 | render() {
15 | return (
16 |
17 |
18 | {ITEMS.map(n => (
19 |
20 | ))}
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/DragItem.css:
--------------------------------------------------------------------------------
1 | .DragItem {
2 | width: 100%;
3 | border: solid 1px black;
4 | padding: 30px;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/DragItem.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { DragSource } from 'react-dnd';
4 | import './DragItem.css';
5 |
6 | class DragItem extends PureComponent {
7 |
8 | static propTypes = {
9 | label: PropTypes.string.isRequired,
10 | };
11 |
12 | render() {
13 | return this.props.dragSource(
14 |
15 | {this.props.label}
16 |
17 | );
18 | }
19 | }
20 |
21 | export default DragSource(
22 | 'foo',
23 | {
24 | beginDrag() {
25 | return {}
26 | }
27 | },
28 | (connect) => ({
29 | dragSource: connect.dragSource(),
30 | })
31 | )(DragItem);
32 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/DragPreview.css:
--------------------------------------------------------------------------------
1 | .DragPreview {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | height: 100vh;
6 | width: 100vw;
7 | pointer-events: none;
8 | z-index: 1000;
9 | }
10 |
11 | .DragPreview__item {
12 | width: 100px;
13 | height: 100px;
14 | background: white;
15 | border: solid 1px black;
16 | }
17 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/DragPreview.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { DragLayer } from 'react-dnd';
3 | import './DragPreview.css';
4 |
5 | class DragPreview extends PureComponent {
6 |
7 | render() {
8 | const {
9 | item,
10 | offset,
11 | } = this.props;
12 |
13 | return (
14 |
15 | {item && (
16 |
24 | )}
25 |
26 | );
27 | }
28 | }
29 |
30 | export default DragLayer(
31 | monitor => ({
32 | item: monitor.getItem(),
33 | offset: monitor.getClientOffset(),
34 | })
35 | )(DragPreview);
36 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/mobile-backend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-dnd-scrollzone",
3 | "version": "5.0.0",
4 | "description": "A cross browser solution to scrolling during drag and drop.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "build": "rm -rf lib && babel src --out-dir lib",
8 | "lint": "eslint src",
9 | "pretest": "npm run lint",
10 | "test": "mocha test",
11 | "prepublish": "in-publish && npm run test && npm run build || not-in-publish",
12 | "publish:major": "npm version major && npm publish",
13 | "publish:minor": "npm version minor && npm publish",
14 | "publish:patch": "npm version patch && npm publish",
15 | "postpublish": "git push origin master --tags"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/azuqua/react-dnd-scrollzone"
20 | },
21 | "bugs": {
22 | "url": "http://github.com/azuqua/react-dnd-scrollzone/issues"
23 | },
24 | "keywords": [
25 | "react",
26 | "drag",
27 | "drop",
28 | "scroll",
29 | "dnd",
30 | "drag and drop",
31 | "polyfill",
32 | "auto"
33 | ],
34 | "author": {
35 | "name": "Nicholas Clawson",
36 | "email": "nickclaw@gmail.com",
37 | "url": "nickclaw.com"
38 | },
39 | "license": "MIT",
40 | "dependencies": {
41 | "hoist-non-react-statics": "3.x",
42 | "lodash.throttle": "^4.0.1",
43 | "prop-types": "^15.5.9",
44 | "raf": "^3.2.0",
45 | "react-display-name": "^0.2.0"
46 | },
47 | "devDependencies": {
48 | "babel-cli": "^6.4.5",
49 | "babel-eslint": "^6.0.4",
50 | "babel-preset-es2015": "^6.3.13",
51 | "babel-preset-react": "^6.5.0",
52 | "babel-preset-stage-1": "^6.3.13",
53 | "babel-register": "^6.4.3",
54 | "chai": "^3.4.1",
55 | "eslint": "^2.12.0",
56 | "eslint-config-airbnb": "^9.0.1",
57 | "eslint-plugin-import": "^1.8.1",
58 | "eslint-plugin-jsx-a11y": "^1.4.2",
59 | "eslint-plugin-react": "^5.1.1",
60 | "in-publish": "^2.0.0",
61 | "mocha": "^2.3.4",
62 | "react": "16.x",
63 | "react-dom": "16.x",
64 | "sinon": "^1.17.2",
65 | "sinon-chai": "^2.8.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { findDOMNode } from 'react-dom';
4 | import throttle from 'lodash.throttle';
5 | import raf from 'raf';
6 | import getDisplayName from 'react-display-name';
7 | import hoist from 'hoist-non-react-statics';
8 | import { noop, intBetween, getCoords } from './util';
9 |
10 | const DEFAULT_BUFFER = 150;
11 |
12 | export function createHorizontalStrength(_buffer) {
13 | return function defaultHorizontalStrength({ x, w, y, h }, point) {
14 | const buffer = Math.min(w / 2, _buffer);
15 | const inRange = point.x >= x && point.x <= x + w;
16 | const inBox = inRange && point.y >= y && point.y <= y + h;
17 |
18 | if (inBox) {
19 | if (point.x < x + buffer) {
20 | return (point.x - x - buffer) / buffer;
21 | } else if (point.x > (x + w - buffer)) {
22 | return -(x + w - point.x - buffer) / buffer;
23 | }
24 | }
25 |
26 | return 0;
27 | };
28 | }
29 |
30 | export function createVerticalStrength(_buffer) {
31 | return function defaultVerticalStrength({ y, h, x, w }, point) {
32 | const buffer = Math.min(h / 2, _buffer);
33 | const inRange = point.y >= y && point.y <= y + h;
34 | const inBox = inRange && point.x >= x && point.x <= x + w;
35 |
36 | if (inBox) {
37 | if (point.y < y + buffer) {
38 | return (point.y - y - buffer) / buffer;
39 | } else if (point.y > (y + h - buffer)) {
40 | return -(y + h - point.y - buffer) / buffer;
41 | }
42 | }
43 |
44 | return 0;
45 | };
46 | }
47 |
48 | export const defaultHorizontalStrength = createHorizontalStrength(DEFAULT_BUFFER);
49 |
50 | export const defaultVerticalStrength = createVerticalStrength(DEFAULT_BUFFER);
51 |
52 |
53 | export default function createScrollingComponent(WrappedComponent) {
54 | class ScrollingComponent extends Component {
55 |
56 | static displayName = `Scrolling(${getDisplayName(WrappedComponent)})`;
57 |
58 | static propTypes = {
59 | onScrollChange: PropTypes.func,
60 | verticalStrength: PropTypes.func,
61 | horizontalStrength: PropTypes.func,
62 | strengthMultiplier: PropTypes.number,
63 | };
64 |
65 | static defaultProps = {
66 | onScrollChange: noop,
67 | verticalStrength: defaultVerticalStrength,
68 | horizontalStrength: defaultHorizontalStrength,
69 | strengthMultiplier: 30,
70 | };
71 |
72 | static contextTypes = {
73 | dragDropManager: PropTypes.object,
74 | };
75 |
76 | constructor(props, ctx) {
77 | super(props, ctx);
78 |
79 | this.scaleX = 0;
80 | this.scaleY = 0;
81 | this.frame = null;
82 |
83 | this.attached = false;
84 | this.dragging = false;
85 | }
86 |
87 | componentDidMount() {
88 | this.container = findDOMNode(this.wrappedInstance);
89 | this.container.addEventListener('dragover', this.handleEvent);
90 | // touchmove events don't seem to work across siblings, so we unfortunately
91 | // have to attach the listeners to the body
92 | window.document.body.addEventListener('touchmove', this.handleEvent);
93 |
94 | this.clearMonitorSubscription = this.context
95 | .dragDropManager
96 | .getMonitor()
97 | .subscribeToStateChange(() => this.handleMonitorChange());
98 | }
99 |
100 | componentWillUnmount() {
101 | this.container.removeEventListener('dragover', this.handleEvent);
102 | window.document.body.removeEventListener('touchmove', this.handleEvent);
103 | this.clearMonitorSubscription();
104 | this.stopScrolling();
105 | }
106 |
107 | handleEvent = (evt) => {
108 | if (this.dragging && !this.attached) {
109 | this.attach();
110 | this.updateScrolling(evt);
111 | }
112 | }
113 |
114 | handleMonitorChange() {
115 | const isDragging = this.context.dragDropManager.getMonitor().isDragging();
116 |
117 | if (!this.dragging && isDragging) {
118 | this.dragging = true;
119 | } else if (this.dragging && !isDragging) {
120 | this.dragging = false;
121 | this.stopScrolling();
122 | }
123 | }
124 |
125 | attach() {
126 | this.attached = true;
127 | window.document.body.addEventListener('dragover', this.updateScrolling);
128 | window.document.body.addEventListener('touchmove', this.updateScrolling);
129 | }
130 |
131 | detach() {
132 | this.attached = false;
133 | window.document.body.removeEventListener('dragover', this.updateScrolling);
134 | window.document.body.removeEventListener('touchmove', this.updateScrolling);
135 | }
136 |
137 | // Update scaleX and scaleY every 100ms or so
138 | // and start scrolling if necessary
139 | updateScrolling = throttle(evt => {
140 | const { left: x, top: y, width: w, height: h } = this.container.getBoundingClientRect();
141 | const box = { x, y, w, h };
142 | const coords = getCoords(evt);
143 |
144 | // calculate strength
145 | this.scaleX = this.props.horizontalStrength(box, coords);
146 | this.scaleY = this.props.verticalStrength(box, coords);
147 |
148 | // start scrolling if we need to
149 | if (!this.frame && (this.scaleX || this.scaleY)) {
150 | this.startScrolling();
151 | }
152 | }, 100, { trailing: false })
153 |
154 | startScrolling() {
155 | let i = 0;
156 | const tick = () => {
157 | const { scaleX, scaleY, container } = this;
158 | const { strengthMultiplier, onScrollChange } = this.props;
159 |
160 | // stop scrolling if there's nothing to do
161 | if (strengthMultiplier === 0 || scaleX + scaleY === 0) {
162 | this.stopScrolling();
163 | return;
164 | }
165 |
166 | // there's a bug in safari where it seems like we can't get
167 | // mousemove events from a container that also emits a scroll
168 | // event that same frame. So we double the strengthMultiplier and only adjust
169 | // the scroll position at 30fps
170 | if (i++ % 2) {
171 | const {
172 | scrollLeft,
173 | scrollTop,
174 | scrollWidth,
175 | scrollHeight,
176 | clientWidth,
177 | clientHeight,
178 | } = container;
179 |
180 | const newLeft = scaleX
181 | ? container.scrollLeft = intBetween(
182 | 0,
183 | scrollWidth - clientWidth,
184 | scrollLeft + scaleX * strengthMultiplier
185 | )
186 | : scrollLeft;
187 |
188 | const newTop = scaleY
189 | ? container.scrollTop = intBetween(
190 | 0,
191 | scrollHeight - clientHeight,
192 | scrollTop + scaleY * strengthMultiplier
193 | )
194 | : scrollTop;
195 |
196 | onScrollChange(newLeft, newTop);
197 | }
198 | this.frame = raf(tick);
199 | };
200 |
201 | tick();
202 | }
203 |
204 | stopScrolling() {
205 | this.detach();
206 | this.scaleX = 0;
207 | this.scaleY = 0;
208 |
209 | if (this.frame) {
210 | raf.cancel(this.frame);
211 | this.frame = null;
212 | }
213 | }
214 |
215 | render() {
216 | const {
217 | // not passing down these props
218 | strengthMultiplier,
219 | verticalStrength,
220 | horizontalStrength,
221 | onScrollChange,
222 |
223 | ...props,
224 | } = this.props;
225 |
226 | return (
227 | { this.wrappedInstance = ref; }}
229 | {...props}
230 | />
231 | );
232 | }
233 | }
234 |
235 | return hoist(ScrollingComponent, WrappedComponent);
236 | }
237 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 |
2 | export function noop() {
3 |
4 | }
5 |
6 | export function intBetween(min, max, val) {
7 | return Math.floor(
8 | Math.min(max, Math.max(min, val))
9 | );
10 | }
11 |
12 |
13 | export function getCoords(evt) {
14 | if (evt.type === 'touchmove') {
15 | return { x: evt.changedTouches[0].clientX, y: evt.changedTouches[0].clientY };
16 | }
17 |
18 | return { x: evt.clientX, y: evt.clientY };
19 | }
20 |
--------------------------------------------------------------------------------
/test/fixtures/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azuqua/react-dnd-scrollzone/334066a1b079aa6e4b0130008f0519942ab27723/test/fixtures/.gitkeep
--------------------------------------------------------------------------------
/test/intBetween.js:
--------------------------------------------------------------------------------
1 | import { intBetween } from '../src/util';
2 |
3 | describe('private intBetween()', () => {
4 |
5 | it('should return val if it is an int between min and max', () => {
6 | expect(intBetween(0, 2, 1)).to.equal(1);
7 | });
8 |
9 | it('should floor the val if it not an int', () => {
10 | expect(intBetween(0, 2, .5)).to.equal(0);
11 | });
12 |
13 | it('should take the floor of the min if its the bigger than val and not an int', () => {
14 | expect(intBetween(.5, 2, -1)).to.equal(0);
15 | });
16 |
17 | it('should take the floor of the max if its lower than val and not an int', () => {
18 | expect(intBetween(0, 1.5, 2)).to.equal(1);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require babel-register
2 | --require test/setup
3 | --check-leaks
4 | --throw-deprecation
5 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import chai from 'chai';
2 | import sinon from 'sinon';
3 | import sinonChai from "sinon-chai";
4 |
5 | chai.should();
6 | chai.use(sinonChai);
7 | global.expect = chai.expect;
8 | global.sinon = sinon;
9 |
--------------------------------------------------------------------------------
/test/strength-creators-test.js:
--------------------------------------------------------------------------------
1 | import { createHorizontalStrength, createVerticalStrength } from '../src';
2 |
3 | describe('strength functions', function() {
4 | let hFn = createHorizontalStrength(150);
5 | let vFn = createVerticalStrength(150);
6 | let box = { x: 0, y: 0, w: 600, h: 600 };
7 | let lilBox = { x: 0, y: 0, w: 100, h: 100 };
8 |
9 | describe('horizontalStrength', function() {
10 | it('should return -1 when all the way at the left', function() {
11 | expect(hFn(box, { x: 0, y: 0 })).to.equal(-1);
12 | });
13 |
14 | it('should return 1 when all the way at the right', function() {
15 | expect(hFn(box, { x: 600, y: 0 })).to.equal(1);
16 | });
17 |
18 | it('should return 0 when in the center', function() {
19 | expect(hFn(box, { x: 300, y: 0 })).to.equal(0);
20 | });
21 |
22 | it('should return 0 when at either buffer boundary', function() {
23 | expect(hFn(box, { x: 150, y: 0 })).to.equal(0);
24 | expect(hFn(box, { x: 450, y: 0 })).to.equal(0);
25 | });
26 |
27 | it('should return 0 when outside the box', function() {
28 | expect(hFn(box, { x: 0, y: -100 })).to.equal(0);
29 | expect(hFn(box, { x: 0, y: 900 })).to.equal(0);
30 | });
31 |
32 | it('should scale linearly from the boundary to respective buffer', function() {
33 | expect(hFn(box, { x: 75, y: 0 })).to.equal(-.5);
34 | expect(hFn(box, { x: 525, y: 0 })).to.equal(.5);
35 | });
36 |
37 | it('should handle buffers larger than the box gracefully', function() {
38 | expect(hFn(lilBox, { x: 50, y: 0 })).to.equal(0);
39 | });
40 | });
41 |
42 | describe('verticalStrength', function() {
43 | it('should return -1 when all the way at the top', function() {
44 | expect(vFn(box, { x: 0, y: 0 })).to.equal(-1);
45 | });
46 |
47 | it('should return 1 when all the way at the bottom', function() {
48 | expect(vFn(box, { x: 0, y: 600 })).to.equal(1);
49 | });
50 |
51 | it('should return 0 when in the center', function() {
52 | expect(vFn(box, { x: 0, y: 300 })).to.equal(0);
53 | });
54 |
55 | it('should return 0 when at the buffer boundary', function() {
56 | expect(vFn(box, { x: 0, y: 150 })).to.equal(0);
57 | expect(vFn(box, { x: 0, y: 450 })).to.equal(0);
58 | });
59 |
60 | it('should return 0 when outside the box', function() {
61 | expect(vFn(box, { x: -100, y: 0 })).to.equal(0);
62 | expect(vFn(box, { x: 900, y: 0 })).to.equal(0);
63 | });
64 |
65 | it('should scale linearly from the boundary to respective buffer', function() {
66 | expect(vFn(box, { x: 0, y: 75 })).to.equal(-.5);
67 | expect(vFn(box, { x: 0, y: 525 })).to.equal(.5);
68 | });
69 |
70 | it('should handle buffers larger than the box gracefully', function() {
71 | expect(vFn(lilBox, { x: 0, y: 50 })).to.equal(0);
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------