aboutsummaryrefslogtreecommitdiff
path: root/src/components/SearchModal.vue
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2022-06-26 21:18:34 +1000
committerTerry Truong <terry06890@gmail.com>2022-06-26 21:18:34 +1000
commit6c5f2d9b7084694812a16607a434e65fa5345d2e (patch)
tree5c89dd2aebf2afe02a8459e9ef45e32332dea1b7 /src/components/SearchModal.vue
parentd2d6f0496ce816e9238e785ed3d0e7bd61b2483b (diff)
Clean up code in SearchModal
Also allow cycling to top/bottom of search suggestions
Diffstat (limited to 'src/components/SearchModal.vue')
-rw-r--r--src/components/SearchModal.vue254
1 files changed, 129 insertions, 125 deletions
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>