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:
committed by
Daniel Pan
parent
87eb52a094
commit
7bd164f0e1
@@ -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>
|
||||||
|
@@ -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} />
|
||||||
|
@@ -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/>
|
||||||
|
@@ -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 (
|
return (
|
||||||
|
<span className="loading-icon loading-tip"></span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="markdown-container" onScroll={this.scrollHandler}>
|
||||||
|
<div className="markdown-content" ref="markdownContainer">
|
||||||
<MarkdownViewerContent
|
<MarkdownViewerContent
|
||||||
renderingContent={this.state.renderingContent} html={this.state.html}
|
renderingContent={this.state.renderingContent} html={this.state.html}
|
||||||
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 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 {
|
.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 {
|
||||||
|
@@ -108,8 +108,10 @@ class Wiki extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onpopstate = (event) => {
|
onpopstate = (event) => {
|
||||||
|
if (event.state && event.state.filePath) {
|
||||||
this.loadFile(event.state.filePath);
|
this.loadFile(event.state.filePath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMenuClick = () => {
|
onMenuClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user