diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-06-26 21:18:34 +1000 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-06-26 21:18:34 +1000 |
| commit | 6c5f2d9b7084694812a16607a434e65fa5345d2e (patch) | |
| tree | 5c89dd2aebf2afe02a8459e9ef45e32332dea1b7 /src/components/SearchModal.vue | |
| parent | d2d6f0496ce816e9238e785ed3d0e7bd61b2483b (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.vue | 254 |
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> |
