├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── examples ├── index.html └── src │ ├── js │ └── index.js │ └── less │ └── index.less ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/** 2 | /examples/build/** 3 | /coverage/** 4 | .nyc_output/** 5 | 6 | .DS_Store 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jake 'Sid' Smith - github.com/JakeSidSmith 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 Reorder (v2) 2 | 3 | __Drag & drop, touch enabled, reorder / sortable list, React component__ 4 | 5 | If you are using v3 alpha, please refer to [this documentation](https://github.com/JakeSidSmith/react-reorder/blob/rework/README.md). 6 | 7 | ## About 8 | 9 | React Reorder is a React component that allows the user to drag-and-drop items in a list (horizontal / vertical) or a grid. 10 | 11 | It fully supports touch devices and auto-scrolls when a component is being dragged (check out the demo, link below). 12 | 13 | It also allows the user to set a hold time (duration before drag begins) allowing additional events like clicking / tapping to be applied. 14 | 15 | Although this project is very new, it has been thoroughly tested in all modern browsers (see supported browsers). 16 | 17 | __[Demo](http://jakesidsmith.github.io/react-reorder/)__ 18 | 19 | ## Installation 20 | 21 | * Using npm 22 | ```shell 23 | npm install react-reorder 24 | ``` 25 | Add `--save` or `-S` to update your package.json 26 | 27 | * Using bower 28 | ```shell 29 | bower install react-reorder 30 | ``` 31 | Add `--save` or `-S` to update your bower.json 32 | 33 | ## Example 34 | 35 | 1. If using requirejs: add `react-reorder` to your `require.config` 36 | 37 | ```javascript 38 | paths: 39 | // : 40 | 'react-reorder': 'react-reorder/reorder' 41 | } 42 | ``` 43 | 44 | 2. If using a module loader (requirejs / browserfiy / commonjs): require `react-reorder` where it will be used in your project 45 | 46 | ```javascript 47 | var Reorder = require('react-reorder'); 48 | ``` 49 | 50 | If using requirejs you'll probably want to wrap your module e.g. 51 | 52 | ```javascript 53 | define(function (require) { 54 | // Require react-reorder here 55 | }); 56 | ``` 57 | 58 | 3. Configuration 59 | 60 | **Note: If your array is an array of primitives (e.g. strings) then `itemKey` is not required as the string itself will be used as the key, however item keys must be unique in any case** 61 | 62 | 1. Using JSX 63 | 64 | ```javascript 65 | 95 | ``` 96 | 97 | 2. Using standard Javascript 98 | 99 | ```javascript 100 | React.createElement(Reorder, { 101 | // The key of each object in your list to use as the element key 102 | itemKey: 'name', 103 | // Lock horizontal to have a vertical list 104 | lock: 'horizontal', 105 | // The milliseconds to hold an item for before dragging begins 106 | holdTime: '500', 107 | // The list to display 108 | list: [ 109 | {name: 'Item 1'}, 110 | {name: 'Item 2'}, 111 | {name: 'Item 3'} 112 | ], 113 | // A template to display for each list item 114 | template: ListItem, 115 | // Function that is called once a reorder has been performed 116 | callback: this.callback, 117 | // Class to be applied to the outer list element 118 | listClass: 'my-list', 119 | // Class to be applied to each list item's wrapper element 120 | itemClass: 'list-item', 121 | // A function to be called if a list item is clicked (before hold time is up) 122 | itemClicked: this.itemClicked, 123 | // The item to be selected (adds 'selected' class) 124 | selected: this.state.selected, 125 | // The key to compare from the selected item object with each item object 126 | selectedKey: 'uuid', 127 | // Allows reordering to be disabled 128 | disableReorder: false 129 | }) 130 | ``` 131 | 132 | 4. Callback functions 133 | 134 | 1. The `callback` function that is called once a reorder has been performed 135 | 136 | ```javascript 137 | function callback(event, itemThatHasBeenMoved, itemsPreviousIndex, itemsNewIndex, reorderedArray) { 138 | // ... 139 | } 140 | ``` 141 | 142 | 2. The `itemClicked` function that is called when an item is clicked before any dragging begins 143 | 144 | ```javascript 145 | function itemClicked(event, itemThatHasBeenClicked, itemsIndex) { 146 | // ... 147 | } 148 | ``` 149 | 150 | **Note: `event` will be the synthetic React event for either `mouseup` or `touchend`, and both contain `clientX` & `clientY` values (for ease of use)** 151 | 152 | ## Compatibility / Requirements 153 | 154 | * Version `2.x` tested and working with React `0.14` 155 | 156 | * Versions `1.x` tested and working with React `0.12` - `0.13` 157 | 158 | * requirejs / commonjs / browserify (__Optional, but recommended*__) 159 | 160 | \* Has only been tested with requirejs & browserify 161 | 162 | ## Supported Browsers 163 | 164 | ### Desktop 165 | 166 | * Internet Explorer 9+ (may support IE8**) 167 | 168 | * Google Chrome (tested in version 39.0.2171.95(64-bit)) 169 | 170 | * Mozilla Firefox (tested in version 33.0) 171 | 172 | * Opera (tested in version 26.0.1656.60) 173 | 174 | * Safari (tested in version 7.1.2 (9537.85.11.5)) 175 | 176 | \** Have not had a chance to test in IE8, but IE8 is supported by React 177 | 178 | 179 | ### Mobile 180 | 181 | * Chrome (tested in version 40.0.2214.89) 182 | 183 | * Safari (tested on iOS 8) 184 | 185 | ## Untested Browsers 186 | 187 | * Internet Explorer 8*** (the lowest version that React supports) 188 | 189 | \*** If anyone could confirm that this works in IE8, that'd be awesome 190 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-reorder", 3 | "description": "Drag & drop, touch enabled, reorderable / sortable list, React component", 4 | "homepage": "https://github.com/JakeSidSmith/react-reorder", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "ignore": [], 8 | "keywords": [ 9 | "react", 10 | "facebook", 11 | "reorder", 12 | "sort", 13 | "reorderable", 14 | "sortable", 15 | "drag and drop", 16 | "mobile", 17 | "touch" 18 | ], 19 | "dependencies": { 20 | "react": "https://github.com/reactjs/react-bower.git#0.12.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Reorder 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Please enable javascript

13 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/src/js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var createReactClass = require('create-react-class'); 6 | var Reorder = require('../../../index'); 7 | 8 | var ListItem = createReactClass({ 9 | render: function () { 10 | return React.createElement('div', { 11 | className: 'inner', 12 | style: { 13 | color: this.props.item.color 14 | } 15 | }, this.props.sharedProps ? this.props.sharedProps.prefix : undefined, this.props.item.name); 16 | } 17 | }); 18 | 19 | var Main = createReactClass({ 20 | callback: function (event, item, index, newIndex, list) { 21 | this.setState({arr: list}); 22 | }, 23 | itemClicked: function (event, item) { 24 | this.setState({ 25 | clickedItem: item === this.state.clickedItem ? undefined : item 26 | }); 27 | }, 28 | itemClicked2: function (event, item) { 29 | this.setState({clickedItem2: item}); 30 | }, 31 | disableToggled: function () { 32 | this.setState({disableReorder: !this.state.disableReorder}); 33 | }, 34 | prefixChanged: function (event) { 35 | var target = event.currentTarget; 36 | this.setState({prefix: target.value}); 37 | }, 38 | 39 | // ---- 40 | 41 | getInitialState: function () { 42 | var list = []; 43 | 44 | for (var i = 0; i < 10; i += 1) { 45 | list.push({name: ['Thing', i].join(' '), color: ['rgb(',(i + 1) * 25, ',', 250 - ((i + 1) * 25),',0)'].join('')}); 46 | } 47 | 48 | return { 49 | arr: list, 50 | prefix: 'Prefix' 51 | }; 52 | }, 53 | render: function () { 54 | return React.createElement('div', {className: 'app'}, 55 | 56 | React.createElement('p', null, React.createElement('strong', null, 'Lock horizontal')), 57 | React.createElement('small', null, 'This example has a hold time of 500 milliseconds before dragging begins, allowing for other events like clicking / tapping to be attached'), 58 | 59 | React.createElement('p', null, 'Selected item: ', this.state.clickedItem ? this.state.clickedItem.name : undefined), 60 | 61 | React.createElement('p', null, 62 | 'Prefix (shared props): ', 63 | React.createElement('input', { 64 | type: 'text', 65 | onChange: this.prefixChanged, 66 | value: this.state.prefix 67 | }) 68 | ), 69 | 70 | React.createElement(Reorder, { 71 | itemKey: 'name', 72 | lock: 'horizontal', 73 | holdTime: '500', 74 | list: this.state.arr, 75 | template: ListItem, 76 | callback: this.callback, 77 | listClass: 'my-list', 78 | itemClass: 'list-item', 79 | itemClicked: this.itemClicked, 80 | selected: this.state.clickedItem, 81 | selectedKey: 'name', 82 | sharedProps: { 83 | prefix: [this.state.prefix, ': '].join('') 84 | }}), 85 | 86 | React.createElement('p', null, React.createElement('strong', null, 'Lock vertical')), 87 | React.createElement('small', null, 'This example has a hold time of 250 milliseconds'), 88 | 89 | React.createElement('p', null, 90 | 'Reorder disabled: ', 91 | React.createElement('input', { 92 | type: 'checkbox', 93 | onChange: this.disableToggled, 94 | value: this.state.disableReorder || false 95 | }), 96 | 'Last item clicked: ', 97 | this.state.clickedItem2 ? this.state.clickedItem2.name : undefined 98 | ), 99 | 100 | React.createElement(Reorder, { 101 | itemKey: 'name', 102 | lock: 'vertical', 103 | holdTime: '250', 104 | list: this.state.arr, 105 | template: ListItem, 106 | callback: this.callback, 107 | listClass: 'my-list-2', 108 | itemClass: 'list-item', 109 | itemClicked: this.itemClicked2, 110 | disableReorder: this.state.disableReorder}), 111 | 112 | React.createElement('p', null, React.createElement('strong', null, 'No lock (grid)')), 113 | React.createElement('small', null, 'This example has a hold time of 0 milliseconds'), 114 | 115 | React.createElement(Reorder, { 116 | itemKey: 'name', 117 | holdTime: '0', 118 | list: this.state.arr, 119 | template: ListItem, 120 | callback: this.callback, 121 | listClass: 'my-list-3', 122 | itemClass: 'list-item'}) 123 | 124 | ); 125 | } 126 | }); 127 | 128 | ReactDOM.render(React.createElement(Main), document.getElementById('app')); 129 | -------------------------------------------------------------------------------- /examples/src/less/index.less: -------------------------------------------------------------------------------- 1 | @BaseFontColor: #333; 2 | 3 | @BaseFontSize: 14px; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, body { 10 | padding: 0; 11 | margin: 0; 12 | font-family: arial, helvetica, sans-serif; 13 | font-size: @BaseFontSize; 14 | color: @BaseFontColor; 15 | -webkit-touch-callout: none; 16 | -webkit-user-select: none; 17 | -khtml-user-select: none; 18 | -moz-user-select: none; 19 | -ms-user-select: none; 20 | user-select: none; 21 | -webkit-tap-highlight-color: transparent; 22 | } 23 | 24 | p, small { 25 | float: left; 26 | width: 100%; 27 | padding: 12px; 28 | margin: 0; 29 | } 30 | 31 | small { 32 | padding-top: 0; 33 | font-size: @BaseFontSize - 2px; 34 | } 35 | 36 | .app { 37 | position: relative; 38 | width: 100%; 39 | max-width: 768px; 40 | overflow: hidden; 41 | margin: auto; 42 | padding: 8px; 43 | } 44 | 45 | .loading { 46 | position: relative; 47 | width: 100%; 48 | margin: 100px auto auto auto; 49 | text-align: center; 50 | padding: 8px; 51 | 52 | p { 53 | float: left; 54 | width: 100%; 55 | font-size: 18px; 56 | margin: 0; 57 | padding: 0; 58 | 59 | small { 60 | font-size: 14px; 61 | } 62 | } 63 | } 64 | 65 | .my-list, 66 | .my-list-2, 67 | .my-list-3 { 68 | float: left; 69 | width: 100%; 70 | height: auto; 71 | border: 1px solid grey; 72 | padding: 8px; 73 | } 74 | 75 | .my-list { 76 | height: 200px; 77 | overflow: auto; 78 | padding-bottom: 0; 79 | } 80 | 81 | .list-item { 82 | float: left; 83 | width: 100%; 84 | height: auto; 85 | padding: 12px; 86 | border: 2px solid lightblue; 87 | margin-bottom: 8px; 88 | transform-origin: 50% 50%; 89 | 90 | &.dragged { 91 | background-color: #EEE; 92 | transform: scale(0.98, 0.98); 93 | opacity: 0.7; 94 | } 95 | 96 | &.selected { 97 | border: 2px solid red; 98 | } 99 | 100 | &.placeholder { 101 | background-color: #CCC; 102 | border: 2px solid #CCC; 103 | 104 | .inner { 105 | visibility: hidden; 106 | } 107 | } 108 | } 109 | 110 | .my-list-2 { 111 | overflow-x: auto; 112 | overflow-y: hidden; 113 | height: 62px; 114 | white-space: nowrap; 115 | 116 | .list-item { 117 | float: none; 118 | width: 80px; 119 | margin-bottom: 0; 120 | white-space: nowrap; 121 | overflow: hidden; 122 | display: inline-block; 123 | } 124 | } 125 | 126 | .my-list-3 { 127 | .list-item { 128 | float: left; 129 | width: percentage(1/2); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var getReorderComponent = function (React, ReactDOM, createReactClass) { 5 | 6 | return createReactClass({ 7 | displayName: 'Reorder', 8 | nonCollisionElement: new RegExp('(^|\\s)(placeholder|dragged)($|\\s)', ''), 9 | constants: { 10 | HOLD_THRESHOLD: 8, 11 | SCROLL_RATE: 1000 / 60, 12 | SCROLL_DISTANCE: 1, 13 | SCROLL_AREA: 50, 14 | SCROLL_MULTIPLIER: 5 15 | }, 16 | preventDefault: function (event) { 17 | event.preventDefault(); 18 | }, 19 | persistEvent: function (event) { 20 | if (typeof event.persist === 'function') { 21 | event.persist(); 22 | } 23 | }, 24 | handleTouchEvents: function (event) { 25 | if (event.touches && event.touches.length) { 26 | this.persistEvent(event); 27 | 28 | event.clientX = event.touches[0].clientX; 29 | event.clientY = event.touches[0].clientY; 30 | } 31 | }, 32 | startDrag: function (dragOffset, draggedStyle) { 33 | if (!this.props.disableReorder) { 34 | this.setState({ 35 | dragOffset: dragOffset, 36 | draggedStyle: draggedStyle, 37 | originalPosition: draggedStyle, 38 | held: true, 39 | moved: false 40 | }); 41 | } 42 | }, 43 | itemDown: function (item, index, event) { 44 | this.handleTouchEvents(event); 45 | 46 | var self = this; 47 | var target = event.currentTarget; 48 | var rect = target.getBoundingClientRect(); 49 | 50 | this.setState({ 51 | held: false, 52 | moved: false 53 | }); 54 | 55 | var dragOffset = { 56 | top: event.clientY - rect.top, 57 | left: event.clientX - rect.left 58 | }; 59 | 60 | this.setState({ 61 | dragged: { 62 | target: target, 63 | item: item, 64 | index: index 65 | } 66 | }); 67 | 68 | var draggedStyle = { 69 | position: 'fixed', 70 | top: rect.top, 71 | left: rect.left, 72 | width: rect.width, 73 | height: rect.height 74 | }; 75 | 76 | // Timeout if holdTime is defined 77 | var holdTime = Math.abs(parseInt(this.props.holdTime)); 78 | 79 | if (holdTime) { 80 | this.holdTimeout = setTimeout(function () { 81 | self.startDrag(dragOffset, draggedStyle); 82 | }, holdTime); 83 | } else { 84 | self.startDrag(dragOffset, draggedStyle); 85 | } 86 | }, 87 | listDown: function (event) { 88 | this.handleTouchEvents(event); 89 | 90 | var self = this; 91 | 92 | var downPos = { 93 | clientY: event.clientY, 94 | clientX: event.clientX, 95 | scrollTop: ReactDOM.findDOMNode(self).scrollTop, 96 | scrollLeft: ReactDOM.findDOMNode(self).scrollLeft 97 | }; 98 | 99 | this.setState({ 100 | downPos: downPos, 101 | pointer: { 102 | clientY: downPos.clientY, 103 | clientX: downPos.clientX 104 | }, 105 | velocity: { 106 | y: 0, 107 | x: 0 108 | }, 109 | movedALittle: false 110 | }); 111 | 112 | // Mouse events 113 | window.addEventListener('mouseup', this.onMouseUp); // Mouse up 114 | window.addEventListener('mousemove', this.onMouseMove); // Mouse move 115 | 116 | // Touch events 117 | window.addEventListener('touchend', this.onMouseUp); // Touch up 118 | window.addEventListener('touchmove', this.onMouseMove); // Touch move 119 | 120 | window.addEventListener('contextmenu', this.preventDefault); 121 | }, 122 | onMouseUp: function (event) { 123 | if (event.type.indexOf('touch') >= 0 && !this.state.movedALittle) { 124 | event.preventDefault(); 125 | } 126 | 127 | // Item clicked 128 | if (typeof this.props.itemClicked === 'function' && !this.state.held && !this.state.moved && this.state.dragged) { 129 | this.props.itemClicked(event, this.state.dragged.item, this.state.dragged.index); 130 | } 131 | 132 | // Reorder callback 133 | if (this.state.held && this.state.dragged && typeof this.props.callback === 'function') { 134 | var listElements = this.nodesToArray(ReactDOM.findDOMNode(this).childNodes); 135 | var newIndex = listElements.indexOf(this.state.dragged.target); 136 | 137 | this.props.callback(event, this.state.dragged.item, this.state.dragged.index, newIndex, this.state.list); 138 | } 139 | 140 | this.setState({ 141 | dragged: undefined, 142 | draggedStyle: undefined, 143 | dragOffset: undefined, 144 | originalPosition: undefined, 145 | downPos: undefined, 146 | held: false, 147 | moved: false 148 | }); 149 | 150 | clearTimeout(this.holdTimeout); 151 | clearInterval(this.scrollIntervalY); 152 | this.scrollIntervalY = undefined; 153 | clearInterval(this.scrollIntervalX); 154 | this.scrollIntervalX = undefined; 155 | 156 | // Mouse events 157 | window.removeEventListener('mouseup', this.onMouseUp); // Mouse up 158 | window.removeEventListener('mousemove', this.onMouseMove); // Mouse move 159 | // Touch events 160 | window.removeEventListener('touchend', this.onMouseUp); // Touch up 161 | window.removeEventListener('touchmove', this.onMouseMove); // Touch move 162 | 163 | window.removeEventListener('contextmenu', this.preventDefault); 164 | }, 165 | getScrollArea: function (value) { 166 | return Math.max(Math.min(value / 4, this.constants.SCROLL_AREA), this.constants.SCROLL_AREA / 5); 167 | }, 168 | dragScrollY: function () { 169 | var element = ReactDOM.findDOMNode(this); 170 | var rect = element.getBoundingClientRect(); 171 | var scrollArea = this.getScrollArea(rect.height); 172 | 173 | var distanceInArea; 174 | if (this.state.pointer.clientY < rect.top + scrollArea) { 175 | distanceInArea = Math.min((rect.top + scrollArea) - this.state.pointer.clientY, scrollArea * 2); 176 | element.scrollTop -= distanceInArea / this.constants.SCROLL_MULTIPLIER; 177 | } else if (this.state.pointer.clientY > rect.bottom - scrollArea) { 178 | distanceInArea = Math.min(this.state.pointer.clientY - (rect.bottom - scrollArea), scrollArea * 2); 179 | element.scrollTop += distanceInArea / this.constants.SCROLL_MULTIPLIER; 180 | } 181 | }, 182 | dragScrollX: function () { 183 | var element = ReactDOM.findDOMNode(this); 184 | var rect = element.getBoundingClientRect(); 185 | var scrollArea = this.getScrollArea(rect.width); 186 | 187 | var distanceInArea; 188 | if (this.state.pointer.clientX < rect.left + scrollArea) { 189 | distanceInArea = Math.min((rect.left + scrollArea) - this.state.pointer.clientX, scrollArea * 2); 190 | element.scrollLeft -= distanceInArea / this.constants.SCROLL_MULTIPLIER; 191 | } else if (this.state.pointer.clientX > rect.right - scrollArea) { 192 | distanceInArea = Math.min(this.state.pointer.clientX - (rect.right - scrollArea), scrollArea * 2); 193 | element.scrollLeft += distanceInArea / this.constants.SCROLL_MULTIPLIER; 194 | } 195 | }, 196 | handleDragScrollY: function (event) { 197 | var rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); 198 | 199 | if (!this.scrollIntervalY && this.props.lock !== 'vertical') { 200 | if (event.clientY < rect.top + this.constants.SCROLL_AREA) { 201 | this.scrollIntervalY = setInterval(this.dragScrollY, this.constants.SCROLL_RATE); 202 | } else if (event.clientY > rect.bottom - this.constants.SCROLL_AREA) { 203 | this.scrollIntervalY = setInterval(this.dragScrollY, this.constants.SCROLL_RATE); 204 | } 205 | } else { 206 | if (event.clientY <= rect.bottom - this.constants.SCROLL_AREA && event.clientY >= rect.top + this.constants.SCROLL_AREA) { 207 | clearInterval(this.scrollIntervalY); 208 | this.scrollIntervalY = undefined; 209 | } 210 | } 211 | }, 212 | handleDragScrollX: function (event) { 213 | var rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); 214 | 215 | if (!this.scrollIntervalX && this.props.lock !== 'horizontal') { 216 | if (event.clientX < rect.left + this.constants.SCROLL_AREA) { 217 | this.scrollIntervalX = setInterval(this.dragScrollX, this.constants.SCROLL_RATE); 218 | } else if (event.clientX > rect.right - this.constants.SCROLL_AREA) { 219 | this.scrollIntervalX = setInterval(this.dragScrollX, this.constants.SCROLL_RATE); 220 | } 221 | } else { 222 | if (event.clientX <= rect.right - this.constants.SCROLL_AREA && event.clientX >= rect.left + this.constants.SCROLL_AREA) { 223 | clearInterval(this.scrollIntervalX); 224 | this.scrollIntervalX = undefined; 225 | } 226 | } 227 | }, 228 | onMouseMove: function (event) { 229 | this.handleTouchEvents(event); 230 | 231 | var pointer = { 232 | clientY: event.clientY, 233 | clientX: event.clientX 234 | }; 235 | 236 | this.setState({ 237 | pointer: pointer, 238 | velocity: { 239 | y: this.state.pointer.clientY - event.clientY, 240 | x: this.state.pointer.clientX - event.clientX 241 | }, 242 | movedALittle: true 243 | }); 244 | 245 | if (this.state.held && this.state.dragged) { 246 | event.preventDefault(); 247 | this.setDraggedPosition(event); 248 | 249 | var listElements = this.nodesToArray(ReactDOM.findDOMNode(this).childNodes); 250 | var collision = this.findCollision(listElements, event); 251 | 252 | if (collision) { 253 | var previousIndex = listElements.indexOf(this.state.dragged.target); 254 | var newIndex = listElements.indexOf(collision); 255 | 256 | this.state.list.splice(newIndex, 0, this.state.list.splice(previousIndex, 1)[0]); 257 | this.setState({list: this.state.list}); 258 | } 259 | 260 | this.handleDragScrollY(event); 261 | this.handleDragScrollX(event); 262 | } else { 263 | if (this.state.downPos) { 264 | // Cancel hold if mouse has moved 265 | if (this.xHasMoved(event) || this.yHasMoved(event)) { 266 | clearTimeout(this.holdTimeout); 267 | this.setState({moved: true}); 268 | } 269 | } 270 | } 271 | }, 272 | xHasMoved: function (event) { 273 | return Math.abs(this.state.downPos.clientX - event.clientX) > this.constants.HOLD_THRESHOLD; 274 | }, 275 | yHasMoved: function (event) { 276 | return Math.abs(this.state.downPos.clientY - event.clientY) > this.constants.HOLD_THRESHOLD; 277 | }, 278 | elementHeightMinusBorders: function (element) { 279 | var rect = element.getBoundingClientRect(); 280 | var computedStyle; 281 | 282 | if (getComputedStyle) { 283 | computedStyle = getComputedStyle(element); 284 | } else { 285 | computedStyle = element.currentStyle; 286 | } 287 | 288 | return rect.height - 289 | parseInt(computedStyle.getPropertyValue('border-top-width') || computedStyle.borderTopWidth) - 290 | parseInt(computedStyle.getPropertyValue('border-bottom-width') || computedStyle.borderBottomWidth); 291 | }, 292 | elementWidthMinusBorders: function (element) { 293 | var rect = element.getBoundingClientRect(); 294 | var computedStyle; 295 | 296 | if (getComputedStyle) { 297 | computedStyle = getComputedStyle(element); 298 | } else { 299 | computedStyle = element.currentStyle; 300 | } 301 | 302 | return rect.width - 303 | parseInt(computedStyle.getPropertyValue('border-left-width') || computedStyle.borderLeftWidth) - 304 | parseInt(computedStyle.getPropertyValue('border-right-width') || computedStyle.borderRightWidth); 305 | }, 306 | setDraggedPosition: function (event) { 307 | var draggedStyle = { 308 | position: this.state.draggedStyle.position, 309 | top: this.state.draggedStyle.top, 310 | left: this.state.draggedStyle.left, 311 | width: this.state.draggedStyle.width, 312 | height: this.state.draggedStyle.height 313 | }; 314 | 315 | if (this.props.lock === 'horizontal') { 316 | draggedStyle.top = event.clientY - this.state.dragOffset.top; 317 | draggedStyle.left = this.state.originalPosition.left; 318 | } else if (this.props.lock === 'vertical') { 319 | draggedStyle.top = this.state.originalPosition.top; 320 | draggedStyle.left = event.clientX - this.state.dragOffset.left; 321 | } else { 322 | draggedStyle.top = event.clientY - this.state.dragOffset.top; 323 | draggedStyle.left = event.clientX - this.state.dragOffset.left; 324 | } 325 | 326 | this.setState({draggedStyle: draggedStyle}); 327 | }, 328 | 329 | // Collision methods 330 | 331 | nodesToArray: function (nodes) { 332 | return Array.prototype.slice.call(nodes, 0); 333 | }, 334 | xCollision: function (rect, event) { 335 | return event.clientX >= rect.left && event.clientX <= rect.right; 336 | }, 337 | yCollision: function (rect, event) { 338 | return event.clientY >= rect.top && event.clientY <= rect.bottom; 339 | }, 340 | findCollision: function (listElements, event) { 341 | for (var i = 0; i < listElements.length; i += 1) { 342 | if (!this.nonCollisionElement.exec(listElements[i].className)) { 343 | var rect = listElements[i].getBoundingClientRect(); 344 | 345 | if (this.props.lock === 'horizontal') { 346 | if (this.yCollision(rect, event)) { 347 | return listElements[i]; 348 | } 349 | } else if (this.props.lock === 'vertical') { 350 | if (this.xCollision(rect, event)) { 351 | return listElements[i]; 352 | } 353 | } else { 354 | if (this.yCollision(rect, event)) { 355 | if (this.xCollision(rect, event)) { 356 | return listElements[i]; 357 | } 358 | } 359 | } 360 | 361 | } 362 | } 363 | 364 | return undefined; 365 | }, 366 | 367 | // ---- View methods 368 | 369 | getDraggedStyle: function (item) { 370 | if (this.state.held && this.state.dragged && this.state.dragged.item === item) { 371 | return this.state.draggedStyle; 372 | } 373 | return undefined; 374 | }, 375 | getDraggedClass: function (item) { 376 | if (this.state.held && this.state.dragged && this.state.dragged.item === item) { 377 | return 'dragged'; 378 | } 379 | return undefined; 380 | }, 381 | getPlaceholderClass: function (item) { 382 | if (this.state.held && this.state.dragged && this.state.dragged.item === item) { 383 | return 'placeholder'; 384 | } 385 | return undefined; 386 | }, 387 | getSelectedClass: function (item) { 388 | if (typeof this.props.selected !== 'undefined') { 389 | if (typeof this.props.selectedKey !== 'undefined') { 390 | return this.props.selected[this.props.selectedKey] === item[this.props.selectedKey] ? 'selected' : undefined; 391 | } 392 | return this.props.selected === item ? 'selected' : undefined; 393 | } 394 | return undefined; 395 | }, 396 | 397 | // ---- Default methods 398 | 399 | componentWillUnmount: function () { 400 | clearTimeout(this.holdTimeout); 401 | 402 | clearInterval(this.scrollIntervalY); 403 | this.scrollIntervalY = undefined; 404 | clearInterval(this.scrollIntervalX); 405 | this.scrollIntervalX = undefined; 406 | }, 407 | componentWillReceiveProps: function (props) { 408 | // Updates list when props changed 409 | this.setState({ 410 | list: props.list 411 | }); 412 | }, 413 | getInitialState: function () { 414 | return { 415 | list: this.props.list || [] 416 | }; 417 | }, 418 | render: function () { 419 | var self = this; 420 | 421 | var getPropsTemplate = function (item) { 422 | if (self.props.template) { 423 | return React.createElement(self.props.template, { 424 | item: item, 425 | sharedProps: self.props.sharedProps 426 | }); 427 | } 428 | return item; 429 | }; 430 | 431 | var list = this.state.list.map(function (item, index) { 432 | var itemKey = item[self.props.itemKey] || item; 433 | var itemClass = [self.props.itemClass, self.getPlaceholderClass(item), self.getSelectedClass(item)].join(' '); 434 | return React.createElement('div', { 435 | key: itemKey, 436 | className: itemClass, 437 | onMouseDown: self.itemDown.bind(self, item, index), 438 | onTouchStart: self.itemDown.bind(self, item, index), 439 | }, getPropsTemplate(item)); 440 | }); 441 | 442 | var targetClone = function () { 443 | if (self.state.held && self.state.dragged) { 444 | var itemKey = self.state.dragged.item[self.props.itemKey] || self.state.dragged.item; 445 | var itemClass = [self.props.itemClass, self.getDraggedClass(self.state.dragged.item), self.getSelectedClass(self.state.dragged.item)].join(' '); 446 | return React.createElement('div', { 447 | key: itemKey, 448 | className: itemClass, 449 | style: self.getDraggedStyle(self.state.dragged.item) 450 | }, getPropsTemplate(self.state.dragged.item)); 451 | } 452 | return undefined; 453 | }; 454 | 455 | return React.createElement('div', { 456 | className: this.props.listClass, 457 | onMouseDown: self.listDown, 458 | onTouchStart: self.listDown 459 | }, list, targetClone()); 460 | } 461 | }); 462 | 463 | }; 464 | 465 | // Export for commonjs / browserify 466 | if (typeof exports === 'object' && typeof module !== 'undefined') { 467 | var React = require('react'); 468 | var ReactDOM = require('react-dom'); 469 | var createReactClass = require('create-react-class'); 470 | module.exports = getReorderComponent(React, ReactDOM, createReactClass); 471 | // Export for amd / require 472 | } else if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef 473 | define(['react', 'react-dom', 'create-react-class'], function (ReactAMD, ReactDOMAMD, createReactClassAMD) { // eslint-disable-line no-undef 474 | return getReorderComponent(ReactAMD, ReactDOMAMD, createReactClassAMD); 475 | }); 476 | // Export globally 477 | } else { 478 | var root; 479 | 480 | if (typeof window !== 'undefined') { 481 | root = window; 482 | } else if (typeof global !== 'undefined') { 483 | root = global; 484 | } else if (typeof self !== 'undefined') { 485 | root = self; 486 | } else { 487 | root = this; 488 | } 489 | 490 | root.Reorder = getReorderComponent(root.React, root.ReactDOM, root.createReactClass); 491 | } 492 | 493 | })(); 494 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-reorder", 3 | "version": "2.2.1", 4 | "description": "Drag & drop, touch enabled, reorderable / sortable list, React component", 5 | "author": "Jake 'Sid' Smith", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": { 9 | "start": "http-server examples/ -c-1 -o", 10 | "build-less": "lessc examples/src/less/index.less examples/build/css/index.css && echo 'Less compiled'", 11 | "build-js": "browserify examples/src/js/index.js -o examples/build/js/index.js", 12 | "watch-js": "watchify examples/src/js/index.js -o examples/build/js/index.js -v", 13 | "build-dirs": "mkdir -p examples/build/css/ && mkdir -p examples/build/js/", 14 | "build": "npm run build-dirs && npm run build-less && npm run build-js", 15 | "watch": "npm run watch-js" 16 | }, 17 | "bugs": "https://github.com/JakeSidSmith/react-reorder/issues", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/JakeSidSmith/react-reorder" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "facebook", 25 | "reorder", 26 | "sort", 27 | "reorderable", 28 | "sortable", 29 | "drag and drop", 30 | "mobile", 31 | "touch" 32 | ], 33 | "dependencies": { 34 | "create-react-class": "^15.5.2" 35 | }, 36 | "devDependencies": { 37 | "browserify": "=12.0.1", 38 | "http-server": "=0.8.5", 39 | "less": "=2.7.2", 40 | "react": ">=0.14.7", 41 | "react-dom": ">=0.14.7", 42 | "watchify": "=3.6.1" 43 | }, 44 | "peerDependencies": { 45 | "react": "*", 46 | "react-dom": "*" 47 | } 48 | } 49 | --------------------------------------------------------------------------------