├── .babelrc ├── .gitignore ├── README.md ├── example ├── index.html ├── index.js ├── screenshot.gif └── webpack.config.js ├── lib ├── SortableList.js └── __tests__ │ └── SortableList.spec.js ├── package.json ├── src ├── SortableList.js └── __tests__ │ ├── SortableList.spec.js │ └── __snapshots__ │ └── SortableList.spec.js.snap └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ], 6 | "@babel/preset-react" 7 | ], 8 | "plugins": [ 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .node-version 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-sortable-list 2 | 3 | [![npm version](https://badge.fury.io/js/react-sortable-list.svg)](https://badge.fury.io/js/react-sortable-list) 4 | 5 | ![](https://raw.githubusercontent.com/StevenIseki/react-sortable-list/master/example/screenshot.gif) 6 | 7 | react-sortable-list is a sortable list component using react and html5 drag and drop api. 8 | 9 | ## Installation 10 | 11 | `yarn add react-sortable-list` 12 | 13 | ## Use 14 | 15 | ```jsx 16 | import SortableList from 'react-sortable-list' 17 | import ReactDOM from 'react-dom' 18 | import React, { Component } from 'react' 19 | 20 | class TestComponent extends Component { 21 | render() { 22 | let colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange'] 23 | 24 | return ( 25 |
26 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | ReactDOM.render(, document.getElementById('root')) 33 | ``` 34 | 35 | ## Styles 36 | 37 | Uses styled-components 💅 for the base styling. 38 | 39 | ## Development 40 | yarn 41 | yarn dev 42 | 43 | ## Test 44 | yarn test 45 | 46 | ## Build 47 | yarn 48 | yarn build 49 | 50 | ## Publish 51 | npm login 52 | npm version patch 53 | git add -A 54 | git push origin master 55 | npm publish 56 | 57 | ## License 58 | 59 | [MIT](http://isekivacenz.mit-license.org/) 60 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import SortableList from '../lib/SortableList' // 'react-sortable-list' 2 | import ReactDOM from 'react-dom' 3 | import React, { Component } from 'react' 4 | 5 | class TestComponent extends Component { 6 | render() { 7 | let colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange'] 8 | 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | } 16 | 17 | ReactDOM.render(, document.getElementById('root')) 18 | -------------------------------------------------------------------------------- /example/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-z/react-sortable-list/24308d55b933b3fc3f8446087aad23880a708b97/example/screenshot.gif -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | 3 | module.exports = { 4 | entry: ['./example/index.js'], 5 | devtool: 'inline-source-map', 6 | output: { filename: 'bundle.js', publicPath: '' }, 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.js$/, 11 | use: [ { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/react'] } } ], 12 | exclude: /node_modules/, 13 | } 14 | ] 15 | }, 16 | plugins: [ 17 | new HtmlWebpackPlugin({ title: 'react infinite loader example', template: './example/index.html' }) 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /lib/SortableList.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | var _styledComponents = _interopRequireDefault(require("styled-components")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } 15 | 16 | function _templateObject() { 17 | var data = _taggedTemplateLiteral(["\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul li {\n background: #eee;\n color: #666;\n margin: 0;\n padding: 10px;\n line-height: 1;\n .placeholder {\n background: #03cc85;\n }\n .placeholder:before {\n content: 'Drop here';\n color: #666;\n }\n }\n"]); 18 | 19 | _templateObject = function _templateObject() { 20 | return data; 21 | }; 22 | 23 | return data; 24 | } 25 | 26 | function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } 27 | 28 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 29 | 30 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 31 | 32 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 33 | 34 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 35 | 36 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 37 | 38 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 39 | 40 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 41 | 42 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 43 | 44 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 45 | 46 | /** 47 | * Sortable List module 48 | * A sortable list component using html5 drag and drop api. 49 | **/ 50 | var SortableList = 51 | /*#__PURE__*/ 52 | function (_Component) { 53 | _inherits(SortableList, _Component); 54 | 55 | function SortableList(props) { 56 | var _this; 57 | 58 | _classCallCheck(this, SortableList); 59 | 60 | _this = _possibleConstructorReturn(this, _getPrototypeOf(SortableList).call(this, props)); 61 | var placeholder = document.createElement('li'); 62 | placeholder.className = 'placeholder'; 63 | _this.state = { 64 | data: _this.props.data, 65 | placeholder: placeholder 66 | }; 67 | return _this; 68 | } 69 | /** 70 | * On drag start, set data. 71 | **/ 72 | 73 | 74 | _createClass(SortableList, [{ 75 | key: "dragStart", 76 | value: function dragStart(e) { 77 | this.dragged = e.currentTarget; 78 | e.dataTransfer.effectAllowed = 'move'; 79 | e.dataTransfer.setData('text/html', e.currentTarget); 80 | } 81 | /** 82 | * On drag end, update the data state. 83 | **/ 84 | 85 | }, { 86 | key: "dragEnd", 87 | value: function dragEnd(e) { 88 | this.dragged.style.display = 'block'; 89 | this.dragged.parentNode.removeChild(this.state.placeholder); 90 | var data = this.state.data; 91 | var from = Number(this.dragged.dataset.id); 92 | var to = Number(this.over.dataset.id); 93 | if (from < to) to--; 94 | if (this.nodePlacement == 'after') to++; 95 | data.splice(to, 0, data.splice(from, 1)[0]); 96 | this.setState({ 97 | data: data 98 | }); 99 | } 100 | /** 101 | * On drag over, update items. 102 | **/ 103 | 104 | }, { 105 | key: "dragOver", 106 | value: function dragOver(e) { 107 | e.preventDefault(); 108 | this.dragged.style.display = 'none'; 109 | if (e.target.className == 'placeholder') return; 110 | this.over = e.target; 111 | var relY = e.clientY - this.over.offsetTop; 112 | var height = this.over.offsetHeight / 2; 113 | var parent = e.target.parentNode; 114 | 115 | if (relY > height) { 116 | this.nodePlacement = 'after'; 117 | parent.insertBefore(this.state.placeholder, e.target.nextElementSibling); 118 | } else if (relY < height) { 119 | this.nodePlacement = 'before'; 120 | parent.insertBefore(this.state.placeholder, e.target); 121 | } 122 | } 123 | }, { 124 | key: "render", 125 | value: function render() { 126 | var _this2 = this; 127 | 128 | var data = this.state.data; 129 | var listItems = data.map(function (item, i) { 130 | return _react.default.createElement("li", { 131 | "data-id": i, 132 | key: i, 133 | draggable: "true", 134 | onDragEnd: _this2.dragEnd.bind(_this2), 135 | onDragStart: _this2.dragStart.bind(_this2) 136 | }, item); 137 | }); 138 | return _react.default.createElement(SortableWrapper, null, _react.default.createElement("ul", { 139 | onDragOver: this.dragOver.bind(this) 140 | }, listItems)); 141 | } 142 | }]); 143 | 144 | return SortableList; 145 | }(_react.Component); 146 | 147 | exports.default = SortableList; 148 | 149 | var SortableWrapper = _styledComponents.default.div(_templateObject()); -------------------------------------------------------------------------------- /lib/__tests__/SortableList.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _enzyme = require("enzyme"); 4 | 5 | var _enzymeAdapterReact = _interopRequireDefault(require("enzyme-adapter-react-16")); 6 | 7 | var _react = _interopRequireDefault(require("react")); 8 | 9 | var _reactTestRenderer = _interopRequireDefault(require("react-test-renderer")); 10 | 11 | var _SortableList = _interopRequireDefault(require("../SortableList")); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | /* setup enzyme */ 16 | (0, _enzyme.configure)({ 17 | adapter: new _enzymeAdapterReact.default() 18 | }); 19 | /* setup jsdom */ 20 | 21 | var jsdom = require('jsdom'); 22 | 23 | var JSDOM = jsdom.JSDOM; 24 | var window = new JSDOM('').window; 25 | global.window = window; 26 | global.document = window.document; 27 | test('SortableList renders correctly and matches snapshot', function () { 28 | var colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange']; 29 | 30 | var component = _reactTestRenderer.default.create(_react.default.createElement(_SortableList.default, { 31 | data: colors 32 | })); 33 | 34 | var tree = component.toJSON(); 35 | expect(tree).toMatchSnapshot(); 36 | }); 37 | test('SortableList renders the correct elements and props', function () { 38 | var colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange']; 39 | var wrapper = (0, _enzyme.shallow)(_react.default.createElement(_SortableList.default, { 40 | data: colors 41 | })); 42 | expect(wrapper.instance().props.data).toEqual(colors); 43 | expect(wrapper.find('ul').length).toEqual(1); 44 | expect(wrapper.find('li').length).toEqual(colors.length); 45 | expect(wrapper.find('li').first().prop('draggable')).toEqual('true'); 46 | expect(wrapper.find('li').first().prop('data-id')).toEqual(0); 47 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sortable-list", 3 | "version": "1.1.1", 4 | "description": "A sortable list component using html5 drag and drop api", 5 | "main": "lib/SortableList.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/StevenIseki/react-sortable-list.git" 9 | }, 10 | "author": "Steven Iseki ", 11 | "license": "MIT", 12 | "peerDependencies": { 13 | "react": "^16.0.0" 14 | }, 15 | "dependencies": { 16 | "prop-types": "^15.6.0", 17 | "styled-components": "^4.1.3" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.2.3", 21 | "@babel/core": "7.3.3", 22 | "@babel/preset-env": "7.3.1", 23 | "@babel/preset-react": "7.0.0", 24 | "babel-jest": "^24.7.1", 25 | "babel-loader": "8.0.5", 26 | "enzyme": "3.9.0", 27 | "enzyme-adapter-react-16": "^1.9.1", 28 | "html-webpack-plugin": "3.2.0", 29 | "jest": "^24.7.1", 30 | "jsdom": "13.2.0", 31 | "react": "^16.0.0", 32 | "react-dom": "^16.0.0", 33 | "webpack": "4.29.4", 34 | "webpack-cli": "3.2.3", 35 | "webpack-dev-server": "3.1.14" 36 | }, 37 | "scripts": { 38 | "build": "babel src --out-dir lib", 39 | "dev": "webpack-dev-server --mode=development --port 3000 --inline --hot --config example/webpack.config", 40 | "test": "jest" 41 | }, 42 | "keywords": [ 43 | "react", 44 | "react-component", 45 | "sort", 46 | "list", 47 | "sortable list", 48 | "sortable-list", 49 | "sortable" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/SortableList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | /** 5 | * Sortable List module 6 | * A sortable list component using html5 drag and drop api. 7 | **/ 8 | export default class SortableList extends Component { 9 | constructor(props) { 10 | super(props) 11 | let placeholder = document.createElement('li') 12 | placeholder.className = 'placeholder' 13 | this.state = { data: this.props.data, placeholder: placeholder } 14 | } 15 | 16 | /** 17 | * On drag start, set data. 18 | **/ 19 | dragStart(e) { 20 | this.dragged = e.currentTarget 21 | e.dataTransfer.effectAllowed = 'move' 22 | e.dataTransfer.setData('text/html', e.currentTarget) 23 | } 24 | 25 | /** 26 | * On drag end, update the data state. 27 | **/ 28 | dragEnd(e) { 29 | this.dragged.style.display = 'block' 30 | this.dragged.parentNode.removeChild(this.state.placeholder) 31 | let data = this.state.data 32 | let from = Number(this.dragged.dataset.id) 33 | let to = Number(this.over.dataset.id) 34 | if (from < to) to-- 35 | if (this.nodePlacement == 'after') to++ 36 | data.splice(to, 0, data.splice(from, 1)[0]) 37 | this.setState({ data: data }) 38 | } 39 | 40 | /** 41 | * On drag over, update items. 42 | **/ 43 | dragOver(e) { 44 | e.preventDefault() 45 | this.dragged.style.display = 'none' 46 | if (e.target.className == 'placeholder') return 47 | this.over = e.target 48 | let relY = e.clientY - this.over.offsetTop 49 | let height = this.over.offsetHeight / 2 50 | let parent = e.target.parentNode 51 | if (relY > height) { 52 | this.nodePlacement = 'after' 53 | parent.insertBefore(this.state.placeholder, e.target.nextElementSibling) 54 | } else if (relY < height) { 55 | this.nodePlacement = 'before' 56 | parent.insertBefore(this.state.placeholder, e.target) 57 | } 58 | } 59 | 60 | render() { 61 | const { data } = this.state 62 | const listItems = data.map((item, i) => { 63 | return ( 64 |
  • 71 | {item} 72 |
  • 73 | ) 74 | }) 75 | 76 | return ( 77 | 78 |
      {listItems}
    79 |
    80 | ) 81 | } 82 | } 83 | 84 | const SortableWrapper = styled.div` 85 | ul { 86 | list-style: none; 87 | margin: 0; 88 | padding: 0; 89 | } 90 | 91 | ul li { 92 | background: #eee; 93 | color: #666; 94 | margin: 0; 95 | padding: 10px; 96 | line-height: 1; 97 | .placeholder { 98 | background: #03cc85; 99 | } 100 | .placeholder:before { 101 | content: 'Drop here'; 102 | color: #666; 103 | } 104 | } 105 | ` 106 | -------------------------------------------------------------------------------- /src/__tests__/SortableList.spec.js: -------------------------------------------------------------------------------- 1 | /* setup enzyme */ 2 | import { configure } from 'enzyme' 3 | import Adapter from 'enzyme-adapter-react-16' 4 | configure({ adapter: new Adapter() }) 5 | 6 | /* setup jsdom */ 7 | var jsdom = require('jsdom') 8 | const { JSDOM } = jsdom 9 | const window = new JSDOM('').window 10 | global.window = window 11 | global.document = window.document 12 | 13 | import React from 'react' 14 | import renderer from 'react-test-renderer' 15 | import { shallow } from 'enzyme' 16 | import SortableList from '../SortableList' 17 | 18 | test('SortableList renders correctly and matches snapshot', () => { 19 | let colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange'] 20 | 21 | const component = renderer.create( 22 | 23 | ) 24 | 25 | let tree = component.toJSON() 26 | expect(tree).toMatchSnapshot() 27 | }) 28 | 29 | test('SortableList renders the correct elements and props', () => { 30 | let colors = ['Red', 'Green', 'Blue', 'Yellow', 'Black', 'White', 'Orange'] 31 | 32 | const wrapper = shallow( 33 | 34 | ) 35 | 36 | expect(wrapper.instance().props.data).toEqual(colors) 37 | 38 | expect(wrapper.find('ul').length).toEqual(1) 39 | expect(wrapper.find('li').length).toEqual(colors.length) 40 | expect(wrapper.find('li').first().prop('draggable')).toEqual('true'); 41 | expect(wrapper.find('li').first().prop('data-id')).toEqual(0); 42 | }) 43 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/SortableList.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SortableList renders correctly and matches snapshot 1`] = ` 4 |
    7 |
      10 |
    • 16 | Red 17 |
    • 18 |
    • 24 | Green 25 |
    • 26 |
    • 32 | Blue 33 |
    • 34 |
    • 40 | Yellow 41 |
    • 42 |
    • 48 | Black 49 |
    • 50 |
    • 56 | White 57 |
    • 58 |
    • 64 | Orange 65 |
    • 66 |
    67 |
    68 | `; 69 | --------------------------------------------------------------------------------