import React from 'react'; import PropTypes from 'prop-types'; import MarkdownViewer from '@seafile/seafile-editor/dist/viewer/markdown-viewer'; import { gettext, repoID, slug, serviceURL, isPublicWiki, siteRoot } from '../utils/constants'; import { Card, CardTitle, CardText } from 'reactstrap'; import Loading from './loading'; import { seafileAPI } from '../utils/seafile-api'; import { Utils } from '../utils/utils'; import toaster from './toast'; import '../css/related-files-list.css'; const propTypes = { children: PropTypes.object, isFileLoading: PropTypes.bool.isRequired, markdownContent: PropTypes.string.isRequired, latestContributor: PropTypes.string.isRequired, lastModified: PropTypes.string.isRequired, onLinkClick: PropTypes.func.isRequired, isWiki: PropTypes.bool, isTOCShow: PropTypes.bool, // for dir-column-file component(import repoID is undefined) repoID: PropTypes.string, path: PropTypes.string, }; const contentClass = 'wiki-page-content'; class WikiMarkdownViewer extends React.Component { constructor(props) { super(props); this.state = { activeTitleIndex: 0, relatedFiles: [], }; this.markdownContainer = React.createRef(); this.links = []; this.titlesInfo = []; } componentDidMount() { // Bind event when first loaded this.links = document.querySelectorAll(`.${contentClass} a`); this.links.forEach(link => { link.addEventListener('click', this.onLinkClick); }); this.listRelatedFiles(); } componentWillReceiveProps() { // Unbound event when updating this.links.forEach(link => { link.removeEventListener('click', this.onLinkClick); }); this.listRelatedFiles(); } componentDidUpdate() { // Update completed, rebind event this.links = document.querySelectorAll(`.${contentClass} a`); this.links.forEach(link => { link.addEventListener('click', this.onLinkClick); }); } componentWillUnmount() { // Rebinding events when the component is destroyed this.links.forEach(link => { link.removeEventListener('click', this.onLinkClick); }); } onContentRendered = (markdownViewer) => { this.titlesInfo = markdownViewer.titlesInfo; } listRelatedFiles = () => { // for dir-column-file component(import repoID is undefined) if (this.props.repoID && this.props.path) { seafileAPI.listRelatedFiles(this.props.repoID, this.props.path).then(res => { this.setState({ relatedFiles: res.data.related_files }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); }); } } onLinkClick = (event) => { event.preventDefault(); event.stopPropagation(); let link = ''; if (event.target.tagName !== 'A') { let target = event.target.parentNode; while (target.tagName !== 'A') { target = target.parentNode; } link = target.href; } else { link = event.target.href; } this.props.onLinkClick(link); } onScrollHandler = () => { const contentScrollTop = this.markdownContainer.current.scrollTop + 180; let titlesLength = this.titlesInfo.length; let activeTitleIndex; if (contentScrollTop <= this.titlesInfo[0]) { activeTitleIndex = 0; this.setState({activeTitleIndex: activeTitleIndex}); return; } if (contentScrollTop > this.titlesInfo[titlesLength - 1]) { activeTitleIndex = this.titlesInfo.length - 1; this.setState({activeTitleIndex: activeTitleIndex}); return; } for (let i = 0; i < titlesLength; i++) { if (contentScrollTop > this.titlesInfo[i]) { continue; } else { activeTitleIndex = i - 1; break; } } this.setState({activeTitleIndex: activeTitleIndex}); } changeInlineNode = (item) => { if (item.object == 'inline') { let url, imagePath; // change image url if (item.type == 'image' && isPublicWiki) { url = item.data.src; const re = new RegExp(serviceURL + '/lib/' + repoID +'/file.*raw=1'); // different repo if (re.test(url)) { // get image path let index = url.indexOf('/file'); let index2 = url.indexOf('?'); imagePath = url.substring(index + 5, index2); } else if (/^\.\.\/*/.test(url) || /^\.\/*/.test(url)) { const path = this.props.path; const originalPath = path.slice(0, path.lastIndexOf('/')) + '/' + url; imagePath = Utils.pathNormalize(originalPath); } else { return; } item.data.src = serviceURL + '/view-image-via-public-wiki/?slug=' + slug + '&path=' + imagePath; } else if (item.type == 'link') { url = item.data.href; // change file url if (Utils.isInternalMarkdownLink(url, repoID)) { let path = Utils.getPathFromInternalMarkdownLink(url, repoID); // replace url item.data.href = serviceURL + '/published/' + slug + path; } // change dir url else if (Utils.isInternalDirLink(url, repoID)) { let path = Utils.getPathFromInternalDirLink(url, repoID); // replace url item.data.href = serviceURL + '/published/' + slug + path; } } } return item; } modifyValueBeforeRender = (value) => { let nodes = value.document.nodes; let newNodes = Utils.changeMarkdownNodes(nodes, this.changeInlineNode); value.document.nodes = newNodes; return value; } renderMarkdown = () => { let isTOCShow = true; if (this.props.isTOCShow === false) { isTOCShow = false; } if (this.props.isWiki) { return ( ); } return ( ); } renderRelatedFiles = () => { const relatedFiles = this.state.relatedFiles; if (relatedFiles.length > 0) { return (

{gettext('related files')}

{ relatedFiles.map((relatedFile, index) => { let href = siteRoot + 'lib/' + relatedFile.repo_id + '/file' + Utils.encodePath(relatedFile.path); return(
{relatedFile.name} {relatedFile.repo_name}
); }) }
); } else { return null; } } render() { if (this.props.isFileLoading) { return ; } // In dir-column-file repoID is one of props, width is 100%; In wiki-viewer repoID is not props, width isn't 100% let contentClassName = `${this.props.repoID ? contentClass + ' w-100' : contentClass}`; return (
{this.props.children} {this.renderMarkdown()} {this.props.isWiki && this.renderRelatedFiles()}

{gettext('Last modified by')} {this.props.latestContributor}, {this.props.lastModified}

); } } const defaultProps = { isWiki: false, }; WikiMarkdownViewer.propTypes = propTypes; MarkdownViewer.defaultProps = defaultProps; export default WikiMarkdownViewer;