├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── example ├── Example.tsx ├── grippy.png ├── index.html ├── main.css └── main.tsx ├── index.js.flow ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── MoveContainer.tsx ├── OnUpdate.tsx ├── TemplateContainer.tsx ├── index.test.tsx └── index.tsx ├── test-types └── typescript.tsx └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-transform-runtime", 9 | [ 10 | "add-module-exports", 11 | { 12 | "addDefaultProperty": true 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /example/dist 2 | /dist 3 | *.js.flow 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | jest: true, 5 | node: true, 6 | es6: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:react/recommended', 13 | ], 14 | rules: { 15 | 'react/no-children-prop': ['off'], 16 | 17 | '@typescript-eslint/no-non-null-assertion': ['off'], 18 | '@typescript-eslint/no-explicit-any': ['off'], 19 | '@typescript-eslint/explicit-function-return-type': ['off'], 20 | '@typescript-eslint/no-unused-vars': [ 21 | 'error', 22 | { argsIgnorePattern: '[iI]gnored' }, 23 | ], 24 | '@typescript-eslint/ban-types': ['off'], 25 | '@typescript-eslint/explicit-module-boundary-types': ['off'], 26 | 27 | indent: ['off'], 28 | 'react/jsx-indent': ['error', 2], 29 | 'react/jsx-indent-props': ['error', 2], 30 | 'linebreak-style': ['error', 'unix'], 31 | quotes: ['off'], 32 | semi: ['error', 'always'], 33 | 'no-var': ['error'], 34 | 'brace-style': ['error'], 35 | 'array-bracket-spacing': ['error', 'never'], 36 | 'block-spacing': ['error', 'always'], 37 | 'no-spaced-func': ['error'], 38 | 'no-whitespace-before-property': ['error'], 39 | 'space-before-blocks': ['error', 'always'], 40 | 'keyword-spacing': ['error'], 41 | 42 | // We use Typescript for this 43 | 'react/prop-types': ['off'], 44 | }, 45 | settings: { 46 | react: { 47 | version: '16.6', 48 | }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/\(test\|lib\|example\|samplejson\)/.*\.json 3 | /dist/ 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 20 14 | cache: 'npm' 15 | - run: npm ci --force 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /node_modules 4 | npm-debug.log 5 | /dist 6 | /example/dist 7 | /.cache 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /example/dist 2 | /dist 3 | .cache 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "flow.useNPMPackagedFlow": true, 4 | "eslint.validate": [ 5 | "javascript", 6 | "javascriptreact", 7 | "typescript", 8 | "typescriptreact" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.2.1 (2024-05-23) 2 | 3 | - Reupload package to work around minor file metadata issue caused by Yarn (https://github.com/yarnpkg/yarn/issues/8109). This version contains no other changes. 4 | 5 | ## 4.2.0 (2024-02-19) 6 | 7 | - Added `draggedItem` parameter to the `onDragStart` and `onDragEnd` callback props. 8 | 9 | ## 4.1.0 (2022-08-12) 10 | 11 | - Implemented `onDragStart` and `onDragEnd` props. ([@JayHales](https://github.com/JayHales) in [#52](https://github.com/StreakYC/react-draggable-list/pull/52)) 12 | 13 | ## 4.0.4 (2021-09-22) 14 | 15 | - Updated peerDependencies list to mark compatibility with React 17. 16 | 17 | ## 4.0.3 (2019-06-05) 18 | 19 | - Updated to use immutability-helper 3.0. 20 | 21 | ## 4.0.2 (2019-04-05) 22 | 23 | - Fixed issue where the DraggableList could have an incorrect height during drag depending on the styling of its parents. 24 | 25 | ## 4.0.0 (2018-11-28) 26 | 27 | ### Breaking Changes 28 | 29 | - React v16.6+ is now required. 30 | - The `dragHandle` function prop was removed. Now the Template component is instead given a prop `dragHandleProps` which is an object that must be spread as props on the HTML element to be used as the drag handle. 31 | 32 | ReactDraggableList v3: 33 | 34 | ```js 35 |
36 | {this.props.dragHandle(
drag me
)} 37 |
content
38 |
39 | ``` 40 | 41 | ReactDraggableList v4: 42 | 43 | ```js 44 |
45 |
drag me
46 |
content
47 |
48 | ``` 49 | 50 | ### Improvements 51 | 52 | - No longer uses any deprecated APIs (lifecycle methods and ReactDOM.findDOMNode). 53 | - Fixed bug where the `oldIndex` parameter passed to `onMoveEnd` was incorrect if the `list` prop was updated while the user was dragging an item. 54 | 55 | ## 3.7.0 (2018-11-05) 56 | 57 | - Added TypeScript type definitions. 58 | - Changed Flow type definitions to use `$ReadOnlyArray` where applicable. Users may need to change the type annotations on the function they give to the `onMoveEnd` prop to keep Flow's type-check passing. 59 | 60 | ## 3.6.0 (2018-08-24) 61 | 62 | - Added `constrainDrag` prop. [#30](https://github.com/StreakYC/react-draggable-list/pull/30) 63 | 64 | ## 3.5.3 (2018-05-15) 65 | 66 | - Updated for compatibility with Flow v0.72. 67 | 68 | ## 3.5.2 (2018-04-13) 69 | 70 | - Improved Flow type definitions to cover the proper return type of `getItemInstance`. 71 | 72 | ## 3.5.1 (2018-04-13) 73 | 74 | - Fixed accidental usage of `event` global variable. This didn't cause any user-visible bugs to my knowledge. 75 | 76 | ## 3.5.0 (2018-04-13) 77 | 78 | - Flow types for DraggableList now include a type parameter representing the list item's type, enabling fuller type-checking coverage. 79 | 80 | ## 3.4.1 (2017-10-02) 81 | 82 | - Updated package.json to mark compatibility with React v16. 83 | - Internal: tests now use React v16. 84 | 85 | ## 3.4.0 (2017-09-11) 86 | 87 | - Updated for compatibility with Flow v0.54.1. This made it incompatible with older versions of Flow, so I'm making this update be a semver-minor change so users still on an older Flow version can pin to the previous minor version. Because of its frequent changes, I'm not considering incompatibilities with old Flow versions as semver-major breaking changes. 88 | 89 | ## 3.3.1 (2017-07-07) 90 | 91 | - Updated for compatibility with Flow v0.49.1. 92 | 93 | ## 3.3.0 (2017-04-25) 94 | 95 | - Stop using the newly deprecated `React.PropTypes` and now use the separate prop-types module. 96 | 97 | ## 3.2.1 (2017-03-24) 98 | 99 | - Fixed issue where components didn't re-render when the value of the `commonProps` prop changed. [#20](https://github.com/StreakYC/react-draggable-list/pull/20) 100 | 101 | ## 3.2.0 (2017-03-06) 102 | 103 | - Added `autoScrollMaxSpeed` and `autoScrollRegionSize` props to DraggableList. [#15](https://github.com/StreakYC/react-draggable-list/pull/15) 104 | - Added `commonProps` prop to DraggableList. [#18](https://github.com/StreakYC/react-draggable-list/pull/18) 105 | 106 | ## 3.1.4 (2017-03-06) 107 | 108 | - Updated for compatibility with Flow v0.41. 109 | 110 | ## 3.1.3 (2017-01-24) 111 | 112 | - Updated for compatibility with Flow v0.38. 113 | 114 | ## 3.1.2 (2016-10-26) 115 | 116 | - Fixed issue with the dragged item itself being removed during the post-drag animation. 117 | 118 | ## 3.1.1 (2016-10-25) 119 | 120 | - Fixed handling of items being removed from list during the post-drag animation. 121 | 122 | ## 3.1.0 (2016-10-25) 123 | 124 | - Added `getItemInstance` method. 125 | 126 | ## 3.0.4 (2016-09-26) 127 | 128 | - Fixed Flow type-checking issue when used with newer version of react-motion. 129 | 130 | ## 3.0.3 (2016-09-13) 131 | 132 | - Updated for compatibility with Flow v0.32. 133 | 134 | ## 3.0.2 (2016-08-05) 135 | 136 | - Updated for compatibility with Flow v0.30. 137 | 138 | ## 3.0.1 (2016-06-27) 139 | 140 | - Fixed handling of props being changed while the user is dragging an item. 141 | - Fixed issue where DraggableList could prevent elements from having a natural layout applied after being dragged when the container prop was not used. 142 | 143 | ## 3.0.0 (2016-04-07) 144 | 145 | ### Breaking Changes 146 | 147 | - React v15 is now required. 148 | 149 | ## 2.1.0 (2016-03-02) 150 | 151 | - Added `unsetZIndex` prop. 152 | 153 | ## 2.0.0 (2016-02-29) 154 | 155 | ### Breaking Changes 156 | 157 | - If the `getDragHeight` method isn't present on the template component, the drag height now defaults to the element's natural height instead of the arbitrary height of 30px. 158 | 159 | ## 1.0.6 (2016-02-29) 160 | 161 | - Fixed the DraggableList element changing height when the last item is grabbed. 162 | 163 | ## 1.0.5 (2016-02-25) 164 | 165 | - Re-use dragHandle prop value given to component to reduce amount of re-renders. 166 | 167 | ## 1.0.3 (2016-02-25) 168 | 169 | - Fixed a scroll animation glitch when DraggableList was not in a scrollable container. 170 | 171 | ## 1.0.2 (2016-02-25) 172 | 173 | - Fixed an accuracy issue with the scroll animation on drop. 174 | 175 | ## 1.0.1 (2016-02-25) 176 | 177 | - Fixed animation glitch if you pick up an item while it's still animating. 178 | - Efficiency improvements: minimize amount of re-renders needed during dragging. 179 | 180 | ## 1.0.0 (2016-02-24) 181 | 182 | Initial stable release. 183 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Rewardly, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-draggable-list 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/StreakYC/react-draggable-list/blob/master/LICENSE.txt) [![npm version](https://img.shields.io/npm/v/react-draggable-list.svg?style=flat)](https://www.npmjs.com/package/react-draggable-list) [![Node.js CI](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml/badge.svg)](https://github.com/StreakYC/react-draggable-list/actions/workflows/node.js.yml) 4 | 5 | This component lets you make a user re-orderable list that animates nicely so 6 | that the user can easily move large items: 7 | 8 | ![Example](https://streakyc.github.io/react-draggable-list/video/dragitem.gif) 9 | 10 | The above example can be tried here: 11 | 12 | https://streakyc.github.io/react-draggable-list/example/ 13 | 14 | You can find its code in the `example` directory. The example may be compiled 15 | by running: 16 | 17 | ``` 18 | npm i --force 19 | npm run example-build 20 | # or use this to auto-rebuild on changes: 21 | npm run example-watch 22 | ``` 23 | 24 | ## DraggableList 25 | 26 | This module exports the `DraggableList` React component, which takes the 27 | following props: 28 | 29 | - `list` must be an array of objects representing your list's items. 30 | - `itemKey` must be the name of a property of the list's objects to use as a 31 | key to identify the objects, or it must be a function that takes an object as 32 | an argument and returns a key. 33 | - `template` must be a React component used to render the list items. This must 34 | not be a stateless-functional component. If possible, don't pass a new 35 | class instance on every render. See the next section for more information 36 | on the template including a description of the props passed to the component. 37 | - `onMoveEnd` may be a function which will be called when the user drags and 38 | drops an item to a new position in the list. The arguments to the function 39 | will be `(newList: Array, movedItem: Object, oldIndex: number, newIndex: number)`. A component using DraggableList should immediately store 40 | the newList into its state and then pass the new list (or an equivalent list) 41 | as the `list` prop to DraggableList. 42 | - `container`: If the DraggableList is inside a scrollable element, then this 43 | property should be set to a function which returns a reference to it. When the 44 | user moves an item in the list, the container will be scrolled to keep the 45 | item in view. If the DraggableList is in no scrollable elements besides the 46 | page itself, then a function returning a reference to `document.body` should 47 | be given. 48 | - `springConfig` is an optional object which sets the [SpringHelperConfig 49 | object passed to 50 | React-Motion](https://github.com/chenglou/react-motion/tree/85ca75c6de9ed85937d1c95646b6044a66981eee#--spring-val-number-config-springhelperconfig--opaqueconfig) 51 | for animations. This prop defaults to `{stiffness: 300, damping: 50}`. 52 | - `padding` is an optional number of pixels to leave between items. Defaults to 10. 53 | - `unsetZIndex` is an optional property that defaults to false. If set to true, 54 | then the z-index of all of the list items will be set to "auto" when the list 55 | isn't animating. This may have a small performance cost when the list starts 56 | and stops animating. Use this if you need to avoid having the list item create 57 | a stacking context when it's not being animated. 58 | - `constrainDrag` is an option property that defaults to false. If it is set to 59 | true, then the y-coordinate of a dragged item will be constrained vertically to 60 | the bounds of the list. 61 | - `autoScrollMaxSpeed` is an optional number that allows the scroll speed when 62 | the user drags to the top or bottom of the list to be overridden. 63 | - `autoScrollRegionSize` is an optional number that allows the height of the 64 | region that triggers auto-scrolling when dragged onto to be overridden. 65 | - `commonProps` is an optional value that will be passed as the `commonProps` 66 | prop to every template component instance. 67 | - `onDragStart` is an optional function which is called once a list item starts 68 | being dragged. Receives the dragged item as an argument. 69 | - `onDragEnd` is an optional function which is called once a list item is no longer being dragged. Receives the dragged item as an argument. It differs from `onMoveEnd` in that it's called even if the user does not reorder any items in the lists, like when an item is just picked up and then dropped. 70 | 71 | A DraggableList instance has the following methods: 72 | 73 | - `getItemInstance(key)` will return a reference to the mounted instance of the 74 | template for a given key. 75 | 76 | ## Template 77 | 78 | The template component is passed the following props: 79 | 80 | - `item` is an object from the list prop passed to DraggableList. 81 | - `itemSelected` is a number from 0 to 1. It starts at 0, and quickly increases 82 | to 1 when the item is picked up by the user. This may be used to animate the 83 | item when the user picks it up or drops it. 84 | - `anySelected` is a number from 0 to 1. It starts at 0, and quickly increases 85 | to 1 when any item is picked up by the user. 86 | - `dragHandleProps` is an object which should be spread as props on the HTML 87 | element to be used as the drag handle. The whole item will be draggable by the 88 | wrapped element. See the 89 | [example](https://github.com/StreakYC/react-draggable-list/blob/master/example/Example.tsx) 90 | to see how it should be used. 91 | - `commonProps` will be set to the same value passed as the `commonProps` prop 92 | to the DraggableList component. 93 | 94 | The template component should be styled with max-height set to "100%" for best 95 | results. 96 | 97 | The template component will have its props updated many times quickly during 98 | the animation, so implementing `shouldComponentUpdate` in its children is 99 | highly recommended. 100 | 101 | The template component may have a `getDragHeight` method which may return a 102 | number to set the height in pixels of the item while the user is dragging it. 103 | If the method returns null or is not present, then the drag height will be 104 | equal to the element's natural height. 105 | 106 | ## Bundling Note 107 | 108 | To use this module in browsers, a CommonJS bundler such as Parcel, Browserify, or 109 | Webpack should be used. 110 | 111 | This project relies on the javascript Map object being available globally. A 112 | global polyfill such as [Babel's polyfill](https://babeljs.io/docs/usage/polyfill/) 113 | is required to support [older browsers that don't implement these](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Browser_compatibility). 114 | 115 | ## Types 116 | 117 | Both [TypeScript](https://www.typescriptlang.org/) and 118 | [Flow](https://flowtype.org/) type definitions for this module are included! 119 | The type definitions won't require any configuration to use. 120 | -------------------------------------------------------------------------------- /example/Example.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import * as React from 'react'; 4 | import cx from 'classnames'; 5 | import DraggableList from '../src'; 6 | 7 | interface PlanetListItem { 8 | name: string; 9 | subtitle?: boolean; 10 | } 11 | 12 | interface PlanetProps { 13 | item: PlanetListItem; 14 | itemSelected: number; 15 | dragHandleProps: object; 16 | } 17 | interface PlanetState { 18 | value: number; 19 | } 20 | class PlanetItem extends React.Component { 21 | state = { 22 | value: 0, 23 | }; 24 | 25 | _inc() { 26 | this.setState({ 27 | value: this.state.value + 1, 28 | }); 29 | } 30 | 31 | getDragHeight() { 32 | return this.props.item.subtitle ? 47 : 28; 33 | } 34 | 35 | render() { 36 | const { item, itemSelected, dragHandleProps } = this.props; 37 | const { value } = this.state; 38 | const scale = itemSelected * 0.05 + 1; 39 | const shadow = itemSelected * 15 + 1; 40 | const dragged = itemSelected !== 0; 41 | 42 | return ( 43 |
50 |
51 |

{item.name}

52 | {item.subtitle && ( 53 |
54 | This item has a subtitle visible while dragging 55 |
56 | )} 57 |
58 | some description here 59 |
60 | this planet orbits the sun 61 |
62 | this planet is mostly round 63 |
64 | {item.subtitle && ( 65 |
66 | subtitled planets are better 67 |
68 | and have longer descriptions 69 |
70 | )} 71 |
72 | State works and is retained during movement:{' '} 73 | this._inc()} /> 74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | type ExampleState = { 81 | useContainer: boolean; 82 | list: ReadonlyArray; 83 | }; 84 | export default class Example extends React.Component<{}, ExampleState> { 85 | private _container = React.createRef(); 86 | 87 | state = { 88 | useContainer: false, 89 | list: [ 90 | { name: 'Mercury' }, 91 | { name: 'Venus' }, 92 | { name: 'Earth', subtitle: true }, 93 | { name: 'Mars' }, 94 | { name: 'Jupiter' }, 95 | { name: 'Saturn', subtitle: true }, 96 | { name: 'Uranus', subtitle: true }, 97 | { name: 'Neptune' }, 98 | ], 99 | }; 100 | 101 | private _togglePluto() { 102 | const noPluto = this.state.list.filter((item) => item.name !== 'Pluto'); 103 | if (noPluto.length !== this.state.list.length) { 104 | this.setState({ list: noPluto }); 105 | } else { 106 | this.setState({ list: this.state.list.concat([{ name: 'Pluto' }]) }); 107 | } 108 | } 109 | 110 | private _toggleContainer() { 111 | this.setState({ useContainer: !this.state.useContainer }); 112 | } 113 | 114 | private _onListChange(newList: ReadonlyArray) { 115 | this.setState({ list: newList }); 116 | } 117 | 118 | render() { 119 | const { useContainer } = this.state; 120 | 121 | return ( 122 |
123 |
124 |

125 | This is a demonstration of the{' '} 126 | 127 | react-draggable-list 128 | {' '} 129 | library. 130 |

131 |

132 | Each item has a drag handle visible when the user hovers over them. 133 | The items may have any height, and can each define their own height 134 | to use while being dragged. 135 |

136 |

137 | When the list is reordered, the page will be scrolled if possible to 138 | keep the moved item visible and on the same part of the screen. 139 |

140 |
141 | this._togglePluto()} 145 | /> 146 | this._toggleContainer()} 150 | /> 151 |
152 |
153 |
162 | 163 | itemKey="name" 164 | template={PlanetItem} 165 | list={this.state.list} 166 | onMoveEnd={(newList) => this._onListChange(newList)} 167 | container={() => 168 | useContainer ? this._container.current! : document.body 169 | } 170 | /> 171 |
172 |
Footer here.
173 |
174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /example/grippy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StreakYC/react-draggable-list/09672e398ad806de4b0e3288a03784ff855bec6f/example/grippy.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | React Draggable List Test Page 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | .intro, 2 | footer { 3 | width: 400px; 4 | margin: auto; 5 | } 6 | 7 | .list { 8 | width: 430px; 9 | margin: 10px auto; 10 | padding: 15px; 11 | } 12 | .item { 13 | border: 1px solid black; 14 | overflow: hidden; 15 | transform-origin: 30% 50% 0px; 16 | padding-left: 20px; 17 | background: white; 18 | max-height: 100%; 19 | } 20 | .item .dragHandle { 21 | visibility: hidden; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | cursor: move; 26 | width: 20px; 27 | height: 16px; 28 | background: url('grippy.png') 30% 50% no-repeat no-repeat; 29 | } 30 | .item:hover .dragHandle, 31 | .item.dragged .dragHandle { 32 | visibility: visible; 33 | } 34 | .item h2 { 35 | margin: 0; 36 | } 37 | .item .subtitle { 38 | font-weight: bold; 39 | } 40 | 41 | ::-webkit-scrollbar { 42 | -webkit-appearance: none; 43 | width: 7px; 44 | } 45 | ::-webkit-scrollbar-thumb { 46 | border-radius: 4px; 47 | background-color: rgba(0, 0, 0, 0.5); 48 | -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); 49 | } 50 | -------------------------------------------------------------------------------- /example/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import Example from './Example'; 4 | 5 | const onReady = new Promise((resolve) => { 6 | if (document.readyState === 'complete') { 7 | resolve(undefined); 8 | } else { 9 | document.addEventListener('DOMContentLoaded', resolve, false); 10 | window.addEventListener('load', resolve, false); 11 | } 12 | }); 13 | 14 | function main() { 15 | const mainDiv = document.getElementById('main'); 16 | if (!mainDiv) throw new Error(); 17 | ReactDOM.render(, mainDiv); 18 | } 19 | 20 | onReady.then(main).catch((e) => { 21 | console.error(e, e.stack); // eslint-disable-line no-console 22 | }); 23 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react'; 4 | 5 | export type TemplateProps = { 6 | item: I, 7 | itemSelected: number, 8 | anySelected: number, 9 | dragHandleProps: Object, 10 | commonProps: C, 11 | }; 12 | 13 | export type Props = { 14 | itemKey: string | ((item: I) => string), 15 | template: Class, 16 | list: $ReadOnlyArray, 17 | onMoveEnd?: ?( 18 | newList: $ReadOnlyArray, 19 | movedItem: I, 20 | oldIndex: number, 21 | newIndex: number 22 | ) => void, 23 | container?: ?() => ?HTMLElement, 24 | constrainDrag: boolean, 25 | springConfig: Object, 26 | padding: number, 27 | unsetZIndex: boolean, 28 | autoScrollMaxSpeed: number, 29 | autoScrollRegionSize: number, 30 | commonProps?: C, 31 | }; 32 | 33 | declare export default class DraggableList< 34 | I, 35 | C = *, 36 | T: React.Component<$Shape>, *> = * 37 | > 38 | extends React.Component> 39 | { 40 | getItemInstance(key: string): T; 41 | } 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | modulePathIgnorePatterns: ['/dist/'] 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-draggable-list", 3 | "version": "4.2.1", 4 | "description": "Make a list of draggable collapsible items.", 5 | "main": "dist/src/index.js", 6 | "sideEffects": false, 7 | "scripts": { 8 | "prepare": "rimraf dist && babel -s true -d dist/src/ src/ -x .ts,.tsx --ignore '**/*.test.tsx' && tsc", 9 | "example-build": "esbuild example/main.tsx --bundle --outdir=example/dist --public-path=.", 10 | "example-watch": "npm example-build --watch", 11 | "test": "npm run lint && flow check && jest && tsc --noEmit", 12 | "lint": "eslint . --ext js,jsx,ts,tsx", 13 | "lint-fix": "eslint . --ext js,jsx,ts,tsx --fix" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/StreakYC/react-draggable-list.git" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "react-component", 22 | "animation", 23 | "reorder", 24 | "move", 25 | "drag-and-drop", 26 | "draggable" 27 | ], 28 | "files": [ 29 | "dist", 30 | "index.js.flow" 31 | ], 32 | "author": "Chris Cowan ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/StreakYC/react-draggable-list/issues" 36 | }, 37 | "homepage": "https://github.com/StreakYC/react-draggable-list#readme", 38 | "devDependencies": { 39 | "@babel/cli": "^7.0.0", 40 | "@babel/core": "^7.0.0", 41 | "@babel/plugin-transform-runtime": "^7.0.0", 42 | "@babel/preset-env": "^7.0.0", 43 | "@babel/preset-react": "^7.0.0", 44 | "@babel/preset-typescript": "^7.6.0", 45 | "@types/classnames": "^2.2.9", 46 | "@types/jest": "^29.5.12", 47 | "@types/react": "^17.0.24", 48 | "@types/react-dom": "^17.0.9", 49 | "@types/react-motion": "^0.0.33", 50 | "@typescript-eslint/eslint-plugin": "^7.0.2", 51 | "@typescript-eslint/parser": "^7.0.2", 52 | "babel-jest": "^29.7.0", 53 | "babel-plugin-add-module-exports": "^1.0.0", 54 | "classnames": "^2.2.3", 55 | "esbuild": "^0.19.10", 56 | "eslint": "^8.21.0", 57 | "eslint-plugin-react": "^7.4.0", 58 | "flow-bin": "^0.184.0", 59 | "jest": "^29.7.0", 60 | "jest-environment-jsdom": "^29.7.0", 61 | "pdelay": "^2.0.0", 62 | "prettier": "~2.4.1", 63 | "react": "^17.0.2", 64 | "react-dom": "^17.0.2", 65 | "rimraf": "^5.0.5", 66 | "typescript": "^5.8.3" 67 | }, 68 | "resolutions": { 69 | "@types/react": "^17.0.0", 70 | "@types/react-dom": "^17.0.0" 71 | }, 72 | "dependencies": { 73 | "@babel/runtime": "^7.0.0", 74 | "@types/prop-types": "^15.7.3", 75 | "immutability-helper": "^3.0.0", 76 | "prop-types": "^15.6.0", 77 | "react-motion": "^0.5.2", 78 | "react-multi-ref": "^1.0.0" 79 | }, 80 | "peerDependencies": { 81 | "react": "^16.6.0 || ^17.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MoveContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TemplateContainer from './TemplateContainer'; 3 | 4 | export interface HeightData { 5 | natural: number; 6 | drag: number; 7 | } 8 | 9 | interface Props { 10 | item: I; 11 | template: new (props: any, context?: any) => T; 12 | padding: number; 13 | y: number | undefined; 14 | itemSelected: number; 15 | anySelected: number; 16 | height: HeightData; 17 | zIndex: React.CSSProperties['zIndex']; 18 | makeDragHandleProps: (getY: () => number | undefined) => object; 19 | commonProps: C; 20 | } 21 | 22 | export default class MoveContainer< 23 | I, 24 | C, 25 | T extends React.Component 26 | > extends React.Component> { 27 | private readonly _templateContainer = 28 | React.createRef>(); 29 | private readonly _el = React.createRef(); 30 | 31 | getDOMNode(): HTMLElement { 32 | return this._el.current!; 33 | } 34 | 35 | getTemplate(): T { 36 | return this._templateContainer.current!.getTemplate(); 37 | } 38 | 39 | shouldComponentUpdate(nextProps: Props): boolean { 40 | return ( 41 | this.props.anySelected !== nextProps.anySelected || 42 | this.props.itemSelected !== nextProps.itemSelected || 43 | this.props.item !== nextProps.item || 44 | this.props.template !== nextProps.template || 45 | this.props.y !== nextProps.y || 46 | this.props.height !== nextProps.height || 47 | this.props.zIndex !== nextProps.zIndex || 48 | this.props.commonProps !== nextProps.commonProps 49 | ); 50 | } 51 | 52 | private _dragHandleProps = this.props.makeDragHandleProps(() => this.props.y); 53 | 54 | render() { 55 | const { 56 | item, 57 | y, 58 | padding, 59 | itemSelected, 60 | anySelected, 61 | height, 62 | zIndex, 63 | template, 64 | commonProps, 65 | } = this.props; 66 | 67 | return ( 68 |
86 | 95 |
96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/OnUpdate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface Props { 4 | cb: () => void; 5 | } 6 | 7 | export default class OnUpdate extends React.Component { 8 | public componentDidUpdate() { 9 | this.props.cb(); 10 | } 11 | 12 | public render() { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/TemplateContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface Props { 4 | item: I; 5 | template: new (props: any, context?: any) => T; 6 | itemSelected: number; 7 | anySelected: number; 8 | dragHandleProps: object; 9 | commonProps: C; 10 | } 11 | 12 | export default class TemplateContainer< 13 | I, 14 | C, 15 | T extends React.Component 16 | > extends React.Component> { 17 | private _template: T | undefined; 18 | private readonly _templateSetter = (cmp: any) => { 19 | this._template = cmp; 20 | }; 21 | 22 | public shouldComponentUpdate(nextProps: Props): boolean { 23 | return ( 24 | this.props.anySelected !== nextProps.anySelected || 25 | this.props.itemSelected !== nextProps.itemSelected || 26 | this.props.item !== nextProps.item || 27 | this.props.template !== nextProps.template || 28 | this.props.commonProps !== nextProps.commonProps 29 | ); 30 | } 31 | 32 | public getTemplate(): T { 33 | return this._template!; 34 | } 35 | 36 | public render() { 37 | const { item, itemSelected, anySelected, dragHandleProps, commonProps } = 38 | this.props; 39 | const Template = this.props.template; 40 | 41 | return ( 42 |