Add custom v-focus directive and add escape keybinding to close modal
authorAdam Dullage <redacted>
Wed, 8 May 2024 16:29:46 +0000 (17:29 +0100)
committerAdam Dullage <redacted>
Wed, 8 May 2024 16:29:46 +0000 (17:29 +0100)
client/App.vue
client/components/ConfirmModal.vue
client/components/Modal.vue
client/index.js
client/partials/SearchInput.vue
client/partials/SearchModal.vue
client/views/Note.vue

index 9c1300af9fb6796e40fb138d2b5f0e29ac66ef8f..3f162471d1d472b5b255a5b6edb7d39db845f8c2 100644 (file)
@@ -1,7 +1,7 @@
 <template>\r
   <div class="container mx-auto flex h-screen flex-col px-2 py-4">\r
     <PrimeToast />\r
-    <SearchModal ref="searchModal" />\r
+    <SearchModal v-model="isSearchModalVisible" />\r
     <NavBar\r
       v-if="showNavBar"\r
       ref="navBar"\r
@@ -18,7 +18,7 @@ import { useToast } from "primevue/usetoast";
 import { computed, ref } from "vue";\r
 import { RouterView, useRoute } from "vue-router";\r
 \r
-import { getConfig, apiErrorHandler } from "./api.js";\r
+import { apiErrorHandler, getConfig } from "./api.js";\r
 import PrimeToast from "./components/PrimeToast.vue";\r
 import { useGlobalStore } from "./globalStore.js";\r
 import { loadTheme } from "./helpers.js";\r
@@ -27,9 +27,9 @@ import SearchModal from "./partials/SearchModal.vue";
 import { loadStoredToken } from "./tokenStorage.js";\r
 \r
 const globalStore = useGlobalStore();\r
+const isSearchModalVisible = ref(false);\r
 const navBar = ref();\r
 const route = useRoute();\r
-const searchModal = ref();\r
 const toast = useToast();\r
 \r
 // '/' to search\r
@@ -59,7 +59,7 @@ const showNavBarLogo = computed(() => {
 });\r
 \r
 function toggleSearchModal() {\r
-  searchModal.value.toggle();\r
+  isSearchModalVisible.value = !isSearchModalVisible.value;\r
 }\r
 \r
 loadTheme();\r
index 4036bcdf7f229589c6bc118b47330e6336ea057f..60adcfad676d48d174284d5e9ff1ca1eda004b92 100644 (file)
@@ -1,6 +1,6 @@
 <template>
   <Modal
-    ref="modal"
+    v-model="isVisible"
     :title="title"
     :class="{ 'border border-l-4 border-l-theme-danger': isDanger }"
     :closeHandler="cancelHandler"
     <!-- Buttons -->
     <div class="flex justify-end">
       <CustomButton label="Cancel" @click="cancelHandler" class="mr-2" />
-      <CustomButton :label="confirmButtonText" @click="confirmHandler" danger />
+      <CustomButton
+        v-focus
+        :label="confirmButtonText"
+        @click="confirmHandler"
+        danger
+      />
     </div>
   </Modal>
 </template>
 
 <script setup>
-import { ref } from "vue";
-
 import CustomButton from "./CustomButton.vue";
 import Modal from "./Modal.vue";
 
@@ -28,22 +31,15 @@ const props = defineProps({
   isDanger: Boolean,
 });
 const emit = defineEmits(["confirm", "cancel"]);
-
-const modal = ref();
-
-function toggle() {
-  modal.value.toggle();
-}
+const isVisible = defineModel({ type: Boolean });
 
 function cancelHandler() {
-  modal.value.setVisibility(false);
+  isVisible.value = false;
   emit("cancel");
 }
 
 function confirmHandler() {
-  modal.value.setVisibility(false);
+  isVisible.value = false;
   emit("confirm");
 }
-
-defineExpose({ toggle });
 </script>
index 4b5b2bc17bb9364cad9b5c4df3696edbc7f678bd..9274f9a632c588f1635fe8c4c0398bd60fffa9f4 100644 (file)
@@ -8,11 +8,12 @@
     <div
       class="relative max-w-[500px] grow rounded-lg border border-theme-border bg-theme-background px-6 py-4 shadow-lg"
       :class="$attrs.class"
+      @keyup.esc="close"
     >
       <CustomButton
         v-if="props.showClose"
         :iconPath="mdiWindowClose"
-        @click="close"
+        @click="closeHandler"
         class="absolute right-1 top-1"
       />
       <!-- Title -->
 
 <script setup>
 import { mdiWindowClose } from "@mdi/js";
-import { ref } from "vue";
 
 import CustomButton from "./CustomButton.vue";
 
 defineOptions({
   inheritAttrs: false,
 });
-
 const props = defineProps({
   title: { type: String, default: "Confirm" },
   showClose: { type: Boolean },
-  closeHandler: Function,
-  modalClasses: String,
+  closeHandlerOverride: Function,
 });
+const isVisible = defineModel({ type: Boolean });
 
-const isVisible = ref(false);
-
-function toggle() {
-  isVisible.value = !isVisible.value;
-}
-
-function setVisibility(value) {
-  isVisible.value = value;
-}
-
-function close() {
-  if (props.closeHandler) {
-    props.closeHandler();
+function closeHandler() {
+  if (props.closeHandlerOverride) {
+    props.closeHandlerOverride();
   } else {
-    setVisibility(false);
+    isVisible.value = false;
   }
 }
-
-defineExpose({ toggle, setVisibility });
 </script>
index fbea270906a46a075452b2b75fe1d4eb3b1f2d2f..e2672d52c5d307edd6cedf1e9279d037bd0bca94 100644 (file)
@@ -12,4 +12,12 @@ app.use(router);
 app.use(pinia);\r
 app.use(PrimeVue, { unstyled: true });\r
 app.use(ToastService);\r
+\r
+// Custom v-focus directive to focus on an element when mounted\r
+app.directive("focus", {\r
+  mounted(el) {\r
+    el.focus();\r
+  },\r
+});\r
+\r
 app.mount("#app");\r
index c374c10d63d76f3cf36cea156571540e9ffa868d..af7947bee366cfe3edc2893ae2826352e9c56ea5 100644 (file)
@@ -2,9 +2,9 @@
   <form class="flex w-full" @submit.prevent="search">
     <TextInput
       v-model="searchTerm"
+      v-focus
       :placeholder="props.hidePlaceholder ? '' : 'Search'"
       class="rounded-r-none"
-      ref="textInput"
     />
     <CustomButton
       :iconPath="mdilMagnify"
@@ -17,7 +17,7 @@
 <script setup>
 import { mdilMagnify } from "@mdi/light-js";
 import { useToast } from "primevue/usetoast";
-import { onMounted, ref } from "vue";
+import { ref } from "vue";
 import { useRouter } from "vue-router";
 import * as constants from "../constants";
 
@@ -32,7 +32,6 @@ const props = defineProps({
 const emit = defineEmits(["search"]);
 
 const router = useRouter();
-const textInput = ref();
 const searchTerm = ref(props.initialSearchTerm);
 const toast = useToast();
 
@@ -47,8 +46,4 @@ function search() {
     toast.add(getToastOptions("Error", "Please enter a search term.", true));
   }
 }
-
-onMounted(() => {
-  textInput.value.focus();
-});
 </script>
index 353cf904d856585e72bb28f94ede0222c1dee486..20374faca2cc6339f8bffe3a01bc7be109d1947c 100644 (file)
@@ -1,24 +1,20 @@
 <template>
-  <Modal ref="modal" title="Search">
+  <Modal v-model="isVisible" title="Search">
     <SearchInput @search="toggle" class="mb-4" hidePlaceholder />
     <div class="flex justify-end">
-      <CustomButton label="Close" @click="toggle" />
+      <CustomButton label="Close" @click="toggleHandler" />
     </div>
   </Modal>
 </template>
 
 <script setup>
-import { ref } from "vue";
-
 import CustomButton from "../components/CustomButton.vue";
 import Modal from "../components/Modal.vue";
 import SearchInput from "./SearchInput.vue";
 
-const modal = ref();
+const isVisible = defineModel({ type: Boolean });
 
-function toggle() {
-  modal.value.toggle();
+function toggleHandler() {
+  isVisible.value = !isVisible.value;
 }
-
-defineExpose({ toggle });
 </script>
index c3ccb75c20a760703a267b6247fa954e5b18d911..aa5f5f93c62a91c18aefc541e24aeae6261b9508 100644 (file)
@@ -1,7 +1,7 @@
 <template>
   <!-- Confirm Deletion Modal -->
   <ConfirmModal
-    ref="deleteConfirmModal"
+    v-model="isDeleteModalVisible"
     title="Confirm Deletion"
     :message="`Are you sure you want to delete the note '${note.title}'?`"
     confirmButtonText="Delete"
@@ -99,7 +99,7 @@ const props = defineProps({
 });
 
 const editMode = ref(false);
-const deleteConfirmModal = ref();
+const isDeleteModalVisible = ref(false);
 const isNewNote = computed(() => !props.title);
 const loadingIndicator = ref();
 const note = ref({});
@@ -150,7 +150,7 @@ function editHandler() {
 }
 
 function deleteHandler() {
-  deleteConfirmModal.value.toggle();
+  isDeleteModalVisible.value = true;
 }
 
 function deleteConfirmedHandler() {
git clone https://git.99rst.org/PROJECT