mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 15:09:14 +00:00
Rewrite wiki page with react (#2258)
This commit is contained in:
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;
|
Reference in New Issue
Block a user