diff options
| author | Terry Truong <terry06890@gmail.com> | 2023-01-06 02:32:59 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2023-01-06 02:45:26 +1100 |
| commit | 559902e0211a06b349c4c2f50b0882a8d314f8b7 (patch) | |
| tree | ecdfcb0983454044ecb28ca8f8f4d781b1124047 /src | |
| parent | 442c0bbffc5c372c7ec3510914968f75ab6e4a4f (diff) | |
Jump to and highlight search results
Use a 'searchTarget' prop of Timeline to trigger jumping to a search result.
Make TimeLine prioritise search result in layout.
When querying for events in App, check for a search target, and use incl= to retrieve it.
On server, for the incl= query param, don't include the event if outside specified range.
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.vue | 40 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 57 | ||||
| -rw-r--r-- | src/lib.ts | 15 |
3 files changed, 100 insertions, 12 deletions
diff --git a/src/App.vue b/src/App.vue index 2f5051a..cbd6825 100644 --- a/src/App.vue +++ b/src/App.vue @@ -23,7 +23,7 @@ :style="{backgroundColor: store.color.bg}" ref="contentAreaRef"> <time-line v-for="(state, idx) in timelines" :key="state.id" :vert="vert" :initialState="state" :closeable="timelines.length > 1" - :eventTree="eventTree" :unitCountMaps="unitCountMaps" + :eventTree="eventTree" :unitCountMaps="unitCountMaps" :searchTarget="timelineTargets[idx]" class="grow basis-full min-h-0 outline outline-1" @close="onTimelineClose(idx)" @state-chg="onTimelineChg($event, idx)" @event-display="onEventDisplay" @info-click="onInfoClick"/> @@ -54,9 +54,9 @@ 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, - EventInfo, EventInfoJson, jsonToEventInfo} from './lib'; +import {HistDate, HistEvent, queryServer, + EventResponseJson, jsonToHistEvent, EventInfo, EventInfoJson, jsonToEventInfo, + SCALES, stepDate, TimelineState, cmpHistEvent, dateToUnit, DateRangeTree} from './lib'; import {useStore} from './store'; import {RBTree, rbtree_shallow_copy} from './rbtree'; @@ -75,7 +75,7 @@ function updateAreaDims(){ contentWidth.value = contentAreaEl.offsetWidth; contentHeight.value = contentAreaEl.offsetHeight; } -onMounted(updateAreaDims) +onMounted(updateAreaDims); // Timeline data const timelines: Ref<TimelineState[]> = ref([]); @@ -90,9 +90,10 @@ function addTimeline(){ last.endDate, last.startOffset, last.endOffset, last.scaleIdx )); } + timelineTargets.value.push([null, false]); nextTimelineId++; } -addTimeline(); +onMounted(addTimeline); function onTimelineChg(state: TimelineState, idx: number){ timelines.value[idx] = state; } @@ -113,6 +114,7 @@ function onTimelineClose(idx: number){ return; } timelines.value.splice(idx, 1); + timelineTargets.value.splice(idx, 1); } // For storing and looking up events @@ -175,8 +177,11 @@ async function onEventDisplay( timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){ async function handleEvent( timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){ + let timelineIdx = timelines.value.findIndex((s : TimelineState) => s.id == timelineId); + let targetEvent = timelineTargets.value[timelineIdx][0]; // Skip if range has been queried, and enough of its events have been obtained - if (queriedRanges[scaleIdx].contains([firstDate, lastDate])){ + if (queriedRanges[scaleIdx].contains([firstDate, lastDate]) + && (targetEvent == null || idToEvent.has(targetEvent.id))){ // Get number of events in range, server-side let fullCount = 0; let date = firstDate.clone(); @@ -214,17 +219,21 @@ async function onEventDisplay( } } // Get events from server - if (lastQueriedRange != null && lastQueriedRange[0].equals(firstDate) && lastQueriedRange[1].equals(lastDate)){ + if (lastQueriedRange != null && lastQueriedRange[0].equals(firstDate) && lastQueriedRange[1].equals(lastDate) + && (targetEvent == null || idToEvent.has(targetEvent.id))){ console.log(`INFO: Skipping redundant server request from ${firstDate} to ${lastDate}`); return; } - lastQueriedRange = [firstDate, lastDate] + lastQueriedRange = [firstDate, lastDate]; let urlParams = new URLSearchParams({ type: 'events', range: `${firstDate}.${lastDate}`, scale: String(SCALES[scaleIdx]), limit: String(EVENT_REQ_LIMIT), }); + if (targetEvent != null){ + urlParams.append('incl', String(targetEvent.id)); + } let responseObj: EventResponseJson | null = await queryServer(urlParams); if (responseObj == null){ console.log('WARNING: Server gave null response to event query'); @@ -242,6 +251,12 @@ async function onEventDisplay( titleToEvent.set(event.title, event); } } + if (targetEvent != null){ + if (!idToEvent.has(targetEvent.id)){ + console.log(`WARNING: Server response did not include event matching 'incl=${targetEvent.id}'`); + } + timelineTargets.value[timelineIdx][0] = null; + } // Collect unit counts const unitCounts = responseObj.unitCounts; if (unitCounts == null){ @@ -289,9 +304,14 @@ async function onInfoClick(eventTitle: string){ // For search modal const searchOpen = ref(false); +const timelineTargets = ref([] as [HistEvent | null, boolean][]); // For communicating search results to timelines + // A boolean flag is used to trigger jumping even when the same event occurs twice function onSearch(event: HistEvent){ searchOpen.value = false; - console.log(`Need to jump to event "${event.title}" at ${event.start}`); + // Trigger jump in endmost timeline + let timelineIdx = timelineTargets.value.length - 1; + let oldFlag = timelineTargets.value[timelineIdx]; + timelineTargets.value.splice(timelineIdx, 1, [event, !oldFlag[1]]); } // For resize handling diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index e5fbf34..e41a729 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -76,7 +76,7 @@ import CloseIcon from './icon/CloseIcon.vue'; // Other import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MONTH_NAMES, MIN_CAL_DATE, getDaysInMonth, HistDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, - getEventPrecision, dateToUnit, + getEventPrecision, dateToUnit, dateToScaleDate, moduloPositive, TimelineState, HistEvent} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; @@ -95,6 +95,7 @@ const props = defineProps({ initialState: {type: Object as PropType<TimelineState>, required: true}, eventTree: {type: Object as PropType<RBTree<HistEvent>>, required: true}, unitCountMaps: {type: Object as PropType<Map<number, number>[]>, required: true}, + searchTarget: {type: Object as PropType<[null | HistEvent, boolean]>, required: true}, }); const emit = defineEmits(['close', 'state-chg', 'event-display', 'info-click']); @@ -444,6 +445,11 @@ const idToEvent = computed(() => { // Maps visible event IDs to HistEvents } return map; }); +watch(idToEvent, () => { // Remove highlighting of search results that have become out of range + if (searchEvent.value != null && !idToEvent.value.has(searchEvent.value.id)){ + searchEvent.value = null; + } +}); const idToPos = computed(() => { if (!mounted.value){ return new Map(); @@ -489,6 +495,13 @@ const idToPos = computed(() => { let MAX_ANGLE = 30 / 180 * Math.PI; // Max event-line angle difference (radians) from perpendicular-to-mainline let orderedEvents = [...idToEvent.value.values()]; orderedEvents.sort((x, y) => y.pop - x.pop); + if (searchEvent.value != null && idToEvent.value.has(searchEvent.value.id)){ + // Prioritise layout of a searched-for event + let targetIdx = orderedEvents.findIndex((evt: HistEvent) => evt.id == searchEvent.value!.id); + let temp = orderedEvents[0]; + orderedEvents[0] = orderedEvents[targetIdx]; + orderedEvents[targetIdx] = temp; + } let numUnits = getNumDisplayUnits(); const minOffset = store.spacing; const maxOffset = availLen.value - eventMajorSz.value - store.spacing; @@ -1105,6 +1118,45 @@ function onStateChg(){ } watch(firstDate, onStateChg); +// For jumping to search result +const searchEvent = ref(null as null | HistEvent); // Holds most recent search result +watch(() => props.searchTarget, () => { + const event = props.searchTarget[0]; + if (event == null){ + return; + } + if (!idToPos.value.has(event.id)){ // If not already visible + // Determine new time range + let tempScale = scale.value; + let targetDate = event.start; + if (targetDate.isEarlier(MIN_CAL_DATE) && tempScale < 1){ // Account for jumping out of calendar limits + tempScale = getEventPrecision(event); + } + targetDate = dateToScaleDate(targetDate, tempScale); + const startEndDiff = getUnitDiff(startDate.value, endDate.value, scale.value); + let targetStart = stepDate(targetDate, tempScale, {forward: false, count: Math.floor(startEndDiff / 2)}); + if (targetStart.isEarlier(MIN_DATE)){ + targetStart = MIN_DATE; + } + let targetEnd = stepDate(targetStart, tempScale, {count: startEndDiff}); + if (MAX_DATE.isEarlier(targetEnd)){ + if (targetStart != MIN_DATE){ + targetStart = stepDate(targetStart, tempScale, + {forward: false, count: getUnitDiff(targetEnd, MAX_DATE, tempScale)}); + if (targetStart.isEarlier(MIN_DATE)){ + targetStart = MIN_DATE; + } + } + targetEnd = MAX_DATE; + } + // Jump to range + startDate.value = targetStart; + endDate.value = targetEnd; + scaleIdx.value = SCALES.findIndex((s: number) => s == tempScale); + } + searchEvent.value = event; +}); + // For skipping transitions on startup (and on horz/vert swap) const skipTransition = ref(true); onMounted(() => setTimeout(() => {skipTransition.value = false}, 100)); @@ -1163,13 +1215,14 @@ function eventStyles(eventId: number){ } function eventImgStyles(eventId: number){ const event = idToEvent.value.get(eventId)!; + let isSearchResult = searchEvent.value != null && searchEvent.value.id == eventId; return { width: store.eventImgSz + 'px', height: store.eventImgSz + 'px', //backgroundImage: `url(${getImagePath(event.imgId)})`, backgroundColor: 'black', backgroundSize: 'cover', - borderColor: event.ctg == 'discovery' ? store.color.alt2 : store.color.altDark, + borderColor: isSearchResult ? 'red' : (event.ctg == 'discovery' ? store.color.alt2 : store.color.altDark), borderWidth: '1px', }; } @@ -565,6 +565,21 @@ export function dateToUnit(date: HistDate, scale: number): number { } } } +export function dateToScaleDate(date: HistDate, scale: number): HistDate { + // Returns a date representing the unit on 'scale' that 'date' is within + if (scale == DAY_SCALE){ + return new CalDate(date.year, date.month, date.day); + } else if (scale == MONTH_SCALE){ + return new CalDate(date.year, date.month, 1); + } else { + const year = Math.floor(date.year / scale) * scale; + if (year < MIN_CAL_YEAR){ + return new YearDate(year); + } else { + return new CalDate(year == 0 ? 1 : year, 1, 1); + } + } +} // For sending timeline-bound data to BaseLine export class TimelineState { |
