acklen-keystone
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
294 lines (258 loc) • 7.3 kB
JavaScript
import React, { Component } from 'react';
import SortableTree, { getFlatDataFromTree } from 'react-sortable-tree';
import { browserHistory } from 'react-router';
export default class TreeView extends Component {
constructor (props) {
super(props);
this.state = {
treeData: this.loadData(props.items),
};
this.nodeMoved = this.nodeMoved.bind(this);
this.save = this.save.bind(this);
this.canDrop = this.canDrop.bind(this);
this.getFlatData = this.getFlatData.bind(this);
this.updateContentDimensions = this.updateContentDimensions.bind(this);
this.addMenu = this.addMenu.bind(this);
this.deleteMenu = this.deleteMenu.bind(this);
}
componentDidMount () {
window.addEventListener('resize', this.updateContentDimensions);
this.updateContentDimensions();
}
componentWillReceiveProps (nextProps) {
if (nextProps.items !== this.props.items) {
this.state = {
treeData: this.loadData(nextProps.items),
};
}
}
componentWillUnmount () {
window.removeEventListener('resize', this.updateContentDimensions);
}
updateContentDimensions (alerts) {
const element = document.querySelector('#tree-view-container');
const height = window.innerHeight;
if (element) {
element.style.height = `${height - 380}px`;
}
}
addMenu (node) {
const nodeParent = this.state.treeData
.filter((item) => item.id === node.parent.id);
const defaultValues = {
parent: {
value: node.parent.id,
className: 'disabled-field',
},
type: {
value: nodeParent.length > 0 ? 'content' : 'option',
className: 'hide-field',
},
};
this.props.onAddMenu(defaultValues);
}
editMenu (node) {
browserHistory.push(`/keystone/${this.props.listId}/${node.id}`);
}
deleteMenu (node, e) {
this.props.onDeleteMenu(node, e);
}
findMenuItem (parent, id) {
if (parent.id === id) {
return parent;
} else if (parent.children) {
let result = null;
for (let i = 0; result === null && i < parent.children.length; ++i) {
const child = parent.children[i];
result = this.findMenuItem(child, id);
}
return result;
}
return null;
}
buildTreeView (arr) {
const treeView = [];
const children = {};
for (let i = 0; i < arr.length; ++i) {
const item = arr[i];
const p = item.fields.parent;
const target = !p ? treeView : (children[p.id] || (children[p.id] = []));
let truncatedName = item.name;
if (truncatedName.length > 70) {
truncatedName = `${truncatedName.substr(0, 66)}...`;
}
let truncatedTitle = item.fields.title;
if (truncatedTitle && truncatedTitle.title > 100) {
truncatedTitle = `${truncatedTitle.substr(0, 96)}...`;
}
let itemFound;
if (this.treeView && this.treeView.props.treeData) {
this.treeView.props.treeData.forEach((parent) => {
if (!itemFound) {
itemFound = this.findMenuItem(parent, item.id);
}
if (parent.id === item.id) {
itemFound = parent;
}
});
}
target.push({
id: item.id,
title: truncatedName,
subtitle: truncatedTitle,
expanded: itemFound ? itemFound.expanded : true,
canDrag: item.fields.parent,
parent: item.fields.parent,
state: item.fields.state,
});
}
const findChildren = (parent) => {
if (children[parent.id]) {
parent.children = children[parent.id];
for (let i = 0; i < parent.children.length; ++i) {
findChildren(parent.children[i]);
}
}
};
for (let i = 0; i < treeView.length; ++i) {
findChildren(treeView[i]);
}
return treeView;
}
canShowAddMenu (path) {
return path.length > 0 && path.length < 4;
}
canShowDeleteMenu (node) {
return node.parent && this.props.listId === 'menus';
}
canDrag ({ node }) {
return node.canDrag;
};
canDrop ({ node, nextParent, prevPath, nextPath }) {
let canDrop = nextPath.length > 1 && nextPath.length < 5;
const flatData = this.getFlatData();
if (nextPath.length === 3) {
const children = flatData.filter((item) => item.parent === node.id);
let grandChildren = true;
children.forEach((child) => {
if (grandChildren) {
grandChildren = flatData
.filter((item) => item.parent === child.id).length === 0;
}
});
canDrop = grandChildren;
}
if (nextPath.length === 4) {
const children = flatData.filter((item) => item.parent === node.id);
canDrop = children.length === 0;
}
return nextParent && canDrop;
}
canShowEditMenu (node) {
return node.parent;
}
isOnDraftState (node) {
return node.state === 'draft';
}
nodeMoved ({ treeData, node }) {
const findChildren = (parent) => {
if (parent.children) {
for (let i = 0; i < parent.children.length; ++i) {
const child = parent.children[i];
if (node.id === child.id) {
child.parent = {
id: parent.id,
name: parent.name,
};
}
findChildren(parent.children[i]);
}
}
};
const newTreeData = treeData.map((item) => {
const newItem = item;
findChildren(item);
return newItem;
});
this.setState({ treeData: newTreeData });
this.save();
}
loadData (items) {
return this.buildTreeView(items);
}
getFlatData () {
return getFlatDataFromTree({
treeData: this.state.treeData,
getNodeKey: ({ node }) => node.id,
ignoreCollapsed: false,
}).map(({ node, parentNode, treeIndex }) => ({
id: node.id,
name: node.title,
title: node.subtitle,
parent: parentNode ? parentNode.id : null,
order: treeIndex,
}));
}
save () {
const flatData = this.getFlatData();
this.props.onSave(flatData);
}
render () {
return (
<div id="tree-view-container">
<SortableTree
ref={(treeView) => { this.treeView = treeView; }}
canDrag={this.canDrag}
canDrop={this.canDrop}
treeData={this.state.treeData}
onChange={treeData => this.setState({ treeData })}
onMoveNode={this.nodeMoved}
generateNodeProps={({ node, path }) => ({
buttons: [
<div key={node} style={{ marginRight: 10 }}>
{ this.isOnDraftState(node) ? (
<span style={{ color: 'red' }}>
draft
</span>) : null
}
</div>,
<div key={node} style={{ marginRight: 10 }}>
{ this.canShowAddMenu(path) ? (
<button
onClick={() => this.addMenu({ parent: node })}
disabled={this.props.loading}
style={this.props.loading ? { color: 'lightgray' } : null}
>
<span className="octicon octicon-plus" />
</button>) : null
}
</div>,
<div key={node} style={{ marginRight: 10 }}>
{ this.canShowEditMenu(node) ? (
<button
onClick={() => this.editMenu(node)}
disabled={this.props.loading}
style={this.props.loading ? { color: 'lightgray' } : null}
>
<span className="octicon octicon-pencil" />
</button>) : null
}
</div>,
<div key={node}>
{ this.canShowDeleteMenu(node) ? (
<button
onClick={(e) => this.deleteMenu(node, e)}
disabled={this.props.loading}
style={this.props.loading ? { color: 'lightgray' } : null}
>
<span className="octicon octicon-trashcan" />
</button>) : null
}
</div>,
],
})}
/>
</div>
);
}
}