├── .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 | [](https://badge.fury.io/js/react-sortable-list)
4 |
5 | 
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 |
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 |
--------------------------------------------------------------------------------