├── .gitignore
├── .babelrc
├── package.json
├── LICENSE
├── README.md
├── src
└── Sortable.js
└── dist
└── Sortable.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@wordpress/default"
4 | ]
5 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gutenberg-sortable",
3 | "version": "1.0.5",
4 | "license": "MIT",
5 | "main": "dist/Sortable.js",
6 | "author": "Luc Princen",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com:lucprincen/gutenberg-sortable.git"
10 | },
11 | "dependencies": {
12 | "react-sortable-hoc": "^0.8.3"
13 | },
14 | "devDependencies": {
15 | "@wordpress/babel-preset-default": "^1.1.2",
16 | "babel-cli": "^6.26.0",
17 | "babel-preset-env": "^1.7.0"
18 | },
19 | "scripts": {
20 | "build": "babel src --presets babel-preset-env --out-dir dist"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016, Claudéric Demers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gutenberg Sortable
2 | A Gutenberg component to turn anything into an animated, touch- and a11y-friendly sortable list.
3 | Like these images in a block with desktop wallpapers:
4 |
5 | 
6 |
7 | ---
8 |
9 | ## Features
10 |
11 | - Locked axises
12 | - Events
13 | - Smooth animations
14 | - Auto-scrolling
15 | - Horizontal, vertical and grid lists
16 | - Touch support 👌
17 | - Keyboard support 💪
18 |
19 |
20 | ## Installation
21 |
22 | Using npm:
23 | `$ npm install gutenberg-sortable --save`
24 |
25 | And then, using a module bundler that supports ES2015 modules (like [webpack](https://webpack.js.org/)):
26 | ```javascript
27 | import Sortable from 'gutenberg-sortable';
28 |
29 | //or, if you're not using ES6:
30 | var Sortable = require('gutenberg-sortable');
31 | ```
32 |
33 |
34 | ## Usage
35 |
36 | Here's a basic example of the Sortable gutenberg component, used in a block.
37 | ```javascript
38 | //... skipping the usual registerBlockType settings, and getting straight to the attributes:
39 | attributes: {
40 | images: {
41 | source: 'query',
42 | selector: 'img',
43 | query: {
44 | url: { source: 'attribute', attribute: 'src' },
45 | alt: { source: 'attribute', attribute: 'alt' }
46 | }
47 | }
48 | },
49 | edit( props ) {
50 |
51 | let images = ( !props.attributes.images ? [] : props.attributes.images );
52 | const { className, setAttributes } = props
53 |
54 | return (
55 |
56 |
setAttributes({ images }) }
61 | >
62 | {images.map((image) =>
63 |
64 | )}
65 |
66 |
67 | )
68 | },
69 | //... rest of the block logic
70 | ```
71 |
72 | Let's break that down:
73 |
74 | ### Attributes
75 | When you register an attribute to work with Sortable, it's probably easiest to use a source: 'query' attribute. This makes it so you can just add html or components to your Sortable.
76 |
77 | ### Sortable
78 | Sortable is meant as a wrapper. Wrap it around everything you'd like to be sortable. It will add a parent div around all the children. You should also pass the attribute (in this case images) as a prop called "items". This ensures you will get back the re-sorted prop in your sortable events.
79 |
80 |
81 | ## Options
82 |
83 | There's a few options you can pass the component:
84 |
85 | **axis** - The axis you'd like to sort on. This example is set to 'grid', but X and Y are also available. Y is the default.
86 |
87 | **onSortStart** - What to do when sorting starts. This is a function that will get the node and it's index plus the event as it's properties returned. A simple example for this:
88 | ```javascript
89 | const highlight = ({node, index}, event) => {
90 | node.classList.add('highlight');
91 | console.log( 'the element you\'ve picked up has an index of '+index );
92 | }
93 |
94 |
95 | ```
96 | This will give the picked up node a "highlight" class and log a message with the nodes current index.
97 |
98 |
99 | **onSortEnd** - What to do when sorting has finished. This function will return the items you passed along as a prop, but now reordered according to the users' action. In the basic example above we just reset the attribute with the new sorted values.
100 |
101 | ## FAQ
102 |
103 | #### Module not found: can't resolve React-DOM
104 | If you encounter this error while compiling your block, you haven't loaded in React and React Dom from WP Core as an external in your build process. This is because Sortable uses a native React component to provide certain functionality.
105 | Add the following to your webpack.config.js, and everything should work fine:
106 |
107 | ```javascript
108 | externals: {
109 | 'react': 'React',
110 | 'react-dom': 'ReactDOM'
111 | },
112 | ```
113 |
114 |
115 | ## Dependencies
116 |
117 | Gutenberg Sortable depends on the [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) package by [Claudéric Demers](https://github.com/clauderic).
118 |
119 |
120 | ## Contributions
121 |
122 | All help is welcome! Please leave your feature- and/org pull-request here! 😉
--------------------------------------------------------------------------------
/src/Sortable.js:
--------------------------------------------------------------------------------
1 | import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc';
2 | import classnames from 'classnames';
3 | import React from 'react';
4 |
5 | const { Component } = wp.element;
6 |
7 | class Sortable extends Component {
8 |
9 | //constructor
10 | constructor() {
11 | super(...arguments);
12 |
13 | this.focusIndex = null;
14 |
15 | this.onSortStart = this.onSortStart.bind(this);
16 | this.onSortEnd = this.onSortEnd.bind(this);
17 | this.onKeyDown = this.onKeyDown.bind(this);
18 | }
19 |
20 |
21 | /**
22 | * Get the sortable list:
23 | */
24 | getSortableList() {
25 |
26 | const { items, children, className } = this.props;
27 |
28 | //create the sortable container:
29 | return SortableContainer(() => {
30 |
31 | //loop through all available children
32 | return (
33 |
34 | {children.map((child, index) => {
35 |
36 | child.props['tabindex'] = '0';
37 | child.props['onKeyDown'] = this.onKeyDown;
38 |
39 | //generate a SortableElement using the item and the child
40 | let SortableItem = SortableElement(() => {
41 | return (child)
42 | });
43 |
44 | //set a temporary class so we can find it post-render:
45 | if (index == this.focusIndex) {
46 | child.props['class'] = classnames('sortable-focus', child.props.className);
47 | }
48 |
49 | //display Sortable Element
50 | return (
51 |
52 | )
53 | })}
54 |
55 | )
56 | });
57 | }
58 |
59 |
60 | /**
61 | * Render the component
62 | */
63 | render() {
64 | const items = this.props.items;
65 | const SortableList = this.getSortableList();
66 |
67 | //reset key-focus after refresh:
68 | this.resetKeyboardFocus();
69 |
70 | return (
71 | //return the sortable list, with props from our upper-lever component
72 |
78 | );
79 | }
80 |
81 | /*************************************/
82 | /** Evens */
83 | /*************************************/
84 |
85 | /**
86 | * What to do on sort start ?
87 | *
88 | * @param Object
89 | */
90 | onSortStart({ node, index, collection }, event) {
91 | //run the corresponding function in the upper-lever component:
92 | if (typeof (this.props.onSortStart) === 'function') {
93 | this.props.onSortStart({ node, index, collection }, event);
94 | }
95 | }
96 |
97 | /**
98 | * What to do on sort end?
99 | *
100 | * @param Object holding old and new indexes and the collection
101 | */
102 | onSortEnd({ oldIndex, newIndex }) {
103 |
104 | //create a new items array:
105 | let _items = arrayMove(this.props.items, oldIndex, newIndex);
106 |
107 | //and run the corresponding function in the upper-lever component:
108 | if (typeof (this.props.onSortEnd) === 'function') {
109 | this.props.onSortEnd(_items);
110 | }
111 | }
112 |
113 |
114 | /*************************************/
115 | /** Helpers */
116 | /*************************************/
117 |
118 | /**
119 | * Get a default axis, and allow for the "grid" axis type
120 | */
121 | getAxis() {
122 | if (typeof (this.props.axis) == 'undefined') {
123 | return 'y';
124 | } else if (this.props.axis == 'grid') {
125 | return 'xy';
126 | }
127 |
128 | return this.props.axis;
129 | }
130 |
131 |
132 |
133 |
134 | /*************************************/
135 | /** Keyboard Accesibility */
136 | /*************************************/
137 |
138 | /**
139 | * Keyboard accessibility:
140 | */
141 | onKeyDown(e) {
142 |
143 | if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
144 | e.preventDefault();
145 | e.stopPropagation();
146 | }
147 |
148 | const key = e.keyCode;
149 | const oldIndex = this.getIndex(e.target);
150 | let newIndex = oldIndex;
151 |
152 | switch (key) {
153 |
154 | case 37: //left
155 | case 38: //top
156 | newIndex = parseInt(oldIndex - 1);
157 | break
158 | case 39: //right
159 | case 40: //down
160 | newIndex = parseInt(oldIndex + 1);
161 | break;
162 | }
163 |
164 | if (oldIndex !== newIndex) {
165 | this.focusIndex = newIndex;
166 | this.onSortEnd({ oldIndex, newIndex });
167 | }
168 | }
169 |
170 | /**
171 | * Get the index of a child
172 | *
173 | * @param Element child
174 | */
175 | getIndex(child) {
176 | const parent = child.parentNode;
177 | const children = parent.children;
178 | let i = children.length - 1;
179 | for (; i >= 0; i--) {
180 | if (child == children[i]) {
181 | break;
182 | }
183 | }
184 | return i;
185 | }
186 |
187 | /**
188 | * After a render, reset the keyboard focus:
189 | */
190 | resetKeyboardFocus() {
191 | setTimeout(() => {
192 | if (this.focusIndex !== null) {
193 | const focusElement = document.getElementsByClassName('sortable-focus')[0];
194 | focusElement.focus();
195 | focusElement.classList.remove('sortable-focus');
196 | this.focusIndex = null;
197 | }
198 | }, 10);
199 | }
200 | }
201 |
202 | export default Sortable;
--------------------------------------------------------------------------------
/dist/Sortable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 |
7 | var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
8 |
9 | var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
10 |
11 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
12 |
13 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
14 |
15 | var _createClass2 = require('babel-runtime/helpers/createClass');
16 |
17 | var _createClass3 = _interopRequireDefault(_createClass2);
18 |
19 | var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
20 |
21 | var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
22 |
23 | var _inherits2 = require('babel-runtime/helpers/inherits');
24 |
25 | var _inherits3 = _interopRequireDefault(_inherits2);
26 |
27 | var _reactSortableHoc = require('react-sortable-hoc');
28 |
29 | var _classnames = require('classnames');
30 |
31 | var _classnames2 = _interopRequireDefault(_classnames);
32 |
33 | var _react = require('react');
34 |
35 | var _react2 = _interopRequireDefault(_react);
36 |
37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
38 |
39 | var Component = wp.element.Component;
40 |
41 | var Sortable = function (_Component) {
42 | (0, _inherits3.default)(Sortable, _Component);
43 |
44 | //constructor
45 | function Sortable() {
46 | (0, _classCallCheck3.default)(this, Sortable);
47 |
48 | var _this = (0, _possibleConstructorReturn3.default)(this, (Sortable.__proto__ || (0, _getPrototypeOf2.default)(Sortable)).apply(this, arguments));
49 |
50 | _this.focusIndex = null;
51 |
52 | _this.onSortStart = _this.onSortStart.bind(_this);
53 | _this.onSortEnd = _this.onSortEnd.bind(_this);
54 | _this.onKeyDown = _this.onKeyDown.bind(_this);
55 | return _this;
56 | }
57 |
58 | /**
59 | * Get the sortable list:
60 | */
61 |
62 |
63 | (0, _createClass3.default)(Sortable, [{
64 | key: 'getSortableList',
65 | value: function getSortableList() {
66 | var _this2 = this;
67 |
68 | var _props = this.props,
69 | items = _props.items,
70 | children = _props.children,
71 | className = _props.className;
72 |
73 | //create the sortable container:
74 |
75 | return (0, _reactSortableHoc.SortableContainer)(function () {
76 |
77 | //loop through all available children
78 | return wp.element.createElement(
79 | 'div',
80 | { className: (0, _classnames2.default)('components-sortable', className) },
81 | children.map(function (child, index) {
82 |
83 | child.props['tabindex'] = '0';
84 | child.props['onKeyDown'] = _this2.onKeyDown;
85 |
86 | //generate a SortableElement using the item and the child
87 | var SortableItem = (0, _reactSortableHoc.SortableElement)(function () {
88 | return child;
89 | });
90 |
91 | //set a temporary class so we can find it post-render:
92 | if (index == _this2.focusIndex) {
93 | child.props['class'] = (0, _classnames2.default)('sortable-focus', child.props.className);
94 | }
95 |
96 | //display Sortable Element
97 | return wp.element.createElement(SortableItem, { key: 'item-' + index, index: index, item: items[index] });
98 | })
99 | );
100 | });
101 | }
102 |
103 | /**
104 | * Render the component
105 | */
106 |
107 | }, {
108 | key: 'render',
109 | value: function render() {
110 | var items = this.props.items;
111 | var SortableList = this.getSortableList();
112 |
113 | //reset key-focus after refresh:
114 | this.resetKeyboardFocus();
115 |
116 | return (
117 | //return the sortable list, with props from our upper-lever component
118 | wp.element.createElement(SortableList, {
119 | axis: this.getAxis(),
120 | items: items,
121 | onSortStart: this.onSortStart,
122 | onSortEnd: this.onSortEnd
123 | })
124 | );
125 | }
126 |
127 | /*************************************/
128 | /** Evens */
129 | /*************************************/
130 |
131 | /**
132 | * What to do on sort start ?
133 | *
134 | * @param Object
135 | */
136 |
137 | }, {
138 | key: 'onSortStart',
139 | value: function onSortStart(_ref, event) {
140 | var node = _ref.node,
141 | index = _ref.index,
142 | collection = _ref.collection;
143 |
144 | //run the corresponding function in the upper-lever component:
145 | if (typeof this.props.onSortStart === 'function') {
146 | this.props.onSortStart({ node: node, index: index, collection: collection }, event);
147 | }
148 | }
149 |
150 | /**
151 | * What to do on sort end?
152 | *
153 | * @param Object holding old and new indexes and the collection
154 | */
155 |
156 | }, {
157 | key: 'onSortEnd',
158 | value: function onSortEnd(_ref2) {
159 | var oldIndex = _ref2.oldIndex,
160 | newIndex = _ref2.newIndex;
161 |
162 |
163 | //create a new items array:
164 | var _items = (0, _reactSortableHoc.arrayMove)(this.props.items, oldIndex, newIndex);
165 |
166 | //and run the corresponding function in the upper-lever component:
167 | if (typeof this.props.onSortEnd === 'function') {
168 | this.props.onSortEnd(_items);
169 | }
170 | }
171 |
172 | /*************************************/
173 | /** Helpers */
174 | /*************************************/
175 |
176 | /**
177 | * Get a default axis, and allow for the "grid" axis type
178 | */
179 |
180 | }, {
181 | key: 'getAxis',
182 | value: function getAxis() {
183 | if (typeof this.props.axis == 'undefined') {
184 | return 'y';
185 | } else if (this.props.axis == 'grid') {
186 | return 'xy';
187 | }
188 |
189 | return this.props.axis;
190 | }
191 |
192 | /*************************************/
193 | /** Keyboard Accesibility */
194 | /*************************************/
195 |
196 | /**
197 | * Keyboard accessibility:
198 | */
199 |
200 | }, {
201 | key: 'onKeyDown',
202 | value: function onKeyDown(e) {
203 |
204 | if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
205 | e.preventDefault();
206 | e.stopPropagation();
207 | }
208 |
209 | var key = e.keyCode;
210 | var oldIndex = this.getIndex(e.target);
211 | var newIndex = oldIndex;
212 |
213 | switch (key) {
214 |
215 | case 37: //left
216 | case 38:
217 | //top
218 | newIndex = parseInt(oldIndex - 1);
219 | break;
220 | case 39: //right
221 | case 40:
222 | //down
223 | newIndex = parseInt(oldIndex + 1);
224 | break;
225 | }
226 |
227 | if (oldIndex !== newIndex) {
228 | this.focusIndex = newIndex;
229 | this.onSortEnd({ oldIndex: oldIndex, newIndex: newIndex });
230 | }
231 | }
232 |
233 | /**
234 | * Get the index of a child
235 | *
236 | * @param Element child
237 | */
238 |
239 | }, {
240 | key: 'getIndex',
241 | value: function getIndex(child) {
242 | var parent = child.parentNode;
243 | var children = parent.children;
244 | var i = children.length - 1;
245 | for (; i >= 0; i--) {
246 | if (child == children[i]) {
247 | break;
248 | }
249 | }
250 | return i;
251 | }
252 |
253 | /**
254 | * After a render, reset the keyboard focus:
255 | */
256 |
257 | }, {
258 | key: 'resetKeyboardFocus',
259 | value: function resetKeyboardFocus() {
260 | var _this3 = this;
261 |
262 | setTimeout(function () {
263 | if (_this3.focusIndex !== null) {
264 | var focusElement = document.getElementsByClassName('sortable-focus')[0];
265 | focusElement.focus();
266 | focusElement.classList.remove('sortable-focus');
267 | _this3.focusIndex = null;
268 | }
269 | }, 10);
270 | }
271 | }]);
272 | return Sortable;
273 | }(Component);
274 |
275 | exports.default = Sortable;
--------------------------------------------------------------------------------