1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-28 19:51:34 +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'; 'use strict';
const fs = require('fs'); const fs = require('fs');
@ -20,7 +21,7 @@ const getClientEnvironment = require('./env');
const paths = require('./paths'); const paths = require('./paths');
const modules = require('./modules'); const modules = require('./modules');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); 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 webpackBundleTracker = require('webpack-bundle-tracker');
const ForkTsCheckerWebpackPlugin = const ForkTsCheckerWebpackPlugin =
@ -135,34 +136,34 @@ module.exports = function (webpackEnv) {
config: false, config: false,
plugins: !useTailwind plugins: !useTailwind
? [ ? [
'postcss-flexbugs-fixes', 'postcss-flexbugs-fixes',
[ [
'postcss-preset-env', 'postcss-preset-env',
{ {
autoprefixer: { autoprefixer: {
flexbox: 'no-2009', 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,
},
],
], ],
// 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, sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
}, },
@ -444,7 +445,7 @@ module.exports = function (webpackEnv) {
}, },
], ],
], ],
plugins: [ plugins: [
// isEnvDevelopment && // isEnvDevelopment &&
// shouldUseReactRefresh && // shouldUseReactRefresh &&
@ -478,7 +479,7 @@ module.exports = function (webpackEnv) {
cacheDirectory: true, cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled // See #6846 for context on why cacheCompression is disabled
cacheCompression: false, cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules // Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode // code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines. // show incorrect code and set breakpoints on the wrong lines.
@ -608,7 +609,7 @@ module.exports = function (webpackEnv) {
}, },
plugins: [ plugins: [
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
process: "process/browser.js", process: 'process/browser.js',
}), }),
new NodePolyfillPlugin({ new NodePolyfillPlugin({
excludeAliases: ['console'], 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/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "0.3.22", "@seafile/sdoc-editor": "0.3.22",
"@seafile/seafile-calendar": "0.0.12", "@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/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4", "@uiw/react-codemirror": "^4.19.4",
"classnames": "^2.2.6", "classnames": "^2.2.6",
@ -17,9 +17,9 @@
"crypto-js": "4.2.0", "crypto-js": "4.2.0",
"deep-copy": "1.4.2", "deep-copy": "1.4.2",
"glamor": "^2.20.40", "glamor": "^2.20.40",
"i18next": "22.4.6", "i18next": "^17.0.13",
"i18next-browser-languagedetector": "7.0.1", "i18next-browser-languagedetector": "^3.0.3",
"i18next-xhr-backend": "3.2.2", "i18next-xhr-backend": "^3.1.2",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"MD5": "^1.3.0", "MD5": "^1.3.0",
"moment": "^2.22.2", "moment": "^2.22.2",
@ -32,7 +32,7 @@
"react-chartjs-2": "^2.8.0", "react-chartjs-2": "^2.8.0",
"react-cookies": "^0.1.0", "react-cookies": "^0.1.0",
"react-dom": "17.0.0", "react-dom": "17.0.0",
"react-i18next": "12.1.1", "react-i18next": "^10.12.2",
"react-responsive": "9.0.2", "react-responsive": "9.0.2",
"react-select": "5.7.0", "react-select": "5.7.0",
"react-transition-group": "4.4.5", "react-transition-group": "4.4.5",

View File

@ -2,7 +2,7 @@ import i18n from 'i18next';
import Backend from 'i18next-xhr-backend'; import Backend from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { mediaUrl } from './utils/constants'; import { mediaUrl } from '../utils/constants';
const lang = window.app.pageOptions.lang; const lang = window.app.pageOptions.lang;
@ -14,7 +14,7 @@ i18n
lng: lang, lng: lang,
fallbackLng: 'en', fallbackLng: 'en',
ns: ['seafile-editor'], ns: ['seafile-editor'],
defaultNS: 'translations', defaultNS: 'seafile-editor',
whitelist: ['en', 'zh-CN', 'fr', 'de', 'cs', 'es', 'es-AR', 'es-MX', 'ru'], 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 { #root {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -168,3 +173,10 @@
margin-left: 0.5rem; margin-left: 0.5rem;
color: #888; 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!
import React from 'react'; import React, { Suspense } from 'react';
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import { I18nextProvider } from 'react-i18next'; 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 MarkdownEditor from './pages/markdown-editor';
import Loading from './components/loading';
import './index.css'; import './index.css';
ReactDom.render( ReactDom.render(
<I18nextProvider i18n={ i18n } > <I18nextProvider i18n={ i18n } >
<MarkdownEditor /> <Suspense fallback={<Loading />}>
<MarkdownEditor />
</Suspense>
</I18nextProvider>, </I18nextProvider>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -23,3 +23,7 @@
.collab-users-dropdown.dropdown { .collab-users-dropdown.dropdown {
margin-right: 6px; 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 parentPath = this.filePath.substring(0, this.filePath.lastIndexOf('/'));
let libName = encodeURIComponent(repoName); let libName = encodeURIComponent(repoName);
let path = Utils.encodePath(parentPath); let path = Utils.encodePath(parentPath);

View File

@ -23,7 +23,6 @@ const propTypes = {
onEdit: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired,
toggleNewDraft: PropTypes.func.isRequired, toggleNewDraft: PropTypes.func.isRequired,
toggleStar: PropTypes.func.isRequired, toggleStar: PropTypes.func.isRequired,
openParentDirectory: PropTypes.func.isRequired,
openDialogs: PropTypes.func.isRequired, openDialogs: PropTypes.func.isRequired,
showFileHistory: PropTypes.bool.isRequired, showFileHistory: PropTypes.bool.isRequired,
toggleHistory: PropTypes.func.isRequired, toggleHistory: PropTypes.func.isRequired,
@ -31,6 +30,7 @@ const propTypes = {
readOnly: PropTypes.bool.isRequired, readOnly: PropTypes.bool.isRequired,
contentChanged: PropTypes.bool.isRequired, contentChanged: PropTypes.bool.isRequired,
saving: PropTypes.bool.isRequired, saving: PropTypes.bool.isRequired,
onSaveEditorContent: PropTypes.func.isRequired,
showDraftSaved: PropTypes.bool.isRequired, showDraftSaved: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired,
lockedByMe: 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)}`; location.href = `seafile://openfile?repo_id=${encodeURIComponent(repoID)}&path=${encodeURIComponent(path)}`;
}; };
openParentDirectory = () => {
const { editorApi } = this.props;
window.location.href = editorApi.getParentDictionaryUrl();
};
render() { render() {
let { contentChanged, saving, isLocked, lockedByMe } = this.props; 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') { if (this.props.editorMode === 'rich') {
return ( return (
@ -71,27 +74,7 @@ class HeaderToolbar extends React.Component {
mediaUrl={mediaUrl} mediaUrl={mediaUrl}
isStarred={this.props.fileInfo.isStarred} 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"> <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) && {(seafileCollabServer && this.props.collabUsers.length > 0) &&
<CollabUsersButton <CollabUsersButton
className="collab-users-dropdown" className="collab-users-dropdown"
@ -100,24 +83,43 @@ class HeaderToolbar extends React.Component {
/> />
} }
<ButtonGroup> <ButtonGroup>
<ButtonItem text={gettext('Open parent directory')} id={'parentDirectory'} <ButtonItem
icon={'fa fa-folder-open'} onMouseDown={this.props.openParentDirectory}/> text={gettext('Open parent directory')}
{(canLockUnlockFile && !isLocked) && id={'parentDirectory'}
<ButtonItem id="lock-unlock-file" icon='fa fa-lock' text={gettext('Lock')} onMouseDown={this.props.toggleLockFile}/> icon={'fa fa-folder-open'}
} onMouseDown={this.openParentDirectory}
{(canLockUnlockFile && lockedByMe) && />
<ButtonItem id="lock-unlock-file" icon='fa fa-unlock' text={gettext('Unlock')} onMouseDown={this.props.toggleLockFile}/> {(canLockUnlockFile && !isLocked) && (
} <ButtonItem
{canGenerateShareLink && id="lock-unlock-file"
<ButtonItem id={'shareBtn'} text={gettext('Share')} icon={'fa fa-share-alt'} icon='fa fa-lock'
onMouseDown={this.props.toggleShareLinkDialog}/> 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 ? {saving ?
<button type={'button'} aria-label={gettext('Saving...')} className={'btn btn-icon btn-secondary btn-active'}> <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} <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 && ( {canDownloadFile && (
<ButtonItem <ButtonItem
@ -127,14 +129,14 @@ class HeaderToolbar extends React.Component {
onClick={this.downloadFile} onClick={this.downloadFile}
/> />
)} )}
{this.props.fileInfo.permission == 'rw' && {this.props.fileInfo.permission == 'rw' && (
<ButtonItem <ButtonItem
id="open-via-client" id="open-via-client"
icon="sf3-font sf3-font-desktop" icon="sf3-font sf3-font-desktop"
text={gettext('Open via Client')} text={gettext('Open via Client')}
onClick={this.openFileViaClient} onClick={this.openFileViaClient}
/> />
} )}
</ButtonGroup> </ButtonGroup>
<MoreMenu <MoreMenu
readOnly={this.props.readOnly} readOnly={this.props.readOnly}
@ -170,7 +172,7 @@ class HeaderToolbar extends React.Component {
editorMode={this.props.editorMode} editorMode={this.props.editorMode}
onEdit={this.props.onEdit} onEdit={this.props.onEdit}
toggleShareLinkDialog={this.props.toggleShareLinkDialog} toggleShareLinkDialog={this.props.toggleShareLinkDialog}
openParentDirectory={this.props.openParentDirectory} openParentDirectory={this.openParentDirectory}
showFileHistory={this.props.showFileHistory} showFileHistory={this.props.showFileHistory}
toggleHistory={this.props.toggleHistory} toggleHistory={this.props.toggleHistory}
isSmallScreen={true} isSmallScreen={true}
@ -179,7 +181,9 @@ class HeaderToolbar extends React.Component {
</div> </div>
</div> </div>
); );
} else if (this.props.editorMode === 'plain') { }
if (this.props.editorMode === 'plain') {
return ( return (
<div className="sf-md-viewer-topbar"> <div className="sf-md-viewer-topbar">
<div className="sf-md-viewer-topbar-first d-flex justify-content-between"> <div className="sf-md-viewer-topbar-first d-flex justify-content-between">
@ -194,9 +198,10 @@ class HeaderToolbar extends React.Component {
/> />
} }
<ButtonGroup> <ButtonGroup>
{ saving ? {saving ?
<button type={'button'} className={'btn btn-icon btn-secondary btn-active'}> <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} /> <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> </div>
</div> </div>
); );
} }
return null;
} }
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { EXTERNAL_EVENTS, EventBus } from '@seafile/seafile-editor';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Tooltip } from 'reactstrap'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Tooltip } from 'reactstrap';
import { gettext, canGenerateShareLink } from '../../../utils/constants'; import { gettext, canGenerateShareLink } from '../../../utils/constants';
@ -32,7 +33,12 @@ class MoreMenu extends React.PureComponent {
}; };
dropdownToggle = () => { 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 = () => { downloadFile = () => {
@ -51,18 +57,18 @@ class MoreMenu extends React.PureComponent {
</DropdownToggle> </DropdownToggle>
<DropdownMenu className="drop-list" right={true}> <DropdownMenu className="drop-list" right={true}>
{(!this.props.readOnly && editorMode === 'rich') && {(!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') && {(!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 && {!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') && {(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 && <DropdownItem onClick={this.props.openParentDirectory}>{gettext('Open parent directory')}</DropdownItem>}
{isSmall && canGenerateShareLink && <DropdownItem onMouseDown={this.props.toggleShareLinkDialog}>{gettext('Share')}</DropdownItem>} {isSmall && canGenerateShareLink && <DropdownItem onClick={this.props.toggleShareLinkDialog}>{gettext('Share')}</DropdownItem>}
{(isSmall && this.props.showFileHistory) && {(isSmall && this.props.showFileHistory) &&
<DropdownItem onMouseDown={this.props.toggleHistory}>{gettext('History')}</DropdownItem> <DropdownItem onClick={this.props.toggleHistory}>{gettext('History')}</DropdownItem>
} }
{isSmall && canDownloadFile && {isSmall && canDownloadFile &&
<DropdownItem onClick={this.downloadFile}>{gettext('Download')}</DropdownItem> <DropdownItem onClick={this.downloadFile}>{gettext('Download')}</DropdownItem>

View File

@ -1,21 +1,21 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import io from 'socket.io-client'; 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 { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { gettext, isDocs, mediaUrl } from '../../utils/constants'; import { gettext, isDocs, mediaUrl } from '../../utils/constants';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import ShareDialog from '../../components/dialog/share-dialog'; import ShareDialog from '../../components/dialog/share-dialog';
import InsertFileDialog from '../../components/dialog/insert-file-dialog'; import InsertFileDialog from '../../components/dialog/insert-file-dialog';
import LocalDraftDialog from '../../components/dialog/local-draft-dialog';
import HeaderToolbar from './header-toolbar'; import HeaderToolbar from './header-toolbar';
import SeafileEditor from './seafile-editor';
import editorApi from './editor-api'; import editorApi from './editor-api';
import DetailListView from './detail-list-view';
import '../../css/markdown-viewer/markdown-editor.css'; import '../../css/markdown-viewer/markdown-editor.css';
const CryptoJS = require('crypto-js'); const CryptoJS = require('crypto-js');
const URL = require('url-parse'); const URL = require('url-parse');
const { repoID, filePath, fileName, draftID, isDraft, hasDraft, isLocked, lockedByMe } = window.app.pageOptions; const { repoID, filePath, fileName, draftID, isDraft, hasDraft, isLocked, lockedByMe } = window.app.pageOptions;
const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config; const { siteRoot, serviceUrl, seafileCollabServer } = window.app.config;
const userInfo = window.app.userInfo; const userInfo = window.app.userInfo;
@ -75,6 +75,9 @@ class MarkdownEditor extends React.Component {
this.socket_id = socket.id; this.socket_id = socket.id;
}); });
} }
this.editorRef = React.createRef();
this.isParticipant = false;
this.editorSelection = null;
} }
toggleLockFile = () => { toggleLockFile = () => {
@ -121,6 +124,8 @@ class MarkdownEditor extends React.Component {
receivePresenceData(data) { receivePresenceData(data) {
let collabUsers = [];
let editingUsers = [];
switch(data.response) { switch(data.response) {
case 'user_join': case 'user_join':
toaster.notify(`user ${data.user.name} joined`, { 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; return;
case 'user_editing': case 'user_editing':
toaster.danger(`user ${data.user.name} is editing this file!`, { 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 = () => { checkDraft = () => {
let draftKey = editorApi.getDraftKey(); let draftKey = editorApi.getDraftKey();
let draft = localStorage.getItem(draftKey); let draft = localStorage.getItem(draftKey);
@ -227,9 +230,6 @@ class MarkdownEditor extends React.Component {
openDialogs = (option) => { openDialogs = (option) => {
switch (option) { switch (option) {
case 'help':
window.richMarkdownEditor.showHelpDialog();
break;
case 'share_link': case 'share_link':
this.setState({ this.setState({
showMarkdownEditorDialog: true, 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() { async componentDidMount() {
const fileIcon = Utils.getFileIconUrl(fileName, 192); const fileIcon = Utils.getFileIconUrl(fileName, 192);
@ -276,7 +264,6 @@ class MarkdownEditor extends React.Component {
// get file content // get file content
const fileContentRes = await seafileAPI.getFileContent(downloadUrl); const fileContentRes = await seafileAPI.getFileContent(downloadUrl);
const markdownContent = fileContentRes.data; const markdownContent = fileContentRes.data;
const value = deserialize(markdownContent);
// init permission // init permission
let hasPermission = permission === 'rw' || permission === 'cloud-edit'; let hasPermission = permission === 'rw' || permission === 'cloud-edit';
@ -300,7 +287,7 @@ class MarkdownEditor extends React.Component {
loading: false, loading: false,
fileInfo: {...fileInfo, mtime, size, starred, permission, lastModifier, id}, fileInfo: {...fileInfo, mtime, size, starred, permission, lastModifier, id},
markdownContent, markdownContent,
value, value: '',
readOnly: !hasPermission || hasDraft, readOnly: !hasPermission || hasDraft,
}); });
@ -333,8 +320,36 @@ class MarkdownEditor extends React.Component {
window.location.href = url; window.location.href = url;
} }
}, 100); }, 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 = () => { listFileTags = () => {
seafileAPI.listFileTags(repoID, filePath).then(res => { seafileAPI.listFileTags(repoID, filePath).then(res => {
let fileTagList = res.data.file_tags; 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 = () => { toggleShareLinkDialog = () => {
this.openDialogs('share_link'); this.openDialogs('share_link');
}; };
onInsertImageToggle = (selection) => {
this.editorSelection = selection;
this.openDialogs('insert_file');
};
toggleHistory = () => { toggleHistory = () => {
window.location.href = siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + Utils.encodePath(filePath); window.location.href = siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + Utils.encodePath(filePath);
}; };
getInsertLink = (repoID, filePath) => { getInsertLink = (repoID, filePath) => {
const selection = this.editorSelection;
const fileName = Utils.getFileName(filePath); const fileName = Utils.getFileName(filePath);
const suffix = fileName.slice(fileName.indexOf('.') + 1); const suffix = fileName.slice(fileName.indexOf('.') + 1);
const eventBus = EventBus.getInstance();
if (IMAGE_SUFFIXES.includes(suffix)) { if (IMAGE_SUFFIXES.includes(suffix)) {
let innerURL = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(filePath) + '?raw=1'; 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; return;
} }
let innerURL = serviceUrl + '/lib/' + repoID + '/file' + Utils.encodePath(filePath); 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) => { addParticipants = () => {
this.setState({ contentChanged: value }); 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) => { onContentChanged = () => {
this.setState({ saving: value }); 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() { render() {
if (this.state.loading) { const { loading, editorMode, markdownContent, fileInfo, fileTagList } = this.state;
return (
<div className="empty-loading-page">
<div className="lds-ripple page-centered"><div></div><div></div></div>
</div>
);
}
return ( return (
<Fragment> <Fragment>
@ -470,10 +483,9 @@ class MarkdownEditor extends React.Component {
collabUsers={this.state.collabUsers} collabUsers={this.state.collabUsers}
fileInfo={this.state.fileInfo} fileInfo={this.state.fileInfo}
toggleStar={this.toggleStar} toggleStar={this.toggleStar}
openParentDirectory={this.openParentDirectory}
openDialogs={this.openDialogs} openDialogs={this.openDialogs}
toggleShareLinkDialog={this.toggleShareLinkDialog} toggleShareLinkDialog={this.toggleShareLinkDialog}
onEdit={this.onEdit} onEdit={this.setEditorMode}
toggleNewDraft={editorApi.createDraftFile} toggleNewDraft={editorApi.createDraftFile}
showFileHistory={this.state.isShowHistory ? false : true } showFileHistory={this.state.isShowHistory ? false : true }
toggleHistory={this.toggleHistory} toggleHistory={this.toggleHistory}
@ -481,50 +493,28 @@ class MarkdownEditor extends React.Component {
editorMode={this.state.editorMode} editorMode={this.state.editorMode}
contentChanged={this.state.contentChanged} contentChanged={this.state.contentChanged}
saving={this.state.saving} saving={this.state.saving}
onSaveEditorContent={this.onSaveEditorContent}
showDraftSaved={this.state.showDraftSaved} showDraftSaved={this.state.showDraftSaved}
isLocked={this.state.isLocked} isLocked={this.state.isLocked}
lockedByMe={this.state.lockedByMe} lockedByMe={this.state.lockedByMe}
toggleLockFile={this.toggleLockFile} toggleLockFile={this.toggleLockFile}
/> />
<SeafileEditor <div className='sf-md-viewer-content'>
scriptSource={mediaUrl + 'js/mathjax/tex-svg.js'} <RichMarkdownEditor
fileInfo={this.state.fileInfo} ref={this.editorRef}
markdownContent={this.state.markdownContent} mode={editorMode}
editorApi={editorApi} isFetching={loading}
collabUsers={this.state.collabUsers} initValue={fileName}
setFileInfoMtime={this.setFileInfoMtime} value={markdownContent}
setEditorMode={this.setEditorMode} editorApi={editorApi}
setContent={this.setContent} onSave={this.onSaveEditorContent}
draftID={draftID} onContentChanged={this.onContentChanged}
isDraft={isDraft} mathJaxSource={mediaUrl + 'js/mathjax/tex-svg.js'}
mode={this.state.mode} isSupportInsertSeafileImage={true}
emitSwitchEditor={this.emitSwitchEditor} >
hasDraft={hasDraft} <DetailListView fileInfo={fileInfo} fileTagList={fileTagList} onFileTagChanged={this.onFileTagChanged}/>
editorMode={this.state.editorMode} </RichMarkdownEditor>
siteRoot={siteRoot} </div>
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}
/>
}
{this.state.showMarkdownEditorDialog && ( {this.state.showMarkdownEditorDialog && (
<React.Fragment> <React.Fragment>
{this.state.showInsertFileDialog && {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;