From 559902e0211a06b349c4c2f50b0882a8d314f8b7 Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Fri, 6 Jan 2023 02:32:59 +1100
Subject: 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.
---
 src/components/TimeLine.vue | 57 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 55 insertions(+), 2 deletions(-)

(limited to 'src/components/TimeLine.vue')

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',
 	};
 }
-- 
cgit v1.2.3

