fix: month view events list
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+139
-38
@@ -6,11 +6,18 @@ import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
|
|||||||
import type { CalendarInstance } from '@/types/instance';
|
import type { CalendarInstance } from '@/types/instance';
|
||||||
|
|
||||||
const EVENT_HEIGHT = 22; // Height of each event row in pixels
|
const EVENT_HEIGHT = 22; // Height of each event row in pixels
|
||||||
const MAX_VISIBLE_EVENTS = 3; // Maximum event rows to show before "+N more"
|
const DATE_ROW_HEIGHT = 34; // Space reserved at the top of each cell for the date label
|
||||||
|
|
||||||
|
interface DayCell {
|
||||||
|
date: Date;
|
||||||
|
currentMonth: boolean;
|
||||||
|
visibleInstances: CalendarInstance[];
|
||||||
|
hiddenCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface WeekData {
|
interface WeekData {
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
days: { date: Date; currentMonth: boolean }[];
|
days: DayCell[];
|
||||||
multiDaySegments: MultiDaySegment[];
|
multiDaySegments: MultiDaySegment[];
|
||||||
laneCount: number;
|
laneCount: number;
|
||||||
}
|
}
|
||||||
@@ -49,23 +56,22 @@ const monthLabel = computed(() => {
|
|||||||
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track container size for dynamic calculations
|
// Track the calendar body size so each week row shows as many event rows as fit
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const bodyRef = ref<HTMLElement | null>(null);
|
||||||
const containerHeight = ref(600);
|
const bodyHeight = ref(600);
|
||||||
|
let bodyResizeObserver: ResizeObserver | null = null;
|
||||||
function updateContainerHeight() {
|
|
||||||
if (containerRef.value) {
|
|
||||||
containerHeight.value = containerRef.value.clientHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateContainerHeight();
|
bodyResizeObserver = new ResizeObserver(entries => {
|
||||||
window.addEventListener('resize', updateContainerHeight);
|
bodyHeight.value = entries[0].contentRect.height;
|
||||||
|
});
|
||||||
|
if (bodyRef.value) {
|
||||||
|
bodyResizeObserver.observe(bodyRef.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', updateContainerHeight);
|
bodyResizeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate how many weeks are needed for this month
|
// Calculate how many weeks are needed for this month
|
||||||
@@ -82,6 +88,12 @@ const weeksNeeded = computed(() => {
|
|||||||
return Math.ceil((daysBeforeFirst + daysInMonth) / 7);
|
return Math.ceil((daysBeforeFirst + daysInMonth) / 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Event rows that fit in one week row, given the available height
|
||||||
|
const maxEventRows = computed(() => {
|
||||||
|
const weekHeight = bodyHeight.value / weeksNeeded.value;
|
||||||
|
return Math.max(1, Math.floor((weekHeight - DATE_ROW_HEIGHT) / EVENT_HEIGHT));
|
||||||
|
});
|
||||||
|
|
||||||
// Build weeks with multiday event segments
|
// Build weeks with multiday event segments
|
||||||
const weeks = computed<WeekData[]>(() => {
|
const weeks = computed<WeekData[]>(() => {
|
||||||
const year = props.currentDate.getFullYear();
|
const year = props.currentDate.getFullYear();
|
||||||
@@ -100,15 +112,15 @@ const weeks = computed<WeekData[]>(() => {
|
|||||||
const weekEnd = new Date(weekStart);
|
const weekEnd = new Date(weekStart);
|
||||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||||
|
|
||||||
const days: { date: Date; currentMonth: boolean }[] = [];
|
const baseDays: { date: Date; currentMonth: boolean }[] = [];
|
||||||
for (let d = 0; d < 7; d++) {
|
for (let d = 0; d < 7; d++) {
|
||||||
days.push({
|
baseDays.push({
|
||||||
date: new Date(current),
|
date: new Date(current),
|
||||||
currentMonth: current.getMonth() === month,
|
currentMonth: current.getMonth() === month,
|
||||||
});
|
});
|
||||||
current.setDate(current.getDate() + 1);
|
current.setDate(current.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get multiday segments for this week
|
// Get multiday segments for this week
|
||||||
const instances = instancesStore.instancesForRange({
|
const instances = instancesStore.instancesForRange({
|
||||||
startMs: weekStart.getTime(),
|
startMs: weekStart.getTime(),
|
||||||
@@ -125,15 +137,40 @@ const weeks = computed<WeekData[]>(() => {
|
|||||||
7,
|
7,
|
||||||
date => date.getDay(),
|
date => date.getDay(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When lanes overflow, drop the last one(s) so a "+N more" row fits below
|
||||||
|
const maxRows = maxEventRows.value;
|
||||||
|
const visibleLaneCount = laneCount > maxRows ? Math.max(0, maxRows - 1) : laneCount;
|
||||||
|
const visibleSegments = segments.filter(segment => segment.lane < visibleLaneCount);
|
||||||
|
const hiddenSegments = segments.filter(segment => segment.lane >= visibleLaneCount);
|
||||||
|
|
||||||
|
const availableRows = Math.max(0, maxRows - visibleLaneCount);
|
||||||
|
const days = baseDays.map((day, dayIndex) => {
|
||||||
|
const singleDayEvents = getSingleDayEvents(day.date);
|
||||||
|
const hiddenMultiDay = hiddenSegments.filter(
|
||||||
|
segment => dayIndex >= segment.startCol && dayIndex < segment.startCol + segment.span,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (hiddenMultiDay === 0 && singleDayEvents.length <= availableRows) {
|
||||||
|
return { ...day, visibleInstances: singleDayEvents, hiddenCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleInstances = singleDayEvents.slice(0, Math.max(0, availableRows - 1));
|
||||||
|
return {
|
||||||
|
...day,
|
||||||
|
visibleInstances,
|
||||||
|
hiddenCount: hiddenMultiDay + singleDayEvents.length - visibleInstances.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
weeksData.push({
|
weeksData.push({
|
||||||
startDate: weekStart,
|
startDate: weekStart,
|
||||||
days,
|
days,
|
||||||
multiDaySegments: segments,
|
multiDaySegments: visibleSegments,
|
||||||
laneCount: Math.min(laneCount, MAX_VISIBLE_EVENTS),
|
laneCount: visibleLaneCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return weeksData;
|
return weeksData;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,15 +179,36 @@ function getSingleDayEvents(date: Date): CalendarInstance[] {
|
|||||||
return instancesStore.instancesForDay(date).filter(instance => !instance.multiDay);
|
return instancesStore.instancesForDay(date).filter(instance => !instance.multiDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count hidden events (multiday beyond MAX + single day beyond remaining space)
|
// "+N more" popover listing all of a day's events
|
||||||
function getHiddenCount(cell: { date: Date }, weekLaneCount: number): number {
|
const morePopover = ref<{ open: boolean; target: HTMLElement | null; date: Date | null }>({
|
||||||
const singleDayEvents = getSingleDayEvents(cell.date);
|
open: false,
|
||||||
const visibleSingleDay = Math.max(0, MAX_VISIBLE_EVENTS - weekLaneCount);
|
target: null,
|
||||||
const hiddenSingleDay = Math.max(0, singleDayEvents.length - visibleSingleDay);
|
date: null,
|
||||||
|
});
|
||||||
// Also count multiday events in lanes beyond MAX_VISIBLE_EVENTS for this day
|
|
||||||
// (This would need more complex tracking - simplified for now)
|
function showMorePopover(event: MouseEvent, date: Date) {
|
||||||
return hiddenSingleDay;
|
morePopover.value = {
|
||||||
|
open: true,
|
||||||
|
target: event.currentTarget as HTMLElement,
|
||||||
|
date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const morePopoverInstances = computed<CalendarInstance[]>(() => {
|
||||||
|
return morePopover.value.date ? instancesStore.instancesForDay(morePopover.value.date) : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const morePopoverLabel = computed(() => {
|
||||||
|
return morePopover.value.date?.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
}) ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function onPopoverEventClick(instance: CalendarInstance) {
|
||||||
|
morePopover.value.open = false;
|
||||||
|
emit('event-click', instance.entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isToday(date: Date): boolean {
|
function isToday(date: Date): boolean {
|
||||||
@@ -166,7 +224,7 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="month-view" ref="containerRef">
|
<div class="month-view">
|
||||||
<!-- Navigation Controls -->
|
<!-- Navigation Controls -->
|
||||||
<div class="view-controls">
|
<div class="view-controls">
|
||||||
<div class="view-navigation">
|
<div class="view-navigation">
|
||||||
@@ -185,7 +243,7 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Week rows -->
|
<!-- Week rows -->
|
||||||
<div class="calendar-body">
|
<div class="calendar-body" ref="bodyRef">
|
||||||
<div v-for="(week, weekIndex) in weeks" :key="weekIndex" class="calendar-week">
|
<div v-for="(week, weekIndex) in weeks" :key="weekIndex" class="calendar-week">
|
||||||
<!-- Base layer: Day cells grid -->
|
<!-- Base layer: Day cells grid -->
|
||||||
<div class="days-grid">
|
<div class="days-grid">
|
||||||
@@ -208,7 +266,7 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
<!-- Single-day events -->
|
<!-- Single-day events -->
|
||||||
<div class="cell-events">
|
<div class="cell-events">
|
||||||
<div
|
<div
|
||||||
v-for="instance in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
|
v-for="instance in cell.visibleInstances"
|
||||||
:key="instance.key"
|
:key="instance.key"
|
||||||
class="single-day-event"
|
class="single-day-event"
|
||||||
:style="{ backgroundColor: getEventColor(instance) }"
|
:style="{ backgroundColor: getEventColor(instance) }"
|
||||||
@@ -218,12 +276,12 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
>
|
>
|
||||||
{{ instance.label || 'Untitled' }}
|
{{ instance.label || 'Untitled' }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="getHiddenCount(cell, week.laneCount) > 0"
|
v-if="cell.hiddenCount > 0"
|
||||||
class="more-events"
|
class="more-events"
|
||||||
@click.stop="$emit('date-click', cell.date)"
|
@click.stop="showMorePopover($event, cell.date)"
|
||||||
>
|
>
|
||||||
+{{ getHiddenCount(cell, week.laneCount) }} more
|
+{{ cell.hiddenCount }} more
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +301,7 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
backgroundColor: getEventColor(segment.instance),
|
backgroundColor: getEventColor(segment.instance),
|
||||||
left: `calc(${segment.startCol} / 7 * 100% + 4px)`,
|
left: `calc(${segment.startCol} / 7 * 100% + 4px)`,
|
||||||
width: `calc(${segment.span} / 7 * 100% - 8px)`,
|
width: `calc(${segment.span} / 7 * 100% - 8px)`,
|
||||||
top: `${34 + segment.lane * EVENT_HEIGHT}px`,
|
top: `${DATE_ROW_HEIGHT + segment.lane * EVENT_HEIGHT}px`,
|
||||||
}"
|
}"
|
||||||
@click.stop="$emit('event-click', segment.instance.entity)"
|
@click.stop="$emit('event-click', segment.instance.entity)"
|
||||||
@mouseenter="$emit('event-hover', { event: $event, entity: segment.instance.entity })"
|
@mouseenter="$emit('event-hover', { event: $event, entity: segment.instance.entity })"
|
||||||
@@ -256,6 +314,34 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- "+N more" popover: all events for the selected day -->
|
||||||
|
<v-menu
|
||||||
|
v-if="morePopover.target"
|
||||||
|
v-model="morePopover.open"
|
||||||
|
:activator="morePopover.target"
|
||||||
|
location="bottom"
|
||||||
|
:offset="4"
|
||||||
|
>
|
||||||
|
<v-card min-width="220" max-width="320">
|
||||||
|
<div class="more-popover-header">{{ morePopoverLabel }}</div>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="instance in morePopoverInstances"
|
||||||
|
:key="instance.key"
|
||||||
|
@click="onPopoverEventClick(instance)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<span
|
||||||
|
class="more-popover-dot"
|
||||||
|
:style="{ backgroundColor: getEventColor(instance) }"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ instance.label || 'Untitled' }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -481,4 +567,19 @@ function getEventColor(instance: CalendarInstance): string {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "+N more" popover */
|
||||||
|
.more-popover-header {
|
||||||
|
padding: 8px 16px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-popover-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user