mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-13 05:39:59 +00:00
add wiki outline (#2263)
This commit is contained in:
committed by
Daniel Pan
parent
87eb52a094
commit
7bd164f0e1
@@ -3,10 +3,10 @@ import Search from './search';
|
||||
import MarkdownViewer from './markdown-viewer';
|
||||
import Account from './account';
|
||||
import { repoID, serviceUrl, slug, siteRoot } from './constance';
|
||||
|
||||
|
||||
import { processorGetAST } from '@seafile/seafile-editor/src/lib/seafile-markdown2html';
|
||||
|
||||
class MainPanel extends Component {
|
||||
|
||||
onMenuClick = () => {
|
||||
this.props.onMenuClick();
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class MainPanel extends Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="main-panel o-hidden">
|
||||
<div className="wiki-main-panel o-hidden">
|
||||
<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>
|
||||
<div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}>
|
||||
@@ -50,12 +50,13 @@ class MainPanel extends Component {
|
||||
{pathElem}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cur-view-main-con">
|
||||
<div className="cur-view-container">
|
||||
<MarkdownViewer
|
||||
markdownContent={this.props.content}
|
||||
latestContributor={this.props.latestContributor}
|
||||
lastModified = {this.props.lastModified}
|
||||
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>
|
||||
|
@@ -13,7 +13,7 @@ class SidePanel extends Component {
|
||||
|
||||
render() {
|
||||
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">
|
||||
<a href={siteRoot} id="logo">
|
||||
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
|
||||
|
@@ -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() {
|
||||
return (
|
||||
<div id="account">
|
||||
@@ -107,7 +118,7 @@ class Account extends Component {
|
||||
<div className="sf-popover-con">
|
||||
<div className="item o-hidden">
|
||||
<span>
|
||||
<img src={this.state.avatarURL} width="36" height="36" className="avatar" />
|
||||
{this.renderAvatar()}
|
||||
</span>
|
||||
<div className="txt">
|
||||
{this.state.userName} <br/>
|
||||
|
@@ -1,7 +1,8 @@
|
||||
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 Prism from 'prismjs';
|
||||
import WikiOutline from './wiki-outline';
|
||||
|
||||
var URL = require('url-parse');
|
||||
|
||||
@@ -47,7 +48,10 @@ class MarkdownViewer extends React.Component {
|
||||
renderingContent: true,
|
||||
renderingOutline: true,
|
||||
html: '',
|
||||
outlineTreeRoot: null
|
||||
outlineTreeRoot: null,
|
||||
navItems: [],
|
||||
activeId: 0,
|
||||
isLoading: true
|
||||
}
|
||||
|
||||
scrollToNode(node) {
|
||||
@@ -56,6 +60,32 @@ class MarkdownViewer extends React.Component {
|
||||
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) {
|
||||
let that = this;
|
||||
|
||||
@@ -67,29 +97,82 @@ class MarkdownViewer extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setContent(nextProps.markdownContent);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let that = this;
|
||||
|
||||
processor.process(this.props.markdownContent, function(err, file) {
|
||||
that.setState({
|
||||
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() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<span className="loading-icon loading-tip"></span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="markdown-container" onScroll={this.scrollHandler}>
|
||||
<div className="markdown-content" ref="markdownContainer">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
91
frontend/src/components/wiki-outline.js
Normal file
91
frontend/src/components/wiki-outline.js
Normal 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;
|
@@ -47,9 +47,100 @@
|
||||
.wiki-main .cur-view-path::after {
|
||||
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-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 {
|
||||
|
@@ -108,8 +108,10 @@ class Wiki extends Component {
|
||||
}
|
||||
|
||||
onpopstate = (event) => {
|
||||
if (event.state && event.state.filePath) {
|
||||
this.loadFile(event.state.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
onMenuClick = () => {
|
||||
this.setState({
|
||||
|
@@ -121,6 +121,40 @@ a:hover { color:#eb8205; }
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
display:inline-block;
|
||||
width:26px;
|
||||
@@ -182,6 +216,7 @@ a:hover { color:#eb8205; }
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
flex: 0 0 25%;
|
||||
display:flex;
|
||||
|
Reference in New Issue
Block a user