diff --git a/package.json b/package.json
index 80d5086d4..ae29becb1 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js
new file mode 100644
index 000000000..ab58dcc44
--- /dev/null
+++ b/src/components/Tree/Node.js
@@ -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 (
+
+
+
+ );
+ }
+
+ 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 (
+
+ {index.children.map((child) => {
+ var childIndex = tree.getIndex(child);
+ return (
+
+ );
+ })}
+
+ );
+ }
+
+ 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 (
+
+
+ {!this.props.rootNode && this.renderCollapse()}
+ {}}
+ >
+ {node.module.name}
+
+
+ {this.renderChildren()}
+
+ );
+ },
+
+ 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;
\ No newline at end of file
diff --git a/src/components/Tree/Tree.js b/src/components/Tree/Tree.js
new file mode 100644
index 000000000..f5d4b8e3f
--- /dev/null
+++ b/src/components/Tree/Tree.js
@@ -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;
\ No newline at end of file
diff --git a/src/components/Tree/Tree.scss b/src/components/Tree/Tree.scss
new file mode 100644
index 000000000..219c4e8e7
--- /dev/null
+++ b/src/components/Tree/Tree.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Tree/UiTree.js b/src/components/Tree/UiTree.js
new file mode 100644
index 000000000..640be2c79
--- /dev/null
+++ b/src/components/Tree/UiTree.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return null;
+ },
+
+ render() {
+ var tree = this.state.tree;
+ var dragging = this.state.dragging;
+ var draggingDom = this.getDraggingDom();
+
+ return (
+
+ {draggingDom}
+
+
+ );
+ },
+
+ 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;
+ // }
+});
diff --git a/src/components/Tree/assets/chevron.svg b/src/components/Tree/assets/chevron.svg
new file mode 100644
index 000000000..4daab5920
--- /dev/null
+++ b/src/components/Tree/assets/chevron.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Tree/index.js b/src/components/Tree/index.js
new file mode 100644
index 000000000..282ed6e1b
--- /dev/null
+++ b/src/components/Tree/index.js
@@ -0,0 +1,2 @@
+import UiTree from './UiTree';
+export default UiTree;
\ No newline at end of file
diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js
index b9bf84956..e0f0f5b47 100644
--- a/src/scenes/DocumentScene/DocumentScene.js
+++ b/src/scenes/DocumentScene/DocumentScene.js
@@ -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 (
+
+ {node.module.name}
+
+ );
+ }
+
+ 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 }
>
-
- { store.isFetching ? (
+ { store.isFetching ? (
+
- ) : (
-
- ) }
-
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ ) }
);
}
diff --git a/src/scenes/DocumentScene/DocumentScene.scss b/src/scenes/DocumentScene/DocumentScene.scss
index c7d614611..2a9b34235 100644
--- a/src/scenes/DocumentScene/DocumentScene.scss
+++ b/src/scenes/DocumentScene/DocumentScene.scss
@@ -1,4 +1,9 @@
.actions {
display: flex;
flex-direction: row;
-}
\ No newline at end of file
+}
+
+.sidebar {
+ width: 250px;
+ padding: 40px 20px;
+}