fix: month view events list
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+134
-33
@@ -6,11 +6,18 @@ import { useChronoInstancesStore } from '@/stores/chronoInstancesStore';
|
||||
import type { CalendarInstance } from '@/types/instance';
|
||||
|
||||
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 {
|
||||
startDate: Date;
|
||||
days: { date: Date; currentMonth: boolean }[];
|
||||
days: DayCell[];
|
||||
multiDaySegments: MultiDaySegment[];
|
||||
laneCount: number;
|
||||
}
|
||||
@@ -49,23 +56,22 @@ const monthLabel = computed(() => {
|
||||
return props.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
});
|
||||
|
||||
// Track container size for dynamic calculations
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const containerHeight = ref(600);
|
||||
|
||||
function updateContainerHeight() {
|
||||
if (containerRef.value) {
|
||||
containerHeight.value = containerRef.value.clientHeight;
|
||||
}
|
||||
}
|
||||
// Track the calendar body size so each week row shows as many event rows as fit
|
||||
const bodyRef = ref<HTMLElement | null>(null);
|
||||
const bodyHeight = ref(600);
|
||||
let bodyResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
updateContainerHeight();
|
||||
window.addEventListener('resize', updateContainerHeight);
|
||||
bodyResizeObserver = new ResizeObserver(entries => {
|
||||
bodyHeight.value = entries[0].contentRect.height;
|
||||
});
|
||||
if (bodyRef.value) {
|
||||
bodyResizeObserver.observe(bodyRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateContainerHeight);
|
||||
bodyResizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
// Calculate how many weeks are needed for this month
|
||||
@@ -82,6 +88,12 @@ const weeksNeeded = computed(() => {
|
||||
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
|
||||
const weeks = computed<WeekData[]>(() => {
|
||||
const year = props.currentDate.getFullYear();
|
||||
@@ -100,9 +112,9 @@ const weeks = computed<WeekData[]>(() => {
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||
|
||||
const days: { date: Date; currentMonth: boolean }[] = [];
|
||||
const baseDays: { date: Date; currentMonth: boolean }[] = [];
|
||||
for (let d = 0; d < 7; d++) {
|
||||
days.push({
|
||||
baseDays.push({
|
||||
date: new Date(current),
|
||||
currentMonth: current.getMonth() === month,
|
||||
});
|
||||
@@ -126,11 +138,36 @@ const weeks = computed<WeekData[]>(() => {
|
||||
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({
|
||||
startDate: weekStart,
|
||||
days,
|
||||
multiDaySegments: segments,
|
||||
laneCount: Math.min(laneCount, MAX_VISIBLE_EVENTS),
|
||||
multiDaySegments: visibleSegments,
|
||||
laneCount: visibleLaneCount,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,15 +179,36 @@ function getSingleDayEvents(date: Date): CalendarInstance[] {
|
||||
return instancesStore.instancesForDay(date).filter(instance => !instance.multiDay);
|
||||
}
|
||||
|
||||
// Count hidden events (multiday beyond MAX + single day beyond remaining space)
|
||||
function getHiddenCount(cell: { date: Date }, weekLaneCount: number): number {
|
||||
const singleDayEvents = getSingleDayEvents(cell.date);
|
||||
const visibleSingleDay = Math.max(0, MAX_VISIBLE_EVENTS - weekLaneCount);
|
||||
const hiddenSingleDay = Math.max(0, singleDayEvents.length - visibleSingleDay);
|
||||
// "+N more" popover listing all of a day's events
|
||||
const morePopover = ref<{ open: boolean; target: HTMLElement | null; date: Date | null }>({
|
||||
open: false,
|
||||
target: null,
|
||||
date: null,
|
||||
});
|
||||
|
||||
// Also count multiday events in lanes beyond MAX_VISIBLE_EVENTS for this day
|
||||
// (This would need more complex tracking - simplified for now)
|
||||
return hiddenSingleDay;
|
||||
function showMorePopover(event: MouseEvent, date: Date) {
|
||||
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 {
|
||||
@@ -166,7 +224,7 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="month-view" ref="containerRef">
|
||||
<div class="month-view">
|
||||
<!-- Navigation Controls -->
|
||||
<div class="view-controls">
|
||||
<div class="view-navigation">
|
||||
@@ -185,7 +243,7 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
</div>
|
||||
|
||||
<!-- Week rows -->
|
||||
<div class="calendar-body">
|
||||
<div class="calendar-body" ref="bodyRef">
|
||||
<div v-for="(week, weekIndex) in weeks" :key="weekIndex" class="calendar-week">
|
||||
<!-- Base layer: Day cells grid -->
|
||||
<div class="days-grid">
|
||||
@@ -208,7 +266,7 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
<!-- Single-day events -->
|
||||
<div class="cell-events">
|
||||
<div
|
||||
v-for="instance in getSingleDayEvents(cell.date).slice(0, MAX_VISIBLE_EVENTS - week.laneCount)"
|
||||
v-for="instance in cell.visibleInstances"
|
||||
:key="instance.key"
|
||||
class="single-day-event"
|
||||
:style="{ backgroundColor: getEventColor(instance) }"
|
||||
@@ -219,11 +277,11 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
{{ instance.label || 'Untitled' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="getHiddenCount(cell, week.laneCount) > 0"
|
||||
v-if="cell.hiddenCount > 0"
|
||||
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>
|
||||
@@ -243,7 +301,7 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
backgroundColor: getEventColor(segment.instance),
|
||||
left: `calc(${segment.startCol} / 7 * 100% + 4px)`,
|
||||
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)"
|
||||
@mouseenter="$emit('event-hover', { event: $event, entity: segment.instance.entity })"
|
||||
@@ -256,6 +314,34 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -481,4 +567,19 @@ function getEventColor(instance: CalendarInstance): string {
|
||||
overflow: hidden;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user