Search UX improvements
authorAdam Dullage <redacted>
Sun, 7 Aug 2022 18:19:04 +0000 (19:19 +0100)
committerAdam Dullage <redacted>
Sun, 7 Aug 2022 18:19:04 +0000 (19:19 +0100)
flatnotes/src/components/App.js
flatnotes/src/components/App.vue
flatnotes/src/components/NavBar.vue
flatnotes/src/components/SearchInput.vue
package-lock.json
package.json

index 54442b91cb3ab683766c7c750ac6f71a6cffc761..28f1104607245533071e4d4e156453efe80fe36b 100644 (file)
@@ -35,9 +35,14 @@ export default {
       let path = window.location.pathname.split("/");
       let basePath = path[1];
 
+      this.$bvModal.hide("search-modal");
+
       // Home Page
       if (basePath == "") {
         this.currentView = this.views.home;
+        this.$nextTick(function() {
+          this.focusSearchInput();
+        });
       }
 
       // Search
@@ -375,7 +380,29 @@ export default {
         });
     },
 
+    focusSearchInput: function() {
+      document.getElementById("search-input").focus();
+    },
+
+    openSearch: function() {
+      if (this.currentView == this.views.home) {
+        this.focusSearchInput();
+        EventBus.$emit("highlight-search-input");
+      } else if (this.currentView != this.views.login) {
+        this.$bvModal.show("search-modal");
+      }
+    },
+
     keyboardShortcuts: function(e) {
+      // If the user is focused on a text input or is editing a note, ignore.
+      if (
+        !["e", "/"].includes(e.key) ||
+        document.activeElement.type == "text" ||
+        (this.currentView == this.views.note && this.editMode == true)
+      ) {
+        return;
+      }
+
       // 'e' to Edit
       if (
         e.key == "e" &&
@@ -386,6 +413,12 @@ export default {
         this.toggleEditMode();
       }
 
+      // '/' to Search
+      if (e.key == "/") {
+        e.preventDefault();
+        this.openSearch();
+      }
+
       // 'CTRL + s' to Save
       // else if (
       //   e.key == "s" &&
@@ -424,6 +457,14 @@ export default {
   },
 
   mounted: function() {
+    let parent = this;
+
     window.addEventListener("popstate", this.route);
+
+    this.$root.$on("bv::modal::shown", function(_, modalId) {
+      if (modalId == "search-modal") {
+        parent.focusSearchInput();
+      }
+    });
   },
 };
index 7eeb8b739b804dfa5534713d762f046f31e1660e..015d77d2759bdb419f35cb495164865e2164886d 100644 (file)
@@ -1,5 +1,10 @@
 <template>
   <div class="container d-flex flex-column align-items-center h-100">
+    <!-- Search Modal -->
+    <b-modal id="search-modal" centered hide-footer hide-header>
+      <SearchInput></SearchInput>
+    </b-modal>
+
     <!-- Nav Bar -->
     <NavBar
       v-if="currentView != views.login"
@@ -8,6 +13,7 @@
       @navigate-home="navigate('/')"
       @new-note="newNote()"
       @logout="logout()"
+      @search="openSearch()"
     ></NavBar>
 
     <!-- Login -->
index 6bf6daec7edfd8e5d2d45f778fb91cab60604775..33761746d3bc0f20d0223ebe9a71bb278e48fd95 100644 (file)
@@ -6,7 +6,7 @@
         src="../assets/logo.svg"
         class="cursor-pointer"
         :class="{ invisible: !showLogo }"
-        @click="navigateHome"
+        @click="$emit('navigate-home')"
       />
     </div>
     <div>
@@ -14,7 +14,7 @@
       <button
         type="button"
         class="btn btn-sm btn-outline-primary mx-1"
-        @click="newNote"
+        @click="$emit('new-note')"
       >
         New Note
       </button>
       <button
         type="button"
         class="btn btn-sm btn-outline-dark mx-1"
-        @click="logout"
+        @click="this.$emit('logout')"
       >
         Log Out
       </button>
+
+      <!-- Search -->
+      <button
+        type="button"
+        id="search-button"
+        class="btn btn-sm btn-outline-dark mx-1"
+        @click="$emit('search')"
+        v-b-tooltip.hover
+        title="Keyboard Shortcut: /"
+      >
+        <BIconSearch />
+      </button>
     </div>
   </div>
 </template>
 </style>
 
 <script>
+import { BIconSearch } from "bootstrap-vue";
+
 export default {
+  components: {
+    BIconSearch,
+  },
+
   props: {
     showLogo: {
       type: Boolean,
       default: true,
     },
   },
-
-  methods: {
-    navigateHome: function () {
-      this.$emit("navigate-home");
-    },
-
-    newNote: function () {
-      this.$emit("new-note");
-    },
-
-    logout: function () {
-      this.$emit("logout");
-    },
-  },
 };
 </script>
index 06d0e907ed86a242c5f63c689751ca175822b1e4..9e32967c31bc6a3bd79e23eea52290554665baa3 100644 (file)
@@ -2,11 +2,12 @@
   <form v-on:submit.prevent="search" class="w-100">
     <div class="input-group w-100">
       <input
+        id="search-input"
         type="text"
         class="form-control"
+        :class="{ highlight: includeHighlightClass }"
         placeholder="Search"
         v-model="searchTermInput"
-        autofocus
       />
       <div class="input-group-append">
         <button class="btn" type="submit">
 </template>
 
 <style lang="scss" scoped>
+@keyframes highlight {
+  from {
+    background-color: #ffa76c5d;
+  }
+  to {
+    background-color: white;
+  }
+}
+
+.highlight {
+  animation-name: highlight;
+  animation-duration: 1.5s;
+}
+
 .btn {
   border: 1px solid #cfd4da;
   svg {
@@ -32,13 +47,14 @@ import * as constants from "../constants";
 import { BIconSearch } from "bootstrap-vue";
 
 export default {
-  comments: {
+  components: {
     BIconSearch,
   },
 
   data: function () {
     return {
       searchTermInput: null,
+      includeHighlightClass: false,
     };
   },
 
@@ -61,6 +77,18 @@ export default {
         });
       }
     },
+
+    highlightSearchInput: function () {
+      let parent = this;
+      this.includeHighlightClass = true;
+      setTimeout(function () {
+        parent.includeHighlightClass = false;
+      }, 1500);
+    },
+  },
+
+  created: function () {
+    EventBus.$on("highlight-search-input", this.highlightSearchInput);
   },
 };
 </script>
index d4c476374e4c5c2afc183ecb2ad6931fd9f904d5..22835836f553249b97139f78fbe1134ff01cebcc 100644 (file)
@@ -8,6 +8,7 @@
       "version": "0.0.0",
       "license": "MIT",
       "devDependencies": {
+        "@popperjs/core": "^2.11.5",
         "@toast-ui/editor-plugin-code-syntax-highlight": "^3.0.0",
         "@toast-ui/vue-editor": "^3.0.2",
         "@vue/component-compiler-utils": "^3.2.2",
         "node": ">= 6.0.0"
       }
     },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.5",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+      "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
+      "dev": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
     "node_modules/@toast-ui/editor": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.0.2.tgz",
         "physical-cpu-count": "^2.0.0"
       }
     },
+    "@popperjs/core": {
+      "version": "2.11.5",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+      "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
+      "dev": true
+    },
     "@toast-ui/editor": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.0.2.tgz",
index f2c49f08d01ce752792f3166f17d552c18d006d6..82161bd2e69803ffa2946cf42460ff6f5fd9ccbe 100644 (file)
@@ -11,6 +11,7 @@
   "author": "Adam Dullage",
   "license": "MIT",
   "devDependencies": {
+    "@popperjs/core": "^2.11.5",
     "@toast-ui/editor-plugin-code-syntax-highlight": "^3.0.0",
     "@toast-ui/vue-editor": "^3.0.2",
     "@vue/component-compiler-utils": "^3.2.2",
git clone https://git.99rst.org/PROJECT