From 6c5f2d9b7084694812a16607a434e65fa5345d2e Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Sun, 26 Jun 2022 21:18:34 +1000
Subject: Clean up code in SearchModal

Also allow cycling to top/bottom of search suggestions
---
 src/components/SearchModal.vue | 254 +++++++++++++++++++++--------------------
 1 file changed, 129 insertions(+), 125 deletions(-)

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

diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 166c9f8..d14173f 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -1,36 +1,37 @@
 <script lang="ts">
+
 import {defineComponent, PropType} from 'vue';
 import SearchIcon from './icon/SearchIcon.vue';
 import LogInIcon from './icon/LogInIcon.vue';
 import InfoIcon from './icon/InfoIcon.vue';
-import {TolMap, SearchSugg, SearchSuggResponse, UiOptions} from '../lib';
-import {LayoutNode} from '../layout';
+import {TolNode, TolMap, UiOptions, SearchSugg, SearchSuggResponse} from '../lib';
+import {LayoutNode, LayoutOptions} from '../layout';
 
-// Displays a search box, and sends search requests
 export default defineComponent({
+	props: {
+		tolMap: {type: Object as PropType<TolMap>, required: true}, // Added to from a search response
+		lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
+		uiOpts: {type: Object as PropType<UiOptions>, required: true},
+	},
 	data(){
 		return {
-			searchSuggs: [] as SearchSugg[],
-			searchHasMoreSuggs: false,
-			focusedSuggIdx: null as null | number, // Denotes a search-suggestion selected using the arrow keys
+			// For search-suggestion requests
 			lastSuggReqTime: 0, // Set when a search-suggestions request is initiated
-			pendingSuggReqUrl: '', // Used by a pendingSuggReq callback to use the latest user input
+			pendingSuggReqUrl: '', // Used by a search-suggestion requester to use the latest user input
 			pendingDelayedSuggReq: 0, // Set via setTimeout() for a non-initial search-suggestions request
+			// Search-suggestion data
+			searchSuggs: [] as SearchSugg[],
+			searchHadMoreSuggs: false,
+			// Other
+			focusedSuggIdx: null as null | number, // Denotes a search-suggestion selected using the arrow keys
 		};
 	},
-	props: {
-		tolMap: {type: Object as PropType<TolMap>, required: true},
-		uiOpts: {type: Object as PropType<UiOptions>, required: true},
-	},
 	computed: {
-		infoIconStyles(): Record<string,string> {
-			let size = '18px';
+		styles(): Record<string,string> {
 			return {
-				width: size,
-				height: size,
-				minWidth: size,
-				minHeight: size,
-				margin: '2px',
+				backgroundColor: this.uiOpts.bgColorAlt,
+				borderRadius: this.uiOpts.borderRadius + 'px',
+				boxShadow: this.uiOpts.shadowNormal,
 			};
 		},
 		suggDisplayStrings(): [string, string, string][] {
@@ -46,7 +47,7 @@ export default defineComponent({
 				} else {
 					strings = [input, '', ''];
 				}
-				// Indicate a distinct canonical-name
+				// Indicate any distinct canonical-name
 				if (sugg.canonicalName != null){
 					strings[2] += ` (aka ${sugg.canonicalName})`;
 				}
@@ -57,64 +58,13 @@ export default defineComponent({
 		},
 	},
 	methods: {
-		onCloseClick(evt: Event){
-			if (evt.target == this.$el){
-				this.$emit('close');
-			}
-		},
-		onSearch(){
-			if (this.focusedSuggIdx == null){
-				this.resolveSearch((this.$refs.searchInput as HTMLInputElement).value.toLowerCase())
-			} else {
-				let sugg = this.searchSuggs[this.focusedSuggIdx]
-				this.resolveSearch(sugg.canonicalName || sugg.name);
-			}
-		},
-		onSearchModeChg(){
-			this.uiOpts.searchJumpMode = !this.uiOpts.searchJumpMode;
-			this.$emit('setting-chg', 'searchJumpMode');
-		},
-		resolveSearch(tolNodeName: string){
-			if (tolNodeName == ''){
-				return;
-			}
-			// 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);
-			url.search += (this.uiOpts.useReducedTree ? '&tree=reduced' : '');
-			fetch(url.toString())
-				.then(response => response.json())
-				.then(obj => {
-					let keys = Object.getOwnPropertyNames(obj);
-					if (keys.length > 0){
-						keys.forEach(key => {
-							if (!this.tolMap.has(key)){
-								this.tolMap.set(key, obj[key])
-							}
-						});
-						this.$emit('search', tolNodeName);
-					} else {
-						// Trigger failure animation
-						let input = this.$refs.searchInput as HTMLInputElement;
-						input.classList.remove('animate-red-then-fade');
-						input.offsetWidth; // Triggers reflow
-						input.classList.add('animate-red-then-fade');
-					}
-				})
-				.catch(error => {
-					console.log('ERROR loading tolnode chain', error);
-				});
-		},
-		focusInput(){
-			(this.$refs.searchInput as HTMLInputElement).focus();
-		},
-		onInput(){
+		// Search-suggestion events
+		async onInput(){
 			let input = this.$refs.searchInput as HTMLInputElement;
 			// Check for empty input
 			if (input.value.length == 0){
 				this.searchSuggs = [];
-				this.searchHasMoreSuggs = false;
+				this.searchHadMoreSuggs = false;
 				this.focusedSuggIdx = null;
 				return;
 			}
@@ -124,67 +74,120 @@ export default defineComponent({
 			url.search = '?name=' + encodeURIComponent(input.value);
 			url.search += this.uiOpts.useReducedTree ? '&tree=reduced' : '';
 			url.search += '&limit=' + this.uiOpts.searchSuggLimit;
-			// Query server, delaying/ignoring if a request was recently sent
+			// Query server, delaying/skipping if a request was recently sent
 			this.pendingSuggReqUrl = url.toString();
-			let doReq = () => {
-				return fetch(this.pendingSuggReqUrl)
-					.then(response => {
-						if (!response.ok){
-							throw new Error('Server response not OK')
-						}
-						return response.json()
-					})
-					.then((results: SearchSuggResponse) => {
-						this.searchSuggs = results.suggs;
-						this.searchHasMoreSuggs = results.hasMore;
-						this.focusedSuggIdx = null;
-					})
-					.catch(error => {
-						console.error('Error encountered during fetch operation', error);
-					});
+			let doReq = async () => {
+				let responseObj: SearchSuggResponse;
+				try {
+					let response = await fetch(this.pendingSuggReqUrl);
+					responseObj = await response.json();
+				} catch (error){
+					console.log('Error with getting search suggestions from server: ' + error)
+					return;
+				}
+				this.searchSuggs = responseObj.suggs;
+				this.searchHadMoreSuggs = responseObj.hasMore;
+				this.focusedSuggIdx = null;
 			};
 			let currentTime = new Date().getTime();
 			if (this.lastSuggReqTime == 0){
 				this.lastSuggReqTime = currentTime;
-				doReq().finally(() => {
-					if (this.lastSuggReqTime == currentTime){
-						this.lastSuggReqTime = 0;
-					}
-				});
+				await doReq();
+				if (this.lastSuggReqTime == currentTime){
+					this.lastSuggReqTime = 0;
+				}
 			} else if (this.pendingDelayedSuggReq == 0){
 				this.lastSuggReqTime = currentTime;
-				this.pendingDelayedSuggReq = setTimeout(() => {
+				this.pendingDelayedSuggReq = setTimeout(async () => {
 					this.pendingDelayedSuggReq = 0;
-					doReq().finally(() => {
-						if (this.lastSuggReqTime == currentTime){
-							this.lastSuggReqTime = 0;
-						}
-					});
+					await doReq();
+					if (this.lastSuggReqTime == currentTime){
+						this.lastSuggReqTime = 0;
+					}
 				}, 300);
 			}
 		},
+		onInfoIconClick(nodeName: string){
+			this.$emit('info-click', nodeName);
+		},
 		onDownKey(){
-			// Select next search-suggestion, if any
+			// Select next search-suggestion, possibly cycling back to top
 			if (this.searchSuggs.length > 0){
 				if (this.focusedSuggIdx == null){
 					this.focusedSuggIdx = 0;
+				} else if (this.focusedSuggIdx == this.searchSuggs.length - 1){
+					this.focusedSuggIdx = null;
 				} else {
-					this.focusedSuggIdx = Math.min(this.focusedSuggIdx + 1, this.searchSuggs.length - 1);
+					this.focusedSuggIdx += 1;
 				}
 			}
 		},
 		onUpKey(){
-			// Select previous search-suggestion, or cancel selection
-			if (this.focusedSuggIdx != null){
-				if (this.focusedSuggIdx == 0){
+			// Select previous search-suggestion, possibly cycling to bottom
+			if (this.searchSuggs.length > 0){
+				if (this.focusedSuggIdx == null){
+					this.focusedSuggIdx = this.searchSuggs.length - 1;
+				} else if (this.focusedSuggIdx == 0){
 					this.focusedSuggIdx = null;
 				} else {
 					this.focusedSuggIdx -= 1;
 				}
 			}
 		},
-		onInfoIconClick(nodeName: string){
-			this.$emit('info-click', nodeName);
+		// Search events
+		onSearch(){
+			if (this.focusedSuggIdx == null){
+				this.resolveSearch((this.$refs.searchInput as HTMLInputElement).value.toLowerCase())
+			} else {
+				let sugg = this.searchSuggs[this.focusedSuggIdx]
+				this.resolveSearch(sugg.canonicalName || sugg.name);
+			}
+		},
+		async resolveSearch(tolNodeName: string){
+			if (tolNodeName == ''){
+				return;
+			}
+			// Ask 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);
+			url.search += (this.uiOpts.useReducedTree ? '&tree=reduced' : '');
+			let responseObj: {[x: string]: TolNode};
+			try {
+				let response = await fetch(url.toString());
+				responseObj = await response.json();
+			} catch (error){
+				console.log('Error with getting tolnode chain: ' + error);
+				return;
+			}
+			let keys = Object.getOwnPropertyNames(responseObj);
+			if (keys.length > 0){
+				keys.forEach(key => {
+					if (!this.tolMap.has(key)){
+						this.tolMap.set(key, responseObj[key])
+					}
+				});
+				this.$emit('search', tolNodeName);
+			} else {
+				// Trigger failure animation
+				let input = this.$refs.searchInput as HTMLInputElement;
+				input.classList.remove('animate-red-then-fade');
+				input.offsetWidth; // Triggers reflow
+				input.classList.add('animate-red-then-fade');
+			}
+		},
+		// Other
+		onSearchModeChg(){
+			this.uiOpts.searchJumpMode = !this.uiOpts.searchJumpMode;
+			this.$emit('setting-chg', 'searchJumpMode');
+		},
+		onClose(evt: Event){
+			if (evt.target == this.$el){
+				this.$emit('close');
+			}
+		},
+		focusInput(){ // Used from external component
+			(this.$refs.searchInput as HTMLInputElement).focus();
 		},
 	},
 	mounted(){
@@ -196,34 +199,35 @@ export default defineComponent({
 </script>
 
 <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
-		bg-stone-50 rounded-md shadow shadow-black flex">
-		<div class="relative">
-			<input type="text" class="block border p-1 m-2" ref="searchInput"
-				@keyup.enter="onSearch" @keyup.esc="onCloseClick"
+<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose">
+	<div class="absolute left-1/2 -translate-x-1/2 top-1/4 w-3/4 -translate-y-1/2 flex" :style="styles">
+		<div class="relative grow m-2">
+			<input type="text" class="block border p-1 w-full" ref="searchInput"
+				@keyup.enter="onSearch" @keyup.esc="onClose"
 				@input="onInput" @keydown.down.prevent="onDownKey" @keydown.up.prevent="onUpKey"/>
-			<div class="absolute top-[100%] w-full">
+			<div class="absolute top-[100%] w-full"
+				:style="{backgroundColor: uiOpts.bgColorAlt, color: uiOpts.textColorAlt}">
 				<div v-for="(sugg, idx) of searchSuggs"
-					:style="{backgroundColor: idx == focusedSuggIdx ? '#a3a3a3' : 'white'}"
-					class="border p-1 hover:underline hover:cursor-pointer"
+					:style="{backgroundColor: idx == focusedSuggIdx ? uiOpts.bgColorAltDark : uiOpts.bgColorAlt}"
+					class="border p-1 hover:underline hover:cursor-pointer flex"
 					@click="resolveSearch(sugg.canonicalName || sugg.name)">
-					<span>{{suggDisplayStrings[idx][0]}}</span>
-					<span class="font-bold">{{suggDisplayStrings[idx][1]}}</span>
-					<span>{{suggDisplayStrings[idx][2]}}</span>
-					<info-icon :style="infoIconStyles"
-						class="float-right text-stone-500 hover:text-stone-900 hover:cursor-pointer"
+					<div class="grow overflow-hidden whitespace-nowrap text-ellipsis">
+						<span>{{suggDisplayStrings[idx][0]}}</span>
+						<span class="font-bold">{{suggDisplayStrings[idx][1]}}</span>
+						<span>{{suggDisplayStrings[idx][2]}}</span>
+					</div>
+					<info-icon class="hover:cursor-pointer my-auto w-5 h-5"
 						@click.stop="onInfoIconClick(sugg.canonicalName || sugg.name)"/>
 				</div>
-				<div v-if="searchHasMoreSuggs" class="bg-white px-1 text-center border">...</div>
+				<div v-if="searchHadMoreSuggs" class="text-center border">...</div>
 			</div>
 		</div>
-		<div class="my-auto hover:cursor-pointer hover:brightness-75 rounded border shadow">
+		<div class="my-auto hover:cursor-pointer hover:brightness-75  rounded border shadow">
 			<search-icon @click.stop="onSearch" class="block w-8 h-8"/>
 		</div>
 		<div class="my-auto mx-2 hover:cursor-pointer hover:brightness-75 rounded">
 			<log-in-icon @click.stop="onSearchModeChg" class="block w-8 h-8"
-				:class="uiOpts.searchJumpMode ? 'text-stone-500' : 'text-stone-300'"/>
+				:class="uiOpts.searchJumpMode ? 'opacity-100' : 'opacity-30'"/>
 		</div>
 	</div>
 </div>
-- 
cgit v1.2.3

