UNPKG

@bigfishtv/cockpit

Version:

633 lines (564 loc) 23.4 kB
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 };