├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── basic │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── DragItem.css │ │ ├── DragItem.js │ │ ├── index.css │ │ └── index.js └── mobile-backend │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ └── src │ ├── App.css │ ├── App.js │ ├── DragItem.css │ ├── DragItem.js │ ├── DragPreview.css │ ├── DragPreview.js │ ├── index.css │ └── index.js ├── package.json ├── src ├── index.js └── util.js └── test ├── fixtures └── .gitkeep ├── intBetween.js ├── mocha.opts ├── setup.js └── strength-creators-test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-1", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | orbs: 5 | general-platform-helpers: azuqua/general-platform-helpers@1.8 6 | 7 | # Define a job to be invoked later in a workflow. 8 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 9 | jobs: 10 | build: 11 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 12 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 13 | docker: 14 | - image: cimg/base:stable 15 | # Add steps to the job 16 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 17 | steps: 18 | - checkout 19 | - run: 20 | name: "build stage" 21 | command: "echo build stage" 22 | test: 23 | docker: 24 | - image: cimg/base:stable 25 | steps: 26 | - run: 27 | name: "test stage" 28 | command: "echo test stage" 29 | package: 30 | docker: 31 | - image: cimg/base:stable 32 | steps: 33 | - run: 34 | name: "package stage" 35 | command: "echo package stage" 36 | # Invoke jobs via workflows 37 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 38 | workflows: 39 | build-and-test: 40 | jobs: 41 | - build 42 | - test: 43 | requires: 44 | - build 45 | - package: 46 | requires: 47 | - test 48 | semgrep: 49 | jobs: 50 | - general-platform-helpers/job-semgrep-prepare: 51 | name: semgrep-prepare 52 | - general-platform-helpers/job-semgrep-scan: 53 | name: "Scan with Semgrep" 54 | requires: 55 | - semgrep-prepare 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | 5 | "ecmaFeatures": { 6 | "modules": true 7 | }, 8 | 9 | "rules": { 10 | "no-unused-vars": [0], 11 | "react/prop-types": [0] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | *.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | src 4 | test 5 | 6 | .babelrc 7 | .eslintrc 8 | .gitignore 9 | .travis.yml 10 | CONTRIBUTING 11 | *.log 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2' 4 | - '5.5' 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### `v5.0.0` 4 | * Added React 16 support 5 | 6 | ### `v4.0.0` 7 | * Change `speed` prop to `strengthMultiplier` 8 | * Adds a hard dependency on using `react-dnd` which was theoretically 9 | optional before. 10 | * Fix double dispatch of `onDragOver` prop 11 | * Default strength functions always return 0 if the point is anywhere 12 | outside the box. 13 | 14 | ### `v3.2.0` 15 | * Use `prop-types` package instead of deprecated `React.PropTypes` 16 | 17 | ### `v3.1.0` 18 | * Add `onScrollChange` prop 19 | 20 | ### `v3.0.0` 21 | * Export a higher order component instead of a component. 22 | * Set displayName on component 23 | * Hoist non-react static properties 24 | 25 | ##### Before (v2) 26 | ```js 27 | import Scrollzone from 'react-dnd-scrollzone'; 28 | const zone = ; 29 | ``` 30 | 31 | ##### After (v3) 32 | ```js 33 | import withScrolling from 'react-dnd-scrollzone'; 34 | const Scrollzone = withScrolling('div'); 35 | const zone = ; 36 | ``` 37 | 38 | ### `v2.0.0` 39 | * Remove `buffer` prop. 40 | * Add `horizontalStrength` and `verticalStrength` props. 41 | * Add `createVerticalStrength` and `createHorizontalStrength` exports. 42 | * Fix bug with strength calculations and large buffers. 43 | * Fix bug with scrolling not always stopping when drop targets are nested. 44 | 45 | ##### Before (v1) 46 | ```js 47 | import Scrollzone from 'react-dnd-scrollzone'; 48 | const zone = ; 49 | ``` 50 | 51 | ##### After (v2) 52 | ```js 53 | import Scrollzone, { createVerticalStrength, createHorizontalStrength } from 'react-dnd-scrollzone'; 54 | const vStrength = createVerticalStrength(300); 55 | const hStrength = createHorizontalStrength(300); 56 | const zone = ; 57 | ``` 58 | 59 | ### `v1.1.0` 60 | * Initial release. 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## PRs and Contributions 4 | * Tests must pass. 5 | * Follow existing coding style. 6 | * If you fix a bug or add a feature, add a test. 7 | 8 | ## Issues 9 | Things that will help get your question issue looked at: 10 | * Full and runnable JS code. 11 | * Clear description of the problem or unexpected behavior. 12 | * Clear description of the expected result. 13 | * Steps you have taken to debug it yourself. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Nicholas Clawson 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 | --------------------------------------------------------------------------------