UNPKG

react-sortable-hoc

Version:

Set of higher-order components to turn any list into a sortable, touch-friendly, animated list

579 lines (475 loc) 18.7 kB
import _extends from 'babel-runtime/helpers/extends'; import _slicedToArray from 'babel-runtime/helpers/slicedToArray'; import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray'; import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _createClass from 'babel-runtime/helpers/createClass'; import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; import _inherits from 'babel-runtime/helpers/inherits'; import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import Manager from '../Manager'; import { closest, events, vendorPrefix, limit, getElementMargin } from '../utils'; import invariant from 'invariant'; // Export Higher Order Sortable Container Component export default function SortableContainer(WrappedComponent) { var _class, _temp; var config = arguments.length <= 1 || arguments[1] === undefined ? { withRef: false } : arguments[1]; return _temp = _class = function (_Component) { _inherits(_class, _Component); function _class(props) { _classCallCheck(this, _class); var _this = _possibleConstructorReturn(this, _Object$getPrototypeOf(_class).call(this)); _this.state = {}; _this.handleStart = function (e) { var _this$props = _this.props; var distance = _this$props.distance; var shouldCancelStart = _this$props.shouldCancelStart; if (e.button === 2 || shouldCancelStart(e)) { return false; } _this._touched = true; _this._pos = { x: e.clientX, y: e.clientY }; var node = closest(e.target, function (el) { return el.sortableInfo != null; }); if (node && !_this.state.sorting && node.sortableInfo) { var useDragHandle = _this.props.useDragHandle; var _node$sortableInfo = node.sortableInfo; var index = _node$sortableInfo.index; var collection = _node$sortableInfo.collection; if (useDragHandle && !closest(e.target, function (el) { return el.sortableHandle != null; })) return; _this.manager.active = { index: index, collection: collection }; if (!distance) { _this.pressTimer = setTimeout(function () { return _this.handlePress(e); }, _this.props.pressDelay); } } }; _this.handleMove = function (e) { var distance = _this.props.distance; if (!_this.state.sorting && _this._touched) { _this._delta = { x: _this._pos.x - e.clientX, y: _this._pos.y - e.clientY }; var delta = Math.abs(_this._delta.x) + Math.abs(_this._delta.y); if (!distance) { _this.cancel(); } else if (delta >= distance) { _this.handlePress(e); } } }; _this.handleEnd = function () { var distance = _this.props.distance; _this._touched = false; if (!distance) { _this.cancel(); } }; _this.cancel = function () { if (!_this.state.sorting) { clearTimeout(_this.pressTimer); _this.manager.active = null; } }; _this.handlePress = function (e) { var active = _this.manager.getActive(); if (active) { var _this$props2 = _this.props; var axis = _this$props2.axis; var onSortStart = _this$props2.onSortStart; var helperClass = _this$props2.helperClass; var hideSortableGhost = _this$props2.hideSortableGhost; var useWindowAsScrollContainer = _this$props2.useWindowAsScrollContainer; var node = active.node; var collection = active.collection; var index = node.sortableInfo.index; var margin = getElementMargin(node); var containerBoundingRect = _this.container.getBoundingClientRect(); _this.node = node; _this.margin = margin; _this.width = node.offsetWidth; _this.height = node.offsetHeight; _this.dimension = axis == 'x' ? _this.width : _this.height; _this.marginOffset = { x: _this.margin.left + _this.margin.right, y: Math.max(_this.margin.top, _this.margin.bottom) }; _this.boundingClientRect = node.getBoundingClientRect(); _this.index = index; _this.newIndex = index; var edge = _this.edge = axis == 'x' ? 'Left' : 'Top'; _this.offsetEdge = _this.getEdgeOffset(edge, node); _this.initialOffset = _this.getOffset(e); _this.initialScroll = _this.scrollContainer['scroll' + edge]; _this.helper = _this.document.body.appendChild(node.cloneNode(true)); _this.helper.style.position = 'fixed'; _this.helper.style.top = _this.boundingClientRect.top - margin.top + 'px'; _this.helper.style.left = _this.boundingClientRect.left - margin.left + 'px'; _this.helper.style.width = _this.width + 'px'; _this.helper.style.boxSizing = 'border-box'; if (hideSortableGhost) { _this.sortableGhost = node; node.style.visibility = 'hidden'; } if (axis == 'x') { _this.minTranslate = (useWindowAsScrollContainer ? 0 : containerBoundingRect.left) - _this.boundingClientRect.left - _this.width / 2; _this.maxTranslate = (useWindowAsScrollContainer ? _this.contentWindow.innerWidth : containerBoundingRect.left + containerBoundingRect.width) - _this.boundingClientRect.left - _this.width / 2; } else { _this.minTranslate = (useWindowAsScrollContainer ? 0 : containerBoundingRect.top) - _this.boundingClientRect.top - _this.height / 2; _this.maxTranslate = (useWindowAsScrollContainer ? _this.contentWindow.innerHeight : containerBoundingRect.top + containerBoundingRect.height) - _this.boundingClientRect.top - _this.height / 2; } if (helperClass) { var _this$helper$classLis; (_this$helper$classLis = _this.helper.classList).add.apply(_this$helper$classLis, _toConsumableArray(helperClass.split(' '))); } _this.listenerNode = e.touches ? node : _this.contentWindow; events.move.forEach(function (eventName) { return _this.listenerNode.addEventListener(eventName, _this.handleSortMove, false); }); events.end.forEach(function (eventName) { return _this.listenerNode.addEventListener(eventName, _this.handleSortEnd, false); }); _this.setState({ sorting: true, sortingIndex: index }); if (onSortStart) onSortStart({ node: node, index: index, collection: collection }, e); } }; _this.handleSortMove = function (e) { var onSortMove = _this.props.onSortMove; e.preventDefault(); // Prevent scrolling on mobile _this.updatePosition(e); _this.animateNodes(); _this.autoscroll(); if (onSortMove) onSortMove(e); }; _this.handleSortEnd = function (e) { var _this$props3 = _this.props; var hideSortableGhost = _this$props3.hideSortableGhost; var onSortEnd = _this$props3.onSortEnd; var collection = _this.manager.active.collection; // Remove the event listeners if the node is still in the DOM if (_this.listenerNode) { events.move.forEach(function (eventName) { return _this.listenerNode.removeEventListener(eventName, _this.handleSortMove); }); events.end.forEach(function (eventName) { return _this.listenerNode.removeEventListener(eventName, _this.handleSortEnd); }); } // Remove the helper from the DOM _this.helper.parentNode.removeChild(_this.helper); if (hideSortableGhost && _this.sortableGhost) { _this.sortableGhost.style.visibility = ''; } var nodes = _this.manager.refs[collection]; for (var i = 0, len = nodes.length; i < len; i++) { var node = nodes[i]; var el = node.node; // Clear the cached offsetTop / offsetLeft value node.edgeOffset = null; // Remove the transforms / transitions el.style[vendorPrefix + 'Transform'] = ''; el.style[vendorPrefix + 'TransitionDuration'] = ''; } if (typeof onSortEnd == 'function') { onSortEnd({ oldIndex: _this.index, newIndex: _this.newIndex, collection: collection }, e); } // Stop autoscroll clearInterval(_this.autoscrollInterval); _this.autoscrollInterval = null; // Update state _this.manager.active = null; _this.setState({ sorting: false, sortingIndex: null }); _this._touched = false; }; _this.autoscroll = function () { var translate = _this.translate; var direction = void 0; var speed = 1; var acceleration = 10; if (translate >= _this.maxTranslate - _this.dimension / 2) { direction = 1; // Scroll Down speed = acceleration * Math.abs((_this.maxTranslate - _this.dimension / 2 - translate) / _this.dimension); } else if (translate <= _this.minTranslate + _this.dimension / 2) { direction = -1; // Scroll Up speed = acceleration * Math.abs((translate - _this.dimension / 2 - _this.minTranslate) / _this.dimension); } if (_this.autoscrollInterval) { clearTimeout(_this.autoscrollInterval); _this.autoscrollInterval = null; _this.isAutoScrolling = false; } if (direction) { _this.autoscrollInterval = setInterval(function () { _this.isAutoScrolling = true; var offset = 1 * speed * direction; _this.scrollContainer['scroll' + _this.edge] += offset; _this.translate += offset; _this.animateNodes(); }, 5); } }; _this.manager = new Manager(); _this.events = { start: _this.handleStart, move: _this.handleMove, end: _this.handleEnd }; invariant(!(props.distance && props.pressDelay), 'Attempted to set both `pressDelay` and `distance` on SortableContainer, you may only use one or the other, not both at the same time.'); return _this; } _createClass(_class, [{ key: 'getChildContext', value: function getChildContext() { return { manager: this.manager }; } }, { key: 'componentDidMount', value: function componentDidMount() { var _this2 = this; var _props = this.props; var contentWindow = _props.contentWindow; var getContainer = _props.getContainer; this.container = typeof getContainer == 'function' ? getContainer(this.getWrappedInstance()) : ReactDOM.findDOMNode(this); this.document = this.container.ownerDocument || document; this.scrollContainer = this.props.useWindowAsScrollContainer ? this.document.body : this.container; this.contentWindow = typeof contentWindow == 'function' ? contentWindow() : contentWindow; var _loop = function _loop(key) { events[key].forEach(function (eventName) { return _this2.container.addEventListener(eventName, _this2.events[key], false); }); }; for (var key in this.events) { _loop(key); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { var _this3 = this; var _loop2 = function _loop2(key) { events[key].forEach(function (eventName) { return _this3.container.removeEventListener(eventName, _this3.events[key]); }); }; for (var key in this.events) { _loop2(key); } } }, { key: 'getEdgeOffset', value: function getEdgeOffset(edge, node) { var offset = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested if (node) { if (node.parentNode !== this.container) { return this.getEdgeOffset(edge, node.parentNode, offset + node['offset' + edge]); } else { return node['offset' + edge] + offset; } } } }, { key: 'getOffset', value: function getOffset(e) { return { x: e.touches ? e.touches[0].clientX : e.clientX, y: e.touches ? e.touches[0].clientY : e.clientY }; } }, { key: 'getLockPixelOffsets', value: function getLockPixelOffsets() { var lockOffset = this.props.lockOffset; if (!Array.isArray(lockOffset)) { lockOffset = [lockOffset, lockOffset]; } invariant(lockOffset.length === 2, 'lockOffset prop of SortableContainer should be a single ' + 'value or an array of exactly two values. Given %s', lockOffset); var _lockOffset = lockOffset; var _lockOffset2 = _slicedToArray(_lockOffset, 2); var minLockOffset = _lockOffset2[0]; var maxLockOffset = _lockOffset2[1]; return [this.getLockPixelOffset(minLockOffset), this.getLockPixelOffset(maxLockOffset)]; } }, { key: 'getLockPixelOffset', value: function getLockPixelOffset(lockOffset) { var offset = lockOffset; var unit = 'px'; if (typeof lockOffset === 'string') { var match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset); invariant(match !== null, 'lockOffset value should be a number or a string of a ' + 'number followed by "px" or "%". Given %s', lockOffset); offset = parseFloat(lockOffset); unit = match[1]; } invariant(isFinite(offset), 'lockOffset value should be a finite. Given %s', lockOffset); if (unit === '%') { offset = offset * this.dimension / 100; } return offset; } }, { key: 'updatePosition', value: function updatePosition(e) { var _props2 = this.props; var axis = _props2.axis; var lockAxis = _props2.lockAxis; var lockToContainerEdges = _props2.lockToContainerEdges; var offset = this.getOffset(e); var translate = { x: offset.x - this.initialOffset.x, y: offset.y - this.initialOffset.y }; this.translate = translate[axis]; if (lockToContainerEdges) { var _getLockPixelOffsets = this.getLockPixelOffsets(); var _getLockPixelOffsets2 = _slicedToArray(_getLockPixelOffsets, 2); var minLockOffset = _getLockPixelOffsets2[0]; var maxLockOffset = _getLockPixelOffsets2[1]; var minOffset = this.dimension / 2 - minLockOffset; var maxOffset = this.dimension / 2 - maxLockOffset; translate[axis] = limit(this.minTranslate + minOffset, this.maxTranslate - maxOffset, translate[axis]); } switch (lockAxis) { case 'x': translate.y = 0; break; case 'y': translate.x = 0; break; } this.helper.style[vendorPrefix + 'Transform'] = 'translate3d(' + translate.x + 'px,' + translate.y + 'px, 0)'; } }, { key: 'animateNodes', value: function animateNodes() { var _props3 = this.props; var axis = _props3.axis; var transitionDuration = _props3.transitionDuration; var hideSortableGhost = _props3.hideSortableGhost; var nodes = this.manager.getOrderedRefs(); var deltaScroll = this.scrollContainer['scroll' + this.edge] - this.initialScroll; var sortingOffset = this.offsetEdge + this.translate + deltaScroll; this.newIndex = null; for (var i = 0, len = nodes.length; i < len; i++) { var _nodes$i = nodes[i]; var node = _nodes$i.node; var edgeOffset = _nodes$i.edgeOffset; var index = node.sortableInfo.index; var dimension = axis == 'x' ? node.offsetWidth : node.offsetHeight; var offset = this.dimension > dimension ? dimension / 2 : this.dimension / 2; var translate = 0; var translateX = 0; var translateY = 0; // If we haven't cached the node's offsetTop / offsetLeft value if (edgeOffset == null) { nodes[i].edgeOffset = edgeOffset = this.getEdgeOffset(this.edge, node); } // If the node is the one we're currently animating, skip it if (index === this.index) { if (hideSortableGhost) { /* * With windowing libraries such as `react-virtualized`, the sortableGhost * node may change while scrolling down and then back up (or vice-versa), * so we need to update the reference to the new node just to be safe. */ this.sortableGhost = node; node.style.visibility = 'hidden'; } continue; } if (transitionDuration) { node.style[vendorPrefix + 'TransitionDuration'] = transitionDuration + 'ms'; } if (index > this.index && sortingOffset + offset >= edgeOffset) { translate = -(this.dimension + this.marginOffset[axis]); this.newIndex = index; } else if (index < this.index && sortingOffset <= edgeOffset + offset) { translate = this.dimension + this.marginOffset[axis]; if (this.newIndex == null) { this.newIndex = index; } } if (axis == 'x') { translateX = translate; } else { translateY = translate; } node.style[vendorPrefix + 'Transform'] = 'translate3d(' + translateX + 'px,' + translateY + 'px,0)'; } if (this.newIndex == null) { this.newIndex = this.index; } } }, { key: 'getWrappedInstance', value: function getWrappedInstance() { invariant(config.withRef, 'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableContainer() call'); return this.refs.wrappedInstance; } }, { key: 'render', value: function render() { var ref = config.withRef ? 'wrappedInstance' : null; return React.createElement(WrappedComponent, _extends({ ref: ref }, this.props, this.state)); } }]); return _class; }(Component), _class.displayName = WrappedComponent.displayName ? 'SortableList(' + WrappedComponent.displayName + ')' : 'SortableList', _class.WrappedComponent = WrappedComponent, _class.defaultProps = { axis: 'y', transitionDuration: 300, pressDelay: 0, distance: 0, useWindowAsScrollContainer: false, hideSortableGhost: true, contentWindow: typeof window !== 'undefined' ? window : null, shouldCancelStart: function shouldCancelStart(e) { // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option` if (['input', 'textarea', 'select', 'option'].indexOf(e.target.tagName.toLowerCase()) !== -1) { return true; // Return true to cancel sorting } }, lockToContainerEdges: false, lockOffset: '50%' }, _class.propTypes = { axis: PropTypes.oneOf(['x', 'y']), distance: PropTypes.number, lockAxis: PropTypes.string, helperClass: PropTypes.string, transitionDuration: PropTypes.number, contentWindow: PropTypes.any, onSortStart: PropTypes.func, onSortMove: PropTypes.func, onSortEnd: PropTypes.func, shouldCancelStart: PropTypes.func, pressDelay: PropTypes.number, useDragHandle: PropTypes.bool, useWindowAsScrollContainer: PropTypes.bool, hideSortableGhost: PropTypes.bool, lockToContainerEdges: PropTypes.bool, lockOffset: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))]), getContainer: PropTypes.func }, _class.childContextTypes = { manager: PropTypes.object.isRequired }, _temp; }