Add ability to save note without exiting edit mode
authorAdam Dullage <redacted>
Wed, 3 Jul 2024 07:14:38 +0000 (08:14 +0100)
committerAdam Dullage <redacted>
Wed, 3 Jul 2024 07:14:38 +0000 (08:14 +0100)
client/components/ConfirmModal.vue
client/components/CustomButton.vue
client/components/Toggle.vue [new file with mode: 0644]
client/views/Note.vue

index 91fb91cfdcf8c192a84ca785674d70742aea5e4d..4988c58928f5238490fcff3efb73d8112f117a11 100644 (file)
@@ -2,8 +2,7 @@
   <Modal
     v-model="isVisible"
     :title="title"
-    :class="{ 'border border-l-4 border-l-theme-danger': isDanger }"
-    :closeHandler="cancelHandler"
+    :closeHandler="emitClose"
     class="px-6 py-4"
   >
     <!-- Title -->
       <CustomButton
         :label="cancelButtonText"
         :style="cancelButtonStyle"
-        @click="cancelHandler"
+        @click="emitClose('cancel')"
+        class="mr-2"
+      />
+      <CustomButton
+        v-if="rejectButtonText"
+        :label="rejectButtonText"
+        :style="rejectButtonStyle"
+        @click="emitClose('reject')"
         class="mr-2"
       />
       <CustomButton
         v-focus
         :label="confirmButtonText"
         :style="confirmButtonStyle"
-        @click="confirmHandler"
+        @click="emitClose('confirm')"
       />
     </div>
   </Modal>
@@ -39,18 +45,14 @@ const props = defineProps({
   confirmButtonText: { type: String, default: "Confirm" },
   cancelButtonStyle: { type: String, default: "subtle" },
   cancelButtonText: { type: String, default: "Cancel" },
-  isDanger: Boolean,
+  rejectButtonStyle: { type: String, default: "danger" },
+  rejectButtonText: { type: String },
 });
-const emit = defineEmits(["confirm", "cancel"]);
+const emit = defineEmits(["confirm", "reject", "cancel"]);
 const isVisible = defineModel({ type: Boolean });
 
-function cancelHandler() {
-  isVisible.value = false;
-  emit("cancel");
-}
-
-function confirmHandler() {
+function emitClose(closeEvent = "cancel") {
   isVisible.value = false;
-  emit("confirm");
+  emit(closeEvent);
 }
 </script>
index 426431252af6999d0991cfb710e8dfa49d1f1cc7..b6fe8c0eac05a7421b46ae932d50c398513e8fb1 100644 (file)
@@ -8,6 +8,8 @@
         style === 'cta',
       'bg-theme-danger text-slate-50 hover:bg-theme-danger/80':
         style === 'danger',
+      'bg-theme-success text-slate-50 hover:bg-theme-success/80':
+        style === 'success',
     }"
   >
     <IconLabel :iconPath="iconPath" :iconSize="iconSize" :label="label" />
@@ -25,7 +27,7 @@ defineProps({
     type: String,
     default: "subtle",
     validator: (value) => {
-      return ["subtle", "cta", "danger"].includes(value);
+      return ["subtle", "cta", "danger", "success"].includes(value);
     },
   },
 });
diff --git a/client/components/Toggle.vue b/client/components/Toggle.vue
new file mode 100644 (file)
index 0000000..753bef8
--- /dev/null
@@ -0,0 +1,25 @@
+<template>
+  <div
+    class="flex cursor-pointer items-center text-nowrap rounded bg-theme-background px-2 py-1 text-theme-text-muted"
+  >
+    <span v-if="label" class="mr-2">{{ label }}</span>
+    <SvgIcon
+      type="mdi"
+      :path="isOn ? mdiToggleSwitch : mdiToggleSwitchOff"
+      :class="{ 'text-theme-brand': isOn }"
+      width="auto"
+      height="1em"
+      viewBox="2 7 20 10"
+    ></SvgIcon>
+  </div>
+</template>
+
+<script setup>
+import SvgIcon from "@jamescoyle/vue-icon";
+import { mdiToggleSwitch, mdiToggleSwitchOff } from "@mdi/js";
+
+defineProps({
+  label: String,
+  isOn: Boolean,
+});
+</script>
index 67a3ce7b78f5dd8bf1659666e7082ada6e1df0f3..685190555dcbaec144cfa6263f38bde27303be8f 100644 (file)
@@ -9,14 +9,17 @@
     @confirm="deleteConfirmedHandler"
   />
 
-  <!-- Confirm Cancellation Modal -->
+  <!-- Save Changes Modal -->
   <ConfirmModal
-    v-model="isCancellationModalVisible"
-    title="Confirm Closure"
-    message="Changes have been made. Are you sure you want to close the note?"
-    confirmButtonText="Close"
-    confirmButtonStyle="danger"
-    @confirm="cancelConfirmedHandler"
+    v-model="isSaveChangesModalVisible"
+    title="Save Changes"
+    message="Do you want to save your changes?"
+    confirmButtonText="Save"
+    confirmButtonStyle="success"
+    rejectButtonText="Discard"
+    rejectButtonStyle="danger"
+    @confirm="saveHandler((close = true))"
+    @reject="closeNote"
   />
 
   <!-- Draft Modal -->
     message="There is an unsaved draft of this note stored in this browser. Do you want to resume the draft version or delete it?"
     confirmButtonText="Resume Draft"
     confirmButtonStyle="cta"
-    cancelButtonText="Delete Draft"
-    cancelButtonStyle="danger"
+    rejectButtonText="Delete Draft"
+    rejectButtonStyle="danger"
     @confirm="setEditMode()"
-    @cancel="
+    @reject="
       clearDraft();
       setEditMode();
     "
 
       <!-- Buttons -->
       <div class="flex shrink-0 self-end md:self-baseline">
-        <div v-show="!editMode">
-          <CustomButton
-            v-if="canModify"
-            :iconPath="mdilDelete"
-            label="Delete"
-            @click="deleteHandler"
-          />
-          <CustomButton
-            v-if="canModify"
-            class="ml-1"
-            :iconPath="mdilPencil"
-            label="Edit"
-            @click="editHandler"
-          />
-        </div>
-        <div v-show="editMode">
-          <CustomButton
-            :iconPath="mdilArrowLeft"
-            label="Cancel"
-            @click="cancelHandler"
-          />
-          <CustomButton
-            class="ml-1"
-            :iconPath="mdilContentSave"
-            label="Save"
-            @click="saveHandler"
-          />
-        </div>
+        <CustomButton
+          v-show="canModify && !editMode"
+          label="Delete"
+          :iconPath="mdilDelete"
+          @click="deleteHandler"
+        />
+        <CustomButton
+          v-show="editMode"
+          label="Save"
+          :iconPath="mdilContentSave"
+          @click="saveHandler((close = false))"
+        />
+        <Toggle
+          v-if="canModify"
+          label="Edit"
+          :isOn="editMode"
+          class="ml-1"
+          @click="toggleEditModeHandler"
+        />
       </div>
     </div>
 
 }
 </style>
 
-
 <script setup>
 import { mdiNoteOffOutline } from "@mdi/js";
-import {
-  mdilArrowLeft,
-  mdilContentSave,
-  mdilDelete,
-  mdilPencil,
-} from "@mdi/light-js";
+import { mdilContentSave, mdilDelete } from "@mdi/light-js";
 import Mousetrap from "mousetrap";
 import { useToast } from "primevue/usetoast";
-import { computed, onMounted, ref, watch } from "vue";
+import { computed, nextTick, onMounted, ref, watch } from "vue";
 import { useRouter } from "vue-router";
 
 import {
@@ -139,6 +127,7 @@ import { Note } from "../classes.js";
 import ConfirmModal from "../components/ConfirmModal.vue";
 import CustomButton from "../components/CustomButton.vue";
 import LoadingIndicator from "../components/LoadingIndicator.vue";
+import Toggle from "../components/Toggle.vue";
 import ToastEditor from "../components/toastui/ToastEditor.vue";
 import ToastViewer from "../components/toastui/ToastViewer.vue";
 import { authTypes } from "../constants.js";
@@ -153,7 +142,7 @@ const canModify = computed(() => globalStore.authType != authTypes.readOnly);
 let draftSaveTimeout = null;
 const editMode = ref(false);
 const globalStore = useGlobalStore();
-const isCancellationModalVisible = ref(false);
+const isSaveChangesModalVisible = ref(false);
 const isDeleteModalVisible = ref(false);
 const isDraftModalVisible = ref(false);
 const isNewNote = computed(() => !props.title);
@@ -202,6 +191,14 @@ function init() {
 }
 
 // Note Editing
+function toggleEditModeHandler() {
+  if (editMode.value) {
+    closeHandler();
+  } else {
+    editHandler();
+  }
+}
+
 function editHandler() {
   const draftContent = loadDraft();
   if (draftContent) {
@@ -238,31 +235,8 @@ function deleteConfirmedHandler() {
     });
 }
 
-// Note Edit Cancellation
-function cancelHandler() {
-  if (
-    newTitle.value != note.value.title ||
-    toastEditor.value.getMarkdown() != note.value.content
-  ) {
-    isCancellationModalVisible.value = true;
-  } else {
-    cancelConfirmedHandler();
-  }
-}
-
-function cancelConfirmedHandler() {
-  clearDraft();
-  setBeforeUnloadConfirmation(false);
-  editMode.value = false;
-  if (!props.title) {
-    router.push({ name: "home" });
-  } else {
-    editMode.value = false;
-  }
-}
-
 // Note Saving
-function saveHandler() {
+function saveHandler(close = false) {
   // Save Default Editor Mode
   saveDefaultEditorMode();
 
@@ -283,27 +257,35 @@ function saveHandler() {
   // Save Note
   let newContent = toastEditor.value.getMarkdown();
   if (isNewNote.value) {
-    saveNew(newTitle.value, newContent);
+    saveNew(newTitle.value, newContent, close);
   } else {
-    saveExisting(newTitle.value, newContent);
+    saveExisting(newTitle.value, newContent, close);
   }
 }
 
-function saveNew(newTitle, newContent) {
+function saveNew(newTitle, newContent, close = false) {
   createNote(newTitle, newContent)
     .then((data) => {
       clearDraft();
       note.value = data;
-      router.push({ name: "note", params: { title: note.value.title } });
-      noteSaveSuccess();
+      router
+        .push({
+          name: "note",
+          params: { title: note.value.title },
+        })
+        .then(() => {
+          // Wait for the route to be updated before setting edit mode to false
+          // as the route is used to determine the action.
+          noteSaveSuccess(close);
+        });
     })
     .catch(noteSaveFailure);
 }
 
-function saveExisting(newTitle, newContent) {
+function saveExisting(newTitle, newContent, close = false) {
   // Return if no changes
   if (newTitle == note.value.title && newContent == note.value.content) {
-    noteSaveSuccess();
+    noteSaveSuccess(close);
     return;
   }
 
@@ -312,7 +294,7 @@ function saveExisting(newTitle, newContent) {
       clearDraft();
       note.value = data;
       router.replace({ name: "note", params: { title: note.value.title } });
-      noteSaveSuccess();
+      noteSaveSuccess(close);
     })
     .catch(noteSaveFailure);
 }
@@ -333,10 +315,34 @@ function noteSaveFailure(error) {
   }
 }
 
-function noteSaveSuccess() {
+function noteSaveSuccess(close = false) {
+  if (close) {
+    closeNote();
+  }
+  toast.add(getToastOptions("Note saved successfully ✓", "Success", "success"));
+}
+
+// Note Closure
+function closeHandler() {
+  if (
+    newTitle.value != note.value.title ||
+    toastEditor.value.getMarkdown() != note.value.content
+  ) {
+    isSaveChangesModalVisible.value = true;
+  } else {
+    closeNote();
+  }
+}
+
+function closeNote() {
+  clearDraft();
   setBeforeUnloadConfirmation(false);
   editMode.value = false;
-  toast.add(getToastOptions("Note saved successfully ✓", "Success", "success"));
+  if (isNewNote.value) {
+    router.push({ name: "home" });
+  } else {
+    editMode.value = false;
+  }
 }
 
 // Image Upload
git clone https://git.99rst.org/PROJECT