├── .gitignore ├── .github └── FUNDING.yml ├── index.ts ├── babel.config.js ├── tsconfig.json ├── rollup.config.js ├── src ├── Draggable.tsx └── Container.tsx ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kutlugsahin] 4 | 5 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import Container from './src/Container'; 2 | import Draggable from './src/Draggable'; 3 | 4 | export * from 'smooth-dnd'; 5 | 6 | export { 7 | Container, 8 | Draggable 9 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | return { 5 | presets: [ 6 | "@babel/preset-typescript", 7 | "@babel/preset-react", 8 | "@babel/preset-env" 9 | ], 10 | plugins: [ 11 | "@babel/plugin-proposal-class-properties" 12 | ] 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "lib": [ 5 | "dom", 6 | "es2017" 7 | ], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | // "noEmit": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "preserveConstEnums": true, 15 | "removeComments": false, 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "target": "es5", 20 | "noImplicitAny": true, 21 | "noImplicitReturns": true, 22 | "strictFunctionTypes": true, 23 | "declaration": true, 24 | "declarationDir": "dist", 25 | "strictNullChecks": true, 26 | "noImplicitThis": false, 27 | } 28 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { uglify } from "rollup-plugin-uglify"; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | const extensions = [ 5 | '.js', '.jsx', '.ts', '.tsx', 6 | ]; 7 | 8 | module.exports = { 9 | input: 'index.ts', 10 | output: { 11 | file: './dist/index.js', 12 | format: 'umd', 13 | sourcemap: false, 14 | name: 'ReactSmoothDnD', 15 | globals: { 16 | 'smooth-dnd': 'SmoothDnD', 17 | 'react': 'React', 18 | 'prop-types': 'PropTypes' 19 | } 20 | }, 21 | plugins: [ 22 | babel({ 23 | extensions, 24 | include: ['./index.ts', 'src/**/*'], 25 | exclude: 'node_modules/**', 26 | }), 27 | commonjs({ 28 | extensions 29 | }), 30 | uglify() 31 | ], 32 | }; -------------------------------------------------------------------------------- /src/Draggable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { constants } from 'smooth-dnd'; 4 | const { 5 | wrapperClass 6 | } = constants; 7 | 8 | export interface DraggableProps { 9 | render?: () => React.ReactElement; 10 | className?: string; 11 | } 12 | 13 | class Draggable extends Component { 14 | public static propsTypes = { 15 | render: PropTypes.func, 16 | className: PropTypes.string, 17 | } 18 | 19 | render() { 20 | if (this.props.render) { 21 | return React.cloneElement(this.props.render(), { className: wrapperClass }); 22 | } 23 | 24 | const clsName = `${this.props.className ? (this.props.className + ' ') : ''}` 25 | return ( 26 |
27 | {this.props.children} 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Draggable; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kutlu Sahin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-smooth-dnd", 3 | "version": "0.11.1", 4 | "description": "Drag and Drop library", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "bundle": "rollup -c --environment INCLUDE_DEPS,BUILD:production", 8 | "tsc": "tsc --emitDeclarationOnly --outDir dist", 9 | "build": "npm run tsc && npm run bundle" 10 | }, 11 | "types": "./dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "sortable", 18 | "drag and drop", 19 | "drag&drop", 20 | "drag", 21 | "drop", 22 | "draggable", 23 | "dnd" 24 | ], 25 | "author": { 26 | "name": "Kutlu Sahin", 27 | "email": "kutlugsahin@gmail.com" 28 | }, 29 | "license": "ISC", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/kutlugsahin/react-smooth-dnd.git" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "7.3.4", 36 | "@babel/plugin-proposal-class-properties": "^7.3.4", 37 | "@babel/preset-env": "7.3.4", 38 | "@babel/preset-react": "^7.0.0", 39 | "@babel/preset-typescript": "7.3.3", 40 | "rollup": "1.4.0", 41 | "rollup-plugin-babel": "4.3.2", 42 | "rollup-plugin-commonjs": "^9.2.1", 43 | "rollup-plugin-uglify": "6.0.2", 44 | "typescript": "3.3.3333", 45 | "@types/react": "^16.3.0" 46 | }, 47 | "peerDependencies": { 48 | "react": "^16.3.0" 49 | }, 50 | "dependencies": { 51 | "smooth-dnd": "0.12.1", 52 | "prop-types":">=15.6.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Container.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, CSSProperties } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { smoothDnD as container, ContainerOptions, SmoothDnD } from 'smooth-dnd'; 4 | import { dropHandlers } from 'smooth-dnd'; 5 | 6 | container.dropHandler = dropHandlers.reactDropHandler().handler; 7 | container.wrapChild = false; 8 | 9 | interface ContainerProps extends ContainerOptions { 10 | render?: (rootRef: React.RefObject) => React.ReactElement; 11 | style?: CSSProperties; 12 | } 13 | 14 | class Container extends Component { 15 | public static propTypes = { 16 | behaviour: PropTypes.oneOf(['move', 'copy', 'drop-zone', 'contain']), 17 | groupName: PropTypes.string, 18 | orientation: PropTypes.oneOf(['horizontal', 'vertical']), 19 | style: PropTypes.object, 20 | dragHandleSelector: PropTypes.string, 21 | nonDragAreaSelector: PropTypes.string, 22 | dragBeginDelay: PropTypes.number, 23 | animationDuration: PropTypes.number, 24 | autoScrollEnabled: PropTypes.bool, 25 | lockAxis: PropTypes.string, 26 | dragClass: PropTypes.string, 27 | dropClass: PropTypes.string, 28 | onDragStart: PropTypes.func, 29 | onDragEnd: PropTypes.func, 30 | onDrop: PropTypes.func, 31 | getChildPayload: PropTypes.func, 32 | shouldAnimateDrop: PropTypes.func, 33 | shouldAcceptDrop: PropTypes.func, 34 | onDragEnter: PropTypes.func, 35 | onDragLeave: PropTypes.func, 36 | render: PropTypes.func, 37 | getGhostParent: PropTypes.func, 38 | removeOnDropOut: PropTypes.bool, 39 | dropPlaceholder: PropTypes.oneOfType([ 40 | PropTypes.shape({ 41 | className: PropTypes.string, 42 | animationDuration: PropTypes.number, 43 | showOnTop: PropTypes.bool, 44 | }), 45 | PropTypes.bool, 46 | ]), 47 | }; 48 | 49 | public static defaultProps = { 50 | behaviour: 'move', 51 | orientation: 'vertical', 52 | }; 53 | 54 | prevContainer: null; 55 | container: SmoothDnD = null!; 56 | containerRef: React.RefObject = React.createRef(); 57 | constructor(props: ContainerProps) { 58 | super(props); 59 | this.getContainerOptions = this.getContainerOptions.bind(this); 60 | this.getContainer = this.getContainer.bind(this); 61 | this.isObjectTypePropsChanged = this.isObjectTypePropsChanged.bind(this); 62 | this.prevContainer = null; 63 | } 64 | 65 | componentDidMount() { 66 | this.prevContainer = this.getContainer(); 67 | this.container = container(this.getContainer(), this.getContainerOptions()); 68 | } 69 | 70 | componentWillUnmount() { 71 | this.container.dispose(); 72 | this.container = null!; 73 | } 74 | 75 | componentDidUpdate(prevProps: ContainerProps) { 76 | if (this.getContainer()) { 77 | if (this.prevContainer && this.prevContainer !== this.getContainer()) { 78 | this.container.dispose(); 79 | this.container = container(this.getContainer(), this.getContainerOptions()); 80 | this.prevContainer = this.getContainer(); 81 | return; 82 | } 83 | 84 | if (this.isObjectTypePropsChanged(prevProps)) { 85 | this.container.setOptions(this.getContainerOptions()) 86 | } 87 | } 88 | } 89 | 90 | isObjectTypePropsChanged(prevProps: ContainerProps) { 91 | const { render, children, style, ...containerOptions } = this.props; 92 | 93 | for (const _key in containerOptions) { 94 | const key = _key as keyof ContainerOptions; 95 | if (containerOptions.hasOwnProperty(key)) { 96 | const prop = containerOptions[key]; 97 | 98 | if (typeof prop !== 'function' && prop !== prevProps[key]) { 99 | return true; 100 | } 101 | } 102 | } 103 | 104 | return false; 105 | } 106 | 107 | render() { 108 | if (this.props.render) { 109 | return this.props.render(this.containerRef); 110 | } else { 111 | return ( 112 |
113 | {this.props.children} 114 |
115 | ); 116 | } 117 | } 118 | 119 | getContainer() { 120 | return this.containerRef.current; 121 | } 122 | 123 | getContainerOptions(): ContainerOptions { 124 | return Object.keys(this.props).reduce((result: ContainerOptions, key: string) => { 125 | const optionName = key as keyof ContainerOptions; 126 | const prop = this.props[optionName]; 127 | 128 | if (typeof prop === 'function') { 129 | result[optionName] = (...params: any[]) => { 130 | return (this.props[optionName] as Function)(...params); 131 | } 132 | } else { 133 | result[optionName] = prop; 134 | } 135 | 136 | return result; 137 | },{}) as ContainerOptions; 138 | } 139 | } 140 | 141 | export default Container; 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-smooth-dnd 2 | 3 | A fast and lightweight drag&drop, sortable library for React with many configuration options covering many d&d scenarios. It uses css transitions for animations so it's hardware accelerated whenever possible. 4 | 5 | This library consists wrapper React components over smooth-dnd library. 6 | 7 | **Show, don't tell !** 8 | ### Demo page 9 | 10 | ### Installation 11 | 12 | ```shell 13 | npm i react-smooth-dnd 14 | ``` 15 | 16 | ## Usage 17 | 18 | #### JSX 19 | 20 | 21 | ```jsx 22 | import React, { Component } from 'react'; 23 | import { Container, Draggable } from 'react-smooth-dnd'; 24 | 25 | class SimpleSortableList extends Component { 26 | render() { 27 | return ( 28 |
29 | 30 | {this.props.items.map(item => { 31 | return ( 32 | 33 | {this.props.renderItem(item)} 34 | 35 | ); 36 | })} 37 | 38 |
39 | ); 40 | } 41 | } 42 | ``` 43 | 44 | ## API 45 | 46 | ### **Container** 47 | 48 | Component that contains the draggable elements or components. Each of its children should be wrapped by **Draggable** component 49 | 50 | 51 | ### Props 52 | 53 | | Property | Type | Default | Description | 54 | |-|:-:|:-:|-| 55 | |children|**Draggable**|null|React children prop. Should be of type **Draggable**. 56 | | orientation |string|`vertical` | Orientation of the container. Can be **horizontal** or **vertical**.| 57 | |behaviour|string|`move`| Property to describe weather the dragging item will be moved or copied to target container. If drop-zone is set no draggable will slide when container dragged over. Can be **move** or **copy** or **drop-zone** or **contain**| 58 | |groupName|string|`undefined`|Draggables can be moved between the containers having the same group names. If not set container will not accept drags from outside. This behaviour can be overriden by shouldAcceptDrop function. See below. 59 | |lockAxis|string|`undefined`|Locks the movement axis of the dragging. Possible values are **x**, **y** or **undefined**. 60 | |dragHandleSelector|string|`undefined`|Css selector to test for enabling dragging. If not given item can be grabbed from anywhere in its boundaries.| 61 | |nonDragAreaSelector|string|`undefined`|Css selector to prevent dragging. Can be useful when you have form elements or selectable text somewhere inside your draggable item. It has a precedence over **dragHandleSelector**.| 62 | |dragBeginDelay|number| `0` (`200` for touch devices)|Time in milisecond. Delay to start dragging after item is pressed. Moving cursor before the delay more than 5px will cancel dragging. 63 | |animationDuration|number|`250`|Animation duration in milisecond. To be consistent this animation duration will be applied to both drop and reorder animations.| 64 | |autoScrollEnabled|boolean|`true`|First scrollable parent will scroll automatically if dragging item is close to boundaries. 65 | |dragClass|string|`undefined`|Class to be added to the ghost item being dragged. The class will be added after it's added to the DOM so any transition in the class will be applied as intended. 66 | |dropClass|string|`undefined`|Class to be added to the ghost item just before the drop animation begins.| 67 | |removeOnDropOut|boolean|`undefined`|When set true onDrop will be called with a removedIndex if you drop element out of any relevant container| 68 | |dropPlaceholder|boolean,object|`undefined`|Options for drop placeholder. **className**, **animationDuration**, **showOnTop**| 69 | |onDragStart|function|`undefined`|*See descriptions below*| 70 | |onDragEnd|function|`undefined`|*See descriptions below*| 71 | |onDropReady|function|`undefined`|*See descriptions below*| 72 | |onDrop|function|`undefined`|*See descriptions below*| 73 | |getChildPayload|function|`undefined`|*See descriptions below*| 74 | |shouldAnimateDrop|function|`undefined`|*See descriptions below*| 75 | |shouldAcceptDrop|function|`undefined`|*See descriptions below*| 76 | |onDragEnter|function|`undefined`|*See descriptions below*| 77 | |onDragLeave|function|`undefined`|*See descriptions below*| 78 | |getGhostParent|function|`undefined`|*See descriptions below*| 79 | |render|function|`undefined`|*See descriptions below*| 80 | 81 | 82 | --- 83 | 84 | ### onDragStart 85 | 86 | The function to be called by all container when drag start. 87 | ```js 88 | function onDragStart({isSource, payload, willAcceptDrop}) { 89 | ... 90 | } 91 | ``` 92 | #### Parameters 93 | - **isSource** : `boolean` : true if it is called by the container which drag starts from otherwise false. 94 | - **payload** : `object` : the payload object that is returned by getChildPayload function. It will be undefined in case getChildPayload is not set. 95 | - **willAcceptDrop** : `boolean` : true if the dragged item can be dropped into the container, otherwise false. 96 | 97 | ### onDragEnd 98 | 99 | The function to be called by all container when drag ends. Called before **onDrop** function 100 | ```js 101 | function onDragEnd({isSource, payload, willAcceptDrop}) { 102 | ... 103 | } 104 | ``` 105 | #### Parameters 106 | - **isSource** : `boolean` : true if it is called by the container which drag starts from, otherwise false. 107 | - **payload** : `object` : the payload object that is returned by getChildPayload function. It will be undefined in case getChildPayload is not set. 108 | - **willAcceptDrop** : `boolean` : true if the dragged item can be dropped into the container, otherwise false. 109 | 110 | ### onDropReady 111 | 112 | The function to be called by the container which is being drag over, when the index of possible drop position changed in container. Basically it is called each time the draggables in a container slides for opening a space for dragged item. **dropResult** is the only parameter passed to the function which contains the following properties. 113 | ```js 114 | function onDropReady(dropResult) { 115 | const { removedIndex, addedIndex, payload, element } = dropResult; 116 | ... 117 | } 118 | ``` 119 | #### Parameters 120 | - **dropResult** : `object` 121 | - **removedIndex** : `number` : index of the removed children. Will be `null` if no item is removed. 122 | - **addedIndex** : `number` : index to add droppped item. Will be `null` if no item is added. 123 | - **payload** : `object` : the payload object retrieved by calling *getChildPayload* function. 124 | - **element** : `DOMElement` : the DOM element that is moved 125 | 126 | ### onDrop 127 | 128 | The function to be called by any relevant container when drop is over. (After drop animation ends). Source container and any container that could accept drop is considered relevant. **dropResult** is the only parameter passed to the function which contains the following properties. 129 | ```js 130 | function onDrop(dropResult) { 131 | const { removedIndex, addedIndex, payload, element } = dropResult; 132 | ... 133 | } 134 | ``` 135 | #### Parameters 136 | - **dropResult** : `object` 137 | - **removedIndex** : `number` : index of the removed children. Will be `null` if no item is removed. 138 | - **addedIndex** : `number` : index to add droppped item. Will be `null` if no item is added. 139 | - **payload** : `object` : the payload object retrieved by calling *getChildPayload* function. 140 | - **element** : `DOMElement` : the DOM element that is moved 141 | 142 | ### getChildPayload 143 | 144 | The function to be called to get the payload object to be passed **onDrop** function. 145 | ```js 146 | function getChildPayload(index) { 147 | return { 148 | ... 149 | } 150 | } 151 | ``` 152 | #### Parameters 153 | - **index** : `number` : index of the child item 154 | #### Returns 155 | - **payload** : `object` 156 | 157 | ### shouldAnimateDrop 158 | 159 | The function to be called by the target container to which the dragged item will be droppped. 160 | Sometimes dragged item's dimensions are not suitable with the target container and dropping animation can be wierd. So it can be disabled by returning **false**. If not set drop animations are enabled. 161 | ```js 162 | function shouldAnimateDrop(sourceContainerOptions, payload) { 163 | return false; 164 | } 165 | ``` 166 | #### Parameters 167 | - **sourceContainerOptions** : `object` : options of the source container. (parent container of the dragged item) 168 | - **payload** : `object` : the payload object retrieved by calling *getChildPayload* function. 169 | #### Returns 170 | - **boolean** : **true / false** 171 | 172 | ### shouldAcceptDrop 173 | 174 | The function to be called by all containers before drag starts to determine the containers to which the drop is possible. Setting this function will override the **groupName** property and only the return value of this function will be taken into account. 175 | 176 | ```js 177 | function shouldAcceptDrop(sourceContainerOptions, payload) { 178 | return true; 179 | } 180 | ``` 181 | #### Parameters 182 | - **sourceContainerOptions** : `object` : options of the source container. (parent container of the dragged item) 183 | - **payload** : `object` : the payload object retrieved by calling *getChildPayload* function. 184 | #### Returns 185 | - **boolean** : **true / false** 186 | 187 | ### onDragEnter 188 | 189 | The function to be called by the relevant container whenever a dragged item enters its boundaries while dragging. 190 | ```js 191 | function onDragEnter() { 192 | ... 193 | } 194 | ``` 195 | 196 | ### onDragLeave 197 | 198 | The function to be called by the relevant container whenever a dragged item leaves its boundaries while dragging. 199 | ```js 200 | function onDragLeave() { 201 | ... 202 | } 203 | ``` 204 | 205 | ### getGhostParent 206 | 207 | The function to be called to get the element that the dragged ghost will be appended. Default parent element is the container itself. 208 | The ghost element is positioned as 'fixed' and appended to given parent element. 209 | But if any anchestor of container has a transform property, ghost element will be positioned relative to that element which breaks the calculations. Thats why if you have any transformed parent element of Containers you should set this property so that it returns any element that has not transformed parent element. 210 | ```js 211 | function getGhostParent() { 212 | // i.e return document.body; 213 | } 214 | ``` 215 | 216 | ### render 217 | 218 | By default Container uses a div element for component root. You can define what to render as root element by using **render** function. If render function is set, **children** prop of Container will be ignored and return value of the render will be used to render draggables. 219 | ```js 220 | function render(ref) { 221 | // return ReactElement 222 | } 223 | ``` 224 | #### Parameters 225 | - **ref** : `object` : React reference object created by *React.createRef()* 226 | #### Returns 227 | - **React Element** 228 | 229 | ```jsx 230 | { 231 | return ( 232 |
    233 | ...place your components here 234 |
235 | ) 236 | }}/> 237 | 238 | ``` 239 | 240 | ### **Draggable** 241 | 242 | Wrapper component for Container's children. Every draggable element should be wrapped with **Draggable** component. 243 | > Make sure to set unique key to **Draggable** especially when it contains other **Container** components 244 | 245 | ### Props 246 | 247 | | Property | Type | Default | Description | 248 | |-|:-:|:-:|-| 249 | |render|function|`undefined`|*See descriptions below*| 250 | 251 | ### render 252 | 253 | By default Draggable uses a div element for component root. You can define what to render as root element by using **render** function. If render function is set, **children** prop of Draggable will be ignored and return value of the render will be used to render draggable. 254 | ```jsx 255 | { 256 | return ( 257 |
  • 258 | ... 259 |
  • 260 | ) 261 | }}/> 262 | 263 | ``` 264 | #### Parameters 265 | - **ref** : `object` : React reference object created by *React.createRef()* 266 | #### Returns 267 | - **React Element** --------------------------------------------------------------------------------