From 9deeb448e550fe28c22e5a9c4acc2adcfed71571 Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Sun, 1 May 2022 00:19:29 +1000
Subject: Enable search-suggestion sending/displaying/selection

---
 src/components/SearchModal.vue | 123 ++++++++++++++++++++++++++++++++---------
 1 file changed, 98 insertions(+), 25 deletions(-)

(limited to 'src/components/SearchModal.vue')

diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index cd4bede..1005b14 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -6,6 +6,13 @@ import type {TolMap} from '../tol';
 
 // Displays a search box, and sends search requests
 export default defineComponent({
+	data(){
+		return {
+			searchSuggs: [] as string[], // Holds suggestions for the search string
+			focusedSuggIdx: null as null | number, // Denotes a search-suggestion selected using the arrow keys
+			lastSuggReqId: 0, // Used to prevent late search-suggestion server-responses from taking effect
+		};
+	},
 	props: {
 		tolMap: {type: Object as PropType<TolMap>, required: true},
 		uiOpts: {type: Object, required: true},
@@ -16,39 +23,95 @@ export default defineComponent({
 				this.$emit('search-close');
 			}
 		},
-		onSearchEnter(){
+		onEnter(){
+			// Check for a focused search-suggestion
+			if (this.focusedSuggIdx != null){
+				this.resolveSearch(this.searchSuggs[this.focusedSuggIdx]);
+				return;
+			}
+			// Ask server if input valid is valid name
 			let input = this.$refs.searchInput as HTMLInputElement;
-			// Query server
 			let url = new URL(window.location.href);
 			url.pathname = '/data/search';
 			url.search = '?name=' + encodeURIComponent(input.value);
 			fetch(url.toString())
 				.then(response => response.json())
-				.then(tolNodeName => {
-					// Search successful. Get nodes in parent-chain, add to tolMap, then emit event.
-					url.pathname = '/data/chain';
-					url.search = '?name=' + encodeURIComponent(tolNodeName);
-					fetch(url.toString())
-						.then(response => response.json())
-						.then(obj => {
-							Object.getOwnPropertyNames(obj).forEach(key => {this.tolMap.set(key, obj[key])});
-							this.$emit('search-node', tolNodeName);
-						})
-						.catch(error => {
-							console.log('ERROR loading tolnode chain', error);
-						});
+				.then(results => {
+					if (results.length == 0){
+						input.value = '';
+						// Trigger failure animation
+						input.classList.remove('animate-red-then-fade');
+						input.offsetWidth; // Triggers reflow
+						input.classList.add('animate-red-then-fade');
+					} else {
+						this.resolveSearch(results[0])
+					}
 				})
 				.catch(error => {
-					input.value = '';
-					// Trigger failure animation
-					input.classList.remove('animate-red-then-fade');
-					input.offsetWidth; // Triggers reflow
-					input.classList.add('animate-red-then-fade');
+					console.log('ERROR getting search results from server', error);
+				});
+		},
+		resolveSearch(tolNodeName: string){
+			// Asks server for nodes in parent-chain, updates tolMap, then emits search event
+			let url = new URL(window.location.href);
+			url.pathname = '/data/chain';
+			url.search = '?name=' + encodeURIComponent(tolNodeName);
+			fetch(url.toString())
+				.then(response => response.json())
+				.then(obj => {
+					Object.getOwnPropertyNames(obj).forEach(key => {this.tolMap.set(key, obj[key])});
+					this.$emit('search-node', tolNodeName);
+				})
+				.catch(error => {
+					console.log('ERROR loading tolnode chain', error);
 				});
 		},
 		focusInput(){
 			(this.$refs.searchInput as HTMLInputElement).focus();
 		},
+		onInput(){
+			let input = this.$refs.searchInput as HTMLInputElement;
+			// Check for empty input
+			if (input.value.length == 0){
+				this.searchSuggs = [];
+				this.focusedSuggIdx = null;
+				return;
+			}
+			// Ask server for search-suggestions
+			let url = new URL(window.location.href);
+			url.pathname = '/data/search';
+			url.search = '?name=' + encodeURIComponent(input.value);
+			this.lastSuggReqId += 1;
+			let suggsId = this.lastSuggReqId;
+			fetch(url.toString())
+				.then(response => response.json())
+				.then(results => {
+					if (this.lastSuggReqId == suggsId){
+						this.searchSuggs = results;
+						this.focusedSuggIdx = null;
+					}
+				})
+		},
+		onDownKey(){
+			// Select next search-suggestion, if any
+			if (this.searchSuggs.length > 0){
+				if (this.focusedSuggIdx == null){
+					this.focusedSuggIdx = 0;
+				} else {
+					this.focusedSuggIdx = Math.min(this.focusedSuggIdx + 1, this.searchSuggs.length - 1);
+				}
+			}
+		},
+		onUpKey(){
+			// Select previous search-suggestion, or cancel selection
+			if (this.focusedSuggIdx != null){
+				if (this.focusedSuggIdx == 0){
+					this.focusedSuggIdx = null;
+				} else {
+					this.focusedSuggIdx -= 1;
+				}
+			}
+		},
 	},
 	mounted(){
 		(this.$refs.searchInput as HTMLInputElement).focus();
@@ -60,12 +123,22 @@ export default defineComponent({
 
 <template>
 <div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onCloseClick">
-	<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 p-3
+	<div class="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 p-2
 		bg-stone-50 rounded-md shadow shadow-black flex gap-1">
-		<input type="text" class="block border"
-			@keyup.enter="onSearchEnter" @keyup.esc="onCloseClick" ref="searchInput"/>
-		<search-icon @click.stop="onSearchEnter" ref="searchIcon"
-			class="block w-6 h-6 ml-1 hover:cursor-pointer hover:bg-stone-200" />
+		<div class="relative">
+			<input type="text" class="block border p-1" ref="searchInput"
+				@keyup.enter="onEnter" @keyup.esc="onCloseClick"
+				@input="onInput" @keydown.down.prevent="onDownKey" @keydown.up.prevent="onUpKey"/>
+			<div class="absolute top-[100%] w-full">
+				<div v-for="(item, idx) of searchSuggs" :key="item"
+					:style="{backgroundColor: idx == focusedSuggIdx ? '#a3a3a3' : 'white'}"
+					class="bg-white border p-1 hover:underline hover:cursor-pointer" @click="resolveSearch(item)">
+					{{item}}
+				</div>
+			</div>
+		</div>
+		<search-icon @click.stop="onEnter" ref="searchIcon"
+			class="block w-8 h-8 ml-1 hover:cursor-pointer hover:bg-stone-200" />
 	</div>
 </div>
 </template>
-- 
cgit v1.2.3

