diff --git a/frontend/src/components/index-viewer.js b/frontend/src/components/index-viewer.js deleted file mode 100644 index e1435cec43..0000000000 --- a/frontend/src/components/index-viewer.js +++ /dev/null @@ -1,300 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { repoID, slug, serviceURL, isPublicWiki } from '../utils/constants'; -import { Utils } from '../utils/utils'; -import { deserialize } from '@seafile/seafile-editor'; -import'../css/index-viewer.css'; - -const viewerPropTypes = { - indexContent: PropTypes.string.isRequired, - onLinkClick: PropTypes.func.isRequired, -}; - -class TreeNode { - - constructor({ name, href, parentNode }) { - this.name = name; - this.href = href; - this.parentNode = parentNode || null; - this.children = []; - } - - setParent(parentNode) { - this.parentNode = parentNode; - } - - addChildren(nodeList) { - nodeList.forEach((node) => { - node.setParent(this); - }); - this.children = nodeList; - } -} - -class IndexContentViewer extends React.Component { - - constructor(props) { - super(props); - this.links = []; - this.treeRoot = new TreeNode({ name: '', href: '' }); - this.state = { - currentPath: '', - }; - } - - UNSAFE_componentWillMount() { - this.getRootNode(); - } - - componentDidMount() { - this.bindClickEvent(); - } - - UNSAFE_componentWillReceiveProps() { - this.removeClickEvent(); - } - - componentDidUpdate() { - this.bindClickEvent(); - } - - componentWillUnmount() { - this.removeClickEvent(); - } - - bindClickEvent = () => { - const contentClass = 'wiki-nav-content'; - this.links = document.querySelectorAll(`.${contentClass} a`); - this.links.forEach(link => { - link.addEventListener('click', this.onLinkClick); - }); - }; - - removeClickEvent = () => { - this.links.forEach(link => { - link.removeEventListener('click', this.onLinkClick); - }); - }; - - onLinkClick = (event) => { - event.preventDefault(); - const currentPath = event.target.getAttribute('data-path'); - if (currentPath === this.state.currentPath) { - return; - } else if (currentPath) { - this.setState({ currentPath: currentPath }); - } - const link = this.getLink(event.target); - if (link) this.props.onLinkClick(link); - }; - - getLink = (node) => { - const tagName = node.tagName; - if (!tagName || tagName === 'HTML') return; - if (tagName === 'A') { - return node.href; - } else { - return this.getLink(node.parentNode); - } - }; - - changeInlineNode = (item) => { - if (item.type == 'link' || item.type === 'image') { - let url; - - // 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)) { - return; - } - // get image path - let index = url.indexOf('/file'); - let index2 = url.indexOf('?'); - const imagePath = url.substring(index + 5, index2); - // replace url - item.data.src = serviceURL + '/view-image-via-public-wiki/?slug=' + slug + '&path=' + imagePath; - } - - else if (item.type == 'link') { - url = item.data.href; - /* eslint-disable */ - let expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ - /* eslint-enable */ - let re = new RegExp(expression); - - // Solving relative paths - if (!re.test(url)) { - item.data.href = serviceURL + '/published/' + slug + '/' + url; - } - // change file url - else 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; - }; - - getRootNode = () => { - let value = deserialize(this.props.indexContent); - const newNodes = Utils.changeMarkdownNodes(value, this.changeInlineNode); - newNodes.forEach((node) => { - if (node.type === 'unordered_list' || node.type === 'ordered_list') { - let treeRoot = this.transSlateToTree(node.children, this.treeRoot); - this.setNodePath(treeRoot, '/'); - this.treeRoot = treeRoot; - } - }); - }; - - setNodePath = (node, parentNodePath) => { - let name = node.name; - let path = parentNodePath === '/' ? parentNodePath + name : parentNodePath + '/' + name; - node.path = path; - if (node.children.length > 0) { - node.children.forEach(child => { - this.setNodePath(child, path); - }); - } - }; - - // slateNodes is list items of an unordered list or ordered list, translate them to treeNode and add to parentTreeNode - transSlateToTree = (slateNodes, parentTreeNode) => { - let treeNodes = slateNodes.map((slateNode) => { - // item has children(unordered list) - if (slateNode.children.length === 2 && (slateNode.children[1].type === 'unordered_list' || slateNode.children[1].type === 'ordered_list')) { - // slateNode.nodes[0] is paragraph, create TreeNode, set name and href - const paragraphNode = slateNode.children[0]; - const treeNode = this.transParagraph(paragraphNode); - // slateNode.nodes[1] is list, set it as TreeNode's children - const listNode = slateNode.children[1]; - // Add sub list items to the tree node - return this.transSlateToTree(listNode.children, treeNode); - } else { - // item doesn't have children list - if (slateNode.children[0] && (slateNode.children[0].type === 'paragraph')) { - return this.transParagraph(slateNode.children[0]); - } else { - // list item contain table/code_block/blockqupta - return new TreeNode({ name: '', href: '' }); - } - } - }); - parentTreeNode.addChildren(treeNodes); - return parentTreeNode; - }; - - // translate slate_paragraph_node to treeNode - transParagraph = (paragraphNode) => { - let treeNode; - if (paragraphNode.children[1] && paragraphNode.children[1].type === 'link') { - // paragraph node is a link node - const linkNode = paragraphNode.children[1]; - const textNode = linkNode.children[0]; - let name = textNode ? textNode.text : ''; - treeNode = new TreeNode({ name: name, href: linkNode.data.href }); - } else if (paragraphNode.children[0]) { - // paragraph first child node is a text node, then get node name - const textNode = paragraphNode.children[0]; - let name = textNode.text ? textNode.text : ''; - treeNode = new TreeNode({ name: name, href: '' }); - } else { - treeNode = new TreeNode({ name: '', href: '' }); - } - return treeNode; - }; - - render() { - return ( -
- -
- ); - } -} - -IndexContentViewer.propTypes = viewerPropTypes; - -const FolderItemPropTypes = { - node: PropTypes.object.isRequired, - bindClickEvent: PropTypes.func.isRequired, - currentPath: PropTypes.string, -}; - -class FolderItem extends React.Component { - - constructor(props) { - super(props); - this.state = { - expanded: false - }; - } - - toggleExpanded = () => { - this.setState({ expanded: !this.state.expanded }, () => { - if (this.state.expanded) this.props.bindClickEvent(); - }); - }; - - renderLink = ({ href, name, path }) => { - const className = `wiki-nav-content ${path === this.props.currentPath ? 'wiki-nav-content-highlight' : ''}`; - if (href && name) { - return
{name}
; - } else if (name) { - return
{name}
; - } else { - return null; - } - }; - - componentDidMount() { - if (this.props.node && !this.props.node.parentNode) { - this.setState({ expanded: true }, () => { - this.props.bindClickEvent(); - }); - } - } - - render() { - const { node } = this.props; - if (node.children.length > 0) { - return ( - - {node.parentNode && - - - {this.state.expanded ? : } - - {this.renderLink(node)} - - } - {this.state.expanded && node.children.map((child, index) => { - return ( -
- -
- ); - })} -
- ); - } else { - return this.renderLink(node); - } - } -} - -FolderItem.propTypes = FolderItemPropTypes; - -export default IndexContentViewer; diff --git a/frontend/src/components/wiki-markdown-viewer.js b/frontend/src/components/wiki-markdown-viewer.js index 40bb21290e..f4327d5a21 100644 --- a/frontend/src/components/wiki-markdown-viewer.js +++ b/frontend/src/components/wiki-markdown-viewer.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { MarkdownViewer } from '@seafile/seafile-editor'; -import { gettext, repoID, slug, serviceURL, isPublicWiki, sharedToken, mediaUrl } from '../utils/constants'; +import { EXTERNAL_EVENTS, EventBus, MarkdownViewer } from '@seafile/seafile-editor'; +import { gettext, isPublicWiki, mediaUrl, repoID, serviceURL, sharedToken, slug } from '../utils/constants'; import Loading from './loading'; import { Utils } from '../utils/utils'; @@ -25,109 +25,30 @@ class WikiMarkdownViewer extends React.Component { constructor(props) { super(props); - this.state = { - activeTitleIndex: 0, - }; - this.markdownContainer = React.createRef(); - this.links = []; - this.titlesInfo = []; + this.scrollRef = React.createRef(); } componentDidMount() { - // Bind event when first loaded - this.links = document.querySelectorAll(`.${contentClass} a`); - this.links.forEach(link => { - link.addEventListener('click', this.onLinkClick); - }); - - this.getTitlesInfo(); + const eventBus = EventBus.getInstance(); + this.unsubscribeLinkClick = eventBus.subscribe(EXTERNAL_EVENTS.ON_LINK_CLICK, this.onLinkClick); } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.markdownContent === nextProps.markdownContent) { - return; - } - // Unbound event when updating - this.links.forEach(link => { - link.removeEventListener('click', this.onLinkClick); - }); - } - - componentDidUpdate() { - // Update completed, rebind event - this.links = document.querySelectorAll(`.${contentClass} a`); - this.links.forEach(link => { - link.addEventListener('click', this.onLinkClick); - }); - if (this.titlesInfo.length === 0) { - this.getTitlesInfo(); - } - } - componentWillUnmount() { - // Unbound events when the component is destroyed - this.links.forEach(link => { - link.removeEventListener('click', this.onLinkClick); - }); + this.unsubscribeLinkClick(); } - getTitlesInfo = () => { - let titlesInfo = []; - const titleDom = document.querySelectorAll('h1[id^="user-content"]')[0]; - if (titleDom) { - const id = titleDom.getAttribute('id'); - let content = id && id.replace('user-content-', ''); - content = content ? `${content} - ${slug}` : slug; - Utils.updateTabTitle(content); - } - 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; - }; - 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; + let target = event.target; + while (!target.dataset || !target.dataset.url) { + target = target.parentNode; } + if (!target) return; + link = target.dataset.url; 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) => { let url, imagePath; @@ -149,21 +70,21 @@ class WikiMarkdownViewer extends React.Component { } item.data.src = serviceURL + '/view-image-via-public-wiki/?slug=' + slug + '&path=' + imagePath; } else if (item.type == 'link') { // change link url - url = item.data.href; + url = item.url; if (Utils.isInternalFileLink(url, repoID)) { // change file url if (Utils.isInternalMarkdownLink(url, repoID)) { let path = Utils.getPathFromInternalMarkdownLink(url, repoID); // replace url - item.data.href = serviceURL + '/published/' + slug + path; + item.url = serviceURL + '/published/' + slug + path; } else { - item.data.href = url.replace(/(.*)lib\/([-0-9a-f]{36})\/file(.*)/g, (match, p1, p2, p3) => { + item.url = url.replace(/(.*)lib\/([-0-9a-f]{36})\/file(.*)/g, (match, p1, p2, p3) => { return `${p1}d/${sharedToken}/files/?p=${p3}&dl=1`; }); } } else if (Utils.isInternalDirLink(url, repoID)) { // change dir url let path = Utils.getPathFromInternalDirLink(url, repoID); // replace url - item.data.href = serviceURL + '/published/' + slug + path; + item.url = serviceURL + '/published/' + slug + path; } } @@ -176,30 +97,16 @@ class WikiMarkdownViewer extends React.Component { }; renderMarkdown = () => { - let isTOCShow = true; - if (this.props.isTOCShow === false) { - isTOCShow = false; - } - if (this.props.isWiki) { - return ( - - ); - } + const { isTOCShow = true, isWiki, markdownContent } = this.props; + const props = { + isShowOutline: isTOCShow, + mathJaxSource: `${mediaUrl}js/mathjax/tex-svg.js`, + value: markdownContent, + scrollRef: this.scrollRef, + ...(isWiki && {beforeRenderCallback: this.modifyValueBeforeRender}) + }; - return ( - - ); + return ; }; render() { @@ -209,7 +116,7 @@ class WikiMarkdownViewer extends React.Component { // 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()} diff --git a/frontend/src/css/lib-content-view.css b/frontend/src/css/lib-content-view.css index f9446a84d6..8f6fca9425 100644 --- a/frontend/src/css/lib-content-view.css +++ b/frontend/src/css/lib-content-view.css @@ -205,8 +205,17 @@ background-color: #f6f6f6; } +.wiki-page-content .sf-slate-viewer-scroll-container { + padding: 0; + background-color: #fff; + border: none; + overflow: inherit; +} + .dir-content-main .wiki-page-content .article { + margin: 0; padding: 0 10px; + border: none; } .wiki-page-content a { diff --git a/frontend/src/css/shared-file-view.css b/frontend/src/css/shared-file-view.css index f488722ae3..d995f4efe5 100644 --- a/frontend/src/css/shared-file-view.css +++ b/frontend/src/css/shared-file-view.css @@ -63,10 +63,4 @@ width: 100%; padding: 10px 20px; } - - .sf-slate-viewer-article-container { - padding: 0 10px; - width: 100%; - margin: 0 !important; - } } diff --git a/frontend/src/css/wiki.css b/frontend/src/css/wiki.css deleted file mode 100644 index eac545744d..0000000000 --- a/frontend/src/css/wiki.css +++ /dev/null @@ -1,251 +0,0 @@ -.wiki-side-panel .panel-top { - background: #fff; -} -.wiki-side-nav { - flex:auto; - display:flex; - flex-direction:column; - overflow:hidden; /* for ff */ - border-right:1px solid #eee; -} - -.wiki-pages-heading { - position: relative; - font-size: 1rem; - font-weight: normal; - padding: 0.5rem 0 0.5rem 2rem; - border-bottom: 1px solid #e8e8e8; - line-height: 1.5; - height: 40px; - background-color: #f9f9f9; - margin-bottom: 0; -} -.heading-icon { - position: absolute; - right: 1rem; - top: 25%; - color: #888; - font-size: 0.8125rem; -} -.wiki-pages-container { - flex: 1; - overflow: hidden; - padding-bottom: 10px; -} -.wiki-pages-container:hover { - overflow: auto; -} -.wiki-pages-container .tree-view { - margin-left: -10px; - padding-left:0; -} -.wiki-pages-container .article { - padding: 0 20px; -} -.wiki-pages-container .tree-view { - margin-top: 14px; -} - -.wiki-md-viewer-rendered-content { - padding: 30px 0 0; -} - -img[src=""] { - opacity: 0; -} - -.wiki-side-panel { - flex: 0 0 20%; - display:flex; - flex-direction:column; - overflow:hidden; -} - -@media (max-width: 767px) { - .wiki-side-panel { - z-index: 1051; - } -} - -.wiki-main-panel { - flex: 1 0 80%; - display:flex; - flex-direction:column; - min-height: 0; -} - -.wiki-main-panel .main-panel-north { - background-color: #fff; -} - -.cur-view-content .wiki-page-container { - margin: 0 -1rem -1.25rem; - padding: 30px 1rem 1.25rem; - display: flex; - flex: 1; - padding-left: 30px; - overflow-y: auto; -} - -.wiki-main-panel .cur-view-content .article { - padding: 0 10px; -} - -.wiki-main-panel .cur-view-content .article h1 { - margin-top: 0; -} - -.cur-view-content .wiki-page-container .outline-h2, -.cur-view-content .wiki-page-container .outline-h3 { - height: 24px; - font-size: 12px; - color: #4d5156; -} - -.cur-view-content .wiki-page-container .outline-h2.active, -.cur-view-content .wiki-page-container .outline-h3.active { - color: #eb8205; -} - -.cur-view-content .wiki-page-container .seafile-markdown-outline { - overflow-y: hidden; - margin-right: 10px; -} - -.cur-view-content .wiki-page-container .seafile-markdown-outline:hover { - overflow-y: auto; -} - -.cur-view-content .wiki-page-content { - width: calc(100% - 200px); - padding-right: 30px; -} - -.cur-view-content .wiki-page-content .seafile-markdown-outline { - position: fixed; - padding-right: 1rem; - top: 79px; - right: 0; - width: 200px; - overflow: hidden; -} - -.wiki-hide { - display: none !important; -} - -@media (max-width: 991.98px) { - .cur-view-content .wiki-page-container { - padding: 0 14px; - padding-top: 30px; - } - .cur-view-content .wiki-page-content { - width: 100%; - padding-right: 0; - } - .cur-view-content .seafile-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 { - position:fixed; - top:10px; -} - -@media (min-width: 768px) { - .wiki-page-ops:before { - content:''; - border-left:1px solid #ddd; - position:absolute; - top:3px; - left:-16px; - bottom:3px; - } -} - -.wiki-page-list-item { - word-break:break-all; - line-height:1.6; - margin:3px 0; -} - -.wiki-page-link, -.wiki-page-link:hover { - font-size:1.15em; - font-weight:normal; - color:#444; - margin-left:5px; -} - -#wiki-page-last-modified { - padding: 40px 10px; - font-size:12px; - color: #666; -} - -.wiki-md-viewer-rendered-content.article h1 { - margin-top: 0; -} - -.wiki-page-content a { - cursor: pointer; -} - -.wiki-side-nav .wiki-page-content a { - color: #212529; - cursor: pointer; -} - -.index-edit { - position: absolute; - right: 0.25rem; - top: 0.25rem; -} - -.wiki-main-panel .wiki-page-content .ml-2 { - text-decoration: underline; -} - -.wiki-main-panel .wiki-page-content .ml-2:hover { - text-decoration: underline; - color:#eb8205; -} diff --git a/frontend/src/pages/wiki/index-md-viewer/index.js b/frontend/src/pages/wiki/index-md-viewer/index.js new file mode 100644 index 0000000000..424d811a85 --- /dev/null +++ b/frontend/src/pages/wiki/index-md-viewer/index.js @@ -0,0 +1,111 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { mdStringToSlate } from '@seafile/seafile-editor'; +import { isPublicWiki, repoID, serviceURL, slug } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; +import { generateNavItems } from '../utils/generate-navs'; +import NavItem from './nav-item'; + +import'./style.css'; + +const viewerPropTypes = { + indexContent: PropTypes.string.isRequired, + onLinkClick: PropTypes.func.isRequired, +}; + +class IndexMdViewer extends React.Component { + + constructor(props) { + super(props); + this.links = []; + this.state = { + currentPath: '', + treeRoot: { name: '', href: '', children: [], isRoot: true }, + }; + } + + componentDidMount() { + const { indexContent } = this.props; + const slateNodes = mdStringToSlate(indexContent); + const newSlateNodes = Utils.changeMarkdownNodes(slateNodes, this.changeInlineNode); + const treeRoot = generateNavItems(newSlateNodes); + this.setState({ + treeRoot: treeRoot, + }); + } + + onLinkClick = (node) => { + const { currentPath } = this.state; + if (node.path === currentPath) return; + if (node.path) { + this.setState({ currentPath: node.path }); + } + if (node.href) this.props.onLinkClick(node.href); + }; + + changeInlineNode = (item) => { + if (item.type == 'link' || item.type === 'image') { + let url; + + // 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)) { + return; + } + // get image path + let index = url.indexOf('/file'); + let index2 = url.indexOf('?'); + const imagePath = url.substring(index + 5, index2); + // replace url + item.data.src = serviceURL + '/view-image-via-public-wiki/?slug=' + slug + '&path=' + imagePath; + } + + else if (item.type == 'link') { + url = item.url; + /* eslint-disable */ + let expression = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ + /* eslint-enable */ + let re = new RegExp(expression); + + // Solving relative paths + if (!re.test(url)) { + item.url = serviceURL + '/published/' + slug + '/' + url; + } + // change file url + else if (Utils.isInternalMarkdownLink(url, repoID)) { + let path = Utils.getPathFromInternalMarkdownLink(url, repoID); + // replace url + item.url = serviceURL + '/published/' + slug + path; + } + // change dir url + else if (Utils.isInternalDirLink(url, repoID)) { + let path = Utils.getPathFromInternalDirLink(url, repoID); + // replace url + item.url = serviceURL + '/published/' + slug + path; + } + } + } + + return item; + }; + + render() { + const { treeRoot, currentPath } = this.state; + return ( +
+ {treeRoot.children.map(node => { + return ( + + ); + })} +
+ ); + } +} + +IndexMdViewer.propTypes = viewerPropTypes; + +export default IndexMdViewer; diff --git a/frontend/src/pages/wiki/index-md-viewer/nav-item.js b/frontend/src/pages/wiki/index-md-viewer/nav-item.js new file mode 100644 index 0000000000..3bfdd0064e --- /dev/null +++ b/frontend/src/pages/wiki/index-md-viewer/nav-item.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const propTypes = { + node: PropTypes.object.isRequired, + currentPath: PropTypes.string, + onLinkClick: PropTypes.func.isRequired, +}; + +class NavItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + expanded: false + }; + } + + toggleExpanded = () => { + const { expanded } = this.state; + this.setState({ expanded: !expanded }); + }; + + onLinkClick = (event) => { + event.preventDefault(); + const { node } = this.props; + const { expanded } = this.state; + if (node.children && node.children.length > 0 && !expanded) { + this.setState({expanded: !expanded}); + return; + } + this.props.onLinkClick(node); + }; + + itemClick = () => { + const { node } = this.props; + const { expanded } = this.state; + if (node.children && node.children.length > 0) { + this.setState({expanded: !expanded}); + return; + } + }; + + renderLink = ({ href, name, path, children }) => { + const { currentPath } = this.props; + const className = classNames('wiki-nav-content', { + 'no-children': !children || children.length === 0, + 'wiki-nav-content-highlight': currentPath === path, + }); + if (href && name) { + return ( +
+ {name} +
+ ); + } + + if (name) { + return
{name}
; + } + + return null; + }; + + render() { + const { node } = this.props; + const { expanded } = this.state; + if (node.children.length > 0) { + return ( +
+ + {} + + {this.renderLink(node)} + {expanded && node.children.map((child, index) => { + return ( + + ); + })} +
+ ); + } + + return this.renderLink(node); + } +} + +NavItem.propTypes = propTypes; + +export default NavItem; diff --git a/frontend/src/css/index-viewer.css b/frontend/src/pages/wiki/index-md-viewer/style.css similarity index 81% rename from frontend/src/css/index-viewer.css rename to frontend/src/pages/wiki/index-md-viewer/style.css index 722233e4db..cc49daacc4 100644 --- a/frontend/src/css/index-viewer.css +++ b/frontend/src/pages/wiki/index-md-viewer/style.css @@ -1,7 +1,13 @@ .wiki-nav-content { margin-top: 18px; } -.wiki-nav-content a, .wiki-nav-content span { + +.wiki-nav-content.no-children { + margin-left: 1rem; +} + +.wiki-nav-content a, +.wiki-nav-content span { color: #4d5156; font-size: 14px; text-overflow: ellipsis; @@ -9,14 +15,17 @@ overflow: hidden; display: block; } -.wiki-nav-content-highlight a { - text-decoration: none; - color: #eb8205; -} + .wiki-nav-content a:hover { text-decoration: none; color: #eb8205; } + +.wiki-nav-content-highlight a { + text-decoration: none; + color: #eb8205; +} + .switch-btn { position: absolute; left: 0; diff --git a/frontend/src/pages/wiki/index.js b/frontend/src/pages/wiki/index.js new file mode 100644 index 0000000000..e18d587b23 --- /dev/null +++ b/frontend/src/pages/wiki/index.js @@ -0,0 +1,510 @@ +import React, { Component } from 'react'; +import moment from 'moment'; +import MediaQuery from 'react-responsive'; +import { Modal } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { slug, siteRoot, initialPath, isDir, sharedToken, hasIndex, lang } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Dirent from '../../models/dirent'; +import TreeNode from '../../components/tree-view/tree-node'; +import treeHelper from '../../components/tree-view/tree-helper'; +import SidePanel from './side-panel'; +import MainPanel from './main-panel'; + +import '../../css/layout.css'; +import '../../css/side-panel.css'; +import '../../css/toolbar.css'; +import '../../css/search.css'; +import './wiki.css'; + +moment.locale(lang); + +class Wiki extends Component { + constructor(props) { + super(props); + this.state = { + path: '', + pathExist: true, + closeSideBar: false, + isViewFile: true, + isDataLoading: false, + direntList: [], + content: '', + permission: '', + lastModified: '', + latestContributor: '', + isTreeDataLoading: true, + treeData: treeHelper.buildTree(), + currentNode: null, + indexNode: null, + indexContent: '', + }; + + window.onpopstate = this.onpopstate; + this.indexPath = '/index.md'; + this.homePath = '/home.md'; + this.pythonWrapper = null; + } + + UNSAFE_componentWillMount() { + if (!Utils.isDesktop()) { + this.setState({ closeSideBar: true }); + } + } + + componentDidMount() { + this.loadSidePanel(initialPath); + this.loadWikiData(initialPath); + + this.links = document.querySelectorAll('#wiki-file-content a'); + this.links.forEach(link => link.addEventListener('click', this.onConentLinkClick)); + } + + componentWillUnmount() { + this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick)); + } + + loadSidePanel = (initialPath) => { + if (hasIndex) { + this.loadIndexNode(); + return; + } + + // load dir list + initialPath = isDir === 'None' ? '/' : initialPath; + this.loadNodeAndParentsByPath(initialPath); + }; + + loadWikiData = (initialPath) => { + this.pythonWrapper = document.getElementById('wiki-file-content'); + if (isDir === 'False') { + // this.showFile(initialPath); + this.setState({path: initialPath}); + return; + } + + // if it is a file list, remove the template content provided by python + this.removePythonWrapper(); + + if (isDir === 'True') { + this.showDir(initialPath); + return; + } + + if (isDir === 'None' && initialPath === '/home.md') { + this.showDir('/'); + return; + } + + if (isDir === 'None') { + this.setState({pathExist: false}); + let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath); + window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl); + } + }; + + loadIndexNode = () => { + seafileAPI.listWikiDir(slug, '/').then(res => { + let tree = this.state.treeData; + this.addFirstResponseListToNode(res.data.dirent_list, tree.root); + let indexNode = tree.getNodeByPath(this.indexPath); + seafileAPI.getWikiFileContent(slug, indexNode.path).then(res => { + this.setState({ + treeData: tree, + indexNode: indexNode, + indexContent: res.data.content, + isTreeDataLoading: false, + }); + }); + }).catch(() => { + this.setState({isLoadFailed: true}); + }); + }; + + showDir = (dirPath) => { + this.removePythonWrapper(); + this.loadDirentList(dirPath); + + // update location url + let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(dirPath); + window.history.pushState({url: fileUrl, path: dirPath}, dirPath, fileUrl); + }; + + showFile = (filePath) => { + this.setState({ + isDataLoading: true, + isViewFile: true, + path: filePath, + }); + + this.removePythonWrapper(); + seafileAPI.getWikiFileContent(slug, filePath).then(res => { + let data = res.data; + this.setState({ + isDataLoading: false, + content: data.content, + permission: data.permission, + lastModified: moment.unix(data.last_modified).fromNow(), + latestContributor: data.latest_contributor, + }); + }); + + const hash = window.location.hash; + let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(filePath) + hash; + if (filePath === '/home.md') { + window.history.replaceState({url: fileUrl, path: filePath}, filePath, fileUrl); + } else { + window.history.pushState({url: fileUrl, path: filePath}, filePath, fileUrl); + } + }; + + loadDirentList = (dirPath) => { + this.setState({isDataLoading: true}); + seafileAPI.listWikiDir(slug, dirPath).then(res => { + let direntList = res.data.dirent_list.map(item => { + let dirent = new Dirent(item); + return dirent; + }); + if (dirPath === '/') { + direntList = direntList.filter(item => { + if (item.type === 'dir') { + let name = item.name.toLowerCase(); + return name !== 'drafts' && name !== 'images' && name !== 'downloads'; + } + return true; + }); + } + direntList = Utils.sortDirents(direntList, 'name', 'asc'); + this.setState({ + path: dirPath, + isViewFile: false, + direntList: direntList, + isDataLoading: false, + }); + }).catch(() => { + this.setState({isLoadFailed: true}); + }); + }; + + loadTreeNodeByPath = (path) => { + let tree = this.state.treeData.clone(); + let node = tree.getNodeByPath(path); + if (!node.isLoaded) { + seafileAPI.listWikiDir(slug, node.path).then(res => { + this.addResponseListToNode(res.data.dirent_list, node); + let parentNode = tree.getNodeByPath(node.parentNode.path); + parentNode.isExpanded = true; + this.setState({ + treeData: tree, + currentNode: node + }); + }); + } else { + let parentNode = tree.getNodeByPath(node.parentNode.path); + parentNode.isExpanded = true; + this.setState({treeData: tree, currentNode: node}); //tree + } + }; + + loadNodeAndParentsByPath = (path) => { + let tree = this.state.treeData.clone(); + if (Utils.isMarkdownFile(path)) { + path = Utils.getDirName(path); + } + seafileAPI.listWikiDir(slug, path, true).then(res => { + let direntList = res.data.dirent_list; + let results = {}; + for (let i = 0; i < direntList.length; i++) { + let object = direntList[i]; + let key = object.parent_dir; + if (!results[key]) { + results[key] = []; + } + results[key].push(object); + } + for (let key in results) { + let node = tree.getNodeByPath(key); + if (!node.isLoaded && node.path === '/') { + this.addFirstResponseListToNode(results[key], node); + } else if (!node.isLoaded) { + this.addResponseListToNode(results[key], node); + } + } + this.setState({ + isTreeDataLoading: false, + treeData: tree + }); + }).catch(() => { + this.setState({isLoadFailed: true}); + }); + }; + + removePythonWrapper = () => { + if (this.pythonWrapper) { + document.body.removeChild(this.pythonWrapper); + this.pythonWrapper = null; + } + }; + + onConentLinkClick = (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.onLinkClick(link); + }; + + onLinkClick = (link) => { + const url = link; + if (Utils.isWikiInternalMarkdownLink(url, slug)) { + let path = Utils.getPathFromWikiInternalMarkdownLink(url, slug); + this.showFile(path); + } else if (Utils.isWikiInternalDirLink(url, slug)) { + let path = Utils.getPathFromWikiInternalDirLink(url, slug); + this.showDir(path); + } else { + window.location.href = url; + } + if (!this.state.closeSideBar) { + this.setState({ closeSideBar: true }); + } + }; + + onpopstate = (event) => { + if (event.state && event.state.path) { + let path = event.state.path; + if (Utils.isMarkdownFile(path)) { + this.showFile(path); + } else { + this.loadDirentList(path); + this.setState({ + path: path, + isViewFile: false + }); + } + } + }; + + onSearchedClick = (item) => { + let path = item.is_dir ? item.path.slice(0, item.path.length - 1) : item.path; + if (this.state.currentPath === path) { + return; + } + + // load sidePanel + let index = -1; + let paths = Utils.getPaths(path); + for (let i = 0; i < paths.length; i++) { + // eslint-disable-next-line no-use-before-define + let node = this.state.treeData.getNodeByPath(node); + if (!node) { + index = i; + break; + } + } + if (index === -1) { // all the data has been loaded already. + let tree = this.state.treeData.clone(); + let node = tree.getNodeByPath(item.path); + treeHelper.expandNode(node); + this.setState({treeData: tree}); + } else { + this.loadNodeAndParentsByPath(path); + } + + // load mainPanel + if (item.is_dir) { + this.showDir(path); + } else { + if (Utils.isMarkdownFile(path)) { + this.showFile(path); + } else { + let url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path); + let newWindow = window.open('about:blank'); + newWindow.location.href = url; + } + } + }; + + onMenuClick = () => { + this.setState({closeSideBar: !this.state.closeSideBar}); + }; + + onMainNavBarClick = (nodePath) => { + let tree = this.state.treeData.clone(); + let node = tree.getNodeByPath(nodePath); + tree.expandNode(node); + this.setState({treeData: tree, currentNode: node}); + this.showDir(node.path); + }; + + onDirentClick = (dirent) => { + let direntPath = Utils.joinPath(this.state.path, dirent.name); + if (dirent.isDir()) { // is dir + this.loadTreeNodeByPath(direntPath); + this.showDir(direntPath); + } else { // is file + if (Utils.isMarkdownFile(direntPath)) { + this.showFile(direntPath); + } else { + const w=window.open('about:blank'); + const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(direntPath); + w.location.href = url; + } + } + }; + + onCloseSide = () => { + this.setState({closeSideBar: !this.state.closeSideBar}); + }; + + onNodeClick = (node) => { + if (!this.state.pathExist) { + this.setState({pathExist: true}); + } + + if (node.object.isDir()) { + if (!node.isLoaded) { + let tree = this.state.treeData.clone(); + node = tree.getNodeByPath(node.path); + seafileAPI.listWikiDir(slug, node.path).then(res => { + this.addResponseListToNode(res.data.dirent_list, node); + tree.collapseNode(node); + this.setState({treeData: tree}); + }); + } + if (node.path === this.state.path) { + if (node.isExpanded) { + let tree = treeHelper.collapseNode(this.state.treeData, node); + this.setState({treeData: tree}); + } else { + let tree = this.state.treeData.clone(); + node = tree.getNodeByPath(node.path); + tree.expandNode(node); + this.setState({treeData: tree}); + } + } + } + + if (node.path === this.state.path ) { + return; + } + + if (node.object.isDir()) { // isDir + this.showDir(node.path); + } else { + if (Utils.isMarkdownFile(node.path)) { + if (node.path !== this.state.path) { + this.showFile(node.path); + } + this.onCloseSide(); + } else { + const w = window.open('about:blank'); + const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(node.path); + w.location.href = url; + } + } + }; + + onNodeCollapse = (node) => { + let tree = treeHelper.collapseNode(this.state.treeData, node); + this.setState({treeData: tree}); + }; + + onNodeExpanded = (node) => { + let tree = this.state.treeData.clone(); + node = tree.getNodeByPath(node.path); + if (!node.isLoaded) { + seafileAPI.listWikiDir(slug, node.path).then(res => { + this.addResponseListToNode(res.data.dirent_list, node); + this.setState({treeData: tree}); + }); + } else { + tree.expandNode(node); + this.setState({treeData: tree}); + } + }; + + addFirstResponseListToNode = (list, node) => { + node.isLoaded = true; + node.isExpanded = true; + let direntList = list.map(item => { + return new Dirent(item); + }); + direntList = direntList.filter(item => { + if (item.type === 'dir') { + let name = item.name.toLowerCase(); + return name !== 'drafts' && name !== 'images' && name !== 'downloads'; + } + return true; + }); + direntList = Utils.sortDirents(direntList, 'name', 'asc'); + + let nodeList = direntList.map(object => { + return new TreeNode({object}); + }); + node.addChildren(nodeList); + }; + + addResponseListToNode = (list, node) => { + node.isLoaded = true; + node.isExpanded = true; + let direntList = list.map(item => { + return new Dirent(item); + }); + direntList = Utils.sortDirents(direntList, 'name', 'asc'); + + let nodeList = direntList.map(object => { + return new TreeNode({object}); + }); + node.addChildren(nodeList); + }; + + render() { + return ( +
+ + + + + +
+ ); + } +} + +export default Wiki; diff --git a/frontend/src/pages/wiki/side-panel.js b/frontend/src/pages/wiki/side-panel.js index 0a2b242a0b..3388a0c90c 100644 --- a/frontend/src/pages/wiki/side-panel.js +++ b/frontend/src/pages/wiki/side-panel.js @@ -1,10 +1,10 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { gettext, siteRoot, repoID, slug, username, permission } from '../../utils/constants'; import Logo from '../../components/logo'; import Loading from '../../components/loading'; import TreeView from '../../components/tree-view/tree-view'; -import IndexContentViewer from '../../components/index-viewer'; +import IndexMdViewer from './index-md-viewer'; const propTypes = { closeSideBar: PropTypes.bool.isRequired, @@ -29,34 +29,30 @@ class SidePanel extends Component { renderIndexView = () => { return ( - -
-
- -
-
+
+
+ +
); }; renderTreeView = () => { return ( - -
- {this.props.treeData && ( - - )} -
-
+
+ {this.props.treeData && ( + + )} +
); }; diff --git a/frontend/src/pages/wiki/utils/generate-navs.js b/frontend/src/pages/wiki/utils/generate-navs.js new file mode 100644 index 0000000000..b58dd0470f --- /dev/null +++ b/frontend/src/pages/wiki/utils/generate-navs.js @@ -0,0 +1,91 @@ +class TreeNode { + + constructor({ name, href, parentNode }) { + this.name = name; + this.href = href; + this.parentNode = parentNode || null; + this.children = []; + } + + setParent(parentNode) { + this.parentNode = parentNode; + } + + addChildren(nodeList) { + // nodeList.forEach((node) => { + // node.setParent(this); + // }); + this.children = nodeList; + } +} + +const setNodePath = (node, parentNodePath) => { + let name = node.name; + let path = parentNodePath === '/' ? parentNodePath + name : parentNodePath + '/' + name; + node.path = path; + if (node.children.length > 0) { + node.children.forEach(child => { + setNodePath(child, path); + }); + } +}; + +// translate slate_paragraph_node to treeNode +const transParagraph = (paragraphNode) => { + let treeNode; + if (paragraphNode.children[1] && paragraphNode.children[1].type === 'link') { + // paragraph node is a link node + const linkNode = paragraphNode.children[1]; + const textNode = linkNode.children[0]; + const name = textNode ? textNode.text : ''; + treeNode = new TreeNode({ name: name, href: linkNode.url }); + } else if (paragraphNode.children[0]) { + // paragraph first child node is a text node, then get node name + const textNode = paragraphNode.children[0]; + const name = textNode.text ? textNode.text : ''; + treeNode = new TreeNode({ name: name, href: '' }); + } else { + treeNode = new TreeNode({ name: '', href: '' }); + } + return treeNode; +}; + +// slateNodes is list items of an unordered list or ordered list, translate them to treeNode and add to parentTreeNode +const transSlateToTree = (slateNodes, parentTreeNode) => { + let treeNodes = slateNodes.map((slateNode) => { + // item has children(unordered list) + if (slateNode.children.length === 2 && (slateNode.children[1].type === 'unordered_list' || slateNode.children[1].type === 'ordered_list')) { + // slateNode.nodes[0] is paragraph, create TreeNode, set name and href + const paragraphNode = slateNode.children[0]; + const treeNode = transParagraph(paragraphNode); + // slateNode.nodes[1] is list, set it as TreeNode's children + const listNode = slateNode.children[1]; + // Add sub list items to the tree node + return transSlateToTree(listNode.children, treeNode); + } else { + // item doesn't have children list + if (slateNode.children[0] && (slateNode.children[0].type === 'paragraph')) { + return transParagraph(slateNode.children[0]); + } else { + // list item contain table/code_block/blockqupta + return new TreeNode({ name: '', href: '' }); + } + } + }); + parentTreeNode.addChildren(treeNodes); + return parentTreeNode; +}; + +export const generateNavItems = (slateNodes) => { + let treeRoot = new TreeNode({ name: '', href: '' }); + slateNodes.forEach((node) => { + if (node.type === 'unordered_list' || node.type === 'ordered_list') { + treeRoot = transSlateToTree(node.children, treeRoot); + setNodePath(treeRoot, '/'); + } + }); + return treeRoot; +}; + + + diff --git a/frontend/src/pages/wiki/wiki.css b/frontend/src/pages/wiki/wiki.css new file mode 100644 index 0000000000..38df6ec63b --- /dev/null +++ b/frontend/src/pages/wiki/wiki.css @@ -0,0 +1,160 @@ +.wiki-side-panel .panel-top { + background: #fff; +} +.wiki-side-nav { + flex:auto; + display:flex; + flex-direction:column; + overflow:hidden; /* for ff */ + border-right:1px solid #eee; +} + +.wiki-pages-heading { + position: relative; + font-size: 1rem; + font-weight: normal; + padding: 0.5rem 0 0.5rem 2rem; + border-bottom: 1px solid #e8e8e8; + line-height: 1.5; + height: 40px; + background-color: #f9f9f9; + margin-bottom: 0; +} +.heading-icon { + position: absolute; + right: 1rem; + top: 25%; + color: #888; + font-size: 0.8125rem; +} +.wiki-pages-container { + flex: 1; + overflow: hidden; + padding-bottom: 10px; +} +.wiki-pages-container:hover { + overflow: auto; +} +.wiki-pages-container .tree-view { + margin-left: -10px; + margin-top: 14px; + padding-left:0; +} + +img[src=""] { + opacity: 0; +} + +.wiki-side-panel { + flex: 0 0 20%; + display:flex; + flex-direction:column; + overflow:hidden; +} + +@media (max-width: 767px) { + .wiki-side-panel { + z-index: 1051; + } +} + +.wiki-main-panel { + flex: 1 0 80%; + display:flex; + flex-direction:column; + min-height: 0; +} + +.wiki-main-panel .main-panel-north { + background-color: #fff; +} + +.cur-view-content .wiki-page-container { + margin: 0 -1rem -1.25rem; + padding: 30px 1rem 1.25rem; + display: flex; + flex: 1; + padding-left: 30px; + overflow-y: auto; +} + +.cur-view-content .wiki-page-content { + width: calc(100% - 200px); + padding-right: 30px; +} + +@media (max-width: 991.98px) { + .cur-view-content .wiki-page-container { + padding: 0 14px; + padding-top: 30px; + } + .cur-view-content .wiki-page-content { + width: 100%; + padding-right: 0; + } +} + +.wiki-page-container .article { + margin: 0; + padding: 0 10px; +} + +.wiki-page-container .article h1 { + margin-top: 0; +} + +.wiki-page-container .article span[data-url] { + cursor: pointer; +} + +.wiki-page-container .article .ml-2 { + text-decoration: underline; +} + +.wiki-page-container .article .ml-2:hover { + text-decoration: underline; + color:#eb8205; +} + +.wiki-page-container .outline-h2, +.wiki-page-container .outline-h3 { + height: 24px; + font-size: 12px; + color: #4d5156; +} + +.wiki-page-container .outline-h2.active, +.wiki-page-container .outline-h3.active { + color: #eb8205; +} + +.wiki-page-container .sf-slate-viewer-scroll-container { + background-color: #fff !important; + padding: 0px !important; + overflow: inherit; +} + +.wiki-page-container .sf-slate-viewer-article-container { + margin: 0 !important; +} + +.wiki-page-container .article { + border: none; +} + +.wiki-page-container .sf-slate-viewer-outline { + top: 79px; + width: 200px; +} + +@media (max-width: 767px) { + .wiki-page-container .article { + padding: 0 !important; + } +} + +#wiki-page-last-modified { + padding: 40px 10px; + font-size:12px; + color: #666; +} diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js index 4dbfb5ea05..a229ac4695 100644 --- a/frontend/src/wiki.js +++ b/frontend/src/wiki.js @@ -1,512 +1,5 @@ -import React, { Component } from 'react'; +import React from 'react'; import ReactDom from 'react-dom'; -import moment from 'moment'; -import MediaQuery from 'react-responsive'; -import { Modal } from 'reactstrap'; -import { slug, siteRoot, initialPath, isDir, sharedToken, hasIndex } from './utils/constants'; -import { Utils } from './utils/utils'; -import { seafileAPI } from './utils/seafile-api'; -import Dirent from './models/dirent'; -import TreeNode from './components/tree-view/tree-node'; -import treeHelper from './components/tree-view/tree-helper'; -import SidePanel from './pages/wiki/side-panel'; -import MainPanel from './pages/wiki/main-panel'; -import { lang } from './utils/constants'; - -import './css/layout.css'; -import './css/side-panel.css'; -import './css/wiki.css'; -import './css/toolbar.css'; -import './css/search.css'; - -moment.locale(lang); - -class Wiki extends Component { - constructor(props) { - super(props); - this.state = { - path: '', - pathExist: true, - closeSideBar: false, - isViewFile: true, - isDataLoading: false, - direntList: [], - content: '', - permission: '', - lastModified: '', - latestContributor: '', - isTreeDataLoading: true, - treeData: treeHelper.buildTree(), - currentNode: null, - indexNode: null, - indexContent: '', - }; - - window.onpopstate = this.onpopstate; - this.indexPath = '/index.md'; - this.homePath = '/home.md'; - this.pythonWrapper = null; - } - - UNSAFE_componentWillMount() { - if (!Utils.isDesktop()) { - this.setState({ closeSideBar: true }); - } - } - - componentDidMount() { - this.loadSidePanel(initialPath); - this.loadWikiData(initialPath); - - this.links = document.querySelectorAll('#wiki-file-content a'); - this.links.forEach(link => link.addEventListener('click', this.onConentLinkClick)); - } - - componentWillUnmount() { - this.links.forEach(link => link.removeEventListener('click', this.onConentLinkClick)); - } - - loadSidePanel = (initialPath) => { - if (hasIndex) { - this.loadIndexNode(); - return; - } - - // load dir list - initialPath = isDir === 'None' ? '/' : initialPath; - this.loadNodeAndParentsByPath(initialPath); - }; - - loadWikiData = (initialPath) => { - this.pythonWrapper = document.getElementById('wiki-file-content'); - if (isDir === 'False') { - // this.showFile(initialPath); - this.setState({path: initialPath}); - return; - } - - // if it is a file list, remove the template content provided by python - this.removePythonWrapper(); - - if (isDir === 'True') { - this.showDir(initialPath); - return; - } - - if (isDir === 'None' && initialPath === '/home.md') { - this.showDir('/'); - return; - } - - if (isDir === 'None') { - this.setState({pathExist: false}); - let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(initialPath); - window.history.pushState({url: fileUrl, path: initialPath}, initialPath, fileUrl); - } - }; - - loadIndexNode = () => { - seafileAPI.listWikiDir(slug, '/').then(res => { - let tree = this.state.treeData; - this.addFirstResponseListToNode(res.data.dirent_list, tree.root); - let indexNode = tree.getNodeByPath(this.indexPath); - seafileAPI.getWikiFileContent(slug, indexNode.path).then(res => { - this.setState({ - treeData: tree, - indexNode: indexNode, - indexContent: res.data.content, - isTreeDataLoading: false, - }); - }); - }).catch(() => { - this.setState({isLoadFailed: true}); - }); - }; - - showDir = (dirPath) => { - this.removePythonWrapper(); - this.loadDirentList(dirPath); - - // update location url - let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(dirPath); - window.history.pushState({url: fileUrl, path: dirPath}, dirPath, fileUrl); - }; - - showFile = (filePath) => { - this.setState({ - isDataLoading: true, - isViewFile: true, - path: filePath, - }); - - this.removePythonWrapper(); - seafileAPI.getWikiFileContent(slug, filePath).then(res => { - let data = res.data; - this.setState({ - isDataLoading: false, - content: data.content, - permission: data.permission, - lastModified: moment.unix(data.last_modified).fromNow(), - latestContributor: data.latest_contributor, - }); - }); - - const hash = window.location.hash; - let fileUrl = siteRoot + 'published/' + slug + Utils.encodePath(filePath) + hash; - if (filePath === '/home.md') { - window.history.replaceState({url: fileUrl, path: filePath}, filePath, fileUrl); - } else { - window.history.pushState({url: fileUrl, path: filePath}, filePath, fileUrl); - } - }; - - loadDirentList = (dirPath) => { - this.setState({isDataLoading: true}); - seafileAPI.listWikiDir(slug, dirPath).then(res => { - let direntList = res.data.dirent_list.map(item => { - let dirent = new Dirent(item); - return dirent; - }); - if (dirPath === '/') { - direntList = direntList.filter(item => { - if (item.type === 'dir') { - let name = item.name.toLowerCase(); - return name !== 'drafts' && name !== 'images' && name !== 'downloads'; - } - return true; - }); - } - direntList = Utils.sortDirents(direntList, 'name', 'asc'); - this.setState({ - path: dirPath, - isViewFile: false, - direntList: direntList, - isDataLoading: false, - }); - }).catch(() => { - this.setState({isLoadFailed: true}); - }); - }; - - loadTreeNodeByPath = (path) => { - let tree = this.state.treeData.clone(); - let node = tree.getNodeByPath(path); - if (!node.isLoaded) { - seafileAPI.listWikiDir(slug, node.path).then(res => { - this.addResponseListToNode(res.data.dirent_list, node); - let parentNode = tree.getNodeByPath(node.parentNode.path); - parentNode.isExpanded = true; - this.setState({ - treeData: tree, - currentNode: node - }); - }); - } else { - let parentNode = tree.getNodeByPath(node.parentNode.path); - parentNode.isExpanded = true; - this.setState({treeData: tree, currentNode: node}); //tree - } - }; - - loadNodeAndParentsByPath = (path) => { - let tree = this.state.treeData.clone(); - if (Utils.isMarkdownFile(path)) { - path = Utils.getDirName(path); - } - seafileAPI.listWikiDir(slug, path, true).then(res => { - let direntList = res.data.dirent_list; - let results = {}; - for (let i = 0; i < direntList.length; i++) { - let object = direntList[i]; - let key = object.parent_dir; - if (!results[key]) { - results[key] = []; - } - results[key].push(object); - } - for (let key in results) { - let node = tree.getNodeByPath(key); - if (!node.isLoaded && node.path === '/') { - this.addFirstResponseListToNode(results[key], node); - } else if (!node.isLoaded) { - this.addResponseListToNode(results[key], node); - } - } - this.setState({ - isTreeDataLoading: false, - treeData: tree - }); - }).catch(() => { - this.setState({isLoadFailed: true}); - }); - }; - - removePythonWrapper = () => { - if (this.pythonWrapper) { - document.body.removeChild(this.pythonWrapper); - this.pythonWrapper = null; - } - }; - - onConentLinkClick = (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.onLinkClick(link); - }; - - onLinkClick = (link) => { - const url = link; - if (Utils.isWikiInternalMarkdownLink(url, slug)) { - let path = Utils.getPathFromWikiInternalMarkdownLink(url, slug); - this.showFile(path); - } else if (Utils.isWikiInternalDirLink(url, slug)) { - let path = Utils.getPathFromWikiInternalDirLink(url, slug); - this.showDir(path); - } else { - window.location.href = url; - } - if (!this.state.closeSideBar) { - this.setState({ closeSideBar: true }); - } - }; - - onpopstate = (event) => { - if (event.state && event.state.path) { - let path = event.state.path; - if (Utils.isMarkdownFile(path)) { - this.showFile(path); - } else { - this.loadDirentList(path); - this.setState({ - path: path, - isViewFile: false - }); - } - } - }; - - onSearchedClick = (item) => { - let path = item.is_dir ? item.path.slice(0, item.path.length - 1) : item.path; - if (this.state.currentPath === path) { - return; - } - - // load sidePanel - let index = -1; - let paths = Utils.getPaths(path); - for (let i = 0; i < paths.length; i++) { - // eslint-disable-next-line no-use-before-define - let node = this.state.treeData.getNodeByPath(node); - if (!node) { - index = i; - break; - } - } - if (index === -1) { // all the data has been loaded already. - let tree = this.state.treeData.clone(); - let node = tree.getNodeByPath(item.path); - treeHelper.expandNode(node); - this.setState({treeData: tree}); - } else { - this.loadNodeAndParentsByPath(path); - } - - // load mainPanel - if (item.is_dir) { - this.showDir(path); - } else { - if (Utils.isMarkdownFile(path)) { - this.showFile(path); - } else { - let url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(path); - let newWindow = window.open('about:blank'); - newWindow.location.href = url; - } - } - }; - - onMenuClick = () => { - this.setState({closeSideBar: !this.state.closeSideBar}); - }; - - onMainNavBarClick = (nodePath) => { - let tree = this.state.treeData.clone(); - let node = tree.getNodeByPath(nodePath); - tree.expandNode(node); - this.setState({treeData: tree, currentNode: node}); - this.showDir(node.path); - }; - - onDirentClick = (dirent) => { - let direntPath = Utils.joinPath(this.state.path, dirent.name); - if (dirent.isDir()) { // is dir - this.loadTreeNodeByPath(direntPath); - this.showDir(direntPath); - } else { // is file - if (Utils.isMarkdownFile(direntPath)) { - this.showFile(direntPath); - } else { - const w=window.open('about:blank'); - const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(direntPath); - w.location.href = url; - } - } - }; - - onCloseSide = () => { - this.setState({closeSideBar: !this.state.closeSideBar}); - }; - - onNodeClick = (node) => { - if (!this.state.pathExist) { - this.setState({pathExist: true}); - } - - if (node.object.isDir()) { - if (!node.isLoaded) { - let tree = this.state.treeData.clone(); - node = tree.getNodeByPath(node.path); - seafileAPI.listWikiDir(slug, node.path).then(res => { - this.addResponseListToNode(res.data.dirent_list, node); - tree.collapseNode(node); - this.setState({treeData: tree}); - }); - } - if (node.path === this.state.path) { - if (node.isExpanded) { - let tree = treeHelper.collapseNode(this.state.treeData, node); - this.setState({treeData: tree}); - } else { - let tree = this.state.treeData.clone(); - node = tree.getNodeByPath(node.path); - tree.expandNode(node); - this.setState({treeData: tree}); - } - } - } - - if (node.path === this.state.path ) { - return; - } - - if (node.object.isDir()) { // isDir - this.showDir(node.path); - } else { - if (Utils.isMarkdownFile(node.path)) { - if (node.path !== this.state.path) { - this.showFile(node.path); - } - this.onCloseSide(); - } else { - const w = window.open('about:blank'); - const url = siteRoot + 'd/' + sharedToken + '/files/?p=' + Utils.encodePath(node.path); - w.location.href = url; - } - } - }; - - onNodeCollapse = (node) => { - let tree = treeHelper.collapseNode(this.state.treeData, node); - this.setState({treeData: tree}); - }; - - onNodeExpanded = (node) => { - let tree = this.state.treeData.clone(); - node = tree.getNodeByPath(node.path); - if (!node.isLoaded) { - seafileAPI.listWikiDir(slug, node.path).then(res => { - this.addResponseListToNode(res.data.dirent_list, node); - this.setState({treeData: tree}); - }); - } else { - tree.expandNode(node); - this.setState({treeData: tree}); - } - }; - - addFirstResponseListToNode = (list, node) => { - node.isLoaded = true; - node.isExpanded = true; - let direntList = list.map(item => { - return new Dirent(item); - }); - direntList = direntList.filter(item => { - if (item.type === 'dir') { - let name = item.name.toLowerCase(); - return name !== 'drafts' && name !== 'images' && name !== 'downloads'; - } - return true; - }); - direntList = Utils.sortDirents(direntList, 'name', 'asc'); - - let nodeList = direntList.map(object => { - return new TreeNode({object}); - }); - node.addChildren(nodeList); - }; - - addResponseListToNode = (list, node) => { - node.isLoaded = true; - node.isExpanded = true; - let direntList = list.map(item => { - return new Dirent(item); - }); - direntList = Utils.sortDirents(direntList, 'name', 'asc'); - - let nodeList = direntList.map(object => { - return new TreeNode({object}); - }); - node.addChildren(nodeList); - }; - - render() { - return ( -
- - - - - -
- ); - } -} +import Wiki from './pages/wiki'; ReactDom.render(, document.getElementById('wrapper'));