mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 10:22:46 +00:00
import markdown viewer from seafile-editor (#2572)
* import markdown viewer from seafile-editor * refactor code * page-scroll
This commit is contained in:
@@ -1,191 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { processor, processorGetAST } from '@seafile/seafile-editor/dist/utils/seafile-markdown2html';
|
import MarkdownViewer from '@seafile/seafile-editor/dist/viewer/markdown-viewer';
|
||||||
import Prism from 'prismjs';
|
|
||||||
import WikiOutline from './wiki-outline';
|
|
||||||
|
|
||||||
var URL = require('url-parse');
|
|
||||||
|
|
||||||
const gettext = window.gettext;
|
const gettext = window.gettext;
|
||||||
|
|
||||||
require('@seafile/seafile-editor/dist/editor/code-hight-package');
|
|
||||||
|
|
||||||
const contentClass = 'wiki-md-viewer-rendered-content';
|
|
||||||
|
|
||||||
const contentPropTypes = {
|
|
||||||
html: PropTypes.string,
|
|
||||||
renderingContent: PropTypes.bool.isRequired,
|
|
||||||
onLinkClick: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class MarkdownViewerContent extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
Prism.highlightAll();
|
|
||||||
var links = document.querySelectorAll(`.${contentClass} a`);
|
|
||||||
links.forEach((li) => {li.addEventListener('click', this.onLinkClick); });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLinkClick = (event) => {
|
|
||||||
this.props.onLinkClick(event);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{ this.props.renderingContent ? (
|
|
||||||
<div className={contentClass + ' article'}>Loading...</div>
|
|
||||||
) : (
|
|
||||||
<div ref={(mdContent) => {this.mdContentRef = mdContent;} }
|
|
||||||
className={contentClass + ' article'}
|
|
||||||
dangerouslySetInnerHTML={{ __html: this.props.html }}/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownViewerContent.propTypes = contentPropTypes;
|
|
||||||
|
|
||||||
|
|
||||||
const viewerPropTypes = {
|
const viewerPropTypes = {
|
||||||
isFileLoading: PropTypes.bool.isRequired,
|
isFileLoading: PropTypes.bool.isRequired,
|
||||||
lastModified: PropTypes.string,
|
lastModified: PropTypes.string,
|
||||||
latestContributor: PropTypes.string,
|
latestContributor: PropTypes.string,
|
||||||
markdownContent: PropTypes.string,
|
markdownContent: PropTypes.string,
|
||||||
onLinkClick: PropTypes.func.isRequired
|
activeTitleIndex: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
class MarkdownViewer extends React.Component {
|
class MarkdownContentViewer extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
renderingContent: true,
|
|
||||||
renderingOutline: true,
|
|
||||||
html: '',
|
|
||||||
outlineTreeRoot: null,
|
|
||||||
navItems: [],
|
|
||||||
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 currentId = '';
|
|
||||||
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) {
|
|
||||||
this.setState({
|
|
||||||
activeId: currentId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNavItemClick = (activeId) => {
|
|
||||||
this.activeIdFromOutLine = activeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent(markdownContent) {
|
|
||||||
let that = this;
|
|
||||||
|
|
||||||
processor.process(markdownContent, function(err, file) {
|
|
||||||
that.setState({
|
|
||||||
html: String(file),
|
|
||||||
renderingContent: false
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// reset the href to jump to the section
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
if (url.hash) {
|
|
||||||
window.location.href = window.location.href;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let that = this;
|
|
||||||
|
|
||||||
processor.process(this.props.markdownContent, function(err, file) {
|
|
||||||
that.setState({
|
|
||||||
html: String(file),
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}else {
|
|
||||||
_this.setState({
|
|
||||||
navItems: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.props.isFileLoading) {
|
if (this.props.isFileLoading) {
|
||||||
@@ -194,26 +21,14 @@ class MarkdownViewer extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="markdown-container" onScroll={this.scrollHandler}>
|
<div className="markdown-content">
|
||||||
<div className="markdown-content" ref="markdownContainer">
|
<MarkdownViewer markdownContent={this.props.markdownContent} showTOC={true} activeTitleIndex={this.props.activeTitleIndex}/>
|
||||||
<MarkdownViewerContent
|
|
||||||
renderingContent={this.state.renderingContent} html={this.state.html}
|
|
||||||
onLinkClick={this.props.onLinkClick}
|
|
||||||
/>
|
|
||||||
<p id="wiki-page-last-modified">{gettext('Last modified by')} {this.props.latestContributor}, <span>{this.props.lastModified}</span></p>
|
<p id="wiki-page-last-modified">{gettext('Last modified by')} {this.props.latestContributor}, <span>{this.props.lastModified}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div className="markdown-outline">
|
|
||||||
<WikiOutline
|
|
||||||
navItems={this.state.navItems}
|
|
||||||
handleNavItemClick={this.handleNavItemClick}
|
|
||||||
activeId={this.state.activeId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkdownViewer.propTypes = viewerPropTypes;
|
MarkdownContentViewer.propTypes = viewerPropTypes;
|
||||||
|
|
||||||
export default MarkdownViewer;
|
export default MarkdownContentViewer;
|
||||||
|
@@ -59,7 +59,7 @@ img[src=""] {
|
|||||||
|
|
||||||
.wiki-main-panel .cur-view-content {
|
.wiki-main-panel .cur-view-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cur-view-content .markdown-container{
|
.cur-view-content .markdown-container{
|
||||||
|
@@ -10,7 +10,7 @@ import ViewModeToolbar from '../../components/toolbar/view-mode-toolbar';
|
|||||||
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
|
import DirOperationToolBar from '../../components/toolbar/dir-operation-toolbar';
|
||||||
import MutipleDirOperationToolbar from '../../components/toolbar/mutilple-dir-operation-toolbar';
|
import MutipleDirOperationToolbar from '../../components/toolbar/mutilple-dir-operation-toolbar';
|
||||||
import CurDirPath from '../../components/cur-dir-path';
|
import CurDirPath from '../../components/cur-dir-path';
|
||||||
import MarkdownViewer from '../../components/markdown-viewer';
|
import MarkdownContentViewer from '../../components/markdown-viewer';
|
||||||
import DirentListView from '../../components/dirent-list-view/dirent-list-view';
|
import DirentListView from '../../components/dirent-list-view/dirent-list-view';
|
||||||
import DirentDetail from '../../components/dirent-detail/dirent-details';
|
import DirentDetail from '../../components/dirent-detail/dirent-details';
|
||||||
import FileUploader from '../../components/file-uploader/file-uploader';
|
import FileUploader from '../../components/file-uploader/file-uploader';
|
||||||
@@ -34,7 +34,6 @@ const propTypes = {
|
|||||||
onSideNavMenuClick: PropTypes.func.isRequired,
|
onSideNavMenuClick: PropTypes.func.isRequired,
|
||||||
onSearchedClick: PropTypes.func.isRequired,
|
onSearchedClick: PropTypes.func.isRequired,
|
||||||
onMainNavBarClick: PropTypes.func.isRequired,
|
onMainNavBarClick: PropTypes.func.isRequired,
|
||||||
onLinkClick: PropTypes.func.isRequired,
|
|
||||||
onItemClick: PropTypes.func.isRequired,
|
onItemClick: PropTypes.func.isRequired,
|
||||||
onAllDirentSelected: PropTypes.func.isRequired,
|
onAllDirentSelected: PropTypes.func.isRequired,
|
||||||
onItemSelected: PropTypes.func.isRequired,
|
onItemSelected: PropTypes.func.isRequired,
|
||||||
@@ -61,7 +60,10 @@ class MainPanel extends Component {
|
|||||||
direntPath: '',
|
direntPath: '',
|
||||||
currentRepo: null,
|
currentRepo: null,
|
||||||
isRepoOwner: false,
|
isRepoOwner: false,
|
||||||
|
activeTitleIndex: -1,
|
||||||
};
|
};
|
||||||
|
this.titlesInfo = null;
|
||||||
|
this.pageScroll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -76,6 +78,10 @@ class MainPanel extends Component {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
let that = this;
|
||||||
|
setTimeout(function() {
|
||||||
|
that.getTitlesInfo();
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchViewMode = (mode) => {
|
switchViewMode = (mode) => {
|
||||||
@@ -121,6 +127,45 @@ class MainPanel extends Component {
|
|||||||
// todo
|
// todo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePageScroll = (e) => {
|
||||||
|
if (this.props.pathExist && this.props.isViewFile && !this.pageScroll && this.titlesInfo.length > 0) {
|
||||||
|
this.pageScroll = true;
|
||||||
|
let that = this;
|
||||||
|
setTimeout(function() {
|
||||||
|
that.pageScroll = false;
|
||||||
|
}, 100);
|
||||||
|
const contentScrollTop = this.refs.curViewContent.scrollTop + 180;
|
||||||
|
let activeTitleIndex;
|
||||||
|
if (contentScrollTop <= this.titlesInfo[0]) {
|
||||||
|
activeTitleIndex = 0;
|
||||||
|
}
|
||||||
|
else if (contentScrollTop > this.titlesInfo[this.titlesInfo.length - 1]) {
|
||||||
|
activeTitleIndex = this.titlesInfo.length - 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (let i = 0; i < this.titlesInfo.length - 1; i++) {
|
||||||
|
if (contentScrollTop > this.titlesInfo[i] && this.titlesInfo[i + 1] &&
|
||||||
|
contentScrollTop < this.titlesInfo[i + 1]) {
|
||||||
|
activeTitleIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
activeTitleIndex: activeTitleIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitlesInfo = () => {
|
||||||
|
let titlesInfo = [];
|
||||||
|
let headingList = document.querySelectorAll('h2[id^="user-content"], h3[id^="user-content"]');
|
||||||
|
for (let i = 0; i < headingList.length; i++) {
|
||||||
|
titlesInfo.push(headingList[i].offsetTop);
|
||||||
|
}
|
||||||
|
this.titlesInfo = titlesInfo;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const ErrMessage = (<div className="message empty-tip err-message"><h2>{gettext('Folder does not exist.')}</h2></div>);
|
const ErrMessage = (<div className="message empty-tip err-message"><h2>{gettext('Folder does not exist.')}</h2></div>);
|
||||||
|
|
||||||
@@ -166,17 +211,17 @@ class MainPanel extends Component {
|
|||||||
onPathClick={this.onMainNavBarClick}
|
onPathClick={this.onMainNavBarClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content" onScroll={this.handlePageScroll} ref="curViewContent">
|
||||||
{!this.props.pathExist ?
|
{!this.props.pathExist ?
|
||||||
ErrMessage :
|
ErrMessage :
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{ this.props.isViewFile ?
|
{ this.props.isViewFile ?
|
||||||
<MarkdownViewer
|
<MarkdownContentViewer
|
||||||
markdownContent={this.props.content}
|
markdownContent={this.props.content}
|
||||||
latestContributor={this.props.latestContributor}
|
latestContributor={this.props.latestContributor}
|
||||||
lastModified = {this.props.lastModified}
|
lastModified = {this.props.lastModified}
|
||||||
onLinkClick={this.props.onLinkClick}
|
|
||||||
isFileLoading={this.props.isFileLoading}
|
isFileLoading={this.props.isFileLoading}
|
||||||
|
activeTitleIndex={this.state.activeTitleIndex}
|
||||||
/> :
|
/> :
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DirentListView
|
<DirentListView
|
||||||
|
@@ -262,19 +262,6 @@ class Wiki extends Component {
|
|||||||
this.setState({direntList: newDirentList});
|
this.setState({direntList: newDirentList});
|
||||||
}
|
}
|
||||||
|
|
||||||
onLinkClick = (event) => {
|
|
||||||
const url = event.target.href;
|
|
||||||
if (this.isInternalMarkdownLink(url)) {
|
|
||||||
let path = this.getPathFromInternalMarkdownLink(url);
|
|
||||||
this.showFile(path);
|
|
||||||
} else if (this.isInternalDirLink(url)) {
|
|
||||||
let path = this.getPathFromInternalDirLink(url);
|
|
||||||
this.showDir(path);
|
|
||||||
} else {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onpopstate = (event) => {
|
onpopstate = (event) => {
|
||||||
if (event.state && event.state.path) {
|
if (event.state && event.state.path) {
|
||||||
let path = event.state.path;
|
let path = event.state.path;
|
||||||
@@ -770,7 +757,6 @@ class Wiki extends Component {
|
|||||||
direntList={this.state.direntList}
|
direntList={this.state.direntList}
|
||||||
selectedDirentList={this.state.selectedDirentList}
|
selectedDirentList={this.state.selectedDirentList}
|
||||||
updateDirent={this.updateDirent}
|
updateDirent={this.updateDirent}
|
||||||
onLinkClick={this.onLinkClick}
|
|
||||||
onSideNavMenuClick={this.onSideNavMenuClick}
|
onSideNavMenuClick={this.onSideNavMenuClick}
|
||||||
onSearchedClick={this.onSearchedClick}
|
onSearchedClick={this.onSearchedClick}
|
||||||
onMainNavBarClick={this.onMainNavBarClick}
|
onMainNavBarClick={this.onMainNavBarClick}
|
||||||
|
Reference in New Issue
Block a user