Add unsaved changes indicator
authorAdam Dullage <redacted>
Wed, 3 Jul 2024 12:15:37 +0000 (13:15 +0100)
committerAdam Dullage <redacted>
Wed, 3 Jul 2024 12:15:37 +0000 (13:15 +0100)
client/components/CustomButton.vue
client/views/Note.vue

index b6fe8c0eac05a7421b46ae932d50c398513e8fb1..fb493f89867bf8d62a5d1b067c9717d1a3ef966f 100644 (file)
@@ -12,6 +12,7 @@
         style === 'success',
     }"
   >
+    <slot></slot>
     <IconLabel :iconPath="iconPath" :iconSize="iconSize" :label="label" />
   </button>
 </template>
index 685190555dcbaec144cfa6263f38bde27303be8f..473fb86c96a73d8f601da218cffdf4b7343c732e 100644 (file)
           label="Save"
           :iconPath="mdilContentSave"
           @click="saveHandler((close = false))"
-        />
+          class="relative"
+        >
+          <!-- Unsaved Changes Indicator -->
+          <div
+            v-show="unsavedChanges"
+            class="absolute right-1 h-1 w-1 rounded-full bg-theme-brand"
+          ></div>
+        </CustomButton>
         <Toggle
           v-if="canModify"
           label="Edit"
@@ -91,7 +98,7 @@
         :initialValue="getInitialEditorValue()"
         :initialEditType="loadDefaultEditorMode()"
         :addImageBlobHook="addImageBlobHook"
-        @change="startDraftSaveTimeout"
+        @change="startContentChangedTimeout"
       />
     </div>
   </LoadingIndicator>
@@ -112,7 +119,7 @@ import { mdiNoteOffOutline } from "@mdi/js";
 import { mdilContentSave, mdilDelete } from "@mdi/light-js";
 import Mousetrap from "mousetrap";
 import { useToast } from "primevue/usetoast";
-import { computed, nextTick, onMounted, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
 import { useRouter } from "vue-router";
 
 import {
@@ -139,7 +146,7 @@ const props = defineProps({
 });
 
 const canModify = computed(() => globalStore.authType != authTypes.readOnly);
-let draftSaveTimeout = null;
+let contentChangedTimeout = null;
 const editMode = ref(false);
 const globalStore = useGlobalStore();
 const isSaveChangesModalVisible = ref(false);
@@ -153,6 +160,7 @@ const router = useRouter();
 const newTitle = ref();
 const toast = useToast();
 const toastEditor = ref();
+const unsavedChanges = ref(false);
 
 // 'e' to edit
 Mousetrap.bind("e", () => {
@@ -211,6 +219,7 @@ function editHandler() {
 function setEditMode() {
   setBeforeUnloadConfirmation(true);
   newTitle.value = note.value.title;
+  unsavedChanges.value = false;
   editMode.value = true;
 }
 
@@ -316,6 +325,7 @@ function noteSaveFailure(error) {
 }
 
 function noteSaveSuccess(close = false) {
+  unsavedChanges.value = false;
   if (close) {
     closeNote();
   }
@@ -324,10 +334,7 @@ function noteSaveSuccess(close = false) {
 
 // Note Closure
 function closeHandler() {
-  if (
-    newTitle.value != note.value.title ||
-    toastEditor.value.getMarkdown() != note.value.content
-  ) {
+  if (isContentChanged()) {
     isSaveChangesModalVisible.value = true;
   } else {
     closeNote();
@@ -403,13 +410,29 @@ function postAttachment(file) {
     });
 }
 
-// Drafts
-function clearDraftSaveTimeout() {
-  if (draftSaveTimeout != null) {
-    clearTimeout(draftSaveTimeout);
+// Content Change Watcher
+function startContentChangedTimeout() {
+  clearContentChangedTimeout();
+  contentChangedTimeout = setTimeout(contentChangedHandler, 1000);
+}
+
+function clearContentChangedTimeout() {
+  if (contentChangedTimeout != null) {
+    clearTimeout(contentChangedTimeout);
+  }
+}
+
+function contentChangedHandler() {
+  if (isContentChanged()) {
+    unsavedChanges.value = true;
+    saveDraft();
+  } else {
+    unsavedChanges.value = false;
+    clearDraft();
   }
 }
 
+// Drafts
 function saveDraft() {
   const content = toastEditor.value.getMarkdown();
   if (content) {
@@ -417,11 +440,6 @@ function saveDraft() {
   }
 }
 
-function startDraftSaveTimeout() {
-  clearDraftSaveTimeout();
-  draftSaveTimeout = setTimeout(saveDraft, 1000);
-}
-
 function clearDraft() {
   localStorage.removeItem(note.value.title);
 }
@@ -474,6 +492,13 @@ function loadDefaultEditorMode() {
   return defaultWysiwygMode || "markdown";
 }
 
+function isContentChanged() {
+  return (
+    newTitle.value != note.value.title ||
+    toastEditor.value.getMarkdown() != note.value.content
+  );
+}
+
 watch(() => props.title, init);
 onMounted(init);
 </script>
git clone https://git.99rst.org/PROJECT