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:
parent
bb5ce5e233
commit
9b21cbc51d
@ -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
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/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",
|
||||||
|
@ -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'],
|
||||||
|
|
@ -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;
|
||||||
|
}
|
@ -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')
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 &&
|
||||||
|
@ -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