From 79efa7883bf89b5be8b01809d136fdaaa6d2868f Mon Sep 17 00:00:00 2001 From: Glen Lalo <127046441+glalo@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:52:20 +0200 Subject: [PATCH] add autosave into localstorage --- client/components/editor.vue | 83 ++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/client/components/editor.vue b/client/components/editor.vue index 7cd577b1..c0b65277 100644 --- a/client/components/editor.vue +++ b/client/components/editor.vue @@ -12,6 +12,14 @@ dense full-width ) + div( + v-if='isDirty' + class='d-inline-flex align-center px-4 py-2 rounded-lg animated fadeInDown' + style='background-color: #d32f2f; color: white; font-weight: 500; letter-spacing: 0.5px;' + ) + v-icon(color='white', size='25', class='mr-2') mdi-alert + div + div.text-body-2 {{ locale !== `de` ? 'Not saved' : 'Nicht gespeichert' }} template(slot='actions') v-btn.mr-3.animated.fadeIn(color='amber', outlined, small, v-if='isConflict', @click='openConflict') .overline.amber--text.mr-3 Conflict @@ -22,6 +30,7 @@ @click.exact='save' @click.ctrl.exact='saveAndClose' :class='{ "is-icon": $vuetify.breakpoint.mdAndDown }' + :disabled="mode !== 'create' && !isDirty" ) v-icon(color='green', :left='$vuetify.breakpoint.lgAndUp') mdi-check span.grey--text(v-if='$vuetify.breakpoint.lgAndUp && mode !== `create` && !isDirty') {{ $t('editor:save.saved') }} @@ -164,6 +173,7 @@ export default { dialogUnsaved: false, exitConfirmed: false, initContentParsed: '', + draftCache: null, savedState: { description: '', isPublished: false, @@ -186,6 +196,7 @@ export default { currentStyling: get('page/scriptCss'), isDirty () { return _.some([ + this.draftCache && Base64.encode(this.draftCache) !== this.initContent, this.initContentParsed !== this.$store.get('editor/content'), this.locale !== this.$store.get('page/locale'), this.path !== this.$store.get('page/path'), @@ -210,7 +221,10 @@ export default { }, currentStyling(newValue) { this.injectCustomCss(newValue) - } + }, + '$store.state.editor.content': _.debounce(function (val) { + this.writeDraft(val) + }, 800) }, created() { this.$store.set('page/id', this.pageId) @@ -237,9 +251,7 @@ export default { }, mounted() { this.$store.set('editor/mode', this.initMode || 'create') - - this.initContentParsed = this.initContent ? Base64.decode(this.initContent) : '' - this.$store.set('editor/content', this.initContentParsed) + this.parseContent() if (this.mode === 'create' && !this.initEditor) { _.delay(() => { this.dialogEditorSelector = true @@ -264,21 +276,56 @@ export default { // this.currentEditor = `editorApi` }, methods: { - openPropsModal(name) { - this.dialogProps = true + draftKey() { + return 'wiki-editor-draft-' + this.$store.get('page/path') }, - showProgressDialog(textKey) { - this.dialogProgress = true + readDraft() { + try { + const raw = localStorage.getItem(this.draftKey()) + if (!raw) return null + try { + const parsed = JSON.parse(raw) + return Base64.decode(parsed.v) + } catch { + return Base64.decode(raw) // legacy + } + } catch (e) { + console.warn('Draft read error', e) + return null + } }, - hideProgressDialog() { - this.dialogProgress = false + writeDraft(content) { + try { + const payload = JSON.stringify({ v: Base64.encode(content), t: new Date().toISOString() }) + localStorage.setItem(this.draftKey(), payload) + this.draftCache = content + } catch (e) { + console.warn('Draft write error', e) + } + }, + removeDraft() { + try { + localStorage.removeItem(this.draftKey()) + } catch (e) { + console.warn('Draft remove error', e) + } + this.draftCache = null }, - openConflict() { - this.$root.$emit('saveConflict') + parseContent() { + const draft = this.readDraft() + this.initContentParsed = (draft !== null && draft !== undefined) ? draft : (this.initContent ? Base64.decode(this.initContent) : ''); + this.$store.set('editor/content', this.initContentParsed) + this.draftCache = draft }, - async save({ rethrow = false, overwrite = false } = {}) { - this.showProgressDialog('saving') + openPropsModal() { this.dialogProps = true }, + showProgressDialog() { this.dialogProgress = true }, + hideProgressDialog() { this.dialogProgress = false }, + openConflict() { this.$root.$emit('saveConflict') }, + + async save({ rethrow = false } = {}) { + this.showProgressDialog() this.isSaving = true + this.removeDraft() const saveTimeoutHandle = setTimeout(() => { throw new Error('Save operation timed out.') @@ -511,8 +558,10 @@ export default { async exit() { if (this.isDirty) { this.dialogUnsaved = true + this.removeDraft() } else { this.exitGo() + this.removeDraft() } }, exitGo() { @@ -585,9 +634,9 @@ export default { background-color: mc('grey', '900') !important; min-height: 100vh; - .application--wrap { - background-color: mc('grey', '900'); - } + .application--wrap { + background-color: mc('grey', '900'); + } &-title-input input { text-align: center;