aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/SearchModal.vue123
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>