diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/BaseLine.vue | 4 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 8 | ||||
| -rw-r--r-- | src/components/SettingsModal.vue | 2 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 71 |
4 files changed, 54 insertions, 31 deletions
diff --git a/src/components/BaseLine.vue b/src/components/BaseLine.vue index 91f5b69..53ab6bd 100644 --- a/src/components/BaseLine.vue +++ b/src/components/BaseLine.vue @@ -7,7 +7,7 @@ <div v-if="props.vert" class="absolute bottom-0 w-full h-6" style="background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1))"></div> </div> - <!-- Timeline spans --> + <!-- Timeline 'spans' --> <TransitionGroup name="fade" v-if="mounted"> <div v-for="(state, idx) in timelines" :key="state.id" class="absolute" :style="spanStyles(idx)"></div> </TransitionGroup> @@ -44,6 +44,7 @@ const periods: Ref<Period[]> = ref([ // ========== For skipping transitions on startup ========== const skipTransition = ref(true); + onMounted(() => setTimeout(() => {skipTransition.value = false}, 100)); // ========== For size and mount-status tracking ========== @@ -68,6 +69,7 @@ const resizeObserver = new ResizeObserver((entries) => { } } }); + onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement)); // ========== For styles ========== diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue index ec1b4cd..addb764 100644 --- a/src/components/SearchModal.vue +++ b/src/components/SearchModal.vue @@ -84,7 +84,7 @@ const focusedSuggIdx = ref(null as null | number); // Index of a suggestion sele const lastReqTime = ref(0); const pendingReqParams = ref(null as null | URLSearchParams); // Holds data for latest request to make -const pendingReqInput = ref(''); // Holds the user input associated with pendingReqData +const pendingReqInput = ref(''); // Holds the user input associated with pendingReqParams const pendingDelayedSuggReq = ref(0); // Set via setTimeout() for making a request despite a previous one still waiting async function onInput(){ @@ -213,6 +213,9 @@ async function resolveSearch(eventTitle: string){ // ========== More event handling ========== +// Focus input on mount +onMounted(() => inputRef.value!.focus()) + function onClose(evt: Event){ if (evt.target == rootRef.value){ emit('close'); @@ -236,9 +239,6 @@ function onInfoIconClick(eventTitle: string){ emit('info-click', eventTitle); } -// Focus input on mount -onMounted(() => inputRef.value!.focus()) - // ========== For styles ========== const styles = computed((): Record<string,string> => { diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index bb1370e..2c4a0df 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -124,11 +124,13 @@ let changedCtg: string | null = null; // Used to defer signalling of a category function onSettingChg(option: string){ store.save(option); + if (option.startsWith('ctgs.')){ changedCtg = option; } else { emit('change', option); } + // Make 'Saved' indicator appear/animate if (!saved.value){ saved.value = true; diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index a040233..6c58ae3 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -4,7 +4,7 @@ @pointercancel="onPointerUp" @pointerout="onPointerUp" @pointerleave="onPointerUp" @wheel.exact="onWheel" @wheel.shift.exact="onShiftWheel"> - <!-- Event count indicators --> + <!-- Event density indicators --> <template v-if="store.showEventCounts"> <div v-for="[tickIdx, count] in tickToCount.entries()" :key="ticks[tickIdx].date.toInt()" :style="countDivStyles(tickIdx, count)" class="absolute animate-fadein"></div> @@ -30,7 +30,7 @@ <!-- Note: Can't use :x2="1" with scaling in :style="", as it makes dashed-lines non-uniform --> </template> - <!-- Main line (unit horizontal line that gets transformed, with extra length to avoid gaps when panning) --> + <!-- Main line (horizontal line that gets transformed, with extra length to avoid gaps when panning) --> <line :stroke="store.color.alt" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/> <!-- Tick markers --> @@ -90,7 +90,7 @@ import CloseIcon from './icon/CloseIcon.vue'; import {WRITING_MODE_HORZ, moduloPositive, animateWithClass, getTextWidth} from '../util'; import { - getDaysInMonth, MIN_CAL_DATE, MONTH_NAMES, HistDate, HistEvent, getImagePath, + getDaysInMonth, MIN_CAL_DATE, MONTH_NAMES, HistDate, HistEvent, getImagePath, dateToYearStr, dateToTickStr, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, getEventPrecision, dateToUnit, dateToScaleDate, TimelineState, @@ -107,14 +107,17 @@ const bgFailRef: Ref<HTMLElement | null> = ref(null); const store = useStore(); const props = defineProps({ - vert: {type: Boolean, required: true}, + vert: {type: Boolean, required: true}, // Display orientation closeable: {type: Boolean, default: true}, + current: {type: Boolean, required: true}, 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}, - current: {type: Boolean, required: true}, + searchTarget: {type: Object as PropType<[null | HistEvent, boolean]>, required: true}, - reset: {type: Boolean, required: true}, + // For triggering a jump to a search result + reset: {type: Boolean, required: true}, // For triggering a bounds reset }); const emit = defineEmits(['close', 'state-chg', 'event-display', 'info-click']); @@ -147,6 +150,7 @@ const resizeObserver = new ResizeObserver((entries) => { } } }); + onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement)); // ========== Computed values used for layout ========== @@ -170,6 +174,9 @@ const mainlineOffset = computed(() => { // Distance from mainline-area line to l // ========== Timeline data ========== +// Note: The visible timeline is divided into 'units', representing time periods on a scale (eg: months, decades). + // If there is space, units of a smaller scale are displayed (called 'minor units', in contrast to 'major units'). + const ID = props.initialState.id as number; const startDate = ref(props.initialState.startDate); // Earliest date in scale to display @@ -200,7 +207,7 @@ const hasMinorScale = computed(() => { // If true, display subset of ticks of ne const minorScaleIdx = computed(() => scaleIdx.value + (hasMinorScale.value ? 1 : 0)); const minorScale = computed(() => SCALES[minorScaleIdx.value]); -// Start/end date/offset initialisation +// Initialise start/end date/offset if (props.initialState.startOffset != null){ startOffset.value = props.initialState.startOffset as number; } @@ -336,6 +343,7 @@ function getMinorTicks(date: HistDate, scaleIdx: number, majorUnitSz: number, ma return minorTicks; } +// Contains the ticks to render, computed from the start/end dates/offsets, the scale, and display area const ticks = computed((): Tick[] => { let ticks: Tick[] = []; if (!mounted.value){ @@ -471,6 +479,7 @@ const lastIdx = computed((): number => { const firstDate = computed(() => firstIdx.value < 0 ? startDate.value : ticks.value[firstIdx.value]!.date); const lastDate = computed(() => lastIdx.value < 0 ? endDate.value : ticks.value[lastIdx.value]!.date); +// True if the first visible tick is at startDate const startIsFirstVisible = computed(() => { if (ticks.value.length == 0){ return true; @@ -490,7 +499,7 @@ const endIsLastVisible = computed(() => { // ========== For displayed events ========== -function dateToOffset(date: HistDate){ // Assumes 'date' is >=firstDate and <=lastDate +function dateToUnitOffset(date: HistDate){ // Assumes 'date' is >=firstDate and <=lastDate // Find containing major tick let tickIdx = firstIdx.value; for (let i = tickIdx + 1; i < lastIdx.value; i++){ @@ -539,11 +548,13 @@ function updateIdToEvent(){ } idToEvent.value = map; } + watch(() => props.eventTree, updateIdToEvent); watch(ticks, updateIdToEvent); const idToPos: Ref<Map<number, [number, number, number, number]>> = ref(new Map()); // Maps event IDs to x/y/w/h const idsToSkipTransition: Ref<Set<number>> = ref(new Set()); // Used to prevent events moving across mainline + type LineCoords = [number, number, number, number]; // x, y, length, angle const eventLines: Ref<Map<number, LineCoords>> = ref(new Map()); // Maps event ID to event line data @@ -610,7 +621,7 @@ function getEventLayout(): Map<number, [number, number, number, number]> { const maxOffset = availLen.value - eventMajorSz.value - store.spacing; for (let event of orderedEvents){ // Get preferred offset in column - let pxOffset = dateToOffset(event.start) / numUnits * availLen.value - eventMajorSz.value / 2; + let pxOffset = dateToUnitOffset(event.start) / numUnits * availLen.value - eventMajorSz.value / 2; let targetOffset = Math.max(Math.min(pxOffset, maxOffset), minOffset); // Find potential positions @@ -775,7 +786,7 @@ function updateLayout(){ // Note: Drawing the line in the reverse direction causes 'detachment' from the mainline during transitions let y2: number; let event = idToEvent.value.get(id)!; - let unitOffset = dateToOffset(event.start); + let unitOffset = dateToUnitOffset(event.start); let posFrac = unitOffset / numUnits; if (props.vert){ x = mainlineOffset.value; @@ -810,11 +821,12 @@ function updateLayout(){ // Notify parent emit('event-display', ID, [...map.keys()], firstDate.value, lastDate.value, minorScaleIdx.value); } + watch(idToEvent, updateLayout); watch(width, updateLayout); watch(height, updateLayout); -// ========== For event-count indicators ========== +// ========== For event density indicators ========== // Maps tick index to event count const tickToCount = computed((): Map<number, number> => { @@ -846,15 +858,15 @@ const timelinePosStr = computed((): string => { const date2 = endIsLastVisible.value ? endDate.value : lastDate.value; if (minorScale.value == DAY_SCALE){ const multiMonth = date1.month != date2.month; - return `${date1.toYearString()} ${MONTH_NAMES[date1.month - 1]}${multiMonth ? ' >' : ''}`; + return `${dateToYearStr(date1)} ${MONTH_NAMES[date1.month - 1]}${multiMonth ? ' >' : ''}`; } else if (minorScale.value == MONTH_SCALE){ const multiYear = date1.year != date2.year; - return `${date1.toYearString()}${multiYear ? ' >' : ''}`; + return `${dateToYearStr(date1)}${multiYear ? ' >' : ''}`; } else { if (date1.year > 0){ - return `${date1.toYearString()} - ${date2.toYearString()}`; + return `${dateToYearStr(date1)} - ${dateToYearStr(date2)}`; } else { - return `${date1.toYearString()} >`; + return `${dateToYearStr(date1)} >`; } } }); @@ -886,7 +898,7 @@ function panTimeline(scrollRatio: number){ } else { // Pan up to an offset of store.defaultEndTickOffset if (store.defaultEndTickOffset == endOffset.value){ - console.log('Reached maximum date limit'); + console.log('INFO: Reached maximum date limit'); animateFailDiv('max'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; @@ -923,7 +935,7 @@ function panTimeline(scrollRatio: number){ } else { // Pan up to an offset of store.defaultEndTickOffset if (store.defaultEndTickOffset == startOffset.value){ - console.log('Reached minimum date limit'); + console.log('INFO: Reached minimum date limit'); animateFailDiv('min'); newStartOffset = startOffset.value; newEndOffset = endOffset.value; @@ -946,7 +958,7 @@ function panTimeline(scrollRatio: number){ } if (newStart.isEarlier(MIN_CAL_DATE, scale.value) && (scale.value == MONTH_SCALE || scale.value == DAY_SCALE)){ - console.log('Unable to pan into dates where months/days are invalid'); + console.log('INFO: Ignored pan into dates where months/days are invalid'); return; } if (!newStart.equals(startDate.value)){ @@ -963,7 +975,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ if (zoomRatio > 1 && startDate.value.equals(MIN_DATE, scale.value) && endDate.value.equals(MAX_DATE, scale.value)){ - console.log('Reached upper scale limit'); + console.log('INFO: Reached upper scale limit'); animateFailDiv('both'); return; } @@ -1003,7 +1015,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ newNumUnits = numUnits; while (numStartSteps < 0){ if (newStart.equals(MIN_CAL_DATE, scale.value) && (scale.value == MONTH_SCALE || scale.value == DAY_SCALE)){ - console.log('Restricting new range to dates where month/day scale is usable'); + console.log('INFO: Restricting new range to dates where month/day scale is usable'); newStartOffset = store.defaultEndTickOffset; break; } @@ -1026,11 +1038,12 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ } newNumUnits += newStartOffset + newEndOffset; } + // Possibly zoom in/out let tickDiff = availLen.value / newNumUnits; if (tickDiff < store.minTickSep){ // Zoom out into new scale if (scaleIdx.value == 0){ - console.log('Reached zoom out limit'); + console.log('INFO: Reached zoom out limit'); animateFailDiv('both'); return; } else { @@ -1096,7 +1109,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ } else { // If trying to zoom in if (scaleIdx.value == SCALES.length - 1){ if (newNumUnits < store.minLastTicks){ - console.log('Reached zoom in limit'); + console.log('INFO: Reached zoom in limit'); animateFailDiv('bg'); return; } @@ -1130,7 +1143,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){ // Account for zooming into sub-year dates before MIN_CAL_DATE if (newStart.isEarlier(MIN_CAL_DATE, newScale) && (newScale == MONTH_SCALE || newScale == DAY_SCALE)){ - console.log('Unable to zoom into range where month/day scale is invalid'); + console.log('INFO: Ignored zooming into range where month/day scale is invalid'); animateFailDiv('bg'); return; } @@ -1291,9 +1304,12 @@ function onStateChg(){ ID, startDate.value, endDate.value, startOffset.value, endOffset.value, scaleIdx.value )); } + watch(startDate, onStateChg); +watch(endDate, onStateChg); // ========== For jumping to search result ========== + const searchEvent = ref(null as null | HistEvent); // Holds most recent search result let pendingSearch = false; @@ -1303,7 +1319,7 @@ watch(() => props.searchTarget, () => { return; } if (MAX_DATE.isEarlier(event.start)){ - console.log('Target is past maximum date'); + console.log('INFO: Ignoring search target past maximum date'); animateFailDiv('max'); return; } @@ -1354,7 +1370,7 @@ watch(idToEvent, () => { } }); -// ========== For resets ========== +// ========== For bound resets ========== watch(() => props.reset, () => { startDate.value = store.initialStartDate; @@ -1392,9 +1408,11 @@ function onKeyDown(evt: KeyboardEvent){ } } } + onMounted(() => { window.addEventListener('keydown', onKeyDown); }); + onUnmounted(() => { window.removeEventListener('keydown', onKeyDown); }); @@ -1434,7 +1452,8 @@ function tickStyles(tick: Tick){ const REF_LABEL = '9999 BC'; // Used as a reference for preventing tick label overlap const refTickLabelWidth = getTextWidth(REF_LABEL, '14px Ubuntu') + 10; -const tickLabelTexts = computed(() => ticks.value.map((tick: Tick) => tick.date.toTickString())); + +const tickLabelTexts = computed(() => ticks.value.map((tick: Tick) => dateToTickStr(tick.date))); const tickLabelStyles = computed((): Record<string,string>[] => { let numMajorUnits = getNumDisplayUnits(); |
