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; +}