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:
parent
bb5ce5e233
commit
9b21cbc51d
frontend
config
package-lock.jsonpackage.jsonsrc
_i18n
css/markdown-viewer
index.jspages/markdown-editor
@ -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
11978
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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'],
|
||||
|
@ -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;
|
||||
}
|
@ -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')
|
||||
);
|
||||
|
@ -23,3 +23,7 @@
|
||||
.collab-users-dropdown.dropdown {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.btn-active[data-active=true] {
|
||||
color: #eb8205;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 &&
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
Loading…
Reference in New Issue
Block a user