1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-12 13:24:52 +00:00

add wiki outline (#2263)

This commit is contained in:
shanshuirenjia
2018-08-10 17:05:29 +08:00
committed by Daniel Pan
parent 87eb52a094
commit 7bd164f0e1
8 changed files with 334 additions and 20 deletions

View File

@@ -3,10 +3,10 @@ import Search from './search';
import MarkdownViewer from './markdown-viewer'; import MarkdownViewer from './markdown-viewer';
import Account from './account'; import Account from './account';
import { repoID, serviceUrl, slug, siteRoot } from './constance'; import { repoID, serviceUrl, slug, siteRoot } from './constance';
import { processorGetAST } from '@seafile/seafile-editor/src/lib/seafile-markdown2html';
class MainPanel extends Component { class MainPanel extends Component {
onMenuClick = () => { onMenuClick = () => {
this.props.onMenuClick(); this.props.onMenuClick();
} }
@@ -30,7 +30,7 @@ class MainPanel extends Component {
}); });
return ( return (
<div className="main-panel o-hidden"> <div className="wiki-main-panel o-hidden">
<div className="main-panel-top panel-top"> <div className="main-panel-top panel-top">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span> <span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span>
<div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}> <div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}>
@@ -50,12 +50,13 @@ class MainPanel extends Component {
{pathElem} {pathElem}
</div> </div>
</div> </div>
<div className="cur-view-main-con"> <div className="cur-view-container">
<MarkdownViewer <MarkdownViewer
markdownContent={this.props.content} markdownContent={this.props.content}
latestContributor={this.props.latestContributor}
lastModified = {this.props.lastModified}
onLinkClick={this.props.onLinkClick} onLinkClick={this.props.onLinkClick}
/> />
<p id="wiki-page-last-modified">Last modified by {this.props.latestContributor}, <span>{this.props.lastModified}</span></p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ class SidePanel extends Component {
render() { render() {
return ( return (
<div className={`side-panel ${this.props.closeSideBar ? "": "left-zero"}`}> <div className={`wiki-side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
<div className="side-panel-top panel-top"> <div className="side-panel-top panel-top">
<a href={siteRoot} id="logo"> <a href={siteRoot} id="logo">
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} /> <img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />

View File

@@ -93,6 +93,17 @@ class Account extends Component {
} }
} }
renderAvatar = () => {
if (this.state.avatarURL) {
return (
<img src={this.state.avatarURL} width="36" height="36" className="avatar" />
)
}
return (
<img src="" width="36" height="36" className="avatar" />
)
}
render() { render() {
return ( return (
<div id="account"> <div id="account">
@@ -107,7 +118,7 @@ class Account extends Component {
<div className="sf-popover-con"> <div className="sf-popover-con">
<div className="item o-hidden"> <div className="item o-hidden">
<span> <span>
<img src={this.state.avatarURL} width="36" height="36" className="avatar" /> {this.renderAvatar()}
</span> </span>
<div className="txt"> <div className="txt">
{this.state.userName} <br/> {this.state.userName} <br/>

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { processor } from '@seafile/seafile-editor/src/lib/seafile-markdown2html'; import { processor, processorGetAST } from '@seafile/seafile-editor/src/lib/seafile-markdown2html';
import TreeView from './tree-view/tree-view'; import TreeView from './tree-view/tree-view';
import Prism from 'prismjs'; import Prism from 'prismjs';
import WikiOutline from './wiki-outline';
var URL = require('url-parse'); var URL = require('url-parse');
@@ -47,7 +48,10 @@ class MarkdownViewer extends React.Component {
renderingContent: true, renderingContent: true,
renderingOutline: true, renderingOutline: true,
html: '', html: '',
outlineTreeRoot: null outlineTreeRoot: null,
navItems: [],
activeId: 0,
isLoading: true
} }
scrollToNode(node) { scrollToNode(node) {
@@ -56,6 +60,32 @@ class MarkdownViewer extends React.Component {
window.location.href = url.toString(); 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 (currentId !== this.state.activeId) {
this.setState({
activeId: currentId
})
}
}
setContent(markdownContent) { setContent(markdownContent) {
let that = this; let that = this;
@@ -67,29 +97,82 @@ class MarkdownViewer extends React.Component {
}) })
} }
componentWillReceiveProps(nextProps) {
this.setContent(nextProps.markdownContent);
}
componentDidMount() { componentDidMount() {
let that = this; let that = this;
processor.process(this.props.markdownContent, function(err, file) { processor.process(this.props.markdownContent, function(err, file) {
that.setState({ that.setState({
html: String(file), html: String(file),
renderingContent: false renderingContent: false,
}); });
});
}
componentWillReceiveProps(nextProps) {
var _this = this;
this.setContent(nextProps.markdownContent);
processorGetAST.run(processorGetAST.parse(nextProps.markdownContent)).then((nodeTree) => {
if (nodeTree && nodeTree.children && nodeTree.children.length) {
var navItems = _this.formatNodeTree(nodeTree);
var currentId = navItems.length > 0 ? navItems[0].id : 0;
_this.setState({
navItems: navItems,
activeId: currentId,
isLoading: false
})
} else {
_this.setState({
isLoading: false
})
}
}); });
} }
formatNodeTree(nodeTree) {
var navItems = [];
var headingList = nodeTree.children.filter(node => {
return (node.type === 'heading' && (node.depth === 2 || node.depth === 3));
});
for (let i = 0; i < headingList.length; i++) {
navItems[i] = {};
navItems[i].id = '#user-content-' + headingList[i].data.id
navItems[i].key = i;
navItems[i].clazz = '';
navItems[i].depth = headingList[i].depth;
for (let child of headingList[i].children) {
if (child.type === "text") {
navItems[i].text = child.value;
break;
}
}
}
return navItems;
}
render() { render() {
if (this.state.isLoading) {
return (
<span className="loading-icon loading-tip"></span>
)
}
return ( return (
<MarkdownViewerContent <div className="markdown-container" onScroll={this.scrollHandler}>
renderingContent={this.state.renderingContent} html={this.state.html} <div className="markdown-content" ref="markdownContainer">
onLinkClick={this.props.onLinkClick} <MarkdownViewerContent
/> renderingContent={this.state.renderingContent} html={this.state.html}
onLinkClick={this.props.onLinkClick}
/>
<p id="wiki-page-last-modified">Last modified by {this.props.latestContributor}, <span>{this.props.lastModified}</span></p>
</div>
<div className="markdown-outline">
<WikiOutline
navItems={this.state.navItems}
handleNavItemClick={this.handleNavItemClick}
activeId={this.state.activeId}
/>
</div>
</div>
) )
} }
} }

View File

@@ -0,0 +1,91 @@
import React from 'react';
class WikiOutlineItem extends React.Component {
handleNavItemClick = () => {
var index = this.props.item.key;
this.props.handleNavItemClick(index)
}
render() {
let item = this.props.item;
let activeIndex = parseInt(this.props.activeIndex);
let levelClass = item.depth === 3 ? " textindent-2" : '';
let activeClass = item.key === activeIndex ? ' wiki-outline-item-active' : '';
let clazz = "wiki-outline-item"+ levelClass + activeClass;
return (
<li className={clazz} data-index={item.key} onClick={this.handleNavItemClick}>
<a href={item.id} title={item.text}>{item.text}</a>
</li>
)
}
}
class WikiOutline extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex : 0,
scrollTop: 0,
}
}
handleNavItemClick = (index) => {
if (index !== this.state.activeIndex) {
this.setState({
activeIndex : index
})
}
}
componentWillReceiveProps(nextProps) {
let _this = this;
let activeId = nextProps.activeId;
let navItems = nextProps.navItems;
let length = navItems.length;
for (let i = 0; i < length; i++) {
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";
}
_this.setState({
activeIndex : item.key,
scrollTop: scrollTop
})
flag = true;
}
if (flag) {
break;
}
}
}
render() {
let style = {top: this.state.scrollTop};
return (
<ul className="wiki-viewer-outline" ref="outlineContainer" style={style}>
{this.props.navItems.map(item => {
return (
<WikiOutlineItem
key={item.key}
item={item}
activeIndex={this.state.activeIndex}
handleNavItemClick={this.handleNavItemClick}
/>
)
})}
</ul>
)
}
}
export default WikiOutline;

View File

@@ -47,9 +47,100 @@
.wiki-main .cur-view-path::after { .wiki-main .cur-view-path::after {
display:none; display:none;
} }
.wiki-main .cur-view-main-con {
img[src=""] {
opacity: 0;
}
.wiki-main-panel {
flex: 1 0 80%;
display:flex;
flex-direction:column;
}
.wiki-side-panel {
flex: 0 0 20%;
display:flex;
flex-direction:column;
overflow:hidden;
}
.cur-view-container {
display: flex;
}
.cur-view-container .markdown-container {
padding-left: 40px; padding-left: 40px;
padding-right: 40px; padding-right: 40px;
display: flex;
overflow: auto;
}
.cur-view-container .markdown-content {
flex: 1;
width: calc(100% - 200px);
padding-right: 40px;
}
.cur-view-container .markdown-outline {
position: sticky;
width: 200px;
padding: 0 18px;
top: 0;
}
.wiki-hide {
display: none !important;
}
@media (max-width: 991.98px) {
.cur-view-container .markdown-container {
padding-right: 40px;
}
.cur-view-container .markdown-content {
padding-right: 0;
}
.cur-view-container .markdown-outline {
display: none;
}
}
.wiki-main .wiki-viewer-outline {
position: relative;
top: 0;
padding: 0;
list-style: none;
border-left: solid 1px #eee;
}
.textindent-2 {
text-indent: 18px;
}
.wiki-main .wiki-outline-item {
padding: 3px 15px;
font-size: 14px;
}
.wiki-outline-item a {
display: block;
color: #444;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wiki-outline-item a:hover {
color: #eb8205;
text-decoration: underline;
}
.wiki-outline-item-active {
border-left: 1px solid #eb8205;
}
.wiki-outline-item-active a {
color: #eb8205 !important;
} }
.wiki-page-ops { .wiki-page-ops {

View File

@@ -108,7 +108,9 @@ class Wiki extends Component {
} }
onpopstate = (event) => { onpopstate = (event) => {
this.loadFile(event.state.filePath); if (event.state && event.state.filePath) {
this.loadFile(event.state.filePath);
}
} }
onMenuClick = () => { onMenuClick = () => {

View File

@@ -121,6 +121,40 @@ a:hover { color:#eb8205; }
} }
/** loading **/ /** loading **/
@-moz-keyframes loading {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.loading-icon { .loading-icon {
display:inline-block; display:inline-block;
width:26px; width:26px;
@@ -182,6 +216,7 @@ a:hover { color:#eb8205; }
display:flex; display:flex;
flex-direction:column; flex-direction:column;
} }
.side-panel { .side-panel {
flex: 0 0 25%; flex: 0 0 25%;
display:flex; display:flex;