integrate Nextcloud Text as editor for notes

This commit is contained in:
korelstar 2020-12-27 12:39:53 +01:00
parent b1a988e1da
commit 21e6dafddc
2 changed files with 39 additions and 116 deletions

View File

@ -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,

View File

@ -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 {