Initial navigation tree

This commit is contained in:
Jori Lallo 2016-06-18 11:40:11 -07:00
parent d2479c199e
commit 591050a1e3
9 changed files with 607 additions and 7 deletions

View File

@ -53,6 +53,7 @@
"http-errors": "^1.4.0",
"imports-loader": "^0.6.5",
"isomorphic-fetch": "^2.2.1",
"js-tree": "^1.1.0",
"json-loader": "^0.5.4",
"jsonwebtoken": "^5.7.0",
"koa": "^2.0.0",

108
src/components/Tree/Node.js Normal file
View File

@ -0,0 +1,108 @@
var React = require('react');
import styles from './Tree.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
var Node = React.createClass({
displayName: 'UITreeNode',
renderCollapse() {
var index = this.props.index;
if(index.children && index.children.length) {
var collapsed = index.node.collapsed;
return (
<span
className={cx(styles.collapse, collapsed ? styles.caretRight : styles.caretDown)}
onMouseDown={function(e) {e.stopPropagation()}}
onClick={this.handleCollapse}
>
<img src={ require("./assets/chevron.svg") } />
</span>
);
}
return null;
},
renderChildren() {
var index = this.props.index;
var tree = this.props.tree;
var dragging = this.props.dragging;
if(index.children && index.children.length) {
var childrenStyles = {};
if (!this.props.rootNode) {
if(index.node.collapsed) childrenStyles.display = 'none';
childrenStyles['paddingLeft'] = this.props.paddingLeft + 'px';
}
return (
<div className={ styles.children } style={childrenStyles}>
{index.children.map((child) => {
var childIndex = tree.getIndex(child);
return (
<Node
tree={tree}
index={childIndex}
key={childIndex.id}
dragging={dragging}
paddingLeft={this.props.paddingLeft}
onCollapse={this.props.onCollapse}
onDragStart={this.props.onDragStart}
/>
);
})}
</div>
);
}
return null;
},
render() {
var tree = this.props.tree;
var index = this.props.index;
var dragging = this.props.dragging;
var node = index.node;
var style = {};
return (
<div className={cx(styles.node, {
placeholder: index.id === dragging,
rootNode: this.props.rootNode,
})} style={style}>
<div className={ styles.inner } ref="inner" onMouseDown={this.handleMouseDown}>
{!this.props.rootNode && this.renderCollapse()}
<span
className={ cx(styles.nodeLabel, { rootLabel: this.props.rootNode }) }
onClick={() => {}}
>
{node.module.name}
</span>
</div>
{this.renderChildren()}
</div>
);
},
handleCollapse(e) {
e.stopPropagation();
var nodeId = this.props.index.id;
if(this.props.onCollapse) this.props.onCollapse(nodeId);
},
handleMouseDown(e) {
var nodeId = this.props.index.id;
var dom = this.refs.inner;
if(this.props.onDragStart) {
this.props.onDragStart(nodeId, dom, e);
}
}
});
module.exports = Node;

View File

@ -0,0 +1,68 @@
var Tree = require('js-tree');
var proto = Tree.prototype;
proto.updateNodesPosition = function() {
var top = 1;
var left = 1;
var root = this.getIndex(1);
var self = this;
root.top = top++;
root.left = left++;
if(root.children && root.children.length) {
walk(root.children, root, left, root.node.collapsed);
}
function walk(children, parent, left, collapsed) {
var height = 1;
children.forEach(function(id) {
var node = self.getIndex(id);
if(collapsed) {
node.top = null;
node.left = null;
} else {
node.top = top++;
node.left = left;
}
if(node.children && node.children.length) {
height += walk(node.children, node, left+1, collapsed || node.node.collapsed);
} else {
node.height = 1;
height += 1;
}
});
if(parent.node.collapsed) parent.height = 1;
else parent.height = height;
return parent.height;
}
};
proto.move = function(fromId, toId, placement) {
if(fromId === toId || toId === 1) return;
var obj = this.remove(fromId);
var index = null;
if(placement === 'before') index = this.insertBefore(obj, toId);
else if(placement === 'after') index = this.insertAfter(obj, toId);
else if(placement === 'prepend') index = this.prepend(obj, toId);
else if(placement === 'append') index = this.append(obj, toId);
// todo: perf
this.updateNodesPosition();
return index;
};
proto.getNodeByTop = function(top) {
var indexes = this.indexes;
for(var id in indexes) {
if(indexes.hasOwnProperty(id)) {
if(indexes[id].top === top) return indexes[id];
}
}
};
module.exports = Tree;

View File

@ -0,0 +1,79 @@
@mixin no-select {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tree {
position: relative;
overflow: hidden;
@include no-select;
}
.draggable {
position: absolute;
opacity: 0.8;
@include no-select;
}
.node {
&.placeholder > * {
visibility: hidden;
}
&.placeholder {
border: 1px dashed #ccc;
}
.inner {
position: relative;
cursor: pointer;
padding-left: 10px;
}
.collapse {
position: absolute;
left: 0;
cursor: pointer;
width: 20px;
height: 25px;
}
.caretRight {
margin-top: 3px;
margin-left: -3px;
}
.caretDown {
transform: rotate(90deg);
margin-left: -4px;
margin-top: 2px;
}
}
.node {
&.placeholder {
border: 1px dashed #1385e5;
}
.inner {
font-size: 14px;
}
.nodeLabel {
display: inline-block;
width: 100%;
padding: 4px 5px;
&.isActive {
background-color: #31363F;
}
}
.rootLabel {
color: #ccc;
}
}

View File

@ -0,0 +1,266 @@
var React = require('react');
var Tree = require('./tree');
var Node = require('./node');
import styles from './Tree.scss';
module.exports = React.createClass({
displayName: 'UITree',
propTypes: {
tree: React.PropTypes.object.isRequired,
paddingLeft: React.PropTypes.number,
renderNode: React.PropTypes.func.isRequired
},
getDefaultProps() {
return {
paddingLeft: 20
};
},
getInitialState() {
return this.init(this.props);
},
componentWillReceiveProps(nextProps) {
if(!this._updated) this.setState(this.init(nextProps));
else this._updated = false;
},
init(props) {
var tree = new Tree(props.tree);
tree.isNodeCollapsed = props.isNodeCollapsed;
tree.renderNode = props.renderNode;
tree.changeNodeCollapsed = props.changeNodeCollapsed;
tree.updateNodesPosition();
return {
tree: tree,
dragging: {
id: null,
x: null,
y: null,
w: null,
h: null
}
};
},
getDraggingDom() {
var tree = this.state.tree;
var dragging = this.state.dragging;
if(dragging && dragging.id) {
var draggingIndex = tree.getIndex(dragging.id);
var draggingStyles = {
top: dragging.y,
left: dragging.x,
width: dragging.w
};
return (
<div className={ styles.draggable } style={draggingStyles}>
<Node
tree={tree}
index={draggingIndex}
paddingLeft={this.props.paddingLeft}
/>
</div>
);
}
return null;
},
render() {
var tree = this.state.tree;
var dragging = this.state.dragging;
var draggingDom = this.getDraggingDom();
return (
<div className={ styles.tree }>
{draggingDom}
<Node
rootNode={ true }
tree={tree}
index={tree.getIndex(1)}
key={1}
paddingLeft={this.props.paddingLeft}
onDragStart={this.dragStart}
onCollapse={this.toggleCollapse}
dragging={dragging && dragging.id}
/>
</div>
);
},
dragStart(id, dom, e) {
this.dragging = {
id: id,
w: dom.offsetWidth,
h: dom.offsetHeight,
x: dom.offsetLeft,
y: dom.offsetTop
};
this._startX = dom.offsetLeft;
this._startY = dom.offsetTop;
this._offsetX = e.clientX;
this._offsetY = e.clientY;
this._start = true;
window.addEventListener('mousemove', this.drag);
window.addEventListener('mouseup', this.dragEnd);
},
// oh
drag(e) {
if(this._start) {
this.setState({
dragging: this.dragging
});
this._start = false;
}
var tree = this.state.tree;
var dragging = this.state.dragging;
var paddingLeft = this.props.paddingLeft;
var newIndex = null;
var index = tree.getIndex(dragging.id);
var collapsed = index.node.collapsed;
var _startX = this._startX;
var _startY = this._startY;
var _offsetX = this._offsetX;
var _offsetY = this._offsetY;
var pos = {
x: _startX + e.clientX - _offsetX,
y: _startY + e.clientY - _offsetY
};
dragging.x = pos.x;
dragging.y = pos.y;
var diffX = dragging.x - paddingLeft/2 - (index.left-2) * paddingLeft;
var diffY = dragging.y - dragging.h/2 - (index.top-2) * dragging.h;
if(diffX < 0) { // left
if(index.parent && !index.next) {
newIndex = tree.move(index.id, index.parent, 'after');
}
} else if(diffX > paddingLeft) { // right
if(index.prev) {
var prevNode = tree.getIndex(index.prev).node;
if(!prevNode.collapsed && !prevNode.leaf) {
newIndex = tree.move(index.id, index.prev, 'append');
}
}
}
if(newIndex) {
index = newIndex;
newIndex.node.collapsed = collapsed;
dragging.id = newIndex.id;
}
if(diffY < 0) { // up
var above = tree.getNodeByTop(index.top-1);
newIndex = tree.move(index.id, above.id, 'before');
} else if(diffY > dragging.h) { // down
if(index.next) {
var below = tree.getIndex(index.next);
if(below.children && below.children.length && !below.node.collapsed) {
newIndex = tree.move(index.id, index.next, 'prepend');
} else {
newIndex = tree.move(index.id, index.next, 'after');
}
} else {
var below = tree.getNodeByTop(index.top+index.height);
if(below && below.parent !== index.id) {
if(below.children && below.children.length) {
newIndex = tree.move(index.id, below.id, 'prepend');
} else {
newIndex = tree.move(index.id, below.id, 'after');
}
}
}
}
if(newIndex) {
newIndex.node.collapsed = collapsed;
dragging.id = newIndex.id;
}
this.setState({
tree: tree,
dragging: dragging
});
},
dragEnd() {
this.setState({
dragging: {
id: null,
x: null,
y: null,
w: null,
h: null
}
});
this.change(this.state.tree);
window.removeEventListener('mousemove', this.drag);
window.removeEventListener('mouseup', this.dragEnd);
},
change(tree) {
this._updated = true;
if(this.props.onChange) this.props.onChange(tree.obj);
},
toggleCollapse(nodeId) {
var tree = this.state.tree;
var index = tree.getIndex(nodeId);
var node = index.node;
node.collapsed = !node.collapsed;
tree.updateNodesPosition();
this.setState({
tree: tree
});
this.change(tree);
},
// buildTreeNumbering(tree) {
// const numberBuilder = (index, node, parentNumbering) => {
// let numbering = parentNumbering ? `${parentNumbering}.${index}` : index;
// let children;
// if (node.children) {
// children = node.children.map((child, childIndex) => {
// return numberBuilder(childIndex+1, child, numbering);
// });
// }
// const data = {
// module: {
// ...node.module,
// index: numbering,
// }
// }
// if (children) {
// data.children = children;
// }
// return data;
// };
// const newTree = {...tree};
// newTree.children = [];
// tree.children.forEach((child, index) => {
// newTree.children.push(numberBuilder(index+1, child));
// })
// return newTree;
// }
});

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 30" x="0px" y="0px"><path d="M8.59,18.16L14.25,12.5L8.59,6.84L7.89,7.55L12.84,12.5L7.89,17.45L8.59,18.16Z"/></svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@ -0,0 +1,2 @@
import UiTree from './UiTree';
export default UiTree;

View File

@ -9,13 +9,48 @@ import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import Document from 'components/Document';
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import Flex from 'components/Flex';
import Tree from 'components/Tree';
import styles from './DocumentScene.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
import treeStyles from 'components/Tree/Tree.scss';
const tree = {
module: {
name: "Introduction",
id: "1",
},
children: [{
collapsed: false,
module: {
name: "dist",
id: "2"
},
children: [
{
module: {
name: "Details",
id: "21",
},
},
{
module: {
name: "Distribution",
id: "22",
},
}
]
}]
};
@observer
class DocumentScene extends React.Component {
state = {
didScroll: false,
tree: tree,
}
componentDidMount = () => {
@ -43,6 +78,26 @@ class DocumentScene extends React.Component {
};
}
renderNode = (node) => {
return (
<span className={ treeStyles.nodeLabel } onClick={this.onClickNode.bind(null, node)}>
{node.module.name}
</span>
);
}
onClickNode = (node) => {
this.setState({
active: node
});
}
handleChange = (tree) => {
this.setState({
tree: tree
});
}
render() {
const doc = store.document;
let title;
@ -74,13 +129,28 @@ class DocumentScene extends React.Component {
titleText={ titleText }
actions={ actions }
>
<CenteredContent>
{ store.isFetching ? (
{ store.isFetching ? (
<CenteredContent>
<AtlasPreviewLoading />
) : (
<Document document={ doc } />
) }
</CenteredContent>
</CenteredContent>
) : (
<Flex flex={ true }>
<div className={ styles.sidebar }>
<Tree
paddingLeft={20}
tree={this.state.tree}
onChange={this.handleChange}
isNodeCollapsed={this.isNodeCollapsed}
renderNode={this.renderNode}
/>
</div>
<Flex flex={ true } justify={ 'center' }>
<CenteredContent>
<Document document={ doc } />
</CenteredContent>
</Flex>
</Flex>
) }
</Layout>
);
}

View File

@ -1,4 +1,9 @@
.actions {
display: flex;
flex-direction: row;
}
}
.sidebar {
width: 250px;
padding: 40px 20px;
}