aboutsummaryrefslogtreecommitdiff
path: root/src/components/SearchModal.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/SearchModal.vue')
-rw-r--r--src/components/SearchModal.vue55
1 files changed, 42 insertions, 13 deletions
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 1818529..607587f 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -2,12 +2,17 @@
<div class="fixed left-0 top-0 w-full h-full bg-black/40" @click="onClose" ref="rootRef">
<div class="absolute left-1/2 -translate-x-1/2 top-1/4 -translate-y-1/2 min-w-3/4 md:min-w-[12cm] flex"
:style="styles">
+ <!-- Input field -->
<input type="text" class="block border p-1 px-2 rounded-l-[inherit] grow" ref="inputRef"
@keyup.enter="onSearch" @keyup.esc="onClose"
@input="onInput" @keydown.down.prevent="onDownKey" @keydown.up.prevent="onUpKey"/>
+
+ <!-- Search button -->
<div class="p-1 hover:cursor-pointer">
<search-icon @click.stop="onSearch" class="w-8 h-8"/>
</div>
+
+ <!-- Search suggestions -->
<div class="absolute top-[100%] w-full overflow-hidden" :style="suggContainerStyles">
<div v-for="(sugg, idx) of searchSuggs" :key="sugg.name + '|' + sugg.canonicalName"
:style="{backgroundColor: idx == focusedSuggIdx ? store.color.bgAltDark : store.color.bgAlt}"
@@ -24,6 +29,8 @@
</div>
<div v-if="searchHadMoreSuggs" class="text-center">&#x2022; &#x2022; &#x2022;</div>
</div>
+
+ <!-- Options -->
<label :style="animateLabelStyles" class="flex gap-1">
<input type="checkbox" v-model="store.searchJumpMode" @change="emit('setting-chg', 'searchJumpMode')"/>
<div class="text-sm">Jump to result</div>
@@ -34,6 +41,7 @@
<script setup lang="ts">
import {ref, computed, onMounted, onUnmounted, PropType} from 'vue';
+
import SearchIcon from './icon/SearchIcon.vue';
import InfoIcon from './icon/InfoIcon.vue';
import {TolNode, TolMap} from '../tol';
@@ -41,28 +49,28 @@ import {LayoutNode, LayoutMap} from '../layout';
import {queryServer, SearchSugg, SearchSuggResponse} from '../lib';
import {useStore} from '../store';
-// Refs
const rootRef = ref(null as HTMLDivElement | null);
const inputRef = ref(null as HTMLInputElement | null);
-// Global store
const store = useStore();
-// Props + events
const props = defineProps({
lytMap: {type: Object as PropType<LayoutMap>, required: true}, // Used to check if a searched-for node exists
activeRoot: {type: Object as PropType<LayoutNode>, required: true}, // Sent to server to reduce response size
tolMap: {type: Object as PropType<TolMap>, required: true}, // Upon a search response, gets new nodes added
});
+
const emit = defineEmits(['search', 'close', 'info-click', 'setting-chg', 'net-wait', 'net-get']);
-// Search-suggestion data
+// ========== Search-suggestion data ==========
+
const searchSuggs = ref([] as SearchSugg[]);
const searchHadMoreSuggs = ref(false);
+const suggsInput = ref(''); // The input that resulted in the current suggestions (used to highlight matching text)
+
const suggDisplayStrings = computed((): [string, string, string, string][] => {
let result: [string, string, string, string][] = [];
let input = suggsInput.value.toLowerCase();
- // For each SearchSugg
for (let sugg of searchSuggs.value){
let idx = sugg.name.indexOf(input);
// Split suggestion text into parts before/within/after an input match
@@ -72,26 +80,30 @@ const suggDisplayStrings = computed((): [string, string, string, string][] => {
} else {
strings = [input, '', '', ''];
}
+
// Indicate any distinct canonical-name
if (sugg.canonicalName != null){
strings[3] = ` (aka ${sugg.canonicalName})`;
}
- //
+
result.push(strings);
}
return result;
});
-const suggsInput = ref(''); // The input that resulted in the current suggestions (used to highlight matching text)
+
const focusedSuggIdx = ref(null as null | number); // Index of a search-suggestion selected using the arrow keys
-// For search-suggestion requests
+// ========== For search-suggestion requests ==========
+
const lastSuggReqTime = ref(0); // Set when a search-suggestions request is initiated
const pendingSuggReqParams = ref(null as null | URLSearchParams);
// Used by a search-suggestion requester to request with the latest user input
const pendingDelayedSuggReq = ref(0); // Set via setTimeout() for a non-initial search-suggestions request
const pendingSuggInput = ref(''); // Used to remember what input triggered a suggestions request
+
async function onInput(){
let input = inputRef.value!;
+
// Check for empty input
if (input.value.length == 0){
searchSuggs.value = [];
@@ -99,6 +111,7 @@ async function onInput(){
focusedSuggIdx.value = null;
return;
}
+
// Get URL params to use for querying search-suggestions
let urlParams = new URLSearchParams({
type: 'sugg',
@@ -106,6 +119,7 @@ async function onInput(){
limit: String(store.searchSuggLimit),
tree: store.tree,
});
+
// Query server, delaying/skipping if a request was recently sent
pendingSuggReqParams.value = urlParams;
pendingSuggInput.value = input.value;
@@ -119,6 +133,7 @@ async function onInput(){
searchSuggs.value = responseObj.suggs;
searchHadMoreSuggs.value = responseObj.hasMore;
suggsInput.value = suggInput;
+
// Auto-select first result if present
if (searchSuggs.value.length > 0){
focusedSuggIdx.value = 0;
@@ -145,7 +160,8 @@ async function onInput(){
}
}
-// For search events
+// ========== For search events ==========
+
function onSearch(){
if (focusedSuggIdx.value == null){
let input = inputRef.value!.value.toLowerCase();
@@ -155,15 +171,18 @@ function onSearch(){
resolveSearch(sugg.canonicalName || sugg.name);
}
}
+
async function resolveSearch(tolNodeName: string){
if (tolNodeName == ''){
return;
}
+
// Check if the node data is already here
if (props.lytMap.has(tolNodeName)){
emit('search', tolNodeName);
return;
}
+
// Ask server for nodes in parent-chain, updates tolMap, then emits search event
let urlParams = new URLSearchParams({
type: 'node',
@@ -195,28 +214,33 @@ async function resolveSearch(tolNodeName: string){
}
}
-// More event handling
+// ========== More event handling ==========
+
function onClose(evt: Event){
if (evt.target == rootRef.value){
emit('close');
}
}
+
function onDownKey(){
if (focusedSuggIdx.value != null){
focusedSuggIdx.value = (focusedSuggIdx.value + 1) % searchSuggs.value.length;
}
}
+
function onUpKey(){
if (focusedSuggIdx.value != null){
focusedSuggIdx.value = (focusedSuggIdx.value - 1 + searchSuggs.value.length) % searchSuggs.value.length;
// The addition after '-1' is to avoid becoming negative
}
}
+
function onInfoIconClick(nodeName: string){
emit('info-click', nodeName);
}
-// For keyboard shortcuts
+// ========== For keyboard shortcuts ==========
+
function onKeyDown(evt: KeyboardEvent){
if (store.disableShortcuts){
return;
@@ -226,13 +250,16 @@ function onKeyDown(evt: KeyboardEvent){
inputRef.value!.focus();
}
}
+
onMounted(() => window.addEventListener('keydown', onKeyDown))
onUnmounted(() => window.removeEventListener('keydown', onKeyDown))
-// Focus input on mount
+// ========== Focus input on mount ==========
+
onMounted(() => inputRef.value!.focus())
-// Styles
+// ========== For styling ==========
+
const styles = computed((): Record<string,string> => {
let br = store.borderRadius;
return {
@@ -241,6 +268,7 @@ const styles = computed((): Record<string,string> => {
boxShadow: store.shadowNormal,
};
});
+
const suggContainerStyles = computed((): Record<string,string> => {
let br = store.borderRadius;
return {
@@ -249,6 +277,7 @@ const suggContainerStyles = computed((): Record<string,string> => {
borderRadius: `0 0 ${br}px ${br}px`,
};
});
+
const animateLabelStyles = computed(() => ({
position: 'absolute',
top: -store.lytOpts.headerSz - 2 + 'px',