integrate Nextcloud Text as editor for notes
This commit is contained in:
parent
b1a988e1da
commit
21e6dafddc
|
@ -6,10 +6,13 @@ namespace OCA\Notes\Controller;
|
||||||
|
|
||||||
use OCA\Notes\Service\NotesService;
|
use OCA\Notes\Service\NotesService;
|
||||||
|
|
||||||
|
use OCA\Viewer\Event\LoadViewer;
|
||||||
|
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http\TemplateResponse;
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
|
@ -19,6 +22,8 @@ class PageController extends Controller {
|
||||||
private $notesService;
|
private $notesService;
|
||||||
/** @var IUserSession */
|
/** @var IUserSession */
|
||||||
private $userSession;
|
private $userSession;
|
||||||
|
/** @var IEventDispatcher */
|
||||||
|
private $eventDispatcher;
|
||||||
/** @IURLGenerator */
|
/** @IURLGenerator */
|
||||||
private $urlGenerator;
|
private $urlGenerator;
|
||||||
|
|
||||||
|
@ -27,11 +32,13 @@ class PageController extends Controller {
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
NotesService $notesService,
|
NotesService $notesService,
|
||||||
IUserSession $userSession,
|
IUserSession $userSession,
|
||||||
|
IEventDispatcher $eventDispatcher,
|
||||||
IURLGenerator $urlGenerator
|
IURLGenerator $urlGenerator
|
||||||
) {
|
) {
|
||||||
parent::__construct($AppName, $request);
|
parent::__construct($AppName, $request);
|
||||||
$this->notesService = $notesService;
|
$this->notesService = $notesService;
|
||||||
$this->userSession = $userSession;
|
$this->userSession = $userSession;
|
||||||
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
$this->urlGenerator = $urlGenerator;
|
$this->urlGenerator = $urlGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +48,7 @@ class PageController extends Controller {
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*/
|
*/
|
||||||
public function index() : TemplateResponse {
|
public function index() : TemplateResponse {
|
||||||
|
$this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer());
|
||||||
$devMode = !is_file(dirname(__FILE__).'/../../js/notes-main.js');
|
$devMode = !is_file(dirname(__FILE__).'/../../js/notes-main.js');
|
||||||
$response = new TemplateResponse(
|
$response = new TemplateResponse(
|
||||||
$this->appName,
|
$this->appName,
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<AppContent :class="{ loading: loading || isManualSave, 'icon-error': !loading && (!note || note.error), 'sidebar-open': sidebarOpen }">
|
<AppContent :class="{ loading: loading, 'icon-error': !loading && (!note || note.error), 'sidebar-open': sidebarOpen }">
|
||||||
<div v-if="!loading && note && !note.error && !note.deleting"
|
<div v-if="!loading && note && !note.error && !note.deleting"
|
||||||
id="note-container"
|
id="note-container"
|
||||||
class="note-container"
|
class="note-container"
|
||||||
:class="{ fullscreen: fullscreen }"
|
:class="{ fullscreen: fullscreen }"
|
||||||
>
|
>
|
||||||
<div class="note-editor">
|
<div class="note-editor">
|
||||||
<div v-show="!note.content" class="placeholder">
|
<component :is="viewer.component"
|
||||||
{{ preview ? t('notes', 'Empty note') : t('notes', 'Write …') }}
|
ref="texteditor"
|
||||||
</div>
|
:fileid="fileId"
|
||||||
<ThePreview v-if="preview" :value="note.content" />
|
:basename="title"
|
||||||
<TheEditor v-else :value="note.content" @input="onEdit" />
|
:active="true"
|
||||||
|
:has-preview="true"
|
||||||
|
mime="text/markdown"
|
||||||
|
class="text-editor"
|
||||||
|
@ready="onEditorReady"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="action-buttons">
|
<span class="action-buttons">
|
||||||
<Actions :open.sync="actionsOpen" menu-align="right">
|
<Actions :open.sync="actionsOpen" menu-align="right">
|
||||||
|
@ -20,13 +25,6 @@
|
||||||
>
|
>
|
||||||
{{ t('notes', 'Details') }}
|
{{ t('notes', 'Details') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
|
||||||
v-tooltip.left="t('notes', 'CTRL + /')"
|
|
||||||
:icon="preview ? 'icon-rename' : 'icon-toggle'"
|
|
||||||
@click="onTogglePreview"
|
|
||||||
>
|
|
||||||
{{ preview ? t('notes', 'Edit') : t('notes', 'Preview') }}
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="icon-fullscreen"
|
icon="icon-fullscreen"
|
||||||
:class="{ active: fullscreen }"
|
:class="{ active: fullscreen }"
|
||||||
|
@ -35,11 +33,6 @@
|
||||||
{{ fullscreen ? t('notes', 'Exit full screen') : t('notes', 'Full screen') }}
|
{{ fullscreen ? t('notes', 'Exit full screen') : t('notes', 'Full screen') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Actions>
|
</Actions>
|
||||||
<button v-show="note.saveError"
|
|
||||||
v-tooltip.right="t('notes', 'Save failed. Click to retry.')"
|
|
||||||
class="action-error icon-error-color"
|
|
||||||
@click="onManualSave"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
|
@ -53,13 +46,10 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
isMobile,
|
isMobile,
|
||||||
} from '@nextcloud/vue'
|
} from '@nextcloud/vue'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
|
||||||
import { emit } from '@nextcloud/event-bus'
|
import { emit } from '@nextcloud/event-bus'
|
||||||
|
|
||||||
import { config } from '../config'
|
import { config } from '../config'
|
||||||
import { fetchNote, refreshNote, saveNote, saveNoteManually, autotitleNote, routeIsNewNote } from '../NotesService'
|
import { autotitleNote, routeIsNewNote } from '../NotesService'
|
||||||
import TheEditor from './EditorEasyMDE'
|
|
||||||
import ThePreview from './EditorMarkdownIt'
|
|
||||||
import store from '../store'
|
import store from '../store'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -69,8 +59,6 @@ export default {
|
||||||
Actions,
|
Actions,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
AppContent,
|
AppContent,
|
||||||
TheEditor,
|
|
||||||
ThePreview,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -90,11 +78,8 @@ export default {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
preview: false,
|
|
||||||
actionsOpen: false,
|
actionsOpen: false,
|
||||||
autosaveTimer: null,
|
|
||||||
autotitleTimer: null,
|
autotitleTimer: null,
|
||||||
refreshTimer: null,
|
|
||||||
etag: null,
|
etag: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -109,25 +94,30 @@ export default {
|
||||||
isNewNote() {
|
isNewNote() {
|
||||||
return routeIsNewNote(this.$route)
|
return routeIsNewNote(this.$route)
|
||||||
},
|
},
|
||||||
isManualSave() {
|
|
||||||
return store.state.app.isManualSave
|
|
||||||
},
|
|
||||||
sidebarOpen() {
|
sidebarOpen() {
|
||||||
return store.state.app.sidebarOpen
|
return store.state.app.sidebarOpen
|
||||||
},
|
},
|
||||||
|
fileId() {
|
||||||
|
return parseInt(this.noteId)
|
||||||
|
},
|
||||||
|
viewer() {
|
||||||
|
return OCA.Viewer.availableHandlers.filter(h => h.id === 'text')[0]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
$route(to, from) {
|
||||||
if (to.name !== from.name || to.params.noteId !== from.params.noteId) {
|
if (to.name !== from.name || to.params.noteId !== from.params.noteId) {
|
||||||
this.fetchData()
|
// this.loading = true
|
||||||
|
this.initNote()
|
||||||
|
this.$refs.texteditor.$children[0].reconnect()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: 'onUpdateTitle',
|
title: 'onUpdateTitle',
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.fetchData()
|
this.initNote()
|
||||||
document.addEventListener('webkitfullscreenchange', this.onDetectFullscreen)
|
document.addEventListener('webkitfullscreenchange', this.onDetectFullscreen)
|
||||||
document.addEventListener('mozfullscreenchange', this.onDetectFullscreen)
|
document.addEventListener('mozfullscreenchange', this.onDetectFullscreen)
|
||||||
document.addEventListener('fullscreenchange', this.onDetectFullscreen)
|
document.addEventListener('fullscreenchange', this.onDetectFullscreen)
|
||||||
|
@ -135,7 +125,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.stopRefreshTimer()
|
|
||||||
document.removeEventListener('webkitfullscreenchange', this.onDetectFullscreen)
|
document.removeEventListener('webkitfullscreenchange', this.onDetectFullscreen)
|
||||||
document.removeEventListener('mozfullscreenchange', this.onDetectFullscreen)
|
document.removeEventListener('mozfullscreenchange', this.onDetectFullscreen)
|
||||||
document.removeEventListener('fullscreenchange', this.onDetectFullscreen)
|
document.removeEventListener('fullscreenchange', this.onDetectFullscreen)
|
||||||
|
@ -145,31 +134,14 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
initNote() {
|
||||||
store.commit('setSidebarOpen', false)
|
store.commit('setSidebarOpen', false)
|
||||||
this.etag = null
|
|
||||||
this.stopRefreshTimer()
|
|
||||||
|
|
||||||
if (this.isMobile) {
|
if (this.isMobile) {
|
||||||
emit('toggle-navigation', { open: false })
|
emit('toggle-navigation', { open: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onUpdateTitle(this.title)
|
this.onUpdateTitle(this.title)
|
||||||
this.loading = true
|
|
||||||
this.preview = false
|
|
||||||
fetchNote(parseInt(this.noteId))
|
|
||||||
.then((note) => {
|
|
||||||
if (note.errorMessage) {
|
|
||||||
showError(note.errorMessage)
|
|
||||||
}
|
|
||||||
this.startRefreshTimer()
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// note not found
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onUpdateTitle(title) {
|
onUpdateTitle(title) {
|
||||||
|
@ -181,11 +153,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onTogglePreview() {
|
|
||||||
this.preview = !this.preview
|
|
||||||
this.actionsOpen = false
|
|
||||||
},
|
|
||||||
|
|
||||||
onDetectFullscreen() {
|
onDetectFullscreen() {
|
||||||
this.fullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen
|
this.fullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen
|
||||||
},
|
},
|
||||||
|
@ -226,38 +193,13 @@ export default {
|
||||||
this.actionsOpen = false
|
this.actionsOpen = false
|
||||||
},
|
},
|
||||||
|
|
||||||
stopRefreshTimer() {
|
onEditorReady() {
|
||||||
if (this.refreshTimer !== null) {
|
console.debug('onEditorReady')
|
||||||
clearTimeout(this.refreshTimer)
|
this.loading = false
|
||||||
this.refreshTimer = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startRefreshTimer() {
|
|
||||||
this.stopRefreshTimer()
|
|
||||||
this.refreshTimer = setTimeout(() => {
|
|
||||||
this.refreshTimer = null
|
|
||||||
this.refreshNote()
|
|
||||||
}, config.interval.note.refresh * 1000)
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshNote() {
|
|
||||||
if (this.note.unsaved) {
|
|
||||||
this.startRefreshTimer()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refreshNote(parseInt(this.noteId), this.etag).then(etag => {
|
|
||||||
if (etag) {
|
|
||||||
this.etag = etag
|
|
||||||
this.$forceUpdate()
|
|
||||||
}
|
|
||||||
this.startRefreshTimer()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onEdit(newContent) {
|
onEdit(newContent) {
|
||||||
if (this.note.content !== newContent) {
|
if (this.note.content !== newContent) {
|
||||||
this.stopRefreshTimer()
|
|
||||||
const note = {
|
const note = {
|
||||||
...this.note,
|
...this.note,
|
||||||
content: newContent,
|
content: newContent,
|
||||||
|
@ -266,18 +208,6 @@ export default {
|
||||||
store.commit('updateNote', note)
|
store.commit('updateNote', note)
|
||||||
this.$forceUpdate()
|
this.$forceUpdate()
|
||||||
|
|
||||||
// queue auto saving note content
|
|
||||||
if (this.autosaveTimer === null) {
|
|
||||||
this.autosaveTimer = setTimeout(() => {
|
|
||||||
this.autosaveTimer = null
|
|
||||||
saveNote(note.id)
|
|
||||||
}, config.interval.note.autosave * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// (re-) start auto refresh timer
|
|
||||||
// TODO should be after save is finished
|
|
||||||
this.startRefreshTimer()
|
|
||||||
|
|
||||||
// stop old autotitle timer
|
// stop old autotitle timer
|
||||||
if (this.autotitleTimer !== null) {
|
if (this.autotitleTimer !== null) {
|
||||||
clearTimeout(this.autotitleTimer)
|
clearTimeout(this.autotitleTimer)
|
||||||
|
@ -296,26 +226,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyPress(event) {
|
onKeyPress(event) {
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
switch (event.key.toLowerCase()) {
|
|
||||||
case 's':
|
|
||||||
event.preventDefault()
|
|
||||||
this.onManualSave()
|
|
||||||
break
|
|
||||||
case '/':
|
|
||||||
event.preventDefault()
|
|
||||||
this.onTogglePreview()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onManualSave() {
|
|
||||||
const note = {
|
|
||||||
...this.note,
|
|
||||||
}
|
|
||||||
store.commit('updateNote', note)
|
|
||||||
saveNoteManually(this.note.id)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -334,6 +244,11 @@ export default {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-editor {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* center editor on large screens */
|
/* center editor on large screens */
|
||||||
@media (min-width: 1600px) {
|
@media (min-width: 1600px) {
|
||||||
.note-editor {
|
.note-editor {
|
||||||
|
|
Loading…
Reference in New Issue