From 442c0bbffc5c372c7ec3510914968f75ab6e4a4f Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Thu, 5 Jan 2023 17:13:03 +1100 Subject: Add partially-complete search modal For now, use placeholder code for jumping to a search result. Add db index for case-insensitive event title searching. Make type=info requests accept title instead of ID (for looking up a searched-for title). Make EventInfo contain an Event field (for showing info in search suggestions). Add titleToEvent map in App, for use by SearchModal to look up searched-for titles. Add keyboard shortcuts to open/close search and info modals. --- src/App.vue | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 13 deletions(-) (limited to 'src/App.vue') diff --git a/src/App.vue b/src/App.vue index 80ce803..2f5051a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,14 +5,17 @@

Histplorer

- - + + - - + + + + +
@@ -23,13 +26,16 @@ :eventTree="eventTree" :unitCountMaps="unitCountMaps" class="grow basis-full min-h-0 outline outline-1" @close="onTimelineClose(idx)" @state-chg="onTimelineChg($event, idx)" @event-display="onEventDisplay" - @event-click="onEventClick"/> + @info-click="onInfoClick"/> - + + + + @@ -40,11 +46,13 @@ import {ref, computed, onMounted, onUnmounted, Ref, shallowRef, ShallowRef} from import TimeLine from './components/TimeLine.vue'; import BaseLine from './components/BaseLine.vue'; import InfoModal from './components/InfoModal.vue'; +import SearchModal from './components/SearchModal.vue'; import IconButton from './components/IconButton.vue'; // Icons -import PlusIcon from './components/icon/PlusIcon.vue'; -import SettingsIcon from './components/icon/SettingsIcon.vue'; import HelpIcon from './components/icon/HelpIcon.vue'; +import SettingsIcon from './components/icon/SettingsIcon.vue'; +import PlusIcon from './components/icon/PlusIcon.vue'; +import SearchIcon from './components/icon/SearchIcon.vue'; // Other import {HistDate, HistEvent, queryServer, EventResponseJson, jsonToHistEvent, SCALES, stepDate, TimelineState, cmpHistEvent, dateToUnit, DateRangeTree, @@ -110,6 +118,7 @@ function onTimelineClose(idx: number){ // For storing and looking up events const eventTree: ShallowRef> = shallowRef(new RBTree(cmpHistEvent)); let idToEvent: Map = new Map(); +let titleToEvent: Map = new Map(); const unitCountMaps: Ref[]> = ref(SCALES.map(() => new Map())); // For each scale, maps units to event counts // For keeping event data under a memory limit @@ -149,6 +158,10 @@ function reduceEvents(){ eventTree.value = newTree; unitCountMaps.value = newMaps; idToEvent = eventsToKeep; + titleToEvent = new Map(); + for (let event of eventsToKeep.values()){ + titleToEvent.set(event.title, event); + } } // For getting events from server const EVENT_REQ_LIMIT = 300; @@ -214,6 +227,7 @@ async function onEventDisplay( }); let responseObj: EventResponseJson | null = await queryServer(urlParams); if (responseObj == null){ + console.log('WARNING: Server gave null response to event query'); return; } queriedRanges[scaleIdx].add([firstDate, lastDate]); @@ -225,6 +239,7 @@ async function onEventDisplay( if (success){ eventAdded = true; idToEvent.set(event.id, event); + titleToEvent.set(event.title, event); } } // Collect unit counts @@ -260,18 +275,25 @@ async function onEventDisplay( } // For info modal -const infoModalEvent = ref(null as HistEvent | null); const infoModalData = ref(null as EventInfo | null); -async function onEventClick(eventId: number){ +async function onInfoClick(eventTitle: string){ // Query server for event info - let urlParams = new URLSearchParams({type: 'info', event: String(eventId)}); + let urlParams = new URLSearchParams({type: 'info', event: eventTitle}); let responseObj: EventInfoJson | null = await queryServer(urlParams); if (responseObj != null){ - infoModalEvent.value = idToEvent.get(eventId)!; infoModalData.value = jsonToEventInfo(responseObj); + } else { + console.log('WARNING: Server gave null response to info query'); } } +// For search modal +const searchOpen = ref(false); +function onSearch(event: HistEvent){ + searchOpen.value = false; + console.log(`Need to jump to event "${event.title}" at ${event.start}`); +} + // For resize handling let lastResizeHdlrTime = 0; // Used to throttle resize handling let afterResizeHdlr = 0; // Used to trigger handler after ending a run of resize events @@ -298,6 +320,33 @@ async function onResize(){ onMounted(() => window.addEventListener('resize', onResize)); onUnmounted(() => window.removeEventListener('resize', onResize)); +// For keyboard shortcuts +function onKeyDown(evt: KeyboardEvent){ + if (store.disableShortcuts){ + return; + } + if (evt.key == 'Escape'){ + if (infoModalData.value != null){ + infoModalData.value = null; + } else if (searchOpen.value){ + searchOpen.value = false; + } + } else if (evt.key == 'f' && evt.ctrlKey){ + evt.preventDefault(); + // Open/focus search bar + if (!searchOpen.value){ + searchOpen.value = true; + } + } +} +onMounted(() => { + window.addEventListener('keydown', onKeyDown); + // Note: Need 'keydown' instead of 'keyup' to override default CTRL-F +}); +onUnmounted(() => { + window.removeEventListener('keydown', onKeyDown); +}); + // Styles const buttonStyles = computed(() => ({ color: store.color.text, -- cgit v1.2.3