From 9deeb448e550fe28c22e5a9c4acc2adcfed71571 Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Sun, 1 May 2022 00:19:29 +1000 Subject: Enable search-suggestion sending/displaying/selection --- backend/server.py | 13 ++--- src/components/SearchModal.vue | 123 ++++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 32 deletions(-) diff --git a/backend/server.py b/backend/server.py index d5c6006..15d0960 100755 --- a/backend/server.py +++ b/backend/server.py @@ -61,9 +61,10 @@ def nodeNameToFile(name, cur): return {"filename": filename, "eolId": eolId, "sourceUrl": sUrl, "license": license, "copyrightOwner": cOwner} def lookupName(name): cur = dbCon.cursor() - cur.execute("SELECT name, alt_name FROM names WHERE alt_name = ?", (name,)) - row = cur.fetchone() - return json.dumps(row[0]) if row != None else None + results = [] + for row in cur.execute("SELECT name, alt_name FROM names WHERE alt_name = ?", (name,)): + results.append(row[0]) + return json.dumps(results) class DbServer(BaseHTTPRequestHandler): def do_GET(self): @@ -128,10 +129,8 @@ class DbServer(BaseHTTPRequestHandler): else: name = nodeObj["parent"] elif reqType == "search": - nameJson = lookupName(name) - if nameJson != None: - self.respondJson(nameJson) - return + self.respondJson(lookupName(name)) + return self.send_response(404) self.end_headers() self.end_headers() 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, 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({ -- cgit v1.2.3