From: Adam Dullage Date: Thu, 16 May 2024 12:58:04 +0000 (+0100) Subject: Implement draft save and resume functionality X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=1355a3d3ceb866d4461741649ca15fba47c499bb;p=flatnotes.git Implement draft save and resume functionality --- diff --git a/client/components/ConfirmModal.vue b/client/components/ConfirmModal.vue index ee43959..91fb91c 100644 --- a/client/components/ConfirmModal.vue +++ b/client/components/ConfirmModal.vue @@ -12,12 +12,17 @@
{{ message }}
- +
@@ -30,7 +35,10 @@ import Modal from "./Modal.vue"; const props = defineProps({ title: { type: String, default: "Confirmation" }, message: String, + confirmButtonStyle: { type: String, default: "cta" }, confirmButtonText: { type: String, default: "Confirm" }, + cancelButtonStyle: { type: String, default: "subtle" }, + cancelButtonText: { type: String, default: "Cancel" }, isDanger: Boolean, }); const emit = defineEmits(["confirm", "cancel"]); diff --git a/client/components/CustomButton.vue b/client/components/CustomButton.vue index f958a41..14009cc 100644 --- a/client/components/CustomButton.vue +++ b/client/components/CustomButton.vue @@ -3,8 +3,11 @@ class="rounded px-2 py-1" :class="{ 'bg-theme-background text-theme-text-muted hover:bg-theme-background-elevated': - !danger, - 'bg-theme-danger hover:bg-theme-danger/80 text-slate-50': danger, + style === 'subtle', + 'border bg-theme-background hover:bg-theme-background-elevated': + style === 'cta', + 'bg-theme-danger text-slate-50 hover:bg-theme-danger/80': + style === 'danger', }" > @@ -18,6 +21,12 @@ defineProps({ iconPath: String, iconSize: String, label: String, - danger: Boolean, + style: { + type: String, + default: "subtle", + validator: (value) => { + return ["subtle", "cta", "danger"].includes(value); + }, + }, }); diff --git a/client/components/toastui/ToastEditor.vue b/client/components/toastui/ToastEditor.vue index 4048ea5..5971607 100644 --- a/client/components/toastui/ToastEditor.vue +++ b/client/components/toastui/ToastEditor.vue @@ -17,6 +17,8 @@ const props = defineProps({ addImageBlobHook: Function, }); +const emit = defineEmits(["change"]); + const editorElement = ref(); let toastEditor; @@ -26,6 +28,11 @@ onMounted(() => { el: editorElement.value, initialValue: props.initialValue, initialEditType: props.initialEditType, + events: { + change: () => { + emit("change"); + }, + }, hooks: props.addImageBlobHook ? { addImageBlobHook: props.addImageBlobHook } : {}, diff --git a/client/views/Note.vue b/client/views/Note.vue index c032169..2cb49e9 100644 --- a/client/views/Note.vue +++ b/client/views/Note.vue @@ -5,7 +5,7 @@ title="Confirm Deletion" :message="`Are you sure you want to delete the note '${note.title}'?`" confirmButtonText="Delete" - isDanger + confirmButtonStyle="danger" @confirm="deleteConfirmedHandler" /> @@ -15,10 +15,26 @@ title="Confirm Closure" :message="`Changes have been made. Are you sure you want to close the note '${note.title}'?`" confirmButtonText="Close" - isDanger + confirmButtonStyle="danger" @confirm="cancelConfirmedHandler" /> + + +
@@ -74,9 +90,10 @@
@@ -118,10 +135,12 @@ const props = defineProps({ }); const canModify = computed(() => globalStore.authType != authTypes.readOnly); +let draftSaveTimeout = null; const editMode = ref(false); const globalStore = useGlobalStore(); const isCancellationModalVisible = ref(false); const isDeleteModalVisible = ref(false); +const isDraftModalVisible = ref(false); const isNewNote = computed(() => !props.title); const loadingIndicator = ref(); const note = ref({}); @@ -167,61 +186,44 @@ function init() { } } -// Helpers -function entityTooLargeToast(entityName) { - toast.add( - getToastOptions( - `This ${entityName} is too large. Please try again with a smaller ${entityName} or adjust your server configuration.`, - "Failure", - "error", - ), - ); -} - -function badFilenameToast(entityName) { - toast.add( - getToastOptions( - 'Due to filename restrictions, the following characters are not allowed: <>:"/\\|?*', - `Invalid ${entityName}`, - "error", - ), - ); -} - -function setBeforeUnloadConfirmation(enable = true) { - if (enable) { - window.onbeforeunload = () => { - return true; - }; +// Note Editing +function editHandler() { + const draftContent = loadDraft(); + if (draftContent) { + isDraftModalVisible.value = true; } else { - window.onbeforeunload = null; + setEditMode(); } } -function saveDefaultEditorMode() { - const isWysiwygMode = toastEditor.value.isWysiwygMode(); - localStorage.setItem( - "defaultEditorMode", - isWysiwygMode ? "wysiwyg" : "markdown", - ); -} - -function loadDefaultEditorMode() { - const defaultWysiwygMode = localStorage.getItem("defaultEditorMode"); - return defaultWysiwygMode || "markdown"; -} - -// Button Handlers -function editHandler() { +function setEditMode() { setBeforeUnloadConfirmation(true); newTitle.value = note.value.title; editMode.value = true; } +function getInitialEditorValue() { + const draftContent = loadDraft(); + return draftContent ? draftContent : note.value.content; +} + +// Note Deletion function deleteHandler() { isDeleteModalVisible.value = true; } +function deleteConfirmedHandler() { + deleteNote(note.value.title) + .then(() => { + toast.add(getToastOptions("Note deleted ✓", "Success", "success")); + router.push({ name: "home" }); + }) + .catch((error) => { + apiErrorHandler(error, toast); + }); +} + +// Note Edit Cancellation function cancelHandler() { if ( newTitle.value != note.value.title || @@ -233,6 +235,18 @@ function cancelHandler() { } } +function cancelConfirmedHandler() { + clearDraft(); + setBeforeUnloadConfirmation(false); + editMode.value = false; + if (!props.title) { + router.push({ name: "home" }); + } else { + editMode.value = false; + } +} + +// Note Saving function saveHandler() { // Save Default Editor Mode saveDefaultEditorMode(); @@ -260,31 +274,10 @@ function saveHandler() { } } -// Additional Logic -function cancelConfirmedHandler() { - setBeforeUnloadConfirmation(false); - editMode.value = false; - if (!props.title) { - router.push({ name: "home" }); - } else { - editMode.value = false; - } -} - -function deleteConfirmedHandler() { - deleteNote(note.value.title) - .then(() => { - toast.add(getToastOptions("Note deleted ✓", "Success", "success")); - router.push({ name: "home" }); - }) - .catch((error) => { - apiErrorHandler(error, toast); - }); -} - function saveNew(newTitle, newContent) { createNote(newTitle, newContent) .then((data) => { + clearDraft(); note.value = data; router.push({ name: "note", params: { title: note.value.title } }); noteSaveSuccess(); @@ -301,6 +294,7 @@ function saveExisting(newTitle, newContent) { updateNote(note.value.title, newTitle, newContent) .then((data) => { + clearDraft(); note.value = data; router.replace({ name: "note", params: { title: note.value.title } }); noteSaveSuccess(); @@ -330,6 +324,7 @@ function noteSaveSuccess() { toast.add(getToastOptions("Note saved successfully ✓", "Success", "success")); } +// Image Upload function addImageBlobHook(file, callback) { const altTextInputValue = document.getElementById( "toastuiAltTextInput", @@ -387,6 +382,77 @@ function postAttachment(file) { }); } +// Drafts +function clearDraftSaveTimeout() { + if (draftSaveTimeout != null) { + clearTimeout(draftSaveTimeout); + } +} + +function saveDraft() { + const content = toastEditor.value.getMarkdown(); + if (content) { + localStorage.setItem(note.value.title, content); + } +} + +function startDraftSaveTimeout() { + clearDraftSaveTimeout(); + draftSaveTimeout = setTimeout(saveDraft, 1000); +} + +function clearDraft() { + localStorage.removeItem(note.value.title); +} + +function loadDraft() { + return localStorage.getItem(note.value.title); +} + +// Helpers +function entityTooLargeToast(entityName) { + toast.add( + getToastOptions( + `This ${entityName} is too large. Please try again with a smaller ${entityName} or adjust your server configuration.`, + "Failure", + "error", + ), + ); +} + +function badFilenameToast(entityName) { + toast.add( + getToastOptions( + 'Due to filename restrictions, the following characters are not allowed: <>:"/\\|?*', + `Invalid ${entityName}`, + "error", + ), + ); +} + +function setBeforeUnloadConfirmation(enable = true) { + if (enable) { + window.onbeforeunload = () => { + return true; + }; + } else { + window.onbeforeunload = null; + } +} + +function saveDefaultEditorMode() { + const isWysiwygMode = toastEditor.value.isWysiwygMode(); + localStorage.setItem( + "defaultEditorMode", + isWysiwygMode ? "wysiwyg" : "markdown", + ); +} + +function loadDefaultEditorMode() { + const defaultWysiwygMode = localStorage.getItem("defaultEditorMode"); + return defaultWysiwygMode || "markdown"; +} + watch(() => props.title, init); onMounted(init);