@bigfishtv/cockpit
Version:
633 lines (564 loc) • 23.4 kB
JavaScript
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 _class, _temp, _class$defaultProps;
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
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; }
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Immutable from 'immutable';
import deepEqual from 'deep-equal';
import keyCode from 'keycode';
import { flatten, flattenWithPath, flattenWithoutCollapsed, getChildByIdImmutable } from '../../utils/treeUtils';
import { isCtrlKeyPressed, isShiftKeyPressed } from '../../utils/selectKeyUtils';
import * as DragTypes from '../../constants/DragTypes';
import TreeItem from '../tree/TreeItem';
import DefaultTreeCell from '../tree/TreeCell';
import DefaultTreeDragLayer from '../tree/TreeDragLayer';
// this is referenced in TreeItem -- it needs to be provided as a prop for react-dnd decorator in order for it to be optionally replaced by tree props
var treeItemSource = {
beginDrag: function beginDrag(props, monitor, component) {
props.beginDrag(props.id);
return { id: props.id, index: props.index };
},
endDrag: function endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
props.endDrag();
}
}
};
// this is referenced in TreeItem -- it needs to be provided as a prop for react-dnd decorator in order for it to be optionally replaced by tree props
var expandTimeout = null;
var EDGE_SIZE = 10;
var PARENT_BELOW_EDGE_SIZE = 50;
var treeItemTarget = {
drop: function drop(props, monitor, component) {
if (!props.reorderable) return;
// goes through all drop targets and resets their forceExpand state so they don't randomly open upon drag if hover expanded
for (var key in monitor.internalMonitor.registry.handlers) {
if (key.charAt(0) == 'T') {
var item = monitor.internalMonitor.registry.handlers[key].component;
if (item.state && item.state.forceExpand) {
item.props.onCollapse();
item.setState({ forceExpand: false });
}
}
}
if (monitor.isOver({ shallow: true })) {
var draggedId = monitor.getItem().id;
var targetId = props.id;
var position = component.state.position;
props.endDrag(draggedId, targetId, position);
}
},
hover: function hover(props, monitor, component) {
if (!props.reorderable) return;
var isOverCurrent = monitor.isOver({ shallow: true });
if (isOverCurrent) {
var ownId = props.id;
var draggedId = monitor.getItem().id;
if (draggedId === ownId || component.props.selected) return;
var boundingRect = ReactDOM.findDOMNode(component).getBoundingClientRect();
var clientOffset = monitor.getClientOffset();
var offsetY = clientOffset.y - boundingRect.top;
var rowHeight = boundingRect.bottom - boundingRect.top;
var bottomEdgeSize = !props.collapsed && props.children && props.children.size > 0 ? PARENT_BELOW_EDGE_SIZE : EDGE_SIZE;
if (clientOffset.y > boundingRect.top && clientOffset.y < boundingRect.bottom) {
if (clientOffset.y > boundingRect.top + EDGE_SIZE && clientOffset.y < boundingRect.top + rowHeight - bottomEdgeSize) {
if (component.props.collapsed && component.state.position != 'into') {
if (expandTimeout !== null) clearTimeout(expandTimeout);
expandTimeout = setTimeout(function () {
component.setState({ forceExpand: true });
}, 1000);
}
component.setState({ position: 'into' });
} else if (offsetY < EDGE_SIZE) {
component.setState({ position: 'above' });
} else {
component.setState({ position: 'below' });
}
}
}
}
};
var Tree = (_temp = _class = function (_Component) {
_inherits(Tree, _Component);
function Tree(props) {
_classCallCheck(this, Tree);
var _this = _possibleConstructorReturn(this, _Component.call(this));
_this.beginDrag = function (id) {
if (!_this.props.reorderable) return;
_this.dragging = true;
var selectedIds = _this.state.selectedIds.indexOf(id) >= 0 ? _this.state.selectedIds : [id];
_this.preDragCollapsedIds = _this.state.collapsedIds.slice();
var collapsedIds = [].concat(selectedIds, _this.state.collapsedIds);
if (!_this.props.uncontrolled) {
_this.props.onCombinationChange({ collapsedIds: collapsedIds, selectedIds: selectedIds });
} else {
_this.setState({ selectedIds: selectedIds, collapsedIds: collapsedIds });
}
};
_this.endDrag = function (draggedId, targetId, position) {
if (!_this.props.reorderable) return;
_this.dragging = false;
// don't do anything if the dragged item ends up over an item being dragged
// or the drag is cancelled
if (!targetId || _this.state.selectedIds.indexOf(targetId) > -1) {
if (!_this.props.uncontrolled) {
_this.props.onCollapseChange(_this.preDragCollapsedIds);
} else {
_this.setState({ collapsedIds: _this.preDragCollapsedIds });
}
return;
}
var newTree = _this.state.InflatedTree;
var FlatTree = flattenWithPath(newTree.toJS());
// find items matching selected ids
var draggedItems = FlatTree.filter(function (item) {
return _this.state.selectedIds.indexOf(item.item.id) >= 0;
});
// remove children from the dragged items list and order items in reverse order
draggedItems = draggedItems.filter(function (item) {
return draggedItems.filter(function (item2) {
if (item2.path.length < item.path.length) {
if (item2.path.join('') == item.path.slice(0, item2.path.length).join('')) {
return true;
}
}
return false;
}).length == 0;
}).sort(function (a, b) {
a = a.path;
b = b.path;
var len = Math.min(a.length, b.length);
for (var i = 0; i < len; i++) {
if (a[i] < b[i]) {
return 1;
} else if (a[i] > b[i]) {
return -1;
}
}
return 0;
});
draggedItems.forEach(function (draggedItem) {
newTree = newTree.deleteIn(draggedItem.path);
});
FlatTree = flattenWithPath(newTree.toJS());
var targetItem = getChildByIdImmutable(targetId, newTree);
var draggedItem = getChildByIdImmutable(draggedId, newTree);
var draggedParentId = draggedItem && draggedItem.get('parent_id') || null;
var targetParentId = targetItem && targetItem.get('parent_id') || null;
var targetPath = FlatTree.filter(function (item) {
return item.item.id === targetId;
})[0].path;
var targetParentPath = targetParentId ? FlatTree.filter(function (item) {
return item.item.id === targetParentId;
})[0].path : null;
var targetIndex = targetPath[targetPath.length - 1];
var children = null;
var changes = [];
var index = 0;
switch (position) {
case 'into':
if (targetId == draggedParentId) {
break;
}
draggedItems = draggedItems.reverse();
if (targetItem.get('children')) {
draggedItems.map(function (draggedItem, i) {
index = targetItem.get('children').size + i;
newTree = newTree.setIn(targetPath.concat(['children', index]), Immutable.fromJS(draggedItem.item));
changes.push({
id: draggedItem.item.id,
parent_id: targetId,
index: index
});
});
} else {
newTree = newTree.setIn(targetPath.concat(['children']), Immutable.fromJS(draggedItems.map(function (draggedItem) {
return draggedItem.item;
})));
draggedItems.map(function (draggedItem, i) {
changes.push({
id: draggedItem.item.id,
parent_id: targetId,
index: i
});
});
}
break;
case 'above':
if (!targetParentId) {
draggedItems.map(function (draggedItem, i) {
newTree = newTree.splice(targetIndex, 0, Immutable.fromJS(draggedItem.item));
changes.push({
id: draggedItem.item.id,
parent_id: null,
index: targetIndex
});
});
} else {
draggedItems.map(function (draggedItem, i) {
children = newTree.getIn(targetParentPath.concat(['children'])).splice(targetIndex, 0, Immutable.fromJS(draggedItem.item));
newTree = newTree.setIn(targetParentPath.concat(['children']), children);
changes.push({
id: draggedItem.item.id,
parent_id: targetParentId,
index: targetIndex
});
});
}
break;
case 'below':
if (!targetParentId) {
draggedItems.map(function (draggedItem, i) {
newTree = newTree.splice(targetIndex + 1, 0, Immutable.fromJS(draggedItem.item));
changes.push({
id: draggedItem.item.id,
parent_id: null,
index: targetIndex + 1
});
});
} else {
draggedItems.map(function (draggedItem, i) {
children = newTree.getIn(targetParentPath.concat(['children'])).splice(targetIndex + 1, 0, Immutable.fromJS(draggedItem.item));
newTree = newTree.setIn(targetParentPath.concat(['children']), children);
changes.push({
id: draggedItem.item.id,
parent_id: targetParentId,
index: targetIndex + 1
});
});
}
break;
}
if (!_this.props.uncontrolled) {
_this.props.onCollapseChange(_this.preDragCollapsedIds);
_this.props.onChange(_this.props.immutable ? newTree : newTree.toJS(), changes);
} else {
_this.setState({ collapsedIds: _this.preDragCollapsedIds, InflatedTree: newTree });
}
};
_this.lastSelectedId = null;
_this.lastSelectedList = [];
_this.dragging = false;
_this.handleKeyDown = _this.handleKeyDown.bind(_this);
_this.state = {
InflatedTree: props.immutable ? props.value : Immutable.fromJS(props.value),
selectedIds: props.selectedIds,
collapsedIds: props.collapsedIds
};
return _this;
}
Tree.prototype.componentDidMount = function componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
};
Tree.prototype.componentWillUnmount = function componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
};
Tree.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
var treeEqual = this.props.immutable ? nextProps.value == this.props.value : deepEqual(nextProps.value, this.props.value);
if (!treeEqual) {
this.setState({ InflatedTree: this.props.immutable ? nextProps.value : Immutable.fromJS(nextProps.value) });
}
if (!this.props.uncontrolled) {
this.setState({
selectedIds: nextProps.selectedIds,
collapsedIds: nextProps.collapsedIds
});
}
};
Tree.prototype.handleKeyDown = function handleKeyDown(event) {
var _this2 = this;
var immutable = this.props.immutable;
var _state = this.state,
InflatedTree = _state.InflatedTree,
selectedIds = _state.selectedIds,
collapsedIds = _state.collapsedIds;
if (!InflatedTree) return;
var data = flattenWithoutCollapsed(InflatedTree.toJS(), collapsedIds);
if (!data || !data.length) return;
var key = keyCode(event);
var activeElement = document.activeElement;
if (!activeElement || activeElement.nodeName !== 'INPUT' && activeElement.nodeName !== 'TEXTAREA') {
if (key === 'up') {
event.preventDefault();
var index = this.lastSelectedId === null ? data.length - 1 : data.reduce(function (value, row, i) {
return row.id === _this2.lastSelectedId ? i - 1 : value;
}, null);
var row = data[index <= 0 ? 0 : index];
this.handleSelect(immutable ? Immutable.Map(row) : row);
} else if (key === 'down') {
event.preventDefault();
var _index = this.lastSelectedId === null ? 0 : data.reduce(function (value, row, i) {
return row.id === _this2.lastSelectedId ? i + 1 : value;
}, null);
var _row = data[_index >= data.length - 1 ? data.length - 1 : _index];
this.handleSelect(immutable ? Immutable.Map(_row) : _row);
} else if ((key === 'left' || key === 'right') && selectedIds.length) {
event.preventDefault();
var newCollapsedIds = collapsedIds.slice();
data.map(function (item) {
if (selectedIds.indexOf(item.id) >= 0) {
if (collapsedIds.indexOf(item.id) < 0) newCollapsedIds.push(item.id);else newCollapsedIds.splice(collapsedIds.indexOf(item.id), 1);
}
});
if (!this.props.uncontrolled) {
this.props.onCollapseChange(newCollapsedIds);
} else {
this.setState({ collapsedIds: newCollapsedIds });
}
} else if (key === 'esc') {
event.preventDefault();
this.handleDeselectAll();
} else if (key === 'a' && isCtrlKeyPressed()) {
event.preventDefault();
this.handleSelectAll(data);
} else if (key === 'enter' && selectedIds.length === 1) {
var _row2 = data.filter(function (item) {
return item.id == selectedIds[0];
})[0];
this.handleDoubleClick(immutable ? Immutable.Map(_row2) : _row2);
}
}
};
Tree.prototype.handleSelect = function handleSelect(item) {
var _this3 = this;
var immutable = Immutable.Map.isMap(item);
var _props = this.props,
multiselect = _props.multiselect,
stickySelect = _props.stickySelect;
var _state2 = this.state,
selectedIds = _state2.selectedIds,
InflatedTree = _state2.InflatedTree;
var id = immutable ? item.get('id') : item.id;
if (!multiselect || !isCtrlKeyPressed() && !isShiftKeyPressed()) {
selectedIds = selectedIds.length === 1 && selectedIds[0] === id && !stickySelect ? [] : [id];
} else if (isCtrlKeyPressed()) {
if (selectedIds.indexOf(id) >= 0) {
selectedIds = selectedIds.filter(function (_id) {
return _id !== id;
});
} else {
selectedIds = [].concat(selectedIds, [id]);
}
} else if (isShiftKeyPressed()) {
var tree = flatten(immutable ? InflatedTree.toJS() : InflatedTree);
var lastIndex = tree.indexOf(tree.filter(function (item) {
return item.id === _this3.lastSelectedId;
})[0]);
var nextIndex = tree.indexOf(tree.filter(function (item) {
return item.id === id;
})[0]);
var lower = Math.min(lastIndex, nextIndex);
var upper = Math.max(lastIndex, nextIndex);
var ids = tree.filter(function (item, i) {
return i >= lower && i <= upper;
}).map(function (item) {
return item.id;
});
selectedIds = [].concat(this.lastSelectedList, ids);
}
// remove duplicate ids
selectedIds = selectedIds.filter(function (val, i) {
return selectedIds.indexOf(val) === i;
});
this.lastSelectedId = id;
this.lastSelectedList = selectedIds.slice();
if (this.props.uncontrolled) {
this.setState({ selectedIds: selectedIds });
}
this.props.onSelectionChange(selectedIds);
};
Tree.prototype.handleCollapse = function handleCollapse(item) {
var immutable = Immutable.Map.isMap(item);
var id = immutable ? item.get('id') : item.id;
var collapsedIds = this.state.collapsedIds;
if (collapsedIds.indexOf(id) >= 0) collapsedIds.splice(collapsedIds.indexOf(id), 1);else collapsedIds.push(id);
if (!this.props.uncontrolled) {
this.props.onCollapseChange(collapsedIds);
} else {
this.setState({ collapsedIds: collapsedIds });
}
};
Tree.prototype.handleDoubleClick = function handleDoubleClick(item) {
if (Immutable.Map.isMap(item) && !this.props.immutable) item = item.toJS();else if (!Immutable.Map.isMap(item) && this.props.immutable) item = Immutable.Map(item);
this.props.onSelectItem && this.props.onSelectItem(item);
};
Tree.prototype.handleSelectAll = function handleSelectAll() {
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
var _state3 = this.state,
InflatedTree = _state3.InflatedTree,
collapsedIds = _state3.collapsedIds;
data = data || flattenWithoutCollapsed(InflatedTree.toJS(), collapsedIds);
var selectedIds = data.map(function (item) {
return item.id;
});
this.lastSelectedList = selectedIds.slice();
if (this.props.uncontrolled) {
this.setState({ selectedIds: selectedIds });
}
this.props.onSelectionChange(selectedIds);
};
Tree.prototype.handleDeselectAll = function handleDeselectAll() {
var selectedIds = [];
this.lastSelectedList = [];
if (this.props.uncontrolled) {
this.setState({ selectedIds: selectedIds });
}
this.props.onSelectionChange(selectedIds);
};
Tree.prototype.renderList = function renderList(list) {
var _this4 = this;
if (!list) return null;
return list.map(function (item, key) {
return _this4.renderItem(item, key);
});
};
Tree.prototype.treeItemTarget = function treeItemTarget() {
if (typeof this.props.treeItemTarget == 'function') {
return this.props.treeItemTarget();
} else {
return this.props.treeItemTarget;
}
};
Tree.prototype.treeItemSource = function treeItemSource() {
if (typeof this.props.treeItemSource == 'function') {
return this.props.treeItemSource();
} else {
return this.props.treeItemSource;
}
};
Tree.prototype.renderItem = function renderItem(item, key) {
var _this5 = this;
var id = item.get('id');
var selected = this.state.selectedIds.indexOf(id) >= 0 || this.state.selectedIds.length === 0 && id === null;
var collapsed = this.state.collapsedIds.indexOf(id) >= 0;
var _item$toObject = item.toObject(),
children = _item$toObject.children,
rest = _objectWithoutProperties(_item$toObject, ['children']);
var showIndicator = children && children.size > 0;
var _props2 = this.props,
TreeCell = _props2.TreeCell,
dropTargetType = _props2.dropTargetType,
dragTargetType = _props2.dragTargetType,
reorderable = _props2.reorderable,
breadcrumbs = _props2.breadcrumbs;
return React.createElement(
TreeItem,
_extends({
key: key,
TreeCell: TreeCell
}, rest, {
dropTargetType: dropTargetType,
dragTargetType: dragTargetType,
treeItemTarget: this.treeItemTarget(),
treeItemSource: this.treeItemSource(),
beginDrag: this.beginDrag,
endDrag: this.endDrag,
onClick: function onClick() {
return _this5.handleSelect(item);
},
onDoubleClick: function onDoubleClick() {
return _this5.handleDoubleClick(item);
},
selected: selected,
collapsed: collapsed,
reorderable: reorderable,
breadcrumbs: breadcrumbs,
onCollapse: function onCollapse() {
return _this5.handleCollapse(item);
},
showIndicator: showIndicator }),
showIndicator && this.renderList(item.get('children'))
);
};
Tree.prototype.render = function render() {
var TreeDragLayer = this.props.TreeDragLayer;
var _state4 = this.state,
InflatedTree = _state4.InflatedTree,
selectedIds = _state4.selectedIds;
var nestedTree = this.renderList(InflatedTree);
return React.createElement(
'div',
{ className: 'tree' },
React.createElement(
'div',
{ className: 'tree-items' },
nestedTree
),
React.createElement(TreeDragLayer, { selectedIds: selectedIds, InflatedTree: InflatedTree, dragging: this.dragging })
);
};
return Tree;
}(Component), _class.propTypes = {
/** Tree data provided as an array of objects. Objects can contain key 'children' which contains the same data structure, repeat recursively */
value: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
/** Whether or not component needs to handle its own state e.g. selectedIds & collapsedIds */
uncontrolled: PropTypes.bool,
/** Whether or not to convert data to Immutable data -- changes what callback functions give */
immutable: PropTypes.bool,
/** Allow selection of multiple cells */
multiselect: PropTypes.bool,
/** Allow cells to be reordable */
reorderable: PropTypes.bool,
/** Enable stickSelect -- as if CTRL key were held down */
stickySelect: PropTypes.bool,
/** Wrap cells in breadcrumbs for easy navigation -- passed on to TreeItem */
breadcrumbs: PropTypes.bool,
/** Only applicable if component is controlled */
selectedIds: PropTypes.arrayOf(PropTypes.number),
/** Only applicable if component is controlled */
collapsedIds: PropTypes.arrayOf(PropTypes.number),
/** See Constants/dragTypes */
dropTargetType: PropTypes.string,
/** ReactDnD drag target functions, has defaults */
treeItemTarget: PropTypes.oneOfType([PropTypes.objectOf(PropTypes.func), PropTypes.func]),
/** ReactDnD drag source functions, has defaults */
treeItemSource: PropTypes.oneOfType([PropTypes.objectOf(PropTypes.func), PropTypes.func]),
/** Called on tree structure change e.g. re-order, delete. (Will return immutable data if prop is set) */
onChange: PropTypes.func,
/** Called on cell select, ie. double click. (Will return immutable data if prop is set) */
onSelectItem: PropTypes.func,
/** Called on cell selection, probably only useful if controlled. (Will return immutable data if prop is set) */
onSelectionChange: PropTypes.func,
/** Called on cell (de)collapse, probably only useful if controlled. (Will return immutable data if prop is set) */
onCollapseChange: PropTypes.func,
/** Is a combination of selectedItems and collapsedItems, probably only useful if controlled. (Will return immutable data if prop is set) */
onCombinationChange: PropTypes.func
}, _class.defaultProps = (_class$defaultProps = {
value: [],
uncontrolled: false,
immutable: false,
multiselect: true,
reorderable: true,
stickySelect: true,
breadcrumbs: true,
selectedIds: [],
collapsedIds: [],
dropTargetType: DragTypes.TREE_ITEM,
dragTargetType: DragTypes.TREE_ITEM,
treeItemTarget: treeItemTarget,
treeItemSource: treeItemSource,
TreeCell: DefaultTreeCell,
TreeDragLayer: DefaultTreeDragLayer,
onChange: function onChange() {
return console.warn('[Tree] no onChange prop provided');
},
onSelectItem: function onSelectItem() {
return console.warn('[Tree] no onSelectItem prop provided');
},
onSelectionChange: function onSelectionChange() {
return console.warn('[Tree] no onSelectionChange prop provided');
},
onCollapseChange: function onCollapseChange() {
return console.warn('[Tree] no onCollapseChange prop provided');
}
}, _class$defaultProps['onSelectionChange'] = function onSelectionChange() {
return console.warn('[Tree] no onSelectionChange prop provided');
}, _class$defaultProps.onCombinationChange = function onCombinationChange() {
return console.warn('[Tree] no onCombinationChange prop provided');
}, _class$defaultProps), _temp);
export { Tree as default };