Files
apps/calendar/Week/WeekHeaderRow.js
metacryst 0d6c7683ff init
2026-04-28 20:05:00 -05:00

160 lines
6.3 KiB
JavaScript

import calendarUtil from "../calendarUtil.js";
class WeekHeaderRow extends Shadow {
constructor(groupedDays, calendars, onDayTap = null) {
super()
this.groupedDays = groupedDays;
this.calendars = calendars;
this.onDayTap = onDayTap;
}
render() {
const allDayEvents = this.collectAllDayEvents();
const maxEventsPerDay = Math.max(0, ...this.groupedDays.map(g => g.allDay.length))
VStack(() => {
HStack(() => {
this.groupedDays.forEach((group, index) => {
const day = group.day;
const today = calendarUtil.isToday(day);
const isLast = index === this.groupedDays.length - 1;
VStack(() => {
h3(day.getDate())
.margin(0)
.fontSize(1.35, em)
.fontWeight("700")
.lineHeight("1")
.textAlign("center")
p(day.toLocaleDateString("en-US", { weekday: "short" }).toUpperCase())
.margin(0)
.fontSize(0.72, em)
.fontWeight("600")
.letterSpacing(0.04, em)
.opacity(today ? 1 : 0.5)
.textAlign("center")
})
.color(today ? "var(--quillred)" : "var(--headertext)")
.flex(1)
.width(0, px)
.minWidth(0)
.justifyContent("center")
.alignItems("center")
.gap(0.5, em)
.paddingTop(0.85, em)
.background(today ? "var(--desktop-item-background)" : "transparent")
.borderRight(isLast ? "1px solid transparent" : "1px solid var(--divider)")
.boxSizing("border-box")
.paddingBottom(maxEventsPerDay > 0 ? (maxEventsPerDay * 2.0) + 0.7 : 0.35, em)
.cursor("pointer")
.onTap(() => { this.onDayTap(day) })
})
})
.width(100, pct)
.alignItems("stretch")
this.allDayRow(allDayEvents, maxEventsPerDay);
})
.width(100, pct)
.position("relative")
.background("var(--sidebottombars)")
.borderBottom("1px solid var(--divider)")
}
allDayRow(allDayEvents, maxEventsPerDay) {
if (allDayEvents.length === 0) return;
const rowHeight = 1.75;
const gap = 0.25;
const totalHeight = maxEventsPerDay * rowHeight + (maxEventsPerDay - 1) * gap;
ZStack(() => {
allDayEvents.forEach(({ event, startIndex, endIndex, clippedLeft, clippedRight }) => {
this.spanningEvent(event, startIndex, endIndex, rowHeight, gap, clippedLeft, clippedRight);
})
})
.position("absolute")
.bottom(0.25, em)
.left(0, px)
.width(100, pct)
.height(totalHeight, em)
.boxSizing("border-box")
.pointerEvents("none")
}
spanningEvent(event, startIndex, endIndex, rowHeight, gap, clippedLeft, clippedRight) {
const totalDays = this.groupedDays.length;
const spanCount = endIndex - startIndex + 1;
const leftPct = (startIndex / totalDays) * 100;
const widthPct = (spanCount / totalDays) * 100;
const color = calendarUtil.getCalendarColor(this.calendars, event.calendars.find(id => this.calendars.some(c => c.id === id)) ?? event.calendars[0]);
const id = event.id ?? event.title;
const slot = this.groupedDays[startIndex].allDay.findIndex(e => (e.id ?? e.title) === id);
const topEm = slot * (rowHeight + gap);
const borderLeft = clippedLeft ? 0 : 0.25;
const borderRight = clippedRight ? 0 : 0.25;
const leftPad = clippedLeft ? 0 : 1.25;
const rightPad = clippedRight ? 0 : 1.25;
HStack(() => {
p(event.title)
.margin(0)
.fontSize(0.72, em)
.fontWeight("600")
.color("white")
.whiteSpace("nowrap")
.overflow("hidden")
})
.position("absolute")
.top(topEm, em)
.left(leftPct + leftPad, pct)
.width(widthPct - leftPad - rightPad, pct)
.height(rowHeight, em)
.padding(0.35, em)
.background(color)
.borderTopLeftRadius(`${borderLeft}em`)
.borderBottomLeftRadius(`${borderLeft}em`)
.borderTopRightRadius(`${borderRight}em`)
.borderBottomRightRadius(`${borderRight}em`)
.alignItems("center")
.boxSizing("border-box")
.overflow("hidden")
.pointerEvents("auto")
.cursor("pointer")
.onTap(() => $("bottomsheet-").showEvent(event))
}
collectAllDayEvents() {
const seen = new Map();
// Key by id + time_start date: events spanning multiple days share the same time_start
// so they merge into one bar; different occurrences of the same recurring template
// have different time_start dates and render as separate bars.
const eventKey = (event) => {
const d = event.time_start instanceof Date ? event.time_start : new Date(event.time_start);
return `${event.id ?? event.title}_${calendarUtil.toDateInput(d)}`;
};
this.groupedDays.forEach((group, index) => {
group.allDay.forEach(event => {
const key = eventKey(event);
if (!seen.has(key)) {
seen.set(key, { event, startIndex: index, endIndex: index });
} else {
seen.get(key).endIndex = index;
}
});
});
const lastIndex = this.groupedDays.length - 1;
return Array.from(seen.values()).map(entry => ({
...entry,
clippedLeft: entry.startIndex === 0 && calendarUtil.startOfDay(entry.event.time_start) < calendarUtil.startOfDay(this.groupedDays[0].day),
clippedRight: entry.endIndex === lastIndex && calendarUtil.startOfDay(entry.event.time_end) > calendarUtil.startOfDay(this.groupedDays[lastIndex].day)
}));
}
}
register(WeekHeaderRow)