2018-05-02 06:09:58 +00:00
|
|
|
import React from 'react';
|
2018-07-27 03:48:21 +00:00
|
|
|
import SeafileEditor from '@seafile/seafile-editor';
|
2018-05-02 06:09:58 +00:00
|
|
|
import 'whatwg-fetch';
|
2019-03-14 02:15:25 +00:00
|
|
|
import { Value, Document, Block } from 'slate';
|
2019-02-14 02:41:59 +00:00
|
|
|
import { seafileAPI } from './utils/seafile-api';
|
2018-12-19 08:07:56 +00:00
|
|
|
import { Utils } from './utils/utils';
|
2019-03-14 02:15:25 +00:00
|
|
|
import { gettext } from './utils/constants';
|
2019-03-14 10:14:11 +00:00
|
|
|
import io from 'socket.io-client';
|
|
|
|
import toaster from './components/toast';
|
2019-01-16 03:49:00 +00:00
|
|
|
import ModalPortal from './components/modal-portal';
|
|
|
|
import EditFileTagDialog from './components/dialog/edit-filetag-dialog';
|
|
|
|
import ListRelatedFileDialog from './components/dialog/list-related-file-dialog';
|
|
|
|
import AddRelatedFileDialog from './components/dialog/add-related-file-dialog';
|
2019-02-25 10:17:04 +00:00
|
|
|
import ShareDialog from './components/dialog/share-dialog';
|
2019-03-14 02:15:25 +00:00
|
|
|
import CommentDialog from './components/markdown-view/comment-dialog';
|
2019-03-11 13:08:25 +00:00
|
|
|
import MarkdownViewerSlate from '@seafile/seafile-editor/dist/viewer/markdown-viewer-slate';
|
2019-03-14 10:14:11 +00:00
|
|
|
import { serialize, deserialize } from '@seafile/seafile-editor/dist/utils/slate2markdown';
|
|
|
|
import LocalDraftDialog from '@seafile/seafile-editor/dist/components/local-draft-dialog';
|
2019-03-14 02:15:25 +00:00
|
|
|
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
|
2019-03-11 13:08:25 +00:00
|
|
|
import MarkdownViewerToolbar from './components/toolbar/markdown-viewer-toolbar';
|
2019-03-15 04:11:32 +00:00
|
|
|
import HistoryList from './components/markdown-view/history-list';
|
|
|
|
import CommentPanel from './components/file-view/comment-panel';
|
|
|
|
import OutlineView from './components/markdown-view/outline';
|
2019-03-14 02:15:25 +00:00
|
|
|
import Loading from './components/loading';
|
2019-03-14 10:14:11 +00:00
|
|
|
import { findRange } from '@seafile/slate-react';
|
2019-03-14 02:15:25 +00:00
|
|
|
|
|
|
|
import './css/markdown-viewer/markdown-editor.css';
|
2019-03-11 13:08:25 +00:00
|
|
|
|
|
|
|
const CryptoJS = require('crypto-js');
|
2019-03-15 04:11:32 +00:00
|
|
|
const URL = require('url-parse');
|
2019-03-14 10:14:11 +00:00
|
|
|
const { repoID, repoName, filePath, fileName, mode, draftID, isDraft, hasDraft } = window.app.pageOptions;
|
2019-02-25 10:17:04 +00:00
|
|
|
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
|
2018-07-27 03:48:21 +00:00
|
|
|
const userInfo = window.app.userInfo;
|
2019-02-25 10:17:04 +00:00
|
|
|
const userName = userInfo.username;
|
|
|
|
let dirPath = '/';
|
2018-05-02 06:09:58 +00:00
|
|
|
|
|
|
|
function getImageFileNameWithTimestamp() {
|
|
|
|
var d = Date.now();
|
2018-09-29 10:32:53 +00:00
|
|
|
return 'image-' + d.toString() + '.png';
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
|
|
|
|
2018-10-15 07:51:29 +00:00
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
class EditorUtilities {
|
2018-07-27 03:48:21 +00:00
|
|
|
|
|
|
|
constructor () {
|
|
|
|
this.repoID = repoID;
|
|
|
|
this.filePath = filePath;
|
|
|
|
this.serviceUrl = serviceUrl;
|
2019-03-05 05:50:12 +00:00
|
|
|
this.name = userInfo.name;
|
2018-12-29 08:28:04 +00:00
|
|
|
this.contact_email = userInfo.contact_email;
|
2019-02-27 04:22:13 +00:00
|
|
|
this.fileName = fileName;
|
|
|
|
this.userName = userName;
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
saveContent(content) {
|
2018-07-27 03:48:21 +00:00
|
|
|
return (
|
|
|
|
seafileAPI.getUpdateLink(repoID, dirPath).then((res) => {
|
|
|
|
const uploadLink = res.data;
|
2018-09-29 10:32:53 +00:00
|
|
|
return seafileAPI.updateFile(uploadLink, filePath, fileName, content);
|
2018-05-02 06:09:58 +00:00
|
|
|
})
|
2018-09-29 10:32:53 +00:00
|
|
|
);
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
|
|
|
|
2019-02-18 12:26:55 +00:00
|
|
|
unStarItem () {
|
2018-07-27 03:48:21 +00:00
|
|
|
return (
|
2019-02-18 12:26:55 +00:00
|
|
|
seafileAPI.unStarItem(this.repoID, this.filePath)
|
2018-09-29 10:32:53 +00:00
|
|
|
);
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
|
|
|
|
2019-02-18 12:26:55 +00:00
|
|
|
starItem() {
|
2018-07-27 03:48:21 +00:00
|
|
|
return (
|
2019-02-18 12:26:55 +00:00
|
|
|
seafileAPI.starItem(this.repoID, this.filePath)
|
2018-09-29 10:32:53 +00:00
|
|
|
);
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getParentDectionaryUrl() {
|
|
|
|
let parentPath = this.filePath.substring(0, this.filePath.lastIndexOf('/'));
|
2018-12-19 08:07:56 +00:00
|
|
|
let libName = encodeURIComponent(repoName);
|
|
|
|
let path = Utils.encodePath(parentPath);
|
2019-02-11 07:10:57 +00:00
|
|
|
return this.serviceUrl + '/library/' + this.repoID + '/' + libName + path;
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
_getImageURL(fileName) {
|
2018-12-29 08:28:04 +00:00
|
|
|
const url = this.serviceUrl + '/lib/' + repoID + '/file/images/auto-upload/' + fileName + '?raw=1';
|
2018-05-02 06:09:58 +00:00
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadImage = (imageFile) => {
|
2018-07-27 03:48:21 +00:00
|
|
|
return (
|
|
|
|
seafileAPI.getUploadLink(repoID, dirPath).then((res) => {
|
|
|
|
let uploadLinkComponent = res.data;
|
2018-09-29 10:32:53 +00:00
|
|
|
const uploadLink = uploadLinkComponent + '?ret-json=1';
|
2018-05-02 06:09:58 +00:00
|
|
|
const name = getImageFileNameWithTimestamp();
|
|
|
|
const blob = imageFile.slice(0, -1, 'image/png');
|
|
|
|
const newFile = new File([blob], name, {type: 'image/png'});
|
|
|
|
const formData = new FormData();
|
2018-09-29 10:32:53 +00:00
|
|
|
formData.append('parent_dir', '/');
|
2018-12-29 08:28:04 +00:00
|
|
|
formData.append('relative_path', 'images/auto-upload');
|
2018-09-29 10:32:53 +00:00
|
|
|
formData.append('file', newFile);
|
|
|
|
return {uploadLink, formData};
|
2018-07-27 03:48:21 +00:00
|
|
|
}).then(({ uploadLink, formData}) => {
|
2018-09-29 10:32:53 +00:00
|
|
|
return seafileAPI.uploadImage(uploadLink, formData);
|
2018-07-27 03:48:21 +00:00
|
|
|
}).then ((res) => {
|
|
|
|
let resArr = res.data[0];
|
|
|
|
let filename = resArr.name;
|
2018-05-02 06:09:58 +00:00
|
|
|
return this._getImageURL(filename);
|
2018-07-27 03:48:21 +00:00
|
|
|
})
|
2018-09-29 10:32:53 +00:00
|
|
|
);
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
|
2018-11-05 14:03:13 +00:00
|
|
|
uploadLocalImage = (imageFile) => {
|
|
|
|
return (
|
|
|
|
seafileAPI.getUploadLink(repoID, dirPath).then((res) => {
|
|
|
|
const uploadLink = res.data + '?ret-json=1';
|
|
|
|
const newFile = new File([imageFile], imageFile.name, {type: imageFile.type});
|
|
|
|
const formData = new FormData();
|
|
|
|
formData.append('parent_dir', '/');
|
2018-12-29 08:28:04 +00:00
|
|
|
formData.append('relative_path', 'images/auto-upload');
|
2018-11-05 14:03:13 +00:00
|
|
|
formData.append('file', newFile);
|
|
|
|
return seafileAPI.uploadImage(uploadLink, formData);
|
|
|
|
}).then ((res) => {
|
|
|
|
return this._getImageURL(res.data[0].name);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
getFileURL(fileNode) {
|
|
|
|
var url;
|
2018-08-28 03:34:03 +00:00
|
|
|
if (fileNode.type === 'file') {
|
|
|
|
if (fileNode.isImage()) {
|
2019-01-11 02:58:10 +00:00
|
|
|
url = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(fileNode.path()) + '?raw=1';
|
2018-08-28 03:34:03 +00:00
|
|
|
} else {
|
2019-01-11 02:58:10 +00:00
|
|
|
url = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(fileNode.path());
|
2018-08-28 03:34:03 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
} else {
|
2019-01-11 02:58:10 +00:00
|
|
|
url = serviceUrl + '/library/' + repoID + '/' + encodeURIComponent(repoName) + Utils.encodePath(fileNode.path());
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
isInternalFileLink(url) {
|
2019-01-11 02:58:10 +00:00
|
|
|
var re = new RegExp(this.serviceUrl + '/lib/[0-9a-f-]{36}/file.*');
|
2018-05-02 06:09:58 +00:00
|
|
|
return re.test(url);
|
|
|
|
}
|
2018-08-28 03:34:03 +00:00
|
|
|
|
|
|
|
isInternalDirLink(url) {
|
2019-01-11 02:58:10 +00:00
|
|
|
var re = new RegExp(serviceUrl + '/library/' + '[0-9a-f\-]{36}.*');
|
2018-08-28 03:34:03 +00:00
|
|
|
return re.test(url);
|
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
getFiles() {
|
2018-07-27 03:48:21 +00:00
|
|
|
return seafileAPI.listDir(repoID, dirPath, { recursive: true} ).then((response) => {
|
2019-01-22 10:27:51 +00:00
|
|
|
var files = response.data.dirent_list.map((item) => {
|
2018-07-27 03:48:21 +00:00
|
|
|
return {
|
|
|
|
name: item.name,
|
|
|
|
type: item.type === 'dir' ? 'dir' : 'file',
|
|
|
|
parent_path: item.parent_dir
|
2018-09-29 10:32:53 +00:00
|
|
|
};
|
|
|
|
});
|
2018-07-27 03:48:21 +00:00
|
|
|
return files;
|
2018-09-29 10:32:53 +00:00
|
|
|
});
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
2018-08-06 10:29:12 +00:00
|
|
|
|
|
|
|
getFileHistory() {
|
2019-03-14 02:15:25 +00:00
|
|
|
return seafileAPI.getFileHistory(repoID, filePath);
|
2018-08-06 10:29:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getFileInfo() {
|
2019-03-14 02:15:25 +00:00
|
|
|
return seafileAPI.getFileInfo(repoID, filePath);
|
2018-08-06 10:29:12 +00:00
|
|
|
}
|
2018-08-22 10:07:32 +00:00
|
|
|
|
2018-12-28 06:25:25 +00:00
|
|
|
getRepoInfo(newRepoID) {
|
2019-03-14 02:15:25 +00:00
|
|
|
return seafileAPI.getRepoInfo(newRepoID);
|
2018-12-28 06:25:25 +00:00
|
|
|
}
|
|
|
|
|
2018-08-22 10:07:32 +00:00
|
|
|
getInternalLink() {
|
2018-09-29 10:32:53 +00:00
|
|
|
return seafileAPI.getInternalLink(repoID, filePath);
|
2018-08-22 10:07:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getShareLink() {
|
|
|
|
return seafileAPI.getShareLink(repoID, filePath);
|
|
|
|
}
|
|
|
|
|
2018-12-19 02:17:07 +00:00
|
|
|
createShareLink (repoID, filePath, userPassword, userValidDays, permissions) {
|
|
|
|
return seafileAPI.createShareLink(repoID, filePath, userPassword, userValidDays, permissions);
|
2018-08-22 10:07:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
deleteShareLink(token){
|
2018-09-29 10:32:53 +00:00
|
|
|
return seafileAPI.deleteShareLink(token);
|
2018-08-22 10:07:32 +00:00
|
|
|
}
|
|
|
|
|
2018-09-19 02:47:56 +00:00
|
|
|
getDraftKey() {
|
2018-09-29 10:32:53 +00:00
|
|
|
return (repoID + filePath);
|
|
|
|
}
|
2018-09-27 04:01:56 +00:00
|
|
|
|
|
|
|
getFileContent(url) {
|
|
|
|
return seafileAPI.getFileContent(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
listFileHistoryRecords(page, perPage) {
|
2019-03-14 02:15:25 +00:00
|
|
|
return seafileAPI.listFileHistoryRecords(repoID, filePath, page, perPage);
|
2018-09-27 04:01:56 +00:00
|
|
|
}
|
|
|
|
|
2019-03-04 11:50:11 +00:00
|
|
|
getFileHistoryVersion(commitID, filePath) {
|
2018-09-27 04:01:56 +00:00
|
|
|
return seafileAPI.getFileRevision(repoID, commitID, filePath);
|
|
|
|
}
|
2018-10-15 07:51:29 +00:00
|
|
|
|
2018-10-30 03:07:01 +00:00
|
|
|
getCommentsNumber() {
|
2019-02-27 04:22:13 +00:00
|
|
|
return seafileAPI.getCommentsNumber(this.repoID, filePath);
|
2018-10-30 03:07:01 +00:00
|
|
|
}
|
|
|
|
|
2018-11-16 09:57:35 +00:00
|
|
|
postComment(comment, detail) {
|
|
|
|
return seafileAPI.postComment(this.repoID, this.filePath, comment, detail);
|
2018-10-30 03:07:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
listComments() {
|
|
|
|
return seafileAPI.listComments(this.repoID, this.filePath);
|
|
|
|
}
|
|
|
|
|
2019-03-14 02:15:25 +00:00
|
|
|
updateComment(commentID, resolved, detail, newComment) {
|
|
|
|
return seafileAPI.updateComment(this.repoID, commentID, resolved, detail, newComment);
|
2018-10-30 03:07:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
deleteComment(commentID) {
|
|
|
|
return seafileAPI.deleteComment(this.repoID, commentID);
|
|
|
|
}
|
|
|
|
|
|
|
|
getUserAvatar(size) {
|
|
|
|
return seafileAPI.getUserAvatar(userName, size);
|
|
|
|
}
|
2018-11-01 09:52:59 +00:00
|
|
|
|
|
|
|
goDraftPage() {
|
2019-03-05 07:37:51 +00:00
|
|
|
window.location.href = serviceUrl + '/drafts/' + draftID + '/';
|
2018-11-01 09:52:59 +00:00
|
|
|
}
|
2018-11-02 03:03:10 +00:00
|
|
|
|
|
|
|
createDraftFile() {
|
|
|
|
return seafileAPI.createDraft(repoID, filePath).then(res => {
|
2019-03-07 09:15:30 +00:00
|
|
|
window.location.href = serviceUrl + '/lib/' + res.data.origin_repo_id + '/file' + Utils.encodePath(res.data.draft_file_path) + '?mode=edit';
|
2018-11-22 09:00:23 +00:00
|
|
|
});
|
2018-11-02 03:03:10 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
|
2019-03-05 07:37:51 +00:00
|
|
|
publishDraftFile() {
|
|
|
|
return seafileAPI.publishDraft(draftID).then(res => {
|
2019-03-07 09:15:30 +00:00
|
|
|
window.location.href = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(res.data.published_file_path);
|
2019-03-05 07:37:51 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-12-29 08:28:04 +00:00
|
|
|
fileMetaData() {
|
|
|
|
return seafileAPI.fileMetaData(repoID, filePath);
|
|
|
|
}
|
2019-01-16 03:49:00 +00:00
|
|
|
|
|
|
|
listFileTags = () => {
|
|
|
|
return seafileAPI.listFileTags(repoID, filePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
listRepoTags = () => {
|
|
|
|
return seafileAPI.listRepoTags(repoID);
|
|
|
|
}
|
2019-02-28 10:33:35 +00:00
|
|
|
|
|
|
|
markdownLint(slateValue) {
|
|
|
|
return seafileAPI.markdownLint(slateValue);
|
|
|
|
}
|
2018-11-09 10:03:10 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
|
|
|
|
const editorUtilities = new EditorUtilities();
|
|
|
|
|
2018-09-21 06:16:15 +00:00
|
|
|
class MarkdownEditor extends React.Component {
|
2018-05-02 06:09:58 +00:00
|
|
|
constructor(props) {
|
2018-09-29 10:32:53 +00:00
|
|
|
super(props);
|
2019-03-11 13:08:25 +00:00
|
|
|
this.timer = null;
|
|
|
|
this.localDraft = '';
|
|
|
|
this.autoSave = false;
|
|
|
|
this.draftRichValue = '';
|
|
|
|
this.draftPlainValue = '';
|
2018-09-29 10:32:53 +00:00
|
|
|
this.state = {
|
|
|
|
markdownContent: '',
|
2019-03-14 02:15:25 +00:00
|
|
|
oldMarkdownContent: '',
|
2018-09-29 10:32:53 +00:00
|
|
|
loading: true,
|
|
|
|
mode: 'editor',
|
|
|
|
fileInfo: {
|
|
|
|
repoID: repoID,
|
|
|
|
name: fileName,
|
|
|
|
path: filePath,
|
|
|
|
mtime: null,
|
|
|
|
size: 0,
|
|
|
|
starred: false,
|
|
|
|
permission: '',
|
|
|
|
lastModifier: '',
|
2018-12-29 08:28:04 +00:00
|
|
|
id: '',
|
2018-09-29 10:32:53 +00:00
|
|
|
},
|
2019-03-11 13:08:25 +00:00
|
|
|
editorMode: 'viewer',
|
2018-09-29 10:32:53 +00:00
|
|
|
collabServer: seafileCollabServer ? seafileCollabServer : null,
|
2018-12-28 06:25:25 +00:00
|
|
|
relatedFiles: [],
|
2019-01-16 03:49:00 +00:00
|
|
|
fileTagList: [],
|
2019-03-11 13:08:25 +00:00
|
|
|
localDraftDialog: false,
|
2019-01-16 03:49:00 +00:00
|
|
|
showRelatedFileDialog: false,
|
|
|
|
showEditFileTagDialog: false,
|
|
|
|
showAddRelatedFileDialog: false,
|
|
|
|
showMarkdownEditorDialog: false,
|
2019-02-25 10:17:04 +00:00
|
|
|
showShareLinkDialog: false,
|
2019-03-14 02:15:25 +00:00
|
|
|
showCommentDialog: false,
|
2019-03-11 13:08:25 +00:00
|
|
|
showDraftSaved: false,
|
|
|
|
collabUsers: userInfo ?
|
|
|
|
[{user: userInfo, is_editing: false}] : [],
|
|
|
|
commentsNumber: null,
|
2019-03-14 02:15:25 +00:00
|
|
|
loadingDiff: false,
|
|
|
|
value: null,
|
2019-03-15 04:11:32 +00:00
|
|
|
isShowComments: false,
|
|
|
|
isShowHistory: false,
|
|
|
|
isShowOutline: true,
|
2018-09-29 10:32:53 +00:00
|
|
|
};
|
2019-03-11 13:08:25 +00:00
|
|
|
|
|
|
|
if (this.state.collabServer) {
|
|
|
|
const socket = io(this.state.collabServer);
|
|
|
|
this.socket = socket;
|
|
|
|
socket.on('presence', (data) => this.receivePresenceData(data));
|
|
|
|
socket.on('repo_update', (data) => this.receiveUpdateData(data));
|
|
|
|
socket.on('connect', () => {
|
|
|
|
this.socket_id = socket.id;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
emitSwitchEditor = (is_editing=false) => {
|
|
|
|
if (userInfo && this.state.collabServer) {
|
|
|
|
const { repoID, path } = this.state.fileInfo;
|
|
|
|
this.socket.emit('presence', {
|
|
|
|
request: 'editing',
|
2019-03-14 10:14:11 +00:00
|
|
|
doc_id: CryptoJS.MD5(repoID+path).toString(),
|
|
|
|
user: userInfo,
|
|
|
|
is_editing,
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
receiveUpdateData (data) {
|
|
|
|
let currentTime = new Date();
|
|
|
|
if ((parseFloat(currentTime - this.lastModifyTime)/1000) <= 5) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
editorUtilities.fileMetaData().then((res) => {
|
|
|
|
if (res.data.id !== this.state.fileInfo.id) {
|
|
|
|
toaster.notify(
|
|
|
|
<span>
|
2019-03-14 02:15:25 +00:00
|
|
|
{gettext('This file has been updated.')}
|
|
|
|
<a href='' >{' '}{gettext('Refresh')}</a>
|
2019-03-11 13:08:25 +00:00
|
|
|
</span>,
|
|
|
|
{id: 'repo_updated', duration: 3600});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
receivePresenceData(data) {
|
|
|
|
switch(data.response) {
|
|
|
|
case 'user_join':
|
|
|
|
toaster.notify(`user ${data.user.name} joined`, {
|
|
|
|
duration: 3
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
|
|
|
|
case 'user_left':
|
|
|
|
toaster.notify(`user ${data.user.name} left`, {
|
|
|
|
duration: 3
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
case 'update_users':
|
|
|
|
for (var prop in data.users) {
|
|
|
|
if (data.users.hasOwnProperty(prop)) {
|
|
|
|
if (prop === this.socket_id) {
|
|
|
|
data.users[prop]['myself'] = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.setState({collabUsers: Object.values(data.users)});
|
|
|
|
return;
|
|
|
|
case 'user_editing':
|
|
|
|
toaster.danger(`user ${data.user.name} is editing this file!`, {
|
|
|
|
duration: 3
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
default:
|
|
|
|
console.log('unknown response type: ' + data.response);
|
|
|
|
return;
|
|
|
|
}
|
2018-09-29 10:32:53 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
|
2019-01-16 03:49:00 +00:00
|
|
|
toggleCancel = () => {
|
|
|
|
this.setState({
|
|
|
|
showRelatedFileDialog: false,
|
|
|
|
showEditFileTagDialog: false,
|
|
|
|
showAddRelatedFileDialog: false,
|
|
|
|
showMarkdownEditorDialog: false,
|
2019-02-25 10:17:04 +00:00
|
|
|
showShareLinkDialog: false,
|
2019-03-14 02:15:25 +00:00
|
|
|
showCommentDialog: false,
|
2019-01-16 03:49:00 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-11 13:08:25 +00:00
|
|
|
setEditorMode = (type) => {
|
|
|
|
this.setState({
|
|
|
|
editorMode: type
|
2019-03-14 10:14:11 +00:00
|
|
|
});
|
2019-03-11 13:08:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setDraftValue = (type, value) => {
|
|
|
|
if (type === 'rich') {
|
2019-03-14 10:14:11 +00:00
|
|
|
this.draftRichValue = value;
|
2019-03-11 13:08:25 +00:00
|
|
|
} else {
|
|
|
|
this.draftPlainValue = value;
|
|
|
|
}
|
|
|
|
}
|
2019-03-14 02:15:25 +00:00
|
|
|
|
2019-03-11 13:08:25 +00:00
|
|
|
setContent = (str) => {
|
2019-03-14 02:15:25 +00:00
|
|
|
let value = deserialize(str);
|
2019-03-11 13:08:25 +00:00
|
|
|
this.setState({
|
2019-03-14 02:15:25 +00:00
|
|
|
markdownContent: str,
|
|
|
|
value: value,
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
checkDraft = () => {
|
|
|
|
let draftKey = editorUtilities.getDraftKey();
|
|
|
|
let draft = localStorage.getItem(draftKey);
|
|
|
|
let that = this;
|
|
|
|
if (draft) {
|
|
|
|
that.setState({
|
|
|
|
localDraftDialog: true,
|
|
|
|
});
|
|
|
|
that.localDraft = draft;
|
|
|
|
localStorage.removeItem(draftKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
useDraft = () => {
|
|
|
|
this.setState({
|
|
|
|
localDraftDialog: false,
|
|
|
|
loading: false,
|
|
|
|
markdownContent: this.localDraft,
|
|
|
|
editorMode: 'rich',
|
|
|
|
});
|
|
|
|
this.emitSwitchEditor(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteDraft = () => {
|
|
|
|
if (this.state.localDraftDialog) {
|
|
|
|
this.setState({
|
|
|
|
localDraftDialog: false,
|
|
|
|
loading: false,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
let draftKey = editorUtilities.getDraftKey();
|
|
|
|
localStorage.removeItem(draftKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
clearTimer = () => {
|
|
|
|
clearTimeout(this.timer);
|
|
|
|
this.timer = null;
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:49:00 +00:00
|
|
|
closeAddRelatedFileDialog = () => {
|
|
|
|
this.setState({
|
|
|
|
showAddRelatedFileDialog: false,
|
|
|
|
showRelatedFileDialog: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
addRelatedFileToggle = () => {
|
|
|
|
this.setState({
|
|
|
|
showRelatedFileDialog: false,
|
|
|
|
showAddRelatedFileDialog: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
openDialogs = (option) => {
|
|
|
|
switch(option)
|
|
|
|
{
|
|
|
|
case 'related_files':
|
2019-03-05 05:50:12 +00:00
|
|
|
if (this.state.relatedFiles.length > 0) {
|
|
|
|
this.setState({
|
|
|
|
showRelatedFileDialog: true,
|
|
|
|
showMarkdownEditorDialog: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.setState({
|
|
|
|
showAddRelatedFileDialog: true,
|
|
|
|
showMarkdownEditorDialog: true,
|
|
|
|
});
|
|
|
|
}
|
2019-01-16 03:49:00 +00:00
|
|
|
break;
|
|
|
|
case 'tags':
|
|
|
|
this.setState({
|
|
|
|
showEditFileTagDialog: true,
|
|
|
|
showMarkdownEditorDialog: true,
|
|
|
|
});
|
|
|
|
break;
|
2019-02-25 10:17:04 +00:00
|
|
|
case 'share_link':
|
|
|
|
this.setState({
|
|
|
|
showMarkdownEditorDialog: true,
|
|
|
|
showShareLinkDialog: true,
|
|
|
|
});
|
|
|
|
break;
|
2019-03-14 02:15:25 +00:00
|
|
|
case 'comment':
|
|
|
|
this.setState({
|
|
|
|
showMarkdownEditorDialog: true,
|
|
|
|
showCommentDialog: true,
|
|
|
|
});
|
|
|
|
break;
|
2019-01-16 03:49:00 +00:00
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-11 13:08:25 +00:00
|
|
|
componentWillUnmount() {
|
|
|
|
this.socket.emit('repo_update', {
|
|
|
|
request: 'unwatch_update',
|
2019-03-14 10:14:11 +00:00
|
|
|
repo_id: this.props.c.repoID,
|
|
|
|
user: {
|
|
|
|
name: editorUtilities.name,
|
|
|
|
username: editorUtilities.username,
|
|
|
|
contact_email: editorUtilities.contact_email,
|
|
|
|
},
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
2019-03-14 02:15:25 +00:00
|
|
|
document.removeEventListener('selectionchange', this.setBtnPosition);
|
2019-03-11 13:08:25 +00:00
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
componentDidMount() {
|
2018-07-27 03:48:21 +00:00
|
|
|
|
|
|
|
seafileAPI.getFileInfo(repoID, filePath).then((res) => {
|
2018-12-29 08:28:04 +00:00
|
|
|
let { mtime, size, starred, permission, last_modifier_name, id } = res.data;
|
2018-09-29 10:32:53 +00:00
|
|
|
let lastModifier = last_modifier_name;
|
2018-08-06 10:29:12 +00:00
|
|
|
|
2018-07-27 03:48:21 +00:00
|
|
|
this.setState((prevState, props) => ({
|
|
|
|
fileInfo: {
|
|
|
|
...prevState.fileInfo,
|
|
|
|
mtime,
|
|
|
|
size,
|
2018-08-06 10:29:12 +00:00
|
|
|
starred,
|
|
|
|
permission,
|
2018-12-29 08:28:04 +00:00
|
|
|
lastModifier,
|
|
|
|
id
|
2018-07-27 03:48:21 +00:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
seafileAPI.getFileDownloadLink(repoID, filePath).then((res) => {
|
|
|
|
const downLoadUrl = res.data;
|
|
|
|
seafileAPI.getFileContent(downLoadUrl).then((res) => {
|
2019-03-11 13:08:25 +00:00
|
|
|
const contentLength = res.data.length;
|
|
|
|
let isBlankFile = (contentLength === 0 || contentLength === 1);
|
|
|
|
let hasPermission = (this.state.fileInfo.permission === 'rw');
|
2019-03-13 03:01:09 +00:00
|
|
|
let isEditMode = mode === 'edit' ? true : false;
|
2019-03-14 02:15:25 +00:00
|
|
|
let value = deserialize(res.data);
|
2018-07-27 03:48:21 +00:00
|
|
|
this.setState({
|
|
|
|
markdownContent: res.data,
|
2019-03-11 13:08:25 +00:00
|
|
|
loading: false,
|
|
|
|
// Goto rich edit page
|
|
|
|
// First, the user has the relevant permissions, otherwise he can only enter the viewer interface or cannot access
|
|
|
|
// case1: If file is draft file
|
|
|
|
// case2: If mode == 'edit' and the file has no draft
|
|
|
|
// case3: The length of markDownContent is 1 when clear all content in editor and the file has no draft
|
|
|
|
editorMode: (hasPermission && (isDraft || (isEditMode && !hasDraft) || (isBlankFile && !hasDraft))) ? 'rich' : 'viewer',
|
2019-03-14 02:15:25 +00:00
|
|
|
value: value,
|
2018-09-29 10:32:53 +00:00
|
|
|
});
|
|
|
|
});
|
2018-07-27 03:48:21 +00:00
|
|
|
});
|
2018-09-29 10:32:53 +00:00
|
|
|
});
|
2019-03-11 13:08:25 +00:00
|
|
|
if (userInfo && this.socket) {
|
|
|
|
const { repoID, path } = this.state.fileInfo;
|
|
|
|
this.socket.emit('presence', {
|
|
|
|
request: 'join_room',
|
2019-03-14 10:14:11 +00:00
|
|
|
doc_id: CryptoJS.MD5(repoID+path).toString(),
|
|
|
|
user: userInfo
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.socket.emit('repo_update', {
|
|
|
|
request: 'watch_update',
|
2019-03-14 10:14:11 +00:00
|
|
|
repo_id: editorUtilities.repoID,
|
|
|
|
user: {
|
|
|
|
name: editorUtilities.name,
|
|
|
|
username: editorUtilities.username,
|
|
|
|
contact_email: editorUtilities.contact_email,
|
|
|
|
},
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
this.checkDraft();
|
2019-01-16 03:49:00 +00:00
|
|
|
this.listRelatedFiles();
|
|
|
|
this.listFileTags();
|
2019-03-11 13:08:25 +00:00
|
|
|
this.getCommentsNumber();
|
2019-03-14 02:15:25 +00:00
|
|
|
|
|
|
|
document.addEventListener('selectionchange', this.setBtnPosition);
|
|
|
|
setTimeout(() => {
|
|
|
|
let url = new URL(window.location.href);
|
|
|
|
if (url.hash) {
|
|
|
|
window.location.href = window.location.href;
|
|
|
|
}
|
|
|
|
}, 100);
|
2019-01-16 03:49:00 +00:00
|
|
|
}
|
2018-12-28 06:25:25 +00:00
|
|
|
|
2019-01-16 03:49:00 +00:00
|
|
|
listRelatedFiles = () => {
|
2018-12-28 06:25:25 +00:00
|
|
|
seafileAPI.listRelatedFiles(repoID, filePath).then(res => {
|
|
|
|
this.setState({
|
|
|
|
relatedFiles: res.data.related_files
|
2019-01-16 03:49:00 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
listFileTags = () => {
|
|
|
|
seafileAPI.listFileTags(repoID, filePath).then(res => {
|
|
|
|
let fileTagList = res.data.file_tags;
|
|
|
|
for (let i = 0, length = fileTagList.length; i < length; i++) {
|
|
|
|
fileTagList[i].id = fileTagList[i].file_tag_id;
|
|
|
|
}
|
|
|
|
this.setState({
|
|
|
|
fileTagList: fileTagList
|
|
|
|
});
|
2018-12-28 06:25:25 +00:00
|
|
|
});
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
|
|
|
|
2019-01-16 03:49:00 +00:00
|
|
|
onRelatedFileChange = () => {
|
|
|
|
this.listRelatedFiles();
|
|
|
|
}
|
|
|
|
|
|
|
|
onFileTagChanged = () => {
|
|
|
|
this.listFileTags();
|
|
|
|
}
|
|
|
|
|
2019-03-11 13:08:25 +00:00
|
|
|
setFileInfoMtime = (fileInfo) => {
|
|
|
|
this.setState({
|
|
|
|
fileInfo: Object.assign({}, this.state.fileInfo, { mtime: fileInfo.mtime, id: fileInfo.id, lastModifier: fileInfo.last_modifier_name })
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
toggleStar = () => {
|
|
|
|
let starrd = this.state.fileInfo.starred;
|
|
|
|
if (starrd) {
|
|
|
|
editorUtilities.unStarItem().then((response) => {
|
|
|
|
this.setState({
|
|
|
|
fileInfo: Object.assign({}, this.state.fileInfo, {starred: !starrd})
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else if (!starrd) {
|
|
|
|
editorUtilities.starItem().then((response) => {
|
|
|
|
this.setState({
|
|
|
|
fileInfo: Object.assign({}, this.state.fileInfo, {starred: !starrd})
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
autoSaveDraft = () => {
|
|
|
|
let that = this;
|
|
|
|
if (that.timer) {
|
|
|
|
return;
|
2019-03-14 10:14:11 +00:00
|
|
|
}
|
|
|
|
else {
|
2019-03-11 13:08:25 +00:00
|
|
|
that.timer = setTimeout(() => {
|
|
|
|
let str = '';
|
|
|
|
if (this.state.editorMode == 'rich') {
|
|
|
|
let value = this.draftRichValue;
|
|
|
|
str = serialize(value.toJSON());
|
|
|
|
}
|
|
|
|
else if (this.state.editorMode == 'plain') {
|
|
|
|
str = this.draftPlainValue;
|
|
|
|
}
|
|
|
|
let draftKey = editorUtilities.getDraftKey();
|
|
|
|
localStorage.setItem(draftKey, str);
|
|
|
|
that.setState({
|
2019-03-14 10:14:11 +00:00
|
|
|
showDraftSaved: true
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
|
|
|
setTimeout(() => {
|
|
|
|
that.setState({
|
2019-03-14 10:14:11 +00:00
|
|
|
showDraftSaved: false
|
2019-03-11 13:08:25 +00:00
|
|
|
});
|
2019-03-14 10:14:11 +00:00
|
|
|
}, 3000);
|
2019-03-11 13:08:25 +00:00
|
|
|
that.timer = null;
|
2019-03-14 10:14:11 +00:00
|
|
|
}, 60000);
|
|
|
|
}
|
2019-03-11 13:08:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
backToParentDirectory = () => {
|
|
|
|
window.location.href = editorUtilities.getParentDectionaryUrl();
|
|
|
|
}
|
|
|
|
|
|
|
|
onEdit = (event) => {
|
|
|
|
event.preventDefault();
|
|
|
|
this.setEditorMode('rich');
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleShareLinkDialog = () => {
|
|
|
|
this.openDialogs('share_link');
|
|
|
|
}
|
|
|
|
|
|
|
|
getCommentsNumber = () => {
|
|
|
|
editorUtilities.getCommentsNumber().then((res) => {
|
|
|
|
let commentsNumber = res.data[Object.getOwnPropertyNames(res.data)[0]];
|
|
|
|
this.setState({
|
|
|
|
commentsNumber: commentsNumber
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onCommentAdded = () => {
|
|
|
|
this.getCommentsNumber();
|
2019-03-14 02:15:25 +00:00
|
|
|
this.toggleCancel();
|
|
|
|
}
|
|
|
|
|
|
|
|
showDiffViewer = () => {
|
|
|
|
this.setState({
|
|
|
|
loadingDiff: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
setDiffViewerContent = (markdownContent, oldMarkdownContent) => {
|
|
|
|
this.setState({
|
|
|
|
markdownContent: markdownContent,
|
|
|
|
oldMarkdownContent: oldMarkdownContent
|
|
|
|
});
|
|
|
|
this.showDiffViewer();
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadDiffContent = () =>{
|
|
|
|
this.setState({
|
|
|
|
loadingDiff: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
setBtnPosition = (e) => {
|
2019-03-15 04:11:32 +00:00
|
|
|
if (!this.state.isShowComments) return;
|
2019-03-14 02:15:25 +00:00
|
|
|
const nativeSelection = window.getSelection();
|
|
|
|
if (!nativeSelection.rangeCount) {
|
|
|
|
this.range = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (nativeSelection.isCollapsed === false) {
|
|
|
|
const nativeRange = nativeSelection.getRangeAt(0);
|
|
|
|
const focusNode = nativeSelection.focusNode;
|
|
|
|
if ((focusNode.tagName === 'I') ||
|
|
|
|
(focusNode.nodeType !== 3 && focusNode.getAttribute('class') === 'language-type')) {
|
|
|
|
// fix select last paragraph
|
|
|
|
let fragment = nativeRange.cloneContents();
|
|
|
|
let startNode = fragment.firstChild.firstChild;
|
|
|
|
if (!startNode) return;
|
|
|
|
let newNativeRange = document.createRange();
|
|
|
|
newNativeRange.setStartBefore(startNode);
|
|
|
|
newNativeRange.setEndAfter(startNode);
|
|
|
|
|
|
|
|
this.range = findRange(newNativeRange, this.state.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
this.range = findRange(nativeRange, this.state.value);
|
|
|
|
}
|
|
|
|
if (!this.range) return;
|
|
|
|
let rect = nativeRange.getBoundingClientRect();
|
|
|
|
// fix Safari bug
|
|
|
|
if (navigator.userAgent.indexOf('Chrome') < 0 && navigator.userAgent.indexOf('Safari') > 0) {
|
|
|
|
if (nativeRange.collapsed && rect.top == 0 && rect.height == 0) {
|
|
|
|
if (nativeRange.startOffset == 0) {
|
|
|
|
nativeRange.setEnd(nativeRange.endContainer, 1);
|
|
|
|
} else {
|
|
|
|
nativeRange.setStart(nativeRange.startContainer, nativeRange.startOffset - 1);
|
|
|
|
}
|
|
|
|
rect = nativeRange.getBoundingClientRect();
|
|
|
|
if (rect.top == 0 && rect.height == 0) {
|
|
|
|
if (nativeRange.getClientRects().length) {
|
|
|
|
rect = nativeRange.getClientRects()[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let style = this.refs.commentbtn.style;
|
|
|
|
style.top = `${rect.top - 63 + this.refs.markdownContainer.scrollTop}px`;
|
|
|
|
style.right = '0px';
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let style = this.refs.commentbtn.style;
|
|
|
|
style.top = '-1000px';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
addComment = (e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
this.getQuote();
|
|
|
|
this.openDialogs('comment');
|
|
|
|
}
|
|
|
|
|
|
|
|
getQuote = () => {
|
|
|
|
let range = this.range;
|
|
|
|
if (!range) return;
|
|
|
|
const { document } = this.state.value;
|
|
|
|
let { anchor, focus } = range;
|
|
|
|
const anchorText = document.getNode(anchor.key);
|
|
|
|
const focusText = document.getNode(focus.key);
|
|
|
|
const anchorInline = document.getClosestInline(anchor.key);
|
|
|
|
const focusInline = document.getClosestInline(focus.key);
|
|
|
|
// COMPAT: If the selection is at the end of a non-void inline node, and
|
|
|
|
// there is a node after it, put it in the node after instead. This
|
|
|
|
// standardizes the behavior, since it's indistinguishable to the user.
|
|
|
|
if (anchorInline && anchor.offset == anchorText.text.length) {
|
|
|
|
const block = document.getClosestBlock(anchor.key);
|
|
|
|
const nextText = block.getNextText(anchor.key);
|
|
|
|
if (nextText) {
|
|
|
|
range = range.moveAnchorTo(nextText.key, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (focusInline && focus.offset == focusText.text.length) {
|
|
|
|
const block = document.getClosestBlock(focus.key);
|
|
|
|
const nextText = block.getNextText(focus.key);
|
|
|
|
if (nextText) {
|
|
|
|
range = range.moveFocusTo(nextText.key, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let fragment = document.getFragmentAtRange(range);
|
|
|
|
let nodes = this.removeNullNode(fragment.nodes);
|
|
|
|
let newFragment = Document.create({
|
|
|
|
nodes: nodes
|
|
|
|
});
|
|
|
|
let newValue = Value.create({
|
|
|
|
document: newFragment
|
|
|
|
});
|
|
|
|
this.quote = serialize(newValue.toJSON());
|
|
|
|
let selection = document.createSelection(range);
|
|
|
|
selection = selection.setIsFocused(true);
|
|
|
|
this.setState({
|
|
|
|
commentPosition: selection.anchor.path
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
removeNullNode = (oldNodes) => {
|
|
|
|
let newNodes = [];
|
|
|
|
oldNodes.map((node) => {
|
|
|
|
const text = node.text.trim();
|
|
|
|
const childNodes = node.nodes;
|
|
|
|
if (!text) return;
|
|
|
|
if ((childNodes && childNodes.size === 1) || (!childNodes)) {
|
|
|
|
newNodes.push(node);
|
|
|
|
}
|
|
|
|
else if (childNodes.size > 1) {
|
|
|
|
let nodes = this.removeNullNode(childNodes);
|
|
|
|
let newNode = Block.create({
|
|
|
|
nodes: nodes,
|
|
|
|
data: node.data,
|
|
|
|
key: node.key,
|
|
|
|
type: node.type
|
|
|
|
});
|
|
|
|
newNodes.push(newNode);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return newNodes;
|
2019-03-11 13:08:25 +00:00
|
|
|
}
|
|
|
|
|
2019-03-15 04:11:32 +00:00
|
|
|
scrollToNode = (node) => {
|
|
|
|
let url = new URL(window.location.href);
|
|
|
|
url.set('hash', 'user-content-' + node.text);
|
|
|
|
window.location.href = url.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
findScrollContainer = (el, window) => {
|
|
|
|
let parent = el.parentNode;
|
|
|
|
const OVERFLOWS = ['auto', 'overlay', 'scroll'];
|
|
|
|
let scroller;
|
|
|
|
while (!scroller) {
|
|
|
|
if (!parent.parentNode) break;
|
|
|
|
const style = window.getComputedStyle(parent);
|
|
|
|
const { overflowY } = style;
|
|
|
|
if (OVERFLOWS.includes(overflowY)) {
|
|
|
|
scroller = parent;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
parent = parent.parentNode;
|
|
|
|
}
|
|
|
|
if (!scroller) {
|
|
|
|
return window.document.body;
|
|
|
|
}
|
|
|
|
return scroller;
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollToQuote = (path) => {
|
|
|
|
if (!path) return;
|
|
|
|
const win = window;
|
|
|
|
if (path.length > 2) {
|
|
|
|
// deal with code block or chart
|
|
|
|
path[0] = path[0] > 1 ? path[0] - 1 : path[0] + 1;
|
|
|
|
path = path.slice(0, 1);
|
|
|
|
}
|
|
|
|
let node = this.state.value.document.getNode(path);
|
|
|
|
if (!node) {
|
|
|
|
path = path.slice(0, 1);
|
|
|
|
node = this.state.value.document.getNode(path);
|
|
|
|
}
|
|
|
|
if (node) {
|
|
|
|
let element = win.document.querySelector(`[data-key="${node.key}"]`);
|
|
|
|
while (element.tagName === 'CODE') {
|
|
|
|
element = element.parentNode;
|
|
|
|
}
|
|
|
|
const scroller = this.findScrollContainer(element, win);
|
|
|
|
const isWindow = scroller == win.document.body || scroller == win.document.documentElement;
|
|
|
|
if (isWindow) {
|
|
|
|
win.scrollTo(0, element.offsetTop);
|
|
|
|
} else {
|
|
|
|
scroller.scrollTop = element.offsetTop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleHistory = () => {
|
|
|
|
if (this.state.isShowHistory) {
|
|
|
|
this.setState({
|
|
|
|
isShowHistory: false,
|
|
|
|
isShowOutline: true,
|
|
|
|
isShowComments: false,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
isShowHistory: true,
|
|
|
|
isShowOutline: false,
|
|
|
|
isShowComments: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleCommentList = () => {
|
|
|
|
if (this.state.isShowComments) {
|
|
|
|
this.setState({
|
|
|
|
isShowHistory: false,
|
|
|
|
isShowOutline: true,
|
|
|
|
isShowComments: false,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
isShowHistory: false,
|
|
|
|
isShowOutline: false,
|
|
|
|
isShowComments: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-02 06:09:58 +00:00
|
|
|
render() {
|
2019-03-11 13:08:25 +00:00
|
|
|
let component;
|
2019-03-15 04:11:32 +00:00
|
|
|
let sidePanel = (this.state.isShowHistory || this.state.isShowComments) ? true : false;
|
2018-05-02 06:09:58 +00:00
|
|
|
if (this.state.loading) {
|
|
|
|
return (
|
|
|
|
<div className="empty-loading-page">
|
|
|
|
<div className="lds-ripple page-centered"><div></div><div></div></div>
|
|
|
|
</div>
|
2018-09-29 10:32:53 +00:00
|
|
|
);
|
|
|
|
} else if (this.state.mode === 'editor') {
|
2019-03-14 10:14:11 +00:00
|
|
|
if (this.state.editorMode === 'viewer') {
|
|
|
|
component = (
|
|
|
|
<div className="seafile-md-viewer d-flex flex-column">
|
|
|
|
<MarkdownViewerToolbar
|
|
|
|
hasDraft={hasDraft}
|
|
|
|
isDraft={isDraft}
|
|
|
|
editorUtilities={editorUtilities}
|
|
|
|
collabUsers={this.state.collabUsers}
|
|
|
|
fileInfo={this.state.fileInfo}
|
|
|
|
toggleStar={this.toggleStar}
|
|
|
|
backToParentDirectory={this.backToParentDirectory}
|
|
|
|
openDialogs={this.openDialogs}
|
|
|
|
fileTagList={this.state.fileTagList}
|
|
|
|
relatedFiles={this.state.relatedFiles}
|
|
|
|
toggleShareLinkDialog={this.toggleShareLinkDialog}
|
|
|
|
onEdit={this.onEdit}
|
|
|
|
toggleNewDraft={editorUtilities.createDraftFile}
|
2019-03-15 04:11:32 +00:00
|
|
|
commentsNumber={this.state.commentsNumber}
|
|
|
|
toggleCommentList={this.toggleCommentList}
|
|
|
|
showFileHistory={true}
|
|
|
|
toggleHistory={this.toggleHistory}
|
2019-03-14 10:14:11 +00:00
|
|
|
/>
|
|
|
|
<div className="seafile-md-viewer d-flex">
|
2019-03-15 04:11:32 +00:00
|
|
|
<div className={sidePanel ? "seafile-md-viewer-container side-panel-on":"seafile-md-viewer-container"} ref="markdownContainer">
|
2019-03-14 10:14:11 +00:00
|
|
|
{
|
2019-03-15 04:11:32 +00:00
|
|
|
this.state.isShowHistory ?
|
2019-03-14 10:14:11 +00:00
|
|
|
<div className="diff-container">
|
|
|
|
<div className="diff-wrapper article">
|
|
|
|
{ this.state.loadingDiff ?
|
|
|
|
<Loading/> :
|
|
|
|
<DiffViewer
|
|
|
|
newMarkdownContent={this.state.markdownContent}
|
|
|
|
oldMarkdownContent={this.state.oldMarkdownContent}
|
|
|
|
/>
|
|
|
|
}
|
2019-03-14 02:15:25 +00:00
|
|
|
</div>
|
2019-03-14 10:14:11 +00:00
|
|
|
</div>
|
2019-03-14 02:15:25 +00:00
|
|
|
:
|
2019-03-15 04:11:32 +00:00
|
|
|
<div className={sidePanel ? "seafile-md-viewer-slate side-panel-on" : "seafile-md-viewer-slate"}>
|
2019-03-14 02:15:25 +00:00
|
|
|
<MarkdownViewerSlate
|
|
|
|
relatedFiles={this.state.relatedFiles}
|
|
|
|
siteRoot={siteRoot}
|
|
|
|
value={this.state.value}
|
|
|
|
/>
|
2019-03-15 04:11:32 +00:00
|
|
|
{this.state.isShowComments &&
|
2019-03-14 02:15:25 +00:00
|
|
|
<i className="fa fa-plus-square seafile-viewer-comment-btn" ref="commentbtn" onMouseDown={this.addComment}></i>}
|
|
|
|
</div>
|
2019-03-14 10:14:11 +00:00
|
|
|
}
|
2019-03-15 04:11:32 +00:00
|
|
|
{
|
|
|
|
this.state.isShowOutline &&
|
|
|
|
<OutlineView
|
|
|
|
isViewer={true}
|
|
|
|
document={this.state.value.document}
|
|
|
|
scrollToNode={this.scrollToNode}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
<div className="seafile-md-viewer-side-panel">
|
|
|
|
{this.state.isShowComments && <CommentPanel toggleCommentPanel={this.toggleCommentList}/>}
|
|
|
|
{
|
|
|
|
this.state.isShowHistory &&
|
|
|
|
<HistoryList
|
|
|
|
editorUtilities={editorUtilities}
|
|
|
|
showDiffViewer={this.showDiffViewer}
|
|
|
|
setDiffViewerContent={this.setDiffViewerContent}
|
|
|
|
reloadDiffContent={this.reloadDiffContent}
|
|
|
|
toggleHistoryPanel={this.toggleHistory}
|
|
|
|
/>
|
|
|
|
}
|
2019-03-14 02:15:25 +00:00
|
|
|
</div>
|
2019-03-11 13:08:25 +00:00
|
|
|
</div>
|
2019-03-14 10:14:11 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
component = <SeafileEditor
|
|
|
|
fileInfo={this.state.fileInfo}
|
|
|
|
markdownContent={this.state.markdownContent}
|
|
|
|
editorUtilities={editorUtilities}
|
|
|
|
collabUsers={this.state.collabUsers}
|
|
|
|
setFileInfoMtime={this.setFileInfoMtime}
|
|
|
|
toggleStar={this.toggleStar}
|
|
|
|
showFileHistory={true}
|
|
|
|
setEditorMode={this.setEditorMode}
|
|
|
|
setContent={this.setContent}
|
|
|
|
draftID={draftID}
|
|
|
|
isDraft={isDraft}
|
|
|
|
mode={this.state.mode}
|
|
|
|
emitSwitchEditor={this.emitSwitchEditor}
|
|
|
|
hasDraft={hasDraft}
|
|
|
|
editorMode={this.state.editorMode}
|
|
|
|
relatedFiles={this.state.relatedFiles}
|
|
|
|
siteRoot={siteRoot}
|
|
|
|
autoSaveDraft={this.autoSaveDraft}
|
|
|
|
setDraftValue={this.setDraftValue}
|
|
|
|
clearTimer={this.clearTimer}
|
|
|
|
openDialogs={this.openDialogs}
|
|
|
|
fileTagList={this.state.fileTagList}
|
|
|
|
deleteDraft={this.deleteDraft}
|
|
|
|
showDraftSaved={this.state.showDraftSaved}
|
2019-03-15 04:11:32 +00:00
|
|
|
/>
|
2019-03-14 10:14:11 +00:00
|
|
|
}
|
2019-03-11 13:08:25 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Fragment>
|
|
|
|
{this.state.localDraftDialog?
|
|
|
|
<LocalDraftDialog
|
|
|
|
localDraftDialog={this.state.localDraftDialog}
|
|
|
|
deleteDraft={this.deleteDraft}
|
|
|
|
useDraft={this.useDraft}/>:
|
2019-03-14 10:14:11 +00:00
|
|
|
null}
|
2019-03-11 13:08:25 +00:00
|
|
|
{component}
|
2019-01-16 03:49:00 +00:00
|
|
|
{this.state.showMarkdownEditorDialog && (
|
2019-02-25 10:17:04 +00:00
|
|
|
<React.Fragment>
|
|
|
|
{this.state.showRelatedFileDialog &&
|
|
|
|
<ModalPortal>
|
2019-01-16 03:49:00 +00:00
|
|
|
<ListRelatedFileDialog
|
|
|
|
repoID={repoID}
|
|
|
|
filePath={filePath}
|
|
|
|
relatedFiles={this.state.relatedFiles}
|
|
|
|
toggleCancel={this.toggleCancel}
|
|
|
|
addRelatedFileToggle={this.addRelatedFileToggle}
|
|
|
|
onRelatedFileChange={this.onRelatedFileChange}
|
|
|
|
/>
|
2019-02-25 10:17:04 +00:00
|
|
|
</ModalPortal>
|
|
|
|
}
|
|
|
|
{this.state.showEditFileTagDialog &&
|
|
|
|
<ModalPortal>
|
2019-01-16 03:49:00 +00:00
|
|
|
<EditFileTagDialog
|
|
|
|
repoID={repoID}
|
|
|
|
filePath={filePath}
|
|
|
|
fileTagList={this.state.fileTagList}
|
|
|
|
toggleCancel={this.toggleCancel}
|
|
|
|
onFileTagChanged={this.onFileTagChanged}
|
|
|
|
/>
|
2019-02-25 10:17:04 +00:00
|
|
|
</ModalPortal>
|
|
|
|
}
|
|
|
|
{this.state.showAddRelatedFileDialog &&
|
|
|
|
<ModalPortal>
|
2019-01-16 03:49:00 +00:00
|
|
|
<AddRelatedFileDialog
|
|
|
|
repoID={repoID}
|
|
|
|
filePath={filePath}
|
|
|
|
toggleCancel={this.closeAddRelatedFileDialog}
|
2019-01-21 08:55:35 +00:00
|
|
|
dirent={this.state.fileInfo}
|
2019-01-16 03:49:00 +00:00
|
|
|
onRelatedFileChange={this.onRelatedFileChange}
|
|
|
|
/>
|
2019-02-25 10:17:04 +00:00
|
|
|
</ModalPortal>
|
|
|
|
}
|
|
|
|
{this.state.showShareLinkDialog &&
|
|
|
|
<ModalPortal>
|
|
|
|
<ShareDialog
|
|
|
|
itemType="file"
|
|
|
|
itemName={this.state.fileInfo.name}
|
|
|
|
itemPath={filePath}
|
|
|
|
repoID={repoID}
|
|
|
|
toggleDialog={this.toggleCancel}
|
|
|
|
isGroupOwnedRepo={false}
|
|
|
|
repoEncrypted={false}
|
|
|
|
/>
|
|
|
|
</ModalPortal>
|
|
|
|
}
|
2019-03-14 02:15:25 +00:00
|
|
|
{this.state.showCommentDialog &&
|
|
|
|
<ModalPortal>
|
|
|
|
<CommentDialog
|
|
|
|
toggleCommentDialog={this.toggleCancel}
|
|
|
|
editorUtilities={editorUtilities}
|
|
|
|
onCommentAdded={this.onCommentAdded}
|
|
|
|
commentPosition={this.state.commentPosition}
|
|
|
|
quote={this.quote}
|
|
|
|
/>
|
|
|
|
</ModalPortal>
|
|
|
|
}
|
2019-02-25 10:17:04 +00:00
|
|
|
</React.Fragment>
|
2019-01-16 03:49:00 +00:00
|
|
|
)}
|
|
|
|
</React.Fragment>
|
2018-05-02 06:09:58 +00:00
|
|
|
);
|
2018-06-07 07:01:41 +00:00
|
|
|
}
|
2018-05-02 06:09:58 +00:00
|
|
|
}
|
|
|
|
}
|
2018-09-29 10:32:53 +00:00
|
|
|
|
2019-03-11 13:08:25 +00:00
|
|
|
export default MarkdownEditor;
|