diff options
| -rw-r--r-- | src/App.vue | 21 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 254 | ||||
| -rw-r--r-- | src/components/TileInfoModal.vue | 2 | ||||
| -rw-r--r-- | src/lib.ts | 4 |
4 files changed, 145 insertions, 136 deletions
diff --git a/src/App.vue b/src/App.vue index a3d0559..1c80acf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -58,10 +58,11 @@ function getDefaultLytOpts(): LayoutOptions { function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { let screenSz = getBreakpoint(); // Reused option values - let textColor = '#fafaf9'; - let bgColor = '#292524', bgColorAlt = '#fafaf9', + let textColor = '#fafaf9', textColorAlt = '#1c1917'; + let bgColor = '#292524', bgColorLight = '#44403c', bgColorDark = '#1c1917', - bgColorLight2 = '#57534e', bgColorDark2 = '#0e0c0b'; + bgColorLight2 = '#57534e', bgColorDark2 = '#0e0c0b', + bgColorAlt = '#fafaf9', bgColorAltDark = '#a8a29e'; let altColor = '#a3e623', altColorDark = '#65a30d'; let accentColor = '#f59e0b'; let scrollGap = getScrollBarWidth(); @@ -69,12 +70,14 @@ function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { return { // Shared coloring/sizing textColor, + textColorAlt, bgColor, - bgColorAlt, bgColorLight, bgColorDark, bgColorLight2, bgColorDark2, + bgColorAlt, + bgColorAltDark, altColor, altColorDark, borderRadius: 5, // px @@ -97,7 +100,7 @@ function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { autoActionDelay: 500, // ms // Other useReducedTree: false, - searchSuggLimit: 5, + searchSuggLimit: 10, searchJumpMode: false, tutorialSkip: false, disabledActions: new Set() as Set<Action>, @@ -258,7 +261,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error){ - console.log('ERROR: Unable to retreive tol-node data', error); + console.log('Error with retreiving tol-node data: ' + error); return false; } Object.getOwnPropertyNames(responseObj).forEach(n => {this.tolMap.set(n, responseObj[n])}); @@ -356,7 +359,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error){ - console.log('ERROR: Unable to retreive tol-node data', error); + console.log('Error with retreiving tol-node data: ' + error); return false; } Object.getOwnPropertyNames(responseObj).forEach(n => {this.tolMap.set(n, responseObj[n])}); @@ -815,7 +818,7 @@ export default defineComponent({ let response = await fetch(urlPath); responseObj = await response.json(); } catch (error) { - console.log('ERROR: Unable to retrieve tree data', error); + console.log('Error with retrieving tree data: ' + error); return; } // Get root node name @@ -1016,7 +1019,7 @@ export default defineComponent({ </div> <!-- Modals --> <transition name="fade"> - <search-modal v-if="searchOpen" :tolMap="tolMap" :uiOpts="uiOpts" ref="searchModal" + <search-modal v-if="searchOpen" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" ref="searchModal" @close="searchOpen = false" @search="onSearch" @info-click="onInfoClick" @setting-chg="onSettingChg" /> </transition> <transition name="fade"> 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> diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue index c8f8047..88c6065 100644 --- a/src/components/TileInfoModal.vue +++ b/src/components/TileInfoModal.vue @@ -112,7 +112,7 @@ export default defineComponent({ let response = await fetch(url.toString()); responseObj = await response.json(); } catch (error){ - console.log("ERROR: Unable to retrieve data from server") + console.log("Error with retrieving data from server: " + error); return; } // Set fields from response @@ -68,12 +68,14 @@ export type Action = export type UiOptions = { // Shared coloring/sizing textColor: string, // CSS color + textColorAlt: string, bgColor: string, - bgColorAlt: string, bgColorLight: string, bgColorDark: string, bgColorLight2: string, bgColorDark2: string, + bgColorAlt: string, + bgColorAltDark: string, altColor: string, altColorDark: string, borderRadius: number, // CSS border-radius value, in px |
