diff --git a/frontend/src/components/SidePanel.js b/frontend/src/components/SidePanel.js index 7813daca40..352479087d 100644 --- a/frontend/src/components/SidePanel.js +++ b/frontend/src/components/SidePanel.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import TreeView from './tree-view/tree-view'; import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance'; import Tree from './tree-view/tree'; -import Node from './tree-view/node' +import { Node } from './tree-view/node' import NodeMenu from './menu-component/node-menu'; import MenuControl from './menu-component/node-menu-control'; const gettext = window.gettext; @@ -82,13 +82,40 @@ class SidePanel extends Component { onAddFolderNode = (dirPath) => { this.props.editorUtilities.createDir(dirPath).then(res => { - this.initializeTreeData() + let tree = this.state.tree_data.copy(); + let index = dirPath.lastIndexOf("/"); + let name = dirPath.substring(index+1); + let parentPath = dirPath.substring(0, index); + if (!parentPath) { + parentPath = "/"; + } + let node = new Node({name : name, type: "dir", isExpanded: false, children: []}); + let parentNode = tree.getNodeByPath(parentPath); + tree.addChildToNode(parentNode, node); + tree.setOneNodeToActived({node}); + this.setState({ + tree_data: tree + }) + }) } onAddFileNode = (filePath) => { this.props.editorUtilities.createFile(filePath).then(res => { - this.initializeTreeData() + let tree = this.state.tree_data.copy(); + let index = filePath.lastIndexOf("/"); + let name = filePath.substring(index+1); + let parentPath = filePath.substring(0, index); + if (!parentPath) { + parentPath = "/"; + } + let node = new Node({name : name, type: "file", isExpanded: false, children: []}); + let parentNode = tree.getNodeByPath(parentPath); + tree.addChildToNode(parentNode, node); + tree.setOneNodeToActived({node}); + this.setState({ + tree_data: tree + }) }) } @@ -98,17 +125,16 @@ class SidePanel extends Component { let filePath = node.path; if (type === 'file') { this.props.editorUtilities.renameFile(filePath, newName).then(res => { - this.initializeTreeData() if (this.isModifyCurrentFile()) { node.name = newName; this.props.onFileClick(null, node); + this.changeActivedNode({node}); } }) } if (type === 'dir') { this.props.editorUtilities.renameDir(filePath, newName).then(res => { - this.initializeTreeData(); if (this.isModifyContainsCurrentFile()) { let currentNode = this.state.currentNode; let nodePath = encodeURI(currentNode.path); @@ -118,6 +144,7 @@ class SidePanel extends Component { if(node){ currentNode.name = newName; this.props.onFileClick(null, node); + this.changeActivedNode({node}) } } }) @@ -129,15 +156,11 @@ class SidePanel extends Component { let filePath = currentNode.path; let type = currentNode.type; if (type === 'file') { - this.props.editorUtilities.deleteFile(filePath).then(res => { - this.initializeTreeData(); - }) + this.props.editorUtilities.deleteFile(filePath); } if (type === 'dir') { - this.props.editorUtilities.deleteDir(filePath).then(res => { - this.initializeTreeData(); - }) + this.props.editorUtilities.deleteDir(filePath); } let isCurrentFile = false; @@ -147,23 +170,39 @@ class SidePanel extends Component { isCurrentFile = this.isModifyCurrentFile(); } + let tree = this.state.tree_data.copy(); + tree.removeNodeFromTree(currentNode); + if (isCurrentFile) { let homeNode = this.getHomeNode(); this.props.onFileClick(null, homeNode); + tree.setNoneNodeActived(); + this.setState({tree_data: tree}) + } else { + this.setState({tree_data: tree}) } } + changeActivedNode(node) { + let tree = this.state.tree_data.copy(); + tree.setOneNodeToActived(node); + this.setState({ + tree_data: tree + }) + } + isModifyCurrentFile() { let name = this.state.currentNode.name; let pathname = window.location.pathname; let currentName = pathname.slice(pathname.lastIndexOf("/") + 1); - return name === currentName; + return encodeURI(name) === currentName; } isModifyContainsCurrentFile() { let pathname = window.location.pathname; let nodePath = this.state.currentNode.path; - if (pathname.indexOf(nodePath)) { + + if (pathname.indexOf(encodeURI(nodePath)) > -1) { return true; } return false; diff --git a/frontend/src/components/markdown-viewer.js b/frontend/src/components/markdown-viewer.js index 0e64dbb4e4..93e2f767e9 100644 --- a/frontend/src/components/markdown-viewer.js +++ b/frontend/src/components/markdown-viewer.js @@ -48,30 +48,43 @@ class MarkdownViewer extends React.Component { super(props); this.state = { renderingContent: true, + renderingOutline: true, html: '', + outlineTreeRoot: null, navItems: [], - activeId: 0, + activeId: 0 }; this.activeIdFromOutLine = null; } + scrollToNode(node) { + let url = new URL(window.location.href); + url.set('hash', 'user-content-' + node.data.id); + window.location.href = url.toString(); + } + scrollHandler = (event) => { - var target = event.target || event.srcElement; - var markdownContainer = this.refs.markdownContainer; - var headingList = markdownContainer.querySelectorAll('[id^="user-content"]'); - var top = target.scrollTop; - var defaultOffset = markdownContainer.offsetTop; var currentId = ''; - for (let i = 0; i < headingList.length; i++) { - let heading = headingList[i]; - if (heading.tagName === 'H1') { - continue; - } - if (top > heading.offsetTop - defaultOffset) { - currentId = '#' + heading.getAttribute('id'); - } else { - break; + if (!this.activeIdFromOutLine) { + var target = event.target || event.srcElement; + var markdownContainer = this.refs.markdownContainer; + var headingList = markdownContainer.querySelectorAll('[id^="user-content"]'); + var top = target.scrollTop; + var defaultOffset = markdownContainer.offsetTop; + for (let i = 0; i < headingList.length; i++) { + let heading = headingList[i]; + if (heading.tagName === 'H1') { + continue; + } + if (top > heading.offsetTop - defaultOffset) { + currentId = '#' + heading.getAttribute('id'); + } else { + break; + } } + } else { + currentId = this.activeIdFromOutLine; + this.activeIdFromOutLine = null; } if (currentId !== this.state.activeId) { @@ -81,6 +94,10 @@ class MarkdownViewer extends React.Component { } } + handleNavItemClick = (activeId) => { + this.activeIdFromOutLine = activeId; + } + setContent(markdownContent) { let that = this; @@ -122,6 +139,10 @@ class MarkdownViewer extends React.Component { navItems: navItems, activeId: currentId }) + }else { + _this.setState({ + navItems: [] + }) } }); } diff --git a/frontend/src/components/tree-view/tree.js b/frontend/src/components/tree-view/tree.js index 182ceb850c..1e8948fe89 100644 --- a/frontend/src/components/tree-view/tree.js +++ b/frontend/src/components/tree-view/tree.js @@ -35,6 +35,141 @@ class Tree { node.children.splice(insertIndex, 0, child); } + removeChildNode(node, child) { + let children = node.children; + let removeNode = null; + let index = null; + for (let i = 0; i < children.length; i++) { + if (child.path === children[i].path) { + removeNode = children[i]; + index = i; + break; + } + } + child.parent = null; + node.children.splice(index, 1); + return removeNode ? removeNode : null; + } + + addNodeToTree(node) { + let parentNode = this.getNodeParentFromTree(node); + this.addChildToNode(parentNode, node); + } + + removeNodeFromTree(node) { + let parentNode = this.getNodeParentFromTree(node); + this.removeChildNode(parentNode, node); + } + + getNodeParentFromTree(node) { + let parentNode = node.parent; + let findNode = null; + function cb(node) { + if(parentNode.path === node.path){ + findNode = node; + return true; + } + return false; + } + this.traverseBF(cb); + return findNode; + } + + getNodeByPath(path) { + let findNode = null; + function cb(node){ + if (node.path === path) { + findNode = node; + return true; + } + return false; + } + this.traverseBF(cb); + return findNode; + } + + traverseDF(callback) { + let stack = []; + let found = false; + stack.unshift(this.root); + let currentNode = stack.shift(); + while (!found && currentNode) { + found = callback(currentNode) == true ? true : false; + if (!found) { + stack.unshift(...currentNode.children); + currentNode = stack.shift(); + } + } + } + + traverseBF(callback) { + let queue = []; + let found = false; + queue.push(this.root); + let currentNode = queue.shift(); + while (!found && currentNode) { + found = callback(currentNode) === true ? true : false; + if (!found) { + queue.push(...currentNode.children); + currentNode = queue.shift(); + } + } + } + + setOneNodeToActived({node}) { + this.setNoneNodeActived(); + let root = this.root; + root.isExpanded = true; + let layer2 = root.hasChildren() ? root.children : null; // direct to replace root child; + let isLayer2 = false; + for (let i = 0; i < layer2.length; i++) { + if (node.path === layer2[i].path) { + isLayer2 = true; + break; + } + } + if (isLayer2) { + return; + } + let replaceNode = null; + let needReplacedNode = null; + while (node.parent) { + let flag = false; + node.parent.isExpanded = true; + for (let i = 0; i < layer2.length; i++) { + if (node.parent.path === layer2[i].path) { + replaceNode = node.parent; + needReplacedNode = layer2[i]; + flag = true; + break; + } + } + if (flag) { + break; + } + node = node.parent; + } + + this.removeChildNode(root, needReplacedNode); + this.addChildToNode(root, replaceNode); + } + + setNoneNodeActived() { + function setNodeToDeactived(node) { + if (node.isExpanded) { + node.isExpanded = false; + if (node.hasChildren()) { + let children = node.children; + children.forEach(function(child) { + setNodeToDeactived(child); + }) + } + } + } + setNodeToDeactived(this.root); + this.root.isExpanded = true; // default to show; + return true; + } /* * parse tree from javascript object diff --git a/frontend/src/components/wiki-outline.js b/frontend/src/components/wiki-outline.js index f7886fb01c..f8d3d8b1a4 100644 --- a/frontend/src/components/wiki-outline.js +++ b/frontend/src/components/wiki-outline.js @@ -3,10 +3,10 @@ import React from 'react'; class WikiOutlineItem extends React.Component { handleNavItemClick = () => { - var index = this.props.item.key; - this.props.handleNavItemClick(index) + var activeId = this.props.item.id; + this.props.handleNavItemClick(activeId) } - + render() { let item = this.props.item; let activeIndex = parseInt(this.props.activeIndex); @@ -32,14 +32,6 @@ class WikiOutline extends React.Component { } } - handleNavItemClick = (index) => { - if (index !== this.state.activeIndex) { - this.setState({ - activeIndex : index - }) - } - } - componentWillReceiveProps(nextProps) { let _this = this; let activeId = nextProps.activeId; @@ -49,14 +41,13 @@ class WikiOutline extends React.Component { let flag = false; let item = navItems[i]; if (item.id === activeId && item.key !== _this.state.activeIndex) { - let direction = item.key > _this.state.activeIndex ? "down" : "up"; - let currentTop = parseInt(_this.state.scrollTop); let scrollTop = 0; - if (item.key > 20 && direction === "down") { - scrollTop = currentTop - 27 + "px"; - } else if (currentTop < 0 && direction === "up") { - scrollTop = currentTop + 27 + "px"; - } + if (item.key > 20) { + scrollTop = - (item.key - 20)*27 + "px"; + if (parseInt(scrollTop) > 0) { // handle scroll quickly; + scrollTop = 0; + } + } _this.setState({ activeIndex : item.key, scrollTop: scrollTop @@ -79,7 +70,7 @@ class WikiOutline extends React.Component { key={item.key} item={item} activeIndex={this.state.activeIndex} - handleNavItemClick={this.handleNavItemClick} + handleNavItemClick={this.props.handleNavItemClick} /> ) })} diff --git a/frontend/src/css/wiki.css b/frontend/src/css/wiki.css index f28e60c172..479c3328e7 100644 --- a/frontend/src/css/wiki.css +++ b/frontend/src/css/wiki.css @@ -64,6 +64,7 @@ img[src=""] { flex: 1 0 80%; display:flex; flex-direction:column; + min-height: 0; } .wiki-side-panel { @@ -72,9 +73,13 @@ img[src=""] { flex-direction:column; overflow:hidden; } +.cur-view-main { + min-height: 0; +} .cur-view-container { display: flex; + min-height: 0; } .cur-view-container .markdown-container { @@ -83,19 +88,21 @@ img[src=""] { display: flex; flex: 1; overflow: auto; + min-height: 0; } .cur-view-container .markdown-content { - flex: 1; - width: calc(100% - 200px); + width: calc(100% - 160px); padding-right: 40px; } .cur-view-container .markdown-outline { - position: sticky; + position: fixed; + padding-right: 18px; + top: 97px; + right: 0; width: 200px; - padding: 0 18px; - top: 0; + overflow: hidden; } .wiki-hide { @@ -107,6 +114,7 @@ img[src=""] { padding-right: 40px; } .cur-view-container .markdown-content { + width: 100%; padding-right: 0; } .cur-view-container .markdown-outline {