1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-12 18:05:05 +00:00

update markdown-editor

This commit is contained in:
杨顺强 2023-12-08 17:58:27 +08:00
parent bb5ce5e233
commit 9b21cbc51d
15 changed files with 10405 additions and 2497 deletions

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line strict
'use strict';
const fs = require('fs');
@ -20,7 +21,7 @@ const getClientEnvironment = require('./env');
const paths = require('./paths');
const modules = require('./modules');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const webpackBundleTracker = require('webpack-bundle-tracker');
const ForkTsCheckerWebpackPlugin =
@ -135,34 +136,34 @@ module.exports = function (webpackEnv) {
config: false,
plugins: !useTailwind
? [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
],
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
'postcss-normalize',
]
: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
stage: 3,
},
],
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
'postcss-normalize',
]
: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
],
},
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
@ -444,7 +445,7 @@ module.exports = function (webpackEnv) {
},
],
],
plugins: [
// isEnvDevelopment &&
// shouldUseReactRefresh &&
@ -478,7 +479,7 @@ module.exports = function (webpackEnv) {
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
@ -608,7 +609,7 @@ module.exports = function (webpackEnv) {
},
plugins: [
new webpack.ProvidePlugin({
process: "process/browser.js",
process: 'process/browser.js',
}),
new NodePolyfillPlugin({
excludeAliases: ['console'],

11978
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"@seafile/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "0.3.22",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "0.4.8",
"@seafile/seafile-editor": "1.0.5",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"classnames": "^2.2.6",
@ -17,9 +17,9 @@
"crypto-js": "4.2.0",
"deep-copy": "1.4.2",
"glamor": "^2.20.40",
"i18next": "22.4.6",
"i18next-browser-languagedetector": "7.0.1",
"i18next-xhr-backend": "3.2.2",
"i18next": "^17.0.13",
"i18next-browser-languagedetector": "^3.0.3",
"i18next-xhr-backend": "^3.1.2",
"is-hotkey": "0.2.0",
"MD5": "^1.3.0",
"moment": "^2.22.2",
@ -32,7 +32,7 @@
"react-chartjs-2": "^2.8.0",
"react-cookies": "^0.1.0",
"react-dom": "17.0.0",
"react-i18next": "12.1.1",
"react-i18next": "^10.12.2",
"react-responsive": "9.0.2",
"react-select": "5.7.0",
"react-transition-group": "4.4.5",

View File

@ -2,7 +2,7 @@ import i18n from 'i18next';
import Backend from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { mediaUrl } from './utils/constants';
import { mediaUrl } from '../utils/constants';
const lang = window.app.pageOptions.lang;
@ -14,7 +14,7 @@ i18n
lng: lang,
fallbackLng: 'en',
ns: ['seafile-editor'],
defaultNS: 'translations',
defaultNS: 'seafile-editor',
whitelist: ['en', 'zh-CN', 'fr', 'de', 'cs', 'es', 'es-AR', 'es-MX', 'ru'],

View File

@ -1,3 +1,8 @@
html, body, #root {
width: 100%;
height: 100%;
}
#root {
display: flex;
flex-direction: column;
@ -168,3 +173,10 @@
margin-left: 0.5rem;
color: #888;
}
.sf-md-viewer-content {
flex: 1;
display: flex;
min-height: 0;
min-width: 0;
}

View File

@ -1,15 +1,18 @@
// Import React!
import React from 'react';
import React, { Suspense } from 'react';
import ReactDom from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n-seafile-editor';
import i18n from './_i18n/i18n-seafile-editor';
import MarkdownEditor from './pages/markdown-editor';
import Loading from './components/loading';
import './index.css';
ReactDom.render(
<I18nextProvider i18n={ i18n } >
<MarkdownEditor />
<Suspense fallback={<Loading />}>
<MarkdownEditor />
</Suspense>
</I18nextProvider>,
document.getElementById('root')
);

View File

@ -23,3 +23,7 @@
.collab-users-dropdown.dropdown {
margin-right: 6px;
}
.btn-active[data-active=true] {
color: #eb8205;
}

View File

@ -45,7 +45,7 @@ class EditorApi {
);
}
getParentDectionaryUrl() {
getParentDictionaryUrl() {
let parentPath = this.filePath.substring(0, this.filePath.lastIndexOf('/'));
let libName = encodeURIComponent(repoName);
let path = Utils.encodePath(parentPath);

View File

@ -23,7 +23,6 @@ const propTypes = {
onEdit: PropTypes.func.isRequired,
toggleNewDraft: PropTypes.func.isRequired,
toggleStar: PropTypes.func.isRequired,
openParentDirectory: PropTypes.func.isRequired,
openDialogs: PropTypes.func.isRequired,
showFileHistory: PropTypes.bool.isRequired,
toggleHistory: PropTypes.func.isRequired,
@ -31,6 +30,7 @@ const propTypes = {
readOnly: PropTypes.bool.isRequired,
contentChanged: PropTypes.bool.isRequired,
saving: PropTypes.bool.isRequired,
onSaveEditorContent: PropTypes.func.isRequired,
showDraftSaved: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
lockedByMe: PropTypes.bool.isRequired,
@ -52,10 +52,13 @@ class HeaderToolbar extends React.Component {
location.href = `seafile://openfile?repo_id=${encodeURIComponent(repoID)}&path=${encodeURIComponent(path)}`;
};
openParentDirectory = () => {
const { editorApi } = this.props;
window.location.href = editorApi.getParentDictionaryUrl();
};
render() {
let { contentChanged, saving, isLocked, lockedByMe } = this.props;
let canPublishDraft = this.props.fileInfo.permission == 'rw';
let canCreateDraft = canPublishDraft && (!this.props.hasDraft && !this.props.isDraft && this.props.isDocs);
if (this.props.editorMode === 'rich') {
return (
@ -71,27 +74,7 @@ class HeaderToolbar extends React.Component {
mediaUrl={mediaUrl}
isStarred={this.props.fileInfo.isStarred}
/>
{(this.props.hasDraft && !this.props.isDraft) &&
<div className='seafile-btn-view-review'>
<div className='tag tag-green'>{gettext('This file is in draft stage.')}
<a className="ml-2" onMouseDown={this.props.editorApi.goDraftPage}>{gettext('View Draft')}</a></div>
</div>
}
<div className="topbar-btn-container">
{canCreateDraft &&
<button onMouseDown={this.props.toggleNewDraft} className="btn btn-success btn-new-draft">
{gettext('New Draft')}</button>
}
{this.props.isDraft &&
<div>
<button type="button" className="btn btn-success seafile-btn-add-review"
onMouseDown={this.props.editorApi.goDraftPage}>{gettext('Start review')}</button>
{canPublishDraft &&
<button type="button" className="btn btn-success seafile-btn-add-review"
onMouseDown={this.props.editorApi.publishDraftFile}>{gettext('Publish')}</button>
}
</div>
}
{(seafileCollabServer && this.props.collabUsers.length > 0) &&
<CollabUsersButton
className="collab-users-dropdown"
@ -100,24 +83,43 @@ class HeaderToolbar extends React.Component {
/>
}
<ButtonGroup>
<ButtonItem text={gettext('Open parent directory')} id={'parentDirectory'}
icon={'fa fa-folder-open'} onMouseDown={this.props.openParentDirectory}/>
{(canLockUnlockFile && !isLocked) &&
<ButtonItem id="lock-unlock-file" icon='fa fa-lock' text={gettext('Lock')} onMouseDown={this.props.toggleLockFile}/>
}
{(canLockUnlockFile && lockedByMe) &&
<ButtonItem id="lock-unlock-file" icon='fa fa-unlock' text={gettext('Unlock')} onMouseDown={this.props.toggleLockFile}/>
}
{canGenerateShareLink &&
<ButtonItem id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'}
onMouseDown={this.props.toggleShareLinkDialog}/>
}
<ButtonItem
text={gettext('Open parent directory')}
id={'parentDirectory'}
icon={'fa fa-folder-open'}
onMouseDown={this.openParentDirectory}
/>
{(canLockUnlockFile && !isLocked) && (
<ButtonItem
id="lock-unlock-file"
icon='fa fa-lock'
text={gettext('Lock')}
onMouseDown={this.props.toggleLockFile}
/>
)}
{(canLockUnlockFile && lockedByMe) && (
<ButtonItem
id="lock-unlock-file"
icon='fa fa-unlock'
text={gettext('Unlock')}
onMouseDown={this.props.toggleLockFile}
/>
)}
{canGenerateShareLink && (
<ButtonItem
id={'shareBtn'}
text={gettext('Share')}
icon={'fa fa-share-alt'}
onMouseDown={this.props.toggleShareLinkDialog}
/>
)}
{saving ?
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button>
<i className={'fa fa-spin fa-spinner'}/>
</button>
:
<ButtonItem text={gettext('Save')} id={'saveButton'} icon={'fa fa-save'} disabled={!contentChanged}
onMouseDown={window.seafileEditor && window.seafileEditor.onRichEditorSave} isActive={contentChanged}/>
onMouseDown={this.props.onSaveEditorContent} isActive={contentChanged}/>
}
{canDownloadFile && (
<ButtonItem
@ -127,14 +129,14 @@ class HeaderToolbar extends React.Component {
onClick={this.downloadFile}
/>
)}
{this.props.fileInfo.permission == 'rw' &&
<ButtonItem
id="open-via-client"
icon="sf3-font sf3-font-desktop"
text={gettext('Open via Client')}
onClick={this.openFileViaClient}
/>
}
{this.props.fileInfo.permission == 'rw' && (
<ButtonItem
id="open-via-client"
icon="sf3-font sf3-font-desktop"
text={gettext('Open via Client')}
onClick={this.openFileViaClient}
/>
)}
</ButtonGroup>
<MoreMenu
readOnly={this.props.readOnly}
@ -170,7 +172,7 @@ class HeaderToolbar extends React.Component {
editorMode={this.props.editorMode}
onEdit={this.props.onEdit}
toggleShareLinkDialog={this.props.toggleShareLinkDialog}
openParentDirectory={this.props.openParentDirectory}
openParentDirectory={this.openParentDirectory}
showFileHistory={this.props.showFileHistory}
toggleHistory={this.props.toggleHistory}
isSmallScreen={true}
@ -179,7 +181,9 @@ class HeaderToolbar extends React.Component {
</div>
</div>
);
} else if (this.props.editorMode === 'plain') {
}
if (this.props.editorMode === 'plain') {
return (
<div className="sf-md-viewer-topbar">
<div className="sf-md-viewer-topbar-first d-flex justify-content-between">
@ -194,9 +198,10 @@ class HeaderToolbar extends React.Component {
/>
}
<ButtonGroup>
{ saving ?
{saving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}>
<i className={'fa fa-spin fa-spinner'}/></button>
<i className={'fa fa-spin fa-spinner'}/>
</button>
:
<ButtonItem id={'saveButton'} text={gettext('Save')} icon={'fa fa-save'} onMouseDown={window.seafileEditor && window.seafileEditor.onPlainEditorSave} disabled={!contentChanged} isActive={contentChanged} />
}
@ -238,10 +243,11 @@ class HeaderToolbar extends React.Component {
/>
</div>
</div>
</div>
);
}
return null;
}
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EXTERNAL_EVENTS, EventBus } from '@seafile/seafile-editor';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Tooltip } from 'reactstrap';
import { gettext, canGenerateShareLink } from '../../../utils/constants';
@ -32,7 +33,12 @@ class MoreMenu extends React.PureComponent {
};
dropdownToggle = () => {
this.setState({ dropdownOpen:!this.state.dropdownOpen });
this.setState({ dropdownOpen: !this.state.dropdownOpen });
};
onHelpModuleToggle = (event) => {
const eventBus = EventBus.getInstance();
eventBus.dispatch(EXTERNAL_EVENTS.ON_HELP_INFO_TOGGLE, true);
};
downloadFile = () => {
@ -51,18 +57,18 @@ class MoreMenu extends React.PureComponent {
</DropdownToggle>
<DropdownMenu className="drop-list" right={true}>
{(!this.props.readOnly && editorMode === 'rich') &&
<DropdownItem onMouseDown={this.props.onEdit.bind(this, 'plain')}>{gettext('Switch to plain text editor')}</DropdownItem>}
<DropdownItem onClick={this.props.onEdit.bind(this, 'plain')}>{gettext('Switch to plain text editor')}</DropdownItem>}
{(!this.props.readOnly && editorMode === 'plain') &&
<DropdownItem onMouseDown={this.props.onEdit.bind(this, 'rich')}>{gettext('Switch to rich text editor')}</DropdownItem>}
<DropdownItem onClick={this.props.onEdit.bind(this, 'rich')}>{gettext('Switch to rich text editor')}</DropdownItem>}
{!isSmall && this.props.showFileHistory &&
<DropdownItem onMouseDown={this.props.toggleHistory}>{gettext('History')}</DropdownItem>}
<DropdownItem onClick={this.props.toggleHistory}>{gettext('History')}</DropdownItem>}
{(this.props.openDialogs && editorMode === 'rich') &&
<DropdownItem onMouseDown={this.props.openDialogs.bind(this, 'help')}>{gettext('Help')}</DropdownItem>
<DropdownItem onClick={this.onHelpModuleToggle}>{gettext('Help')}</DropdownItem>
}
{isSmall && <DropdownItem onMouseDown={this.props.openParentDirectory}>{gettext('Open parent directory')}</DropdownItem>}
{isSmall && canGenerateShareLink && <DropdownItem onMouseDown={this.props.toggleShareLinkDialog}>{gettext('Share')}</DropdownItem>}
{isSmall && <DropdownItem onClick={this.props.openParentDirectory}>{gettext('Open parent directory')}</DropdownItem>}
{isSmall && canGenerateShareLink && <DropdownItem onClick={this.props.toggleShareLinkDialog}>{gettext('Share')}</DropdownItem>}
{(isSmall && this.props.showFileHistory) &&
<DropdownItem onMouseDown={this.props.toggleHistory}>{gettext('History')}</DropdownItem>
<DropdownItem onClick={this.props.toggleHistory}>{gettext('History')}</DropdownItem>
}
{isSmall && canDownloadFile &&
<DropdownItem onClick={this.downloadFile}>{gettext('Download')}</DropdownItem>

View File

@ -1,21 +1,21 @@
import React, { Fragment } from 'react';
import io from 'socket.io-client';
import { serialize, deserialize } from '@seafile/seafile-editor';
import { EXTERNAL_EVENTS, EventBus, RichMarkdownEditor } from '@seafile/seafile-editor';
import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, isDocs, mediaUrl } from '../../utils/constants';
import toaster from '../../components/toast';
import ShareDialog from '../../components/dialog/share-dialog';
import InsertFileDialog from '../../components/dialog/insert-file-dialog';
import LocalDraftDialog from '../../components/dialog/local-draft-dialog';
import HeaderToolbar from './header-toolbar';
import SeafileEditor from './seafile-editor';
import editorApi from './editor-api';
import DetailListView from './detail-list-view';
import '../../css/markdown-viewer/markdown-editor.css';
const CryptoJS = require('crypto-js');
const URL = require('url-parse');
const { repoID, filePath, fileName, draftID, isDraft, hasDraft, isLocked, lockedByMe } = window.app.pageOptions;
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
const userInfo = window.app.userInfo;
@ -75,6 +75,9 @@ class MarkdownEditor extends React.Component {
this.socket_id = socket.id;
});
}
this.editorRef = React.createRef();
this.isParticipant = false;
this.editorSelection = null;
}
toggleLockFile = () => {
@ -121,6 +124,8 @@ class MarkdownEditor extends React.Component {
receivePresenceData(data) {
let collabUsers = [];
let editingUsers = [];
switch(data.response) {
case 'user_join':
toaster.notify(`user ${data.user.name} joined`, {
@ -142,7 +147,13 @@ class MarkdownEditor extends React.Component {
}
}
}
this.setState({collabUsers: Object.values(data.users)});
collabUsers = Object.values(data.users);
editingUsers = collabUsers.filter(ele => ele.is_editing === true && ele.myself === undefined);
if (editingUsers.length > 0) {
const message = gettext('Another user is editing this file!');
toaster.danger(message, {duration: 3});
}
this.setState({ collabUsers });
return;
case 'user_editing':
toaster.danger(`user ${data.user.name} is editing this file!`, {
@ -176,14 +187,6 @@ class MarkdownEditor extends React.Component {
}
};
setContent = (str) => {
let value = deserialize(str);
this.setState({
markdownContent: str,
value: value,
});
};
checkDraft = () => {
let draftKey = editorApi.getDraftKey();
let draft = localStorage.getItem(draftKey);
@ -227,9 +230,6 @@ class MarkdownEditor extends React.Component {
openDialogs = (option) => {
switch (option) {
case 'help':
window.richMarkdownEditor.showHelpDialog();
break;
case 'share_link':
this.setState({
showMarkdownEditorDialog: true,
@ -247,18 +247,6 @@ class MarkdownEditor extends React.Component {
}
};
componentWillUnmount() {
this.socket.emit('repo_update', {
request: 'unwatch_update',
repo_id: editorApi.repoID,
user: {
name: editorApi.name,
username: editorApi.username,
contact_email: editorApi.contact_email,
},
});
}
async componentDidMount() {
const fileIcon = Utils.getFileIconUrl(fileName, 192);
@ -276,7 +264,6 @@ class MarkdownEditor extends React.Component {
// get file content
const fileContentRes = await seafileAPI.getFileContent(downloadUrl);
const markdownContent = fileContentRes.data;
const value = deserialize(markdownContent);
// init permission
let hasPermission = permission === 'rw' || permission === 'cloud-edit';
@ -300,7 +287,7 @@ class MarkdownEditor extends React.Component {
loading: false,
fileInfo: {...fileInfo, mtime, size, starred, permission, lastModifier, id},
markdownContent,
value,
value: '',
readOnly: !hasPermission || hasDraft,
});
@ -333,8 +320,36 @@ class MarkdownEditor extends React.Component {
window.location.href = url;
}
}, 100);
window.addEventListener('beforeunload', this.onUnload);
const eventBus = EventBus.getInstance();
this.unsubscribeInsertSeafileImage = eventBus.subscribe(EXTERNAL_EVENTS.ON_INSERT_IMAGE, this.onInsertImageToggle);
}
componentWillUnmount() {
window.removeEventListener('beforeunload', this.onUnload);
this.unsubscribeInsertSeafileImage();
if (!this.socket) return;
this.socket.emit('repo_update', {
request: 'unwatch_update',
repo_id: editorApi.repoID,
user: {
name: editorApi.name,
username: editorApi.username,
contact_email: editorApi.contact_email,
},
});
}
onUnload = (event) => {
const { contentChanged } = this.state;
if (!contentChanged) return;
this.clearTimer();
const confirmationMessage = gettext('Leave this page? The system may not save your changes.');
event.returnValue = confirmationMessage;
return confirmationMessage;
};
listFileTags = () => {
seafileAPI.listFileTags(repoID, filePath).then(res => {
let fileTagList = res.data.file_tags;
@ -381,84 +396,82 @@ class MarkdownEditor extends React.Component {
});
};
autoSaveDraft = () => {
let that = this;
if (that.timer) {
return;
}
that.timer = setTimeout(() => {
let str = '';
if (this.state.editorMode == 'rich') {
let value = this.draftRichValue;
str = serialize(value);
}
else if (this.state.editorMode == 'plain') {
str = this.draftPlainValue;
}
let draftKey = editorApi.getDraftKey();
localStorage.setItem(draftKey, str);
that.setState({
showDraftSaved: true
});
setTimeout(() => {
that.setState({
showDraftSaved: false
});
}, 3000);
that.timer = null;
}, 60000);
};
openParentDirectory = () => {
window.location.href = editorApi.getParentDectionaryUrl();
};
onEdit = (editorMode) => {
if (editorMode === 'rich') {
window.seafileEditor.switchToRichTextEditor();
return;
}
if (editorMode === 'plain') {
window.seafileEditor.switchToPlainTextEditor();
}
};
toggleShareLinkDialog = () => {
this.openDialogs('share_link');
};
onInsertImageToggle = (selection) => {
this.editorSelection = selection;
this.openDialogs('insert_file');
};
toggleHistory = () => {
window.location.href = siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + Utils.encodePath(filePath);
};
getInsertLink = (repoID, filePath) => {
const selection = this.editorSelection;
const fileName = Utils.getFileName(filePath);
const suffix = fileName.slice(fileName.indexOf('.') + 1);
const eventBus = EventBus.getInstance();
if (IMAGE_SUFFIXES.includes(suffix)) {
let innerURL = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(filePath) + '?raw=1';
window.richMarkdownEditor.addLink(fileName, innerURL, true);
eventBus.dispatch(EXTERNAL_EVENTS.INSERT_IMAGE, { title: fileName, url: innerURL, isImage: true, selection });
return;
}
let innerURL = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(filePath);
window.richMarkdownEditor.addLink(fileName, innerURL);
eventBus.dispatch(EXTERNAL_EVENTS.INSERT_IMAGE, { title: fileName, url: innerURL, selection});
};
onContentChanged = (value) => {
this.setState({ contentChanged: value });
addParticipants = () => {
if (this.isParticipant || !window.showParticipants) return;
const { userName } = editorApi;
const { participants } = this.state;
if (participants && participants.length !== 0) {
const isParticipant = participants.some((participant) => {
return participant.email === userName;
});
if (isParticipant) return;
}
const emails = [userName];
editorApi.addFileParticipants(emails).then((res) => {
this.isParticipant = true;
this.listFileParticipants();
});
};
onSaving = (value) => {
this.setState({ saving: value });
onContentChanged = () => {
this.setState({ contentChanged: true });
};
onSaveEditorContent = () => {
this.setState({ saving: true });
const content = this.editorRef.current.getValue();
editorApi.saveContent(content).then(() => {
this.setState({
saving: false,
contentChanged: false,
});
this.lastModifyTime = new Date();
const message = gettext('Successfully saved');
toaster.success(message, {duration: 2,});
editorApi.getFileInfo().then((res) => {
this.setFileInfoMtime(res.data);
});
this.addParticipants();
}, () => {
this.setState({ saving: false });
const message = gettext('Failed to save');
toaster.danger(message, {duration: 2});
});
};
render() {
if (this.state.loading) {
return (
<div className="empty-loading-page">
<div className="lds-ripple page-centered"><div></div><div></div></div>
</div>
);
}
const { loading, editorMode, markdownContent, fileInfo, fileTagList } = this.state;
return (
<Fragment>
@ -470,10 +483,9 @@ class MarkdownEditor extends React.Component {
collabUsers={this.state.collabUsers}
fileInfo={this.state.fileInfo}
toggleStar={this.toggleStar}
openParentDirectory={this.openParentDirectory}
openDialogs={this.openDialogs}
toggleShareLinkDialog={this.toggleShareLinkDialog}
onEdit={this.onEdit}
onEdit={this.setEditorMode}
toggleNewDraft={editorApi.createDraftFile}
showFileHistory={this.state.isShowHistory ? false : true }
toggleHistory={this.toggleHistory}
@ -481,50 +493,28 @@ class MarkdownEditor extends React.Component {
editorMode={this.state.editorMode}
contentChanged={this.state.contentChanged}
saving={this.state.saving}
onSaveEditorContent={this.onSaveEditorContent}
showDraftSaved={this.state.showDraftSaved}
isLocked={this.state.isLocked}
lockedByMe={this.state.lockedByMe}
toggleLockFile={this.toggleLockFile}
/>
<SeafileEditor
scriptSource={mediaUrl + 'js/mathjax/tex-svg.js'}
fileInfo={this.state.fileInfo}
markdownContent={this.state.markdownContent}
editorApi={editorApi}
collabUsers={this.state.collabUsers}
setFileInfoMtime={this.setFileInfoMtime}
setEditorMode={this.setEditorMode}
setContent={this.setContent}
draftID={draftID}
isDraft={isDraft}
mode={this.state.mode}
emitSwitchEditor={this.emitSwitchEditor}
hasDraft={hasDraft}
editorMode={this.state.editorMode}
siteRoot={siteRoot}
autoSaveDraft={this.autoSaveDraft}
setDraftValue={this.setDraftValue}
clearTimer={this.clearTimer}
openDialogs={this.openDialogs}
deleteDraft={this.deleteDraft}
readOnly={this.state.readOnly}
onContentChanged={this.onContentChanged}
onSaving={this.onSaving}
contentChanged={this.state.contentChanged}
fileTagList={this.state.fileTagList}
onFileTagChanged={this.onFileTagChanged}
participants={this.state.participants}
onParticipantsChange={this.onParticipantsChange}
markdownLint={fileName.toLowerCase() !== 'index.md'}
/>
{this.state.localDraftDialog &&
<LocalDraftDialog
localDraftDialog={this.state.localDraftDialog}
deleteDraft={this.deleteDraft}
closeDraftDialog={this.closeDraftDialog}
useDraft={this.useDraft}
/>
}
<div className='sf-md-viewer-content'>
<RichMarkdownEditor
ref={this.editorRef}
mode={editorMode}
isFetching={loading}
initValue={fileName}
value={markdownContent}
editorApi={editorApi}
onSave={this.onSaveEditorContent}
onContentChanged={this.onContentChanged}
mathJaxSource={mediaUrl + 'js/mathjax/tex-svg.js'}
isSupportInsertSeafileImage={true}
>
<DetailListView fileInfo={fileInfo} fileTagList={fileTagList} onFileTagChanged={this.onFileTagChanged}/>
</RichMarkdownEditor>
</div>
{this.state.showMarkdownEditorDialog && (
<React.Fragment>
{this.state.showInsertFileDialog &&

View File

@ -1,113 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EditorContext, Toolbar, MarkdownEditor, UserHelp } from '@seafile/seafile-editor';
import SidePanel from './side-panel';
import '../css/rich-editor.css';
const propTypes = {
scriptSource: PropTypes.string,
markdownContent: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
issues: PropTypes.object,
fileInfo: PropTypes.object,
readOnly: PropTypes.bool,
editorApi: PropTypes.object,
onSave: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
resetRichValue: PropTypes.func,
fileTagList: PropTypes.array,
onFileTagChanged: PropTypes.func,
participants: PropTypes.array,
onParticipantsChange: PropTypes.func,
openDialogs: PropTypes.func,
};
class RichMarkdownEditor extends React.Component {
constructor(props) {
super(props);
this.state = {
isShowSidePanel: false,
isShowHelpPanel: false,
};
window.richMarkdownEditor = this;
}
toggleSidePanel = () => {
this.setState({
isShowSidePanel: !this.state.isShowSidePanel,
isShowHelpPanel: false,
});
};
showHelpDialog = () => {
this.setState({isShowSidePanel: false, isShowHelpPanel: true});
};
hideHelpDialog = () => {
this.setState({isShowHelpPanel: false});
};
insertRepoFile = () => {
if (this.props.readOnly) return;
this.props.openDialogs && this.props.openDialogs('insert_file');
};
addLink = (fileName, url, isImage) => {
const editorRef = EditorContext.getEditorRef();
editorRef.addLink(fileName, url, isImage);
};
render() {
const hasSidePanel = true;
const { isShowSidePanel, isShowHelpPanel } = this.state;
const { value } = this.props;
const isShowHelpWrapper = isShowSidePanel || isShowHelpPanel;
const helpWrapperStyle = isShowHelpPanel ? {width: '350px'} : {};
return (
<div className='seafile-markdown-editor'>
<div className='markdown-editor-toolbar'>
<Toolbar
hasSidePanel={hasSidePanel}
isShowSidePanel={isShowSidePanel}
toggleSidePanel={this.toggleSidePanel}
insertRepoFile={this.insertRepoFile}
/>
</div>
<div className='markdown-editor-content'>
<div className={`markdown-editor-wrapper ${isShowHelpWrapper ? '' : 'full-screen'}`}>
<MarkdownEditor
scriptSource={this.props.scriptSource}
value={value}
onSave={this.props.onSave}
editorApi={this.props.editorApi}
onChange={this.props.onChange}
resetRichValue={this.props.resetRichValue}
/>
</div>
<div className={`markdown-help-wrapper ${isShowHelpWrapper ? 'show' : ''}`} style={helpWrapperStyle}>
{isShowSidePanel && (
<SidePanel
document={value}
fileInfo={this.props.fileInfo}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
participants={this.props.participants}
onParticipantsChange={this.props.onParticipantsChange}
/>
)}
{isShowHelpPanel && <UserHelp hideHelpDialog={this.hideHelpDialog} />}
</div>
</div>
</div>
);
}
}
RichMarkdownEditor.propTypes = propTypes;
export default RichMarkdownEditor;

View File

@ -1,73 +0,0 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from 'react';
import PropTypes from 'prop-types';
import { Outline as OutlineView } from '@seafile/seafile-editor';
import DetailListView from './detail-list-view';
import '../css/side-panel.css';
const propTypes = {
document: PropTypes.array,
fileInfo: PropTypes.object.isRequired,
fileTagList: PropTypes.array,
onFileTagChanged: PropTypes.func.isRequired,
participants: PropTypes.array,
onParticipantsChange: PropTypes.func.isRequired,
};
class SidePanel extends React.PureComponent {
state = {
navItem: 'outline'
};
onOutlineClick = (event) => {
event.preventDefault();
this.setState({navItem: 'outline'});
};
onDetailClick = (event) => {
event.preventDefault();
this.setState({navItem: 'detail'});
};
render() {
var outlineActive = '';
var detailList = '';
if (this.state.navItem === 'outline') {
outlineActive = 'active';
} else if (this.state.navItem === 'detail') {
detailList = 'active';
}
return (
<div className="side-panel d-flex flex-column">
<ul className="flex-shrink-0 nav justify-content-around bg-white">
<li className="nav-item">
<a className={'nav-link ' + outlineActive} href="" onClick={this.onOutlineClick}><i className="iconfont icon-list-ul"/></a>
</li>
<li className="nav-item">
<a className={'nav-link ' + detailList} href="" onClick={this.onDetailClick}><i className={'iconfont icon-info-circle'}/></a>
</li>
</ul>
<div className="side-panel-content flex-fill">
{this.state.navItem === 'outline' &&
<OutlineView document={this.props.document} />
}
{this.state.navItem === 'detail' &&
<DetailListView
fileInfo={this.props.fileInfo}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
/>
}
</div>
</div>
);
}
}
SidePanel.propTypes = propTypes;
export default SidePanel;

View File

@ -1,268 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'slate';
import { deserialize, serialize, PlainMarkdownEditor } from '@seafile/seafile-editor';
import toaster from '../../../components/toast';
import { gettext } from '../../../utils/constants';
import RichMarkdownEditor from '../rich-markdown-editor';
const propTypes = {
mode: PropTypes.string,
editorMode: PropTypes.string,
readOnly: PropTypes.bool,
isDraft: PropTypes.bool,
scriptSource: PropTypes.string,
markdownContent: PropTypes.string,
editorApi: PropTypes.object.isRequired,
collaUsers: PropTypes.array,
onContentChanged: PropTypes.func.isRequired,
onSaving: PropTypes.func.isRequired,
saving: PropTypes.bool,
fileTagList: PropTypes.array,
onFileTagChanged: PropTypes.func.isRequired,
participants: PropTypes.array.isRequired,
onParticipantsChange: PropTypes.func.isRequired,
markdownLint: PropTypes.bool,
setFileInfoMtime: PropTypes.func.isRequired,
setEditorMode: PropTypes.func,
autoSaveDraft: PropTypes.func,
setDraftValue: PropTypes.func,
clearTimer: PropTypes.func,
deleteDraft: PropTypes.func,
contentChanged: PropTypes.bool,
openDialogs: PropTypes.func,
fileInfo: PropTypes.object.isRequired,
collabUsers: PropTypes.array.isRequired,
emitSwitchEditor: PropTypes.func.isRequired,
isSaving: PropTypes.bool,
collabServer: PropTypes.string,
};
class SeafileEditor extends React.Component {
constructor(props) {
super(props);
const { mode, markdownContent, isDraft } = this.props;
const isEditMode = mode === 'editor' || isDraft;
const richValue = isEditMode ? deserialize(markdownContent) : deserialize('');
this.state = {
initialPlainValue: '',
currentContent: markdownContent,
richValue: richValue,
issues: { issue_list: []}
};
this.lastModifyTime = null;
this.autoSave = false;
this.isParticipant = false;
window.seafileEditor = this;
}
UNSAFE_componentWillMount() {
if (this.props.editorMode === 'rich') {
const document = this.state.richValue;
const firstNode = document[0];
/**
* if the markdown content is empty, the rich value contains
* only a paragraph which contains a empty text node
*
*/
if (document.length === 1 &&
firstNode.type === 'paragraph' &&
firstNode.children.length === 1 &&
Text.isText(firstNode.children[0]) &&
firstNode.children[0].text.length === 0) {
let headerContent = this.props.fileInfo.name.slice(0, this.props.fileInfo.name.lastIndexOf('.'));
const header = {
type: 'header_one',
children: [{text: headerContent, marks: []}]
};
document.push(header);
document.shift();
this.setState({richValue: document});
}
}
}
componentDidMount() {
window.addEventListener('beforeunload', this.onUnload);
// notify current user if others are also editing this file
const { collabUsers } = this.props;
const editingUsers = collabUsers.filter(ele => ele.is_editing === true && ele.myself === undefined);
if (editingUsers.length > 0) {
const message = gettext('Another user is editing this file!');
toaster.danger(message, {duration: 3});
}
}
componentWillUnmount() {
window.removeEventListener('beforeunload', this.onUnload);
}
onUnload = (event) => {
if (!this.props.contentChanged) return;
const confirmationMessage = gettext('Leave this page? The system may not save your changes.');
this.props.clearTimer();
this.props.deleteDraft && this.props.deleteDraft();
event.returnValue = confirmationMessage;
return confirmationMessage;
};
switchToPlainTextEditor = () => {
// TODO: performance, change to do serialize in async way
if (this.props.editorMode === 'rich') {
const value = this.state.richValue;
const str = serialize(value);
this.props.setEditorMode('plain');
this.setState({
initialPlainValue: str,
currentContent: str
});
}
if (this.props.collabServer) {
this.props.emitSwitchEditor(false);
}
};
switchToRichTextEditor = () => {
// TODO: performance, change to do deserialize in async way
this.setState({richValue: deserialize(this.state.currentContent)});
this.props.setEditorMode('rich');
if (this.props.collabServer) {
this.props.emitSwitchEditor(false);
}
};
saveContent = (str) => {
this.props.onSaving(true);
this.props.editorApi.saveContent(str).then(() => {
this.props.onSaving(false);
this.props.onContentChanged(false);
// remove markdown lint temporarily
// if (this.props.markdownLint) {
// const slateValue = this.state.richValue;
// this.props.editorApi.markdownLint(JSON.stringify(slateValue)).then((res) => {
// this.setState({
// issues: res.data
// });
// });
// }
this.lastModifyTime = new Date();
const message = gettext('Successfully saved');
toaster.success(message, {duration: 2,});
this.props.editorApi.getFileInfo().then((res) => {
this.props.setFileInfoMtime(res.data);
});
this.addParticipants();
}, () => {
this.props.onSaving(false);
const message = gettext('Failed to save');
toaster.danger(message, {duration: 2});
});
};
onRichEditorSave = () => {
if (this.props.isSaving) return;
const value = this.state.richValue;
const str = serialize(value);
this.saveContent(str);
this.props.clearTimer();
this.props.deleteDraft && this.props.deleteDraft();
};
onPlainEditorSave = () => {
if (this.props.isSaving) return;
const str = this.state.currentContent;
this.saveContent(str);
this.props.clearTimer();
this.props.deleteDraft && this.props.deleteDraft();
};
resetRichValue = () => {
const value = this.state.richValue;
this.setState({ richValue: value });
};
onChange = (value, operations) => {
if (this.props.editorMode === 'rich') {
this.setState({richValue: value,});
this.props.setDraftValue('rich', this.state.richValue);
const ops = operations.filter(o => {
return o.type !== 'set_selection' && o.type !== 'set_value';
});
if (ops.length !== 0) {
this.props.onContentChanged(true);
if (this.autoSave) this.props.autoSaveDraft();
}
} else {
this.setState({currentContent: value});
this.props.onContentChanged(true);
this.props.setDraftValue('rich', this.state.richValue);
this.props.autoSaveDraft();
}
};
addParticipants = () => {
if (this.isParticipant || !window.showParticipants) return;
const { userName, addFileParticipants } = this.props.editorApi;
const { participants } = this.props;
if (participants && participants.length !== 0) {
this.isParticipant = participants.every((participant) => {
return participant.email === userName;
});
if (this.isParticipant) return;
}
let emails = [userName];
addFileParticipants(emails).then((res) => {
this.isParticipant = true;
this.props.onParticipantsChange();
});
};
render() {
if (this.props.editorMode === 'rich') {
return (
<RichMarkdownEditor
scriptSource={this.props.scriptSource}
readOnly={this.props.readOnly}
value={this.state.richValue}
editorApi={this.props.editorApi}
fileInfo={this.props.fileInfo}
collaUsers={this.props.collaUsers}
onChange={this.onChange}
onSave={this.onRichEditorSave}
resetRichValue={this.resetRichValue}
fileTagList={this.props.fileTagList}
onFileTagChanged={this.props.onFileTagChanged}
participants={this.props.participants}
onParticipantsChange={this.props.onParticipantsChange}
openDialogs={this.props.openDialogs}
/>
);
}
return (
<PlainMarkdownEditor
scriptSource={this.props.scriptSource}
editorApi={this.props.editorApi}
initialValue={this.state.initialPlainValue}
currentContent={this.state.currentContent}
contentChanged={this.props.contentChanged}
fileInfo={this.props.fileInfo}
collabUsers={this.props.collabUsers}
onSave={this.onPlainEditorSave}
onChange={this.onChange}
/>
);
}
}
SeafileEditor.propTypes = propTypes;
export default SeafileEditor;