react-nestable-2
Version:
Drag & drop hierarchical list made as a react component
814 lines (662 loc) • 26.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactAddonsShallowCompare = require('react-addons-shallow-compare');
var _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare);
var _reactAddonsUpdate = require('react-addons-update');
var _reactAddonsUpdate2 = _interopRequireDefault(_reactAddonsUpdate);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _utils = require('../utils');
require('./Nestable.css');
var _NestableItem = require('./NestableItem');
var _NestableItem2 = _interopRequireDefault(_NestableItem);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Nestable = function (_Component) {
_inherits(Nestable, _Component);
function Nestable(props) {
_classCallCheck(this, Nestable);
var _this = _possibleConstructorReturn(this, (Nestable.__proto__ || Object.getPrototypeOf(Nestable)).call(this, props));
_this.collapse = function (itemIds) {
var _this$props = _this.props,
childrenProp = _this$props.childrenProp,
collapsed = _this$props.collapsed;
var items = _this.state.items;
if (itemIds === 'NONE') {
_this.setState({
collapsedGroups: collapsed ? (0, _utils.getAllNonEmptyNodesIds)(items, childrenProp) : []
});
} else if (itemIds === 'ALL') {
_this.setState({
collapsedGroups: collapsed ? [] : (0, _utils.getAllNonEmptyNodesIds)(items, childrenProp)
});
} else if ((0, _utils.isArray)(itemIds)) {
_this.setState({
collapsedGroups: (0, _utils.getAllNonEmptyNodesIds)(items, childrenProp).filter(function (id) {
return itemIds.indexOf(id) > -1 ^ collapsed;
})
});
}
};
_this.startTrackMouse = function () {
document.addEventListener('mousemove', _this.onMouseMove);
document.addEventListener('mouseup', _this.onDragEnd);
document.addEventListener('keydown', _this.onKeyDown);
};
_this.stopTrackMouse = function () {
document.removeEventListener('mousemove', _this.onMouseMove);
document.removeEventListener('mouseup', _this.onDragEnd);
document.removeEventListener('keydown', _this.onKeyDown);
_this.elCopyStyles = null;
};
_this.getItemDepth = function (item) {
var childrenProp = _this.props.childrenProp;
var level = 1;
if (item[childrenProp].length > 0) {
var childrenDepths = item[childrenProp].map(_this.getItemDepth);
level += Math.max.apply(Math, _toConsumableArray(childrenDepths));
}
return level;
};
_this.isCollapsed = function (item) {
var collapsed = _this.props.collapsed;
var collapsedGroups = _this.state.collapsedGroups;
return !!(collapsedGroups.indexOf(item.id) > -1 ^ collapsed);
};
_this.onDragStart = function (e, item) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
_this.el = (0, _utils.closest)(e.target, '.nestable-item');
_this.startTrackMouse();
_this.onMouseMove(e);
_this.setState({
dragItem: item,
itemsOld: _this.state.items
});
};
_this.onDragEnd = function (e, isCancel) {
e && e.preventDefault();
_this.stopTrackMouse();
_this.el = null;
isCancel ? _this.dragRevert() : _this.dragApply();
};
_this.onMouseMove = function (e) {
var _this$props2 = _this.props,
group = _this$props2.group,
threshold = _this$props2.threshold;
var dragItem = _this.state.dragItem;
var clientX = e.clientX,
clientY = e.clientY;
var transformProps = (0, _utils.getTransformProps)(clientX, clientY);
var elCopy = document.querySelector('.nestable-' + group + ' .nestable-drag-layer > .nestable-list');
if (!_this.elCopyStyles) {
var offset = (0, _utils.getOffsetRect)(_this.el);
var scroll = (0, _utils.getTotalScroll)(_this.el);
_this.elCopyStyles = _extends({
marginTop: offset.top - clientY - scroll.top,
marginLeft: offset.left - clientX - scroll.left
}, transformProps);
} else {
_this.elCopyStyles = _extends({}, _this.elCopyStyles, transformProps);
for (var key in transformProps) {
if (transformProps.hasOwnProperty(key)) {
elCopy.style[key] = transformProps[key];
}
}
var diffX = clientX - _this.mouse.last.x;
if (diffX >= 0 && _this.mouse.shift.x >= 0 || diffX <= 0 && _this.mouse.shift.x <= 0) {
_this.mouse.shift.x += diffX;
} else {
_this.mouse.shift.x = 0;
}
_this.mouse.last.x = clientX;
if (Math.abs(_this.mouse.shift.x) > threshold) {
if (_this.mouse.shift.x > 0) {
_this.tryIncreaseDepth(dragItem);
} else {
_this.tryDecreaseDepth(dragItem);
}
_this.mouse.shift.x = 0;
}
}
};
_this.onMouseEnter = function (e, item) {
_this.onReorderItem(e, item);
};
_this.onStartMoveItem = function (e, item) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
_this.setState({
dragItem: item,
itemsOld: _this.state.items,
isKeyBoard: true
});
document.addEventListener('keydown', _this.onKeyDown);
};
_this.onEndMoveItem = function (e) {
e && e.preventDefault();
_this.dragApply();
document.removeEventListener('keydown', _this.onKeyDown);
};
_this.onReorderItem = function (e, item) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
var _this$props3 = _this.props,
collapsed = _this$props3.collapsed,
childrenProp = _this$props3.childrenProp;
var dragItem = _this.state.dragItem;
if (!item || dragItem.id === item.id) return;
var pathFrom = _this.getPathById(dragItem.id);
var pathTo = _this.getPathById(item.id);
// if collapsed by default
// and move last (by count) child
// remove parent node from list of open nodes
var collapseProps = {};
if (collapsed && pathFrom.length > 1) {
var parent = _this.getItemByPath(pathFrom.slice(0, -1));
if (parent[childrenProp].length === 1) {
collapseProps = _this.onToggleCollapse(parent, true);
}
}
_this.moveItem({ dragItem: dragItem, pathFrom: pathFrom, pathTo: pathTo }, collapseProps);
};
_this.onToggleCollapse = function (item, isGetter) {
var collapsed = _this.props.collapsed;
var collapsedGroups = _this.state.collapsedGroups;
var isCollapsed = _this.isCollapsed(item);
var newState = {
collapsedGroups: isCollapsed ^ collapsed ? collapsedGroups.filter(function (id) {
return id !== item.id;
}) : collapsedGroups.concat(item.id)
};
if (isGetter) {
return newState;
} else {
_this.setState(newState);
}
};
_this.onKeyDown = function (e, item) {
var _this$state = _this.state,
dragItem = _this$state.dragItem,
isKeyBoard = _this$state.isKeyBoard;
// SPACE
if (e.which === 32) {
if (!isKeyBoard) {
_this.onStartMoveItem(e, item);
}
} else if (e.which === 27) {
_this.onEndMoveItem(null);
} else if (e.which == 37) {
_this.tryDecreaseDepth(dragItem);
} else if (e.which == 39) {
_this.tryIncreaseDepth(dragItem);
} else if (e.which == 38) {
var prevItem = _this.getPrevItem();
_this.onReorderItem(e, prevItem);
} else if (e.which == 40) {
var nextItem = _this.getNextItem();
_this.onReorderItem(e, nextItem);
}
};
_this.getItemOrder = function (item, items, arr) {
if (!item || !items || items.length === 0) {
return;
}
var childrenProp = _this.props.childrenProp;
for (var i = 0; i < items.length; i++) {
var currentItem = items[i];
arr.push(currentItem);
if (currentItem.id !== item.id && !_this.isCollapsed(currentItem)) {
var newItems = currentItem[childrenProp];
_this.getItemOrder(item, newItems, arr);
}
}
};
_this.getPrevItem = function () {
var _this$state2 = _this.state,
items = _this$state2.items,
dragItem = _this$state2.dragItem;
var itemOrderArr = [];
_this.getItemOrder(dragItem, items, itemOrderArr);
var dragItemIndex = 0;
for (var i = 0; i < itemOrderArr.length; i++) {
if (itemOrderArr[i].id === dragItem.id) {
dragItemIndex = i;
break;
}
}
return dragItemIndex === 0 ? null : itemOrderArr[dragItemIndex - 1];
};
_this.getNextItem = function () {
var _this$state3 = _this.state,
items = _this$state3.items,
dragItem = _this$state3.dragItem;
var itemOrderArr = [];
_this.getItemOrder(dragItem, items, itemOrderArr);
var dragItemIndex = 0;
for (var i = 0; i < itemOrderArr.length; i++) {
if (itemOrderArr[i].id == dragItem.id) {
dragItemIndex = i;
break;
}
}
return dragItemIndex === itemOrderArr.length - 1 ? null : itemOrderArr[dragItemIndex + 1];
};
_this.state = {
items: [],
itemsOld: null, // snap copy in case of canceling drag
dragItem: null,
isDirty: false,
collapsedGroups: [],
isKeyBoard: false
};
_this.el = null;
_this.elCopyStyles = null;
_this.mouse = {
last: { x: 0 },
shift: { x: 0 }
};
return _this;
}
_createClass(Nestable, [{
key: 'componentDidMount',
value: function componentDidMount() {
var _props = this.props,
items = _props.items,
childrenProp = _props.childrenProp;
// make sure every item has property 'children'
items = (0, _utils.listWithChildren)(items, childrenProp);
this.setState({ items: items });
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps) {
var _props2 = this.props,
itemsNew = _props2.items,
childrenProp = _props2.childrenProp;
var isPropsUpdated = (0, _reactAddonsShallowCompare2.default)({ props: prevProps, state: {} }, this.props, {});
if (isPropsUpdated) {
this.stopTrackMouse();
var extra = {};
if (prevProps.collapsed !== this.props.collapsed) {
extra.collapsedGroups = [];
}
this.setState(_extends({
items: (0, _utils.listWithChildren)(itemsNew, childrenProp),
dragItem: null,
isKeyBoard: false,
isDirty: false
}, extra));
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.stopTrackMouse();
}
// ––––––––––––––––––––––––––––––––––––
// Public Methods
// ––––––––––––––––––––––––––––––––––––
// ––––––––––––––––––––––––––––––––––––
// Methods
// ––––––––––––––––––––––––––––––––––––
}, {
key: 'moveItem',
value: function moveItem(_ref) {
var dragItem = _ref.dragItem,
pathFrom = _ref.pathFrom,
pathTo = _ref.pathTo;
var extraProps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var _props3 = this.props,
childrenProp = _props3.childrenProp,
confirmChange = _props3.confirmChange;
var dragItemSize = this.getItemDepth(dragItem);
var items = this.state.items;
// the remove action might affect the next position,
// so update next coordinates accordingly
var realPathTo = this.getRealNextPath(pathFrom, pathTo, dragItemSize);
if (realPathTo.length === 0) return;
// user can validate every movement
var destinationPath = realPathTo.length > pathTo.length ? pathTo : pathTo.slice(0, -1);
var destinationParent = this.getItemByPath(destinationPath);
if (!confirmChange(dragItem, destinationParent)) return;
var removePath = this.getSplicePath(pathFrom, {
numToRemove: 1,
childrenProp: childrenProp
});
var insertPath = this.getSplicePath(realPathTo, {
numToRemove: 0,
itemsToInsert: [dragItem],
childrenProp: childrenProp
});
items = (0, _reactAddonsUpdate2.default)(items, removePath);
items = (0, _reactAddonsUpdate2.default)(items, insertPath);
this.setState(_extends({
items: items,
isDirty: true
}, extraProps));
}
}, {
key: 'tryIncreaseDepth',
value: function tryIncreaseDepth(dragItem) {
if (!dragItem) {
return;
}
var _props4 = this.props,
maxDepth = _props4.maxDepth,
childrenProp = _props4.childrenProp,
collapsed = _props4.collapsed;
var pathFrom = this.getPathById(dragItem.id);
var itemIndex = pathFrom[pathFrom.length - 1];
var newDepth = pathFrom.length + this.getItemDepth(dragItem);
// has previous sibling and isn't at max depth
if (itemIndex > 0 && newDepth <= maxDepth) {
var prevSibling = this.getItemByPath(pathFrom.slice(0, -1).concat(itemIndex - 1));
// previous sibling is not collapsed
if (!prevSibling[childrenProp].length || !this.isCollapsed(prevSibling)) {
var pathTo = pathFrom.slice(0, -1).concat(itemIndex - 1).concat(prevSibling[childrenProp].length);
// if collapsed by default
// and was no children here
// open this node
var collapseProps = {};
if (collapsed && !prevSibling[childrenProp].length) {
collapseProps = this.onToggleCollapse(prevSibling, true);
}
this.moveItem({ dragItem: dragItem, pathFrom: pathFrom, pathTo: pathTo }, collapseProps);
}
}
}
}, {
key: 'tryDecreaseDepth',
value: function tryDecreaseDepth(dragItem) {
if (!dragItem) {
return;
}
var _props5 = this.props,
childrenProp = _props5.childrenProp,
collapsed = _props5.collapsed;
var pathFrom = this.getPathById(dragItem.id);
var itemIndex = pathFrom[pathFrom.length - 1];
// has parent
if (pathFrom.length > 1) {
var parent = this.getItemByPath(pathFrom.slice(0, -1));
// is last (by order) item in array
if (itemIndex + 1 === parent[childrenProp].length) {
var pathTo = pathFrom.slice(0, -1);
pathTo[pathTo.length - 1] += 1;
// if collapsed by default
// and is last (by count) item in array
// remove this node from list of open nodes
var collapseProps = {};
if (collapsed && parent[childrenProp].length === 1) {
collapseProps = this.onToggleCollapse(parent, true);
}
this.moveItem({ dragItem: dragItem, pathFrom: pathFrom, pathTo: pathTo }, collapseProps);
}
}
}
}, {
key: 'dragApply',
value: function dragApply() {
var onChange = this.props.onChange;
var _state = this.state,
items = _state.items,
isDirty = _state.isDirty,
dragItem = _state.dragItem;
this.setState({
itemsOld: null,
dragItem: null,
isKeyboard: false,
isDirty: false
});
onChange && isDirty && onChange(items, dragItem);
}
}, {
key: 'dragRevert',
value: function dragRevert() {
var itemsOld = this.state.itemsOld;
this.setState({
items: itemsOld,
itemsOld: null,
dragItem: null,
isKeyboard: false,
isDirty: false
});
}
// ––––––––––––––––––––––––––––––––––––
// Getter methods
// ––––––––––––––––––––––––––––––––––––
}, {
key: 'getPathById',
value: function getPathById(id) {
var _this2 = this;
var items = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state.items;
var childrenProp = this.props.childrenProp;
var path = [];
items.every(function (item, i) {
if (item.id === id) {
path.push(i);
} else if (item[childrenProp]) {
var childrenPath = _this2.getPathById(id, item[childrenProp]);
if (childrenPath.length) {
path = path.concat(i).concat(childrenPath);
}
}
return path.length === 0;
});
return path;
}
}, {
key: 'getItemByPath',
value: function getItemByPath(path) {
var items = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state.items;
var childrenProp = this.props.childrenProp;
var item = null;
path.forEach(function (index) {
var list = item ? item[childrenProp] : items;
item = list[index];
});
return item;
}
}, {
key: 'getSplicePath',
value: function getSplicePath(path) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var splicePath = {};
var numToRemove = options.numToRemove || 0;
var itemsToInsert = options.itemsToInsert || [];
var lastIndex = path.length - 1;
var currentPath = splicePath;
path.forEach(function (index, i) {
if (i === lastIndex) {
currentPath.$splice = [[index, numToRemove].concat(_toConsumableArray(itemsToInsert))];
} else {
var nextPath = {};
currentPath[index] = _defineProperty({}, options.childrenProp, nextPath);
currentPath = nextPath;
}
});
return splicePath;
}
}, {
key: 'getRealNextPath',
value: function getRealNextPath(prevPath, nextPath, dragItemSize) {
var _props6 = this.props,
childrenProp = _props6.childrenProp,
maxDepth = _props6.maxDepth;
var ppLastIndex = prevPath.length - 1;
var npLastIndex = nextPath.length - 1;
var newDepth = nextPath.length + dragItemSize - 1;
if (prevPath.length < nextPath.length) {
// move into depth
var wasShifted = false;
// if new depth exceeds max, try to put after item instead of into item
if (newDepth > maxDepth && nextPath.length) {
return this.getRealNextPath(prevPath, nextPath.slice(0, -1), dragItemSize);
}
return nextPath.map(function (nextIndex, i) {
if (wasShifted) {
return i === npLastIndex ? nextIndex + 1 : nextIndex;
}
if (typeof prevPath[i] !== 'number') {
return nextIndex;
}
if (nextPath[i] > prevPath[i] && i === ppLastIndex) {
wasShifted = true;
return nextIndex - 1;
}
return nextIndex;
});
} else if (prevPath.length === nextPath.length) {
// if move bottom + move to item with children --> make it a first child instead of swap
if (nextPath[npLastIndex] > prevPath[npLastIndex]) {
var target = this.getItemByPath(nextPath);
if (newDepth < maxDepth && target[childrenProp] && target[childrenProp].length && !this.isCollapsed(target)) {
return nextPath.slice(0, -1).concat(nextPath[npLastIndex] - 1).concat(0);
}
}
}
return nextPath;
}
}, {
key: 'getItemOptions',
value: function getItemOptions() {
var _props7 = this.props,
renderItem = _props7.renderItem,
renderCollapseIcon = _props7.renderCollapseIcon,
handler = _props7.handler,
childrenProp = _props7.childrenProp;
var _state2 = this.state,
dragItem = _state2.dragItem,
isKeyBoard = _state2.isKeyBoard;
return {
dragItem: dragItem,
childrenProp: childrenProp,
renderItem: renderItem,
renderCollapseIcon: renderCollapseIcon,
handler: handler,
isKeyBoard: isKeyBoard,
onDragStart: this.onDragStart,
onMouseEnter: this.onMouseEnter,
onKeyDown: this.onKeyDown,
isCollapsed: this.isCollapsed,
onToggleCollapse: this.onToggleCollapse
};
}
// ––––––––––––––––––––––––––––––––––––
// Click handlers or event handlers
// ––––––––––––––––––––––––––––––––––––
}, {
key: 'renderDragLayer',
// ––––––––––––––––––––––––––––––––––––
// Render methods
// ––––––––––––––––––––––––––––––––––––
value: function renderDragLayer() {
var group = this.props.group;
var dragItem = this.state.dragItem;
var el = document.querySelector('.nestable-' + group + ' .nestable-item-' + dragItem.id);
var listStyles = {};
if (el) {
listStyles.width = el.clientWidth;
}
if (this.elCopyStyles) {
listStyles = _extends({}, listStyles, this.elCopyStyles);
}
var options = this.getItemOptions();
return _react2.default.createElement(
'div',
{ className: 'nestable-drag-layer' },
_react2.default.createElement(
'ol',
{ className: 'nestable-list', style: listStyles },
_react2.default.createElement(_NestableItem2.default, {
item: dragItem,
options: options,
isCopy: true
})
)
);
}
}, {
key: 'render',
value: function render() {
var _props8 = this.props,
group = _props8.group,
className = _props8.className;
var _state3 = this.state,
items = _state3.items,
dragItem = _state3.dragItem,
isKeyBoard = _state3.isKeyBoard;
var options = this.getItemOptions();
return _react2.default.createElement(
'div',
{ className: (0, _classnames2.default)(className, 'nestable', 'nestable-' + group, { 'is-drag-active': dragItem }) },
_react2.default.createElement(
'ol',
{ className: 'nestable-list nestable-group' },
items.map(function (item, i) {
return _react2.default.createElement(_NestableItem2.default, {
key: i,
index: i,
item: item,
options: options
});
})
),
!isKeyBoard && dragItem && this.renderDragLayer()
);
}
}]);
return Nestable;
}(_react.Component);
Nestable.propTypes = {
className: _propTypes2.default.string,
items: _propTypes2.default.arrayOf(_propTypes2.default.shape({
id: _propTypes2.default.any.isRequired
})),
threshold: _propTypes2.default.number,
maxDepth: _propTypes2.default.number,
collapsed: _propTypes2.default.bool,
group: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.string]),
childrenProp: _propTypes2.default.string,
renderItem: _propTypes2.default.func,
renderCollapseIcon: _propTypes2.default.func,
handler: _propTypes2.default.node,
onChange: _propTypes2.default.func,
confirmChange: _propTypes2.default.func
};
Nestable.defaultProps = {
items: [],
threshold: 30,
maxDepth: 10,
collapsed: false,
group: Math.random().toString(36).slice(2),
childrenProp: 'children',
renderItem: function renderItem(_ref2) {
var item = _ref2.item;
return item.toString();
},
onChange: function onChange() {},
confirmChange: function confirmChange() {
return true;
}
};
exports.default = Nestable;