mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-31 14:42:10 +00:00
Rewrite wiki page with react (#2258)
This commit is contained in:
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import SeafileEditor from '@seafile/seafile-editor';
|
||||
import 'whatwg-fetch';
|
||||
import { SeafileAPI } from 'seafile-js';
|
||||
import cookie from 'react-cookies'
|
||||
import cookie from 'react-cookies';
|
||||
let repoID = window.app.pageOptions.repoID;
|
||||
let filePath = window.app.pageOptions.filePath;
|
||||
let fileName = window.app.pageOptions.fileName;
|
||||
let siteRoot = window.app.config.siteRoot;
|
||||
let domain = window.app.pageOptions.domain;
|
||||
let protocol = window.app.pageOptions.protocol;
|
||||
|
||||
let mode = window.app.pageOptions.mode;
|
||||
let dirPath = '/';
|
||||
|
||||
const serviceUrl = window.app.config.serviceUrl;
|
||||
@@ -18,7 +18,7 @@ const userInfo = window.app.userInfo;
|
||||
// init seafileAPI
|
||||
let seafileAPI = new SeafileAPI();
|
||||
let xcsrfHeaders = cookie.load('csrftoken');
|
||||
seafileAPI.initForSeahubUsage({ xcsrfHeaders });
|
||||
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
|
||||
|
||||
function getImageFileNameWithTimestamp() {
|
||||
var d = Date.now();
|
||||
@@ -114,6 +114,18 @@ class EditorUtilities {
|
||||
return files;
|
||||
})
|
||||
}
|
||||
|
||||
getFileHistory() {
|
||||
return (
|
||||
seafileAPI.getFileHistory(repoID, filePath)
|
||||
)
|
||||
}
|
||||
|
||||
getFileInfo() {
|
||||
return (
|
||||
seafileAPI.getFileInfo(repoID, filePath)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +146,8 @@ class App extends React.Component {
|
||||
mtime: null,
|
||||
size: 0,
|
||||
starred: false,
|
||||
permission: '',
|
||||
lastModifier: '',
|
||||
},
|
||||
collabServer: seafileCollabServer ? seafileCollabServer : null,
|
||||
};
|
||||
@@ -142,13 +156,17 @@ class App extends React.Component {
|
||||
componentDidMount() {
|
||||
|
||||
seafileAPI.getFileInfo(repoID, filePath).then((res) => {
|
||||
let { mtime, size, starred } = res.data;
|
||||
let { mtime, size, starred, permission, last_modifier_name } = res.data;
|
||||
let lastModifier = last_modifier_name
|
||||
|
||||
this.setState((prevState, props) => ({
|
||||
fileInfo: {
|
||||
...prevState.fileInfo,
|
||||
mtime,
|
||||
size,
|
||||
starred
|
||||
starred,
|
||||
permission,
|
||||
lastModifier
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -179,6 +197,7 @@ class App extends React.Component {
|
||||
editorUtilities={editorUtilities}
|
||||
userInfo={this.state.collabServer ? userInfo : null}
|
||||
collabServer={this.state.collabServer}
|
||||
mode={mode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
66
frontend/src/components/MainPanel.js
Normal file
66
frontend/src/components/MainPanel.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import Search from './search';
|
||||
import MarkdownViewer from './markdown-viewer';
|
||||
import Account from './account';
|
||||
import { repoID, serviceUrl, slug, siteRoot } from './constance';
|
||||
|
||||
|
||||
|
||||
class MainPanel extends Component {
|
||||
onMenuClick = () => {
|
||||
this.props.onMenuClick();
|
||||
}
|
||||
|
||||
onEditClick = (e) => {
|
||||
// const w=window.open('about:blank')
|
||||
e.preventDefault();
|
||||
window.location.href= serviceUrl + '/lib/' + repoID + '/file' + this.props.filePath + '?mode=edit';
|
||||
}
|
||||
|
||||
render() {
|
||||
var filePathList = this.props.filePath.split('/');
|
||||
var pathElem = filePathList.map((item, index) => {
|
||||
if (item == "") {
|
||||
return;
|
||||
} else {
|
||||
return (
|
||||
<span key={index}><span className="path-split">/</span>{item}</span>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="main-panel o-hidden">
|
||||
<div className="main-panel-top panel-top">
|
||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu" onClick={this.onMenuClick}></span>
|
||||
<div className={`wiki-page-ops ${this.props.permission === 'rw' ? '' : 'hide'}`}>
|
||||
<a className="sf-btn-link btn-white" onClick={this.onEditClick}>Edit Page</a>
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
<Search />
|
||||
<Account seafileAPI={this.props.seafileAPI} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="cur-view-main">
|
||||
<div className="cur-view-path">
|
||||
<div className="path-containter">
|
||||
<a href={siteRoot + 'wikis/'} className="normal">Wikis</a>
|
||||
<span className="path-split">/</span>
|
||||
<a href={siteRoot + 'wikis/' + slug} className="normal">{slug}</a>
|
||||
{pathElem}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cur-view-main-con">
|
||||
<MarkdownViewer
|
||||
markdownContent={this.props.content}
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MainPanel;
|
36
frontend/src/components/SidePanel.js
Normal file
36
frontend/src/components/SidePanel.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { Component } from 'react';
|
||||
import TreeView from './tree-view/tree-view';
|
||||
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance';
|
||||
|
||||
class SidePanel extends Component {
|
||||
closeSide = () => {
|
||||
this.props.onCloseSide();
|
||||
}
|
||||
|
||||
onFileClick = (e, node) => {
|
||||
this.props.onFileClick(e, node)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`side-panel ${this.props.closeSideBar ? "": "left-zero"}`}>
|
||||
<div className="side-panel-top panel-top">
|
||||
<a href={siteRoot} id="logo">
|
||||
<img src={mediaUrl + logoPath} title={siteTitle} alt="logo" width={logoWidth} height={logoHeight} />
|
||||
</a>
|
||||
<a title="Close" aria-label="Close" onClick={this.closeSide} className="sf2-icon-x1 sf-popover-close side-panel-close op-icon d-md-none "></a>
|
||||
</div>
|
||||
<div id="side-nav" className="wiki-side-nav" role="navigation">
|
||||
<h3 className="wiki-pages-heading">Pages</h3>
|
||||
<div className="wiki-pages-container">
|
||||
<TreeView
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
onClick={this.onFileClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default SidePanel;
|
135
frontend/src/components/account.js
Normal file
135
frontend/src/components/account.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import cookie from 'react-cookies';
|
||||
import { keyCodes, bytesToSize } from './utils';
|
||||
import { siteRoot, avatarInfo } from './constance';
|
||||
|
||||
|
||||
class Account extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showInfo: false,
|
||||
userName: '',
|
||||
contactEmail: '',
|
||||
quotaUsage: '',
|
||||
quotaTotal: '',
|
||||
isStaff: false,
|
||||
usageRate: '',
|
||||
avatarURL: '',
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.getAccountInfo();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.handleProps();
|
||||
}
|
||||
|
||||
getContainer = () => {
|
||||
return ReactDOM.findDOMNode(this);
|
||||
}
|
||||
|
||||
handleProps = () => {
|
||||
if (this.state.showInfo) {
|
||||
this.addEvents();
|
||||
} else {
|
||||
this.removeEvents();
|
||||
}
|
||||
}
|
||||
|
||||
addEvents = () => {
|
||||
['click', 'touchstart', 'keyup'].forEach(event =>
|
||||
document.addEventListener(event, this.handleDocumentClick, true)
|
||||
);
|
||||
}
|
||||
|
||||
removeEvents = () => {
|
||||
['click', 'touchstart', 'keyup'].forEach(event =>
|
||||
document.removeEventListener(event, this.handleDocumentClick, true)
|
||||
);
|
||||
}
|
||||
|
||||
handleDocumentClick = (e) => {
|
||||
if (e && (e.which === 3 || (e.type === 'keyup' && e.which !== keyCodes.tab))) return;
|
||||
const container = this.getContainer();
|
||||
|
||||
if (container.contains(e.target) && container !== e.target && (e.type !== 'keyup' || e.which === keyCodes.tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showInfo: !this.state.showInfo,
|
||||
})
|
||||
}
|
||||
|
||||
onClickAccount = () => {
|
||||
this.setState({
|
||||
showInfo: !this.state.showInfo,
|
||||
})
|
||||
}
|
||||
|
||||
getAccountInfo = () => {
|
||||
this.props.seafileAPI.getAccountInfo().then(resp => {
|
||||
this.setState({
|
||||
userName: resp.data.name,
|
||||
contactEmail: resp.data.email,
|
||||
usageRate: resp.data.space_usage,
|
||||
quotaUsage: bytesToSize(resp.data.usage),
|
||||
quotaTotal: bytesToSize(resp.data.total),
|
||||
isStaff: resp.data.is_staff,
|
||||
avatarURL: resp.data.avatar_url
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
renderMenu = () => {
|
||||
if(this.state.isStaff){
|
||||
return (
|
||||
<a href={siteRoot + 'sys/useradmin/'} title="System Admin" className="item">System Admin</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="account">
|
||||
<a id="my-info" onClick={this.onClickAccount} className="account-toggle no-deco d-none d-md-block" aria-label="View profile and more">
|
||||
<span>
|
||||
<img src={this.state.avatarURL} width="36" height="36" className="avatar" />
|
||||
</span> <span className="icon-caret-down vam"></span>
|
||||
</a>
|
||||
<span className="account-toggle sf2-icon-more mobile-icon d-md-none" aria-label="View profile and more" onClick={this.onClickAccount}></span>
|
||||
<div id="user-info-popup" className={`account-popup sf-popover ${this.state.showInfo? '':'hide'}`}>
|
||||
<div className="outer-caret up-outer-caret"><div className="inner-caret"></div></div>
|
||||
<div className="sf-popover-con">
|
||||
<div className="item o-hidden">
|
||||
<span>
|
||||
<img src={this.state.avatarURL} width="36" height="36" className="avatar" />
|
||||
</span>
|
||||
<div className="txt">
|
||||
{this.state.userName} <br/>
|
||||
{this.state.contactEmail}
|
||||
</div>
|
||||
</div>
|
||||
<div id="space-traffic">
|
||||
<div className="item">
|
||||
<p>Used: {this.state.quotaUsage} / {this.state.quotaTotal}</p>
|
||||
<div id="quota-bar">
|
||||
<span id="quota-usage" className="usage" style={{width: this.state.usageRate}}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href={siteRoot + 'profile/'} className="item">Settings</a>
|
||||
{this.renderMenu()}
|
||||
<a href={siteRoot + 'accounts/logout/'} className="item">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Account;
|
14
frontend/src/components/constance.js
Normal file
14
frontend/src/components/constance.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const dirPath = '/';
|
||||
|
||||
export const siteRoot = window.app.config.siteRoot;
|
||||
export const avatarInfo = window.app.config.avatarInfo;
|
||||
export const logoPath = window.app.config.logoPath;
|
||||
export const mediaUrl = window.app.config.mediaUrl;
|
||||
export const siteTitle = window.app.config.siteTitle;
|
||||
export const logoWidth = window.app.config.logoWidth;
|
||||
export const logoHeight = window.app.config.logoHeight;
|
||||
|
||||
export const slug = window.wiki.config.slug;
|
||||
export const repoID = window.wiki.config.repoId;
|
||||
export const serviceUrl = window.wiki.config.serviceUrl;
|
||||
export const initialFilePath = window.wiki.config.initial_file_path;
|
97
frontend/src/components/markdown-viewer.js
Normal file
97
frontend/src/components/markdown-viewer.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { processor } from '@seafile/seafile-editor/src/lib/seafile-markdown2html';
|
||||
import TreeView from './tree-view/tree-view';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
var URL = require('url-parse');
|
||||
|
||||
require('@seafile/seafile-editor/src/lib/code-hight-package');
|
||||
|
||||
const contentClass = "wiki-md-viewer-rendered-content";
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MarkdownViewer extends React.Component {
|
||||
|
||||
state = {
|
||||
renderingContent: true,
|
||||
renderingOutline: true,
|
||||
html: '',
|
||||
outlineTreeRoot: null
|
||||
}
|
||||
|
||||
scrollToNode(node) {
|
||||
let url = new URL(window.location.href);
|
||||
url.set('hash', 'user-content-' + node.data.id);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
setContent(markdownContent) {
|
||||
let that = this;
|
||||
|
||||
processor.process(markdownContent, function(err, file) {
|
||||
that.setState({
|
||||
html: String(file),
|
||||
renderingContent: false
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setContent(nextProps.markdownContent);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let that = this;
|
||||
|
||||
processor.process(this.props.markdownContent, function(err, file) {
|
||||
that.setState({
|
||||
html: String(file),
|
||||
renderingContent: false
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MarkdownViewerContent
|
||||
renderingContent={this.state.renderingContent} html={this.state.html}
|
||||
onLinkClick={this.props.onLinkClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownViewer;
|
19
frontend/src/components/search.js
Normal file
19
frontend/src/components/search.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { Component } from 'react';
|
||||
import { repoID, siteRoot } from './constance';
|
||||
|
||||
class Search extends Component {
|
||||
render() {
|
||||
return (
|
||||
<form id="top-search-form" method="get" action={siteRoot + 'search/'} className="hidden-sm-down search-form">
|
||||
<input
|
||||
type="text" className="search-input" name="q"
|
||||
placeholder="Search files in this wiki"
|
||||
/>
|
||||
<input type="hidden" name="search_repo" value={repoID} />
|
||||
<button type="submit" className="search-submit" aria-label="Submit"><span className="icon-search"></span></button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
127
frontend/src/components/tree-view/node.js
Normal file
127
frontend/src/components/tree-view/node.js
Normal file
@@ -0,0 +1,127 @@
|
||||
|
||||
|
||||
class Node {
|
||||
|
||||
static create(attrs = {}) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `Node` from a JSON `object`.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Node}
|
||||
*/
|
||||
static fromJSON(object) {
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
isExpanded = true,
|
||||
children = [],
|
||||
} = object;
|
||||
|
||||
const node = new Node({
|
||||
name,
|
||||
type,
|
||||
isExpanded,
|
||||
children: children.map(Node.fromJSON),
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
constructor({ name, type, isExpanded, children }) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.children = children ? children : [];
|
||||
this.isExpanded = isExpanded !== undefined ? isExpanded : true;
|
||||
}
|
||||
|
||||
get path() {
|
||||
if (!this.parent) {
|
||||
return this.name;
|
||||
} else {
|
||||
var p = this.parent.path;
|
||||
if (p === "/")
|
||||
return p + this.name;
|
||||
else
|
||||
return p + "/" + this.name;
|
||||
}
|
||||
}
|
||||
|
||||
copy() {
|
||||
var n = new Node({
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
isExpanded: this.isExpanded
|
||||
});
|
||||
n.children = this.children.map(child => { var newChild = child.copy(); newChild.parent = n; return newChild; });
|
||||
return n;
|
||||
}
|
||||
|
||||
isRoot() {
|
||||
return this.parent === undefined;
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.children.length > 0;
|
||||
}
|
||||
|
||||
isImage() {
|
||||
let index = this.name.lastIndexOf(".");
|
||||
if (index == -1) {
|
||||
return false;
|
||||
} else {
|
||||
let type = this.name.substring(index).toLowerCase();
|
||||
if (type == ".png" || type == ".jpg") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isMarkdown() {
|
||||
let index = this.name.lastIndexOf(".");
|
||||
if (index == -1) {
|
||||
return false;
|
||||
} else {
|
||||
let type = this.name.substring(index).toLowerCase();
|
||||
if (type == ".md" || type == ".markdown") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDir() {
|
||||
return this.type == "dir";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a JSON representation of the node.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
var children = []
|
||||
if (this.hasChildren()) {
|
||||
children = this.children.map(m => m.toJSON());
|
||||
}
|
||||
|
||||
const object = {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
isExpanded: this.isExpanded,
|
||||
children: children
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export { Node }
|
135
frontend/src/components/tree-view/tree-node-view.js
Normal file
135
frontend/src/components/tree-view/tree-node-view.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
|
||||
function sortByType(a, b) {
|
||||
if (a.type == "dir" && b.type != "dir") {
|
||||
return -1;
|
||||
} else if (a.type != "dir" && b.type == "dir") {
|
||||
return 1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
}
|
||||
|
||||
class TreeNodeView extends React.Component {
|
||||
|
||||
renderCollapse = () => {
|
||||
const { node } = this.props;
|
||||
|
||||
if (node.hasChildren()) {
|
||||
const { isExpanded } = node;
|
||||
return (
|
||||
<i
|
||||
className={isExpanded ? 'folder-toggle-icon fa fa-caret-down' : 'folder-toggle-icon fa fa-caret-right'}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={this.handleCollapse}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderChildren = () => {
|
||||
const { node } = this.props;
|
||||
if (node.children && node.children.length) {
|
||||
const childrenStyles = {
|
||||
paddingLeft: this.props.paddingLeft
|
||||
};
|
||||
var l = node.children.sort(sortByType);
|
||||
l = l.filter((node) => { return node.type == "dir" || node.isMarkdown(); })
|
||||
|
||||
/*
|
||||
the `key` property is needed. Otherwise there is a warning in the console
|
||||
*/
|
||||
return (
|
||||
<div className="children" style={childrenStyles}>
|
||||
{l.map(child => {
|
||||
return (
|
||||
<TreeNodeView
|
||||
node={child}
|
||||
key={child.path}
|
||||
paddingLeft={this.props.paddingLeft}
|
||||
treeView={this.props.treeView}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node } = this.props;
|
||||
const styles = {};
|
||||
var icon, type;
|
||||
if (node.type === "dir") {
|
||||
icon = <i className="far fa-folder"/>;
|
||||
type = 'dir';
|
||||
} else {
|
||||
let index = node.name.lastIndexOf(".");
|
||||
if (index === -1) {
|
||||
icon = <i className="far fa-file"/>;
|
||||
type = 'file';
|
||||
} else {
|
||||
type = node.name.substring(index).toLowerCase();
|
||||
if (type === ".png" || type === ".jpg") {
|
||||
icon = <i className="far fa-image"/>;
|
||||
type = 'image';
|
||||
} else {
|
||||
icon = <i className="far fa-file"/>;
|
||||
type = 'file';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div type={type}
|
||||
className="tree-node"
|
||||
style={styles}
|
||||
>
|
||||
<div onMouseLeave={this.onMouseLeave} onMouseEnter={this.onMouseEnter}
|
||||
onClick={this.onClick}
|
||||
type={type} className={`tree-node-inner text-nowrap ${node.name === '/'? 'hide': ''}`}>
|
||||
{this.renderCollapse()}
|
||||
<span type={type} className="tree-node-icon">
|
||||
{icon}
|
||||
</span>
|
||||
<span type={type} draggable="true" onDragStart={this.onDragStart}>{node.name}</span>
|
||||
</div>
|
||||
{node.isExpanded ? this.renderChildren() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
let { node } = this.props;
|
||||
this.props.treeView.onClick(e, node);
|
||||
}
|
||||
|
||||
onMouseEnter = e => {
|
||||
let { node } = this.props;
|
||||
this.props.treeView.showImagePreview(e, node);
|
||||
}
|
||||
|
||||
onMouseLeave = e => {
|
||||
this.props.treeView.hideImagePreview(e);
|
||||
}
|
||||
|
||||
handleCollapse = e => {
|
||||
e.stopPropagation();
|
||||
const { node } = this.props;
|
||||
if (this.props.treeView.toggleCollapse) {
|
||||
this.props.treeView.toggleCollapse(node);
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart = e => {
|
||||
const { node } = this.props;
|
||||
this.props.treeView.onDragStart(e, node);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TreeNodeView;
|
143
frontend/src/components/tree-view/tree-view.js
Normal file
143
frontend/src/components/tree-view/tree-view.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import TreeNodeView from './tree-node-view';
|
||||
import Tree from './tree';
|
||||
|
||||
class TreeView extends React.PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
paddingLeft: 20
|
||||
};
|
||||
|
||||
imagePreviewTimeout = null
|
||||
|
||||
state = {
|
||||
tree: new Tree(),
|
||||
loadingFailed: false,
|
||||
imagePreviewPosition: {
|
||||
left: 10+'px',
|
||||
top: 10+'px'
|
||||
},
|
||||
isShowImagePreview: false,
|
||||
imagePreviewLoading: false,
|
||||
imageSrc: '',
|
||||
}
|
||||
|
||||
showImagePreview = (e, node) => {
|
||||
e.persist();
|
||||
|
||||
let type = e.target.getAttribute('type');
|
||||
if (type === 'image') {
|
||||
this.imagePreviewTimeout = setTimeout(() => {
|
||||
let X = e.clientX + 20;
|
||||
let Y = e.clientY - 55;
|
||||
if (e.view.innerHeight < e.clientY + 150) {
|
||||
Y = e.clientY - 219;
|
||||
}
|
||||
this.setState({
|
||||
isShowImagePreview: true,
|
||||
imagePreviewLoading: true,
|
||||
imageSrc: this.props.editorUtilities.getFileURL(node),
|
||||
imagePreviewPosition: {
|
||||
left: X + 'px',
|
||||
top: Y + 'px'
|
||||
}
|
||||
});
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
hideImagePreview = (e) => {
|
||||
clearTimeout(this.imagePreviewTimeout);
|
||||
this.setState({
|
||||
isShowImagePreview: false,
|
||||
imagePreviewLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
imageLoaded = () => {
|
||||
this.setState({
|
||||
imagePreviewLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.editorUtilities.getFiles().then((files) => {
|
||||
// construct the tree object
|
||||
var rootObj = {
|
||||
name: '/',
|
||||
type: 'dir',
|
||||
isExpanded: true
|
||||
}
|
||||
var treeData = new Tree();
|
||||
treeData.parseFromList(rootObj, files);
|
||||
this.setState({
|
||||
tree: treeData
|
||||
})
|
||||
}, () => {
|
||||
console.log("failed to load files");
|
||||
this.setState({
|
||||
loadingFailed: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const tree = this.state.tree;
|
||||
if (!tree.root) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tree-view tree">
|
||||
<TreeNodeView
|
||||
node={tree.root}
|
||||
paddingLeft={20}
|
||||
treeView={this}
|
||||
/>
|
||||
{ this.state.isShowImagePreview &&
|
||||
<div style={this.state.imagePreviewPosition} className={'image-view'}>
|
||||
{ this.state.imagePreviewLoading && <i className={'rotate fa fa-spinner'}/> }
|
||||
<img src={this.state.imageSrc} onLoad={this.imageLoaded} alt=""/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (tree) => {
|
||||
/*
|
||||
this._updated = true;
|
||||
if (this.props.onChange) this.props.onChange(tree.obj);
|
||||
*/
|
||||
}
|
||||
|
||||
toggleCollapse = (node) => {
|
||||
const tree = this.state.tree;
|
||||
node.isExpanded = !node.isExpanded;
|
||||
|
||||
// copy the tree to make PureComponent work
|
||||
this.setState({
|
||||
tree: tree.copy()
|
||||
});
|
||||
|
||||
this.change(tree);
|
||||
}
|
||||
|
||||
onDragStart = (e, node) => {
|
||||
const url = this.props.editorUtilities.getFileURL(node);
|
||||
e.dataTransfer.setData("text/uri-list", url);
|
||||
e.dataTransfer.setData("text/plain", url);
|
||||
}
|
||||
|
||||
onClick = (e, node) => {
|
||||
if (node.isDir()) {
|
||||
this.toggleCollapse(node);
|
||||
return;
|
||||
}
|
||||
this.props.onClick(e, node);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TreeView;
|
113
frontend/src/components/tree-view/tree.js
Normal file
113
frontend/src/components/tree-view/tree.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Node } from './node'
|
||||
|
||||
class Tree {
|
||||
|
||||
constructor() {
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
copy() {
|
||||
var t = new Tree();
|
||||
if (this.root)
|
||||
t.root = this.root.copy();
|
||||
return t;
|
||||
}
|
||||
|
||||
setRoot(dir) {
|
||||
this.root = dir;
|
||||
}
|
||||
|
||||
addChildToNode(node, child) {
|
||||
child.parent = node;
|
||||
node.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
addChild(node, child, insertIndex) {
|
||||
if (!(child instanceof Node)) {
|
||||
throw new TypeError('Child must be of type Node.');
|
||||
}
|
||||
if (insertIndex < 0 || insertIndex > node.children.length) {
|
||||
throw new Error('Invalid index.');
|
||||
}
|
||||
|
||||
child.parent = node;
|
||||
node.children.splice(insertIndex, 0, child);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* parse tree from javascript object
|
||||
*/
|
||||
parse(model) {
|
||||
var node = new Node({
|
||||
name: model.name,
|
||||
type: model.type,
|
||||
isExpanded: model.isExpanded
|
||||
});
|
||||
this.root = node;
|
||||
for (let child of model.children) {
|
||||
this.addChildToNode(node, this.parseNode(child));
|
||||
}
|
||||
}
|
||||
|
||||
parseFromList(rootObj, nodeList) {
|
||||
var root = new Node({
|
||||
name: rootObj.name,
|
||||
type: rootObj.type,
|
||||
isExpanded: rootObj.isExpanded
|
||||
});
|
||||
this.root = root;
|
||||
|
||||
var map = new Map();
|
||||
map.set(root.name, root);
|
||||
|
||||
function joinPath(parent_path, name) {
|
||||
if (parent_path === "/")
|
||||
return parent_path + name;
|
||||
else
|
||||
return parent_path + "/" + name;
|
||||
}
|
||||
|
||||
var treeNodeList = []
|
||||
for (let nodeObj of nodeList) {
|
||||
var node = new Node({
|
||||
name: nodeObj.name,
|
||||
type: nodeObj.type,
|
||||
isExpanded: false
|
||||
});
|
||||
node.parent_path = nodeObj.parent_path;
|
||||
treeNodeList.push(node);
|
||||
if (nodeObj.type === "dir") {
|
||||
map.set(joinPath(nodeObj.parent_path, nodeObj.name), node);
|
||||
}
|
||||
}
|
||||
|
||||
for (let node of treeNodeList) {
|
||||
let p = map.get(node.parent_path);
|
||||
if (p === undefined) {
|
||||
console.log("warning: node " + node.parent_path + " not exist");
|
||||
} else {
|
||||
this.addChildToNode(p, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseNode(model) {
|
||||
var node = new Node({
|
||||
name: model.name,
|
||||
type: model.type,
|
||||
isExpanded: model.isExpanded
|
||||
});
|
||||
if (model.children instanceof Array) {
|
||||
for (let child of model.children) {
|
||||
this.addChildToNode(node, this.parseNode(child));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default Tree;
|
16
frontend/src/components/utils.js
Normal file
16
frontend/src/components/utils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const keyCodes = {
|
||||
esc: 27,
|
||||
space: 32,
|
||||
tab: 9,
|
||||
up: 38,
|
||||
down: 40
|
||||
}
|
||||
|
||||
export function bytesToSize(bytes) {
|
||||
if(bytes < 0) return '--'
|
||||
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
if (bytes === 0) return bytes + sizes[0]
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)), 10)
|
||||
if (i === 0) return bytes + ' ' + sizes[i]
|
||||
return (bytes / (1000 ** i)).toFixed(1) + ' ' + sizes[i]
|
||||
}
|
144
frontend/src/css/initial-style.css
Normal file
144
frontend/src/css/initial-style.css
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
/**** article ****/
|
||||
.article {
|
||||
padding:40px 60px 40px 60px;
|
||||
font-size:14px;
|
||||
line-height:1.6;
|
||||
color:#333;
|
||||
}
|
||||
.article h2,
|
||||
.article h3,
|
||||
.article h4,
|
||||
.article h5,
|
||||
.article h6 {
|
||||
margin: 1.2em 0 0.4em;
|
||||
color:#333;
|
||||
font-weight:bold;
|
||||
}
|
||||
.article h2 {
|
||||
border-bottom: 1px solid #ccc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.article h1 + p, .article h2 + p, .article h3 + p, .article h4 + p, .article h5 + p, .article h6 + p,
|
||||
.article h1 + pre, .article h2 + pre, .article h3 + pre, .article h4 + pre, .article h5 + pre, .article h6 + pre,
|
||||
.article h1 + ul, .article h2 + ul, .article h3 + ul, .article h4 + ul, .article h5 + ul, .article h6 + ul,
|
||||
.article h1 + ol, .article h2 + ol, .article h3 + ol, .article h4 + ol, .article h5 + ol, .article h6 + ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
.article p {
|
||||
margin:0.8em 0;
|
||||
}
|
||||
.article ul {
|
||||
list-style-type:disc;
|
||||
}
|
||||
.article ul,
|
||||
.article ol {
|
||||
padding-left:2em;
|
||||
margin:0.5em 0;
|
||||
}
|
||||
.article li p:first-child {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.article li.task-list-item p:nth-child(2) {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.article li.task-list-item {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
}
|
||||
.article li.task-list-item input[type="checkbox"] {
|
||||
position: absolute;
|
||||
left: -1.8em;
|
||||
top: 0.4em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.article input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.article pre {
|
||||
font-size:13px;
|
||||
padding:16px;
|
||||
background:#f5f7fa;
|
||||
border-radius:3px;
|
||||
-moz-border-radius:3px;
|
||||
-webkit-border-radius:3px;
|
||||
margin:1em 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.article .code p {
|
||||
white-space:pre-wrap;
|
||||
padding:0;
|
||||
margin:0;
|
||||
border:none;
|
||||
}
|
||||
|
||||
.article .html-element.active {
|
||||
border: 1px solid #eb8205;
|
||||
}
|
||||
.article span.html-element {
|
||||
display:inline-block;
|
||||
margin-left:1px;
|
||||
margin-right:1px;
|
||||
background: #f4f4f4;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.article div.html-element {
|
||||
background: #f4f4f4;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 2px;
|
||||
margin:0.8em 0;
|
||||
}
|
||||
|
||||
.article a {
|
||||
font-weight:normal;
|
||||
}
|
||||
|
||||
.article blockquote {
|
||||
color: #777;
|
||||
padding: 0 15px;
|
||||
border-left: 4px solid #DDD;
|
||||
margin: 1.2em 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.article table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-left: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.article tr:nth-child(2n) {
|
||||
background-color: #F8F8F8;
|
||||
}
|
||||
.article th,
|
||||
.article td {
|
||||
padding:6px 13px;
|
||||
}
|
||||
.article table p {
|
||||
margin: 0;
|
||||
}
|
||||
.article table tr, .article table th {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.article table td, .article table th {
|
||||
flex: 1;
|
||||
padding: 10px 10px;
|
||||
border-width: 0 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: #ccc;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.article hr.active {
|
||||
border-top: 1px solid #eb8205;
|
||||
}
|
125
frontend/src/css/side-panel.css
Normal file
125
frontend/src/css/side-panel.css
Normal file
@@ -0,0 +1,125 @@
|
||||
/*tree view */
|
||||
.tree-node:not([type = 'dir']):hover {
|
||||
background-color: rgb(255,239,178);
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
min-width: -moz-max-content;
|
||||
min-width: -webkit-max-content;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tree-node-inner {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
the main reason to icon can not be align is that .folder has a real width it take the place
|
||||
of .tree-node-inner causing tree-node-icon not aligned , use absolute can make sure .tree-node-icon
|
||||
is always at the far left of .tree-node-inner
|
||||
*/
|
||||
.folder-toggle-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
margin-right: 0.4rem;
|
||||
margin-left: 0.1rem;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
user-select: none;
|
||||
height:100%;
|
||||
}
|
||||
.side-panel .nav {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
min-width: 125px;
|
||||
height: 36px;
|
||||
}
|
||||
.side-panel .nav-link {
|
||||
color: #888;
|
||||
}
|
||||
.side-panel .nav-link.active {
|
||||
color: #eb8205;
|
||||
}
|
||||
.side-panel-content {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
height: calc(100% - 36px);
|
||||
overflow: auto;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.image-view {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
z-index: 1004;
|
||||
box-shadow: 0 0 10px #aaa;
|
||||
border-radius: 3px;
|
||||
line-height: 150px;
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-view img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.image-view i {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
line-height: 150px;
|
||||
font-size: 30px;
|
||||
color: #eb8205;
|
||||
-moz-animation: rotate 1.5s ease infinite;
|
||||
-webkit-animation: rotate 1.5s ease infinite;
|
||||
animation: rotate 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.outline-h2 {
|
||||
margin-left: 20px;
|
||||
line-height: 2.5;
|
||||
color:#364149;
|
||||
white-space: nowrap;
|
||||
cursor:pointer;
|
||||
}
|
||||
.outline-h2:hover {
|
||||
color: #eb8205;
|
||||
}
|
||||
.outline-h3 {
|
||||
margin-left: 40px;
|
||||
line-height: 2.5;
|
||||
color:#364149;
|
||||
white-space: nowrap;
|
||||
cursor:pointer;
|
||||
}
|
||||
.outline-h3:hover {
|
||||
color: #eb8205;
|
||||
}
|
||||
.tree-view {
|
||||
padding-left: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
89
frontend/src/css/wiki.css
Normal file
89
frontend/src/css/wiki.css
Normal file
@@ -0,0 +1,89 @@
|
||||
.wiki-side-nav {
|
||||
flex:auto;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
overflow:hidden; /* for ff */
|
||||
border-right:1px solid #eee;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.wiki-pages-heading {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
text-align:center;
|
||||
padding:.5rem 0rem;
|
||||
border-bottom:1px solid #e8e8e8;
|
||||
line-height: 1.5;
|
||||
height:40px;
|
||||
}
|
||||
.wiki-pages-container {
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.wiki-pages-container:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
.wiki-pages-container .tree-view {
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
.wiki-md-viewer-rendered-content {
|
||||
padding: 30px 0 0;
|
||||
}
|
||||
.wiki-pages-container .tree-node-inner {
|
||||
line-height: 1.625;
|
||||
}
|
||||
.wiki-pages-container .folder-toggle-icon {
|
||||
color: #c0c0c0;
|
||||
line-height: 1.625;
|
||||
}
|
||||
.wiki-pages-container .tree-node-icon {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
.wiki-main .cur-view-path {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.wiki-main .cur-view-path::after {
|
||||
display:none;
|
||||
}
|
||||
.wiki-main .cur-view-main-con {
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-top:40px;
|
||||
font-size:12px;
|
||||
color: #666;
|
||||
}
|
@@ -7,7 +7,7 @@ import i18n from './i18n';
|
||||
import './assets/css/fa-solid.css';
|
||||
import './assets/css/fa-regular.css';
|
||||
import './assets/css/fontawesome.css';
|
||||
import './assets/css/seafile-ui.css';
|
||||
import 'seafile-ui';
|
||||
import './index.css';
|
||||
|
||||
let lang = window.app.pageOptions.lang
|
||||
|
154
frontend/src/wiki.js
Normal file
154
frontend/src/wiki.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import SidePanel from './components/SidePanel';
|
||||
import MainPanel from './components/MainPanel';
|
||||
import moment from 'moment';
|
||||
import cookie from 'react-cookies';
|
||||
import { SeafileAPI } from 'seafile-js';
|
||||
import { slug, repoID, serviceUrl, initialFilePath, siteRoot } from './components/constance';
|
||||
import './assets/css/fa-solid.css';
|
||||
import './assets/css/fa-regular.css';
|
||||
import './assets/css/fontawesome.css';
|
||||
import 'seafile-ui';
|
||||
import './css/side-panel.css';
|
||||
import './css/wiki.css';
|
||||
|
||||
// init seafileAPI
|
||||
let seafileAPI = new SeafileAPI();
|
||||
let xcsrfHeaders = cookie.load('csrftoken');
|
||||
seafileAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
|
||||
|
||||
class EditorUtilities {
|
||||
getFiles() {
|
||||
return seafileAPI.listWikiDir(slug).then(items => {
|
||||
const files = items.data.dir_file_list.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
type: item.type === 'dir' ? 'dir' : 'file',
|
||||
isExpanded: item.type === 'dir' ? true : false,
|
||||
parent_path: item.parent_dir,
|
||||
}
|
||||
})
|
||||
return files;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const editorUtilities = new EditorUtilities();
|
||||
|
||||
class Wiki extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
content: '',
|
||||
closeSideBar: false,
|
||||
fileName: '',
|
||||
filePath: '',
|
||||
latestContributor: '',
|
||||
lastModified: '',
|
||||
permission: ''
|
||||
};
|
||||
window.onpopstate = this.onpopstate;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFile(initialFilePath);
|
||||
}
|
||||
|
||||
fileNameFromPath(filePath) {
|
||||
let index = filePath.lastIndexOf("/");
|
||||
if (index == -1) {
|
||||
return "";
|
||||
} else {
|
||||
return filePath.substring(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
isInternalMarkdownLink(url) {
|
||||
var re = new RegExp(serviceUrl + '/lib/' + repoID + '/file' + '.*\.md');
|
||||
return re.test(url);
|
||||
}
|
||||
|
||||
getPathFromInternalMarkdownLink(url) {
|
||||
var re = new RegExp(serviceUrl + '/lib/' + repoID + '/file' + "(.*\.md)");
|
||||
var array = re.exec(url);
|
||||
var path = decodeURIComponent(array[1]);
|
||||
return path;
|
||||
}
|
||||
|
||||
onLinkClick = (event) => {
|
||||
const url = event.target.href;
|
||||
if (this.isInternalMarkdownLink(url)) {
|
||||
let path = this.getPathFromInternalMarkdownLink(url);
|
||||
this.loadFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
onFileClick = (e, node) => {
|
||||
if (node.isMarkdown()) {
|
||||
this.loadFile(node.path);
|
||||
}
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
seafileAPI.getWikiFileContent(slug, filePath)
|
||||
.then(res => {
|
||||
this.setState({
|
||||
content: res.data.content,
|
||||
latestContributor: res.data.latest_contributor,
|
||||
lastModified: moment.unix(res.data.last_modified).fromNow(),
|
||||
permission: res.data.permission,
|
||||
fileName: this.fileNameFromPath(filePath),
|
||||
filePath: filePath
|
||||
})
|
||||
})
|
||||
|
||||
let fileUrl = '/wikis/' + slug + filePath;
|
||||
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
|
||||
}
|
||||
|
||||
onpopstate = (event) => {
|
||||
this.loadFile(event.state.filePath);
|
||||
}
|
||||
|
||||
onMenuClick = () => {
|
||||
this.setState({
|
||||
closeSideBar: !this.state.closeSideBar,
|
||||
})
|
||||
}
|
||||
|
||||
onCloseSide = () => {
|
||||
this.setState({
|
||||
closeSideBar: !this.state.closeSideBar,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="main" className="wiki-main">
|
||||
<SidePanel
|
||||
onFileClick={this.onFileClick}
|
||||
closeSideBar={this.state.closeSideBar}
|
||||
onCloseSide ={this.onCloseSide}
|
||||
editorUtilities={editorUtilities}
|
||||
/>
|
||||
<MainPanel
|
||||
content={this.state.content}
|
||||
fileName={this.state.fileName}
|
||||
filePath={this.state.filePath}
|
||||
onLinkClick={this.onLinkClick}
|
||||
onMenuClick={this.onMenuClick}
|
||||
latestContributor={this.state.latestContributor}
|
||||
lastModified={this.state.lastModified}
|
||||
seafileAPI={seafileAPI}
|
||||
permission={this.state.permission}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render (
|
||||
<Wiki />,
|
||||
document.getElementById('wrapper')
|
||||
)
|
Reference in New Issue
Block a user