diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/SearchModal.vue | 123 |
1 files changed, 98 insertions, 25 deletions
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> |
