init
This commit is contained in:
320
calendar/desktop/DesktopMonthGrid.js
Normal file
320
calendar/desktop/DesktopMonthGrid.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import calendarUtil from "../calendarUtil.js";
|
||||
|
||||
let _saved = null;
|
||||
|
||||
css(`
|
||||
desktopmonthgrid- {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
`)
|
||||
|
||||
class DesktopMonthGrid extends Shadow {
|
||||
constructor(weeks, calendars, weekStartsOn = 0, onEventClick = null, onDayDoubleClick = null) {
|
||||
super()
|
||||
this.weeks = weeks;
|
||||
this.calendars = calendars;
|
||||
this.weekStartsOn = weekStartsOn;
|
||||
this.onEventClick = onEventClick;
|
||||
this.onDayDoubleClick = onDayDoubleClick;
|
||||
this.ghostDay = null;
|
||||
this.maxVisible = 4;
|
||||
|
||||
// Layout in em
|
||||
this.dateFontSize = 0.82;
|
||||
this.dateLineHeight = 1.7;
|
||||
this.paddingTop = 0.35;
|
||||
this.paddingBottom = 0.25;
|
||||
|
||||
this.pillHeight = 1.15;
|
||||
this.pillGap = 0.15;
|
||||
this.overflowLabelHeight = 0.7;
|
||||
this.rowBottomPadding = this.overflowLabelHeight + 0.5;
|
||||
|
||||
this.headerHeight = this.paddingTop + (this.dateFontSize * this.dateLineHeight) + this.paddingBottom;
|
||||
this.rowHeight = this.headerHeight + this.maxVisible * (this.pillHeight + this.pillGap) + this.rowBottomPadding;
|
||||
}
|
||||
|
||||
render() {
|
||||
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
const ordered = Array.from({ length: 7 }, (_, i) => DAY_NAMES[(this.weekStartsOn + i) % 7]);
|
||||
|
||||
VStack(() => {
|
||||
// Day-name header
|
||||
HStack(() => {
|
||||
ordered.forEach(name => {
|
||||
p(name)
|
||||
.margin(0)
|
||||
.fontSize(0.72, em)
|
||||
.fontWeight("500")
|
||||
.letterSpacing("0.015em")
|
||||
.color("var(--headertext)")
|
||||
.opacity(0.5)
|
||||
.flex(1)
|
||||
.textAlign("center")
|
||||
.paddingVertical(0.55, em)
|
||||
.boxSizing("border-box")
|
||||
})
|
||||
})
|
||||
.width(100, pct)
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.flexShrink(0)
|
||||
|
||||
// Week rows
|
||||
VStack(() => {
|
||||
this.weeks.forEach((week, wi) => {
|
||||
this.renderWeekRow(week, wi === this.weeks.length - 1)
|
||||
})
|
||||
})
|
||||
.width(100, pct)
|
||||
.height(this.rowHeight * this.weeks.length, em)
|
||||
.flexShrink(0)
|
||||
})
|
||||
.width(100, pct)
|
||||
.height(100, pct)
|
||||
.overflowY("auto")
|
||||
.boxSizing("border-box")
|
||||
.onAppear(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const monthKey = calendarUtil.toDateInput(this.weeks[2].days[3].day).substring(0, 7);
|
||||
this.scrollTop = (_saved?.monthKey === monthKey) ? _saved.scrollTop : 0;
|
||||
this.addEventListener('scroll', () => {
|
||||
_saved = { monthKey, scrollTop: this.scrollTop };
|
||||
}, { passive: true });
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
renderWeekRow(week, isLastWeek) {
|
||||
const displayWeek = this.ghostDay ? this.injectGhostIntoWeek(week) : week;
|
||||
ZStack(() => {
|
||||
this.renderCellLayer(week, isLastWeek)
|
||||
this.renderPillLayer(displayWeek)
|
||||
})
|
||||
.position("relative")
|
||||
.width(100, pct)
|
||||
.height(this.rowHeight, em)
|
||||
.flexShrink(0)
|
||||
.alignItems("stretch")
|
||||
}
|
||||
|
||||
renderCellLayer(week, isLastWeek) {
|
||||
HStack(() => {
|
||||
week.days.forEach((dayData, di) => {
|
||||
this.renderDayCell(dayData, di === 6, isLastWeek)
|
||||
})
|
||||
})
|
||||
.width(100, pct)
|
||||
.alignItems("stretch")
|
||||
.height(100, pct)
|
||||
}
|
||||
|
||||
renderDayCell(dayData, isLastCol, isLastWeek) {
|
||||
const { day, isCurrentMonth } = dayData;
|
||||
const today = calendarUtil.isToday(day);
|
||||
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
p(day.getDate())
|
||||
.margin(0)
|
||||
.fontSize(this.dateFontSize, em)
|
||||
.fontWeight(today ? "700" : "500")
|
||||
.color(today ? "white" : "var(--headertext)")
|
||||
.background(today ? "var(--quillred)" : "transparent")
|
||||
.width(1.65, em)
|
||||
.height(1.65, em)
|
||||
.borderRadius(5, px)
|
||||
.textAlign("center")
|
||||
.lineHeight("1.65em")
|
||||
.flexShrink(0)
|
||||
.opacity(isCurrentMonth ? 1 : 0.3)
|
||||
})
|
||||
.paddingTop(this.paddingTop, em)
|
||||
.paddingHorizontal(0.4, em)
|
||||
.paddingBottom(this.paddingBottom, em)
|
||||
})
|
||||
.flex(1)
|
||||
.width(0, px)
|
||||
.minWidth(0)
|
||||
.height(100, pct)
|
||||
.background(isCurrentMonth ? "var(--darkaccent)" : "")
|
||||
.borderBottom(isLastWeek ? "1px solid transparent" : "1px solid var(--divider)")
|
||||
.borderRight(isLastCol ? "none" : "1px solid var(--divider)")
|
||||
.boxSizing("border-box")
|
||||
.overflow("hidden")
|
||||
.alignItems("stretch")
|
||||
.onClick((done, e) => {
|
||||
if (e.detail !== 2) return
|
||||
if (!done) {
|
||||
this.ghostDay = day
|
||||
this.rerender()
|
||||
} else {
|
||||
this.onDayDoubleClick(day, () => {
|
||||
this.ghostDay = null
|
||||
this.rerender()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
injectGhostIntoWeek(week) {
|
||||
const col = week.days.findIndex(d =>
|
||||
calendarUtil.startOfDay(d.day).getTime() === calendarUtil.startOfDay(this.ghostDay).getTime()
|
||||
);
|
||||
if (col === -1) return week;
|
||||
|
||||
const ghostEvent = {
|
||||
id: '__ghost__',
|
||||
title: 'New event',
|
||||
all_day: true,
|
||||
time_start: this.ghostDay,
|
||||
time_end: this.ghostDay,
|
||||
calendars: ['__ghost__']
|
||||
};
|
||||
|
||||
const slotMap = week.slotMap.map(colSlots => [...colSlots]);
|
||||
|
||||
let row = 0;
|
||||
while (row < slotMap[col].length && slotMap[col][row] !== null) row++;
|
||||
|
||||
for (let c = 0; c < 7; c++) {
|
||||
while (slotMap[c].length <= row) slotMap[c].push(null);
|
||||
}
|
||||
|
||||
slotMap[col][row] = { event: ghostEvent, isStart: true, isEnd: true, isSingleDay: true };
|
||||
|
||||
const days = week.days.map((d, i) =>
|
||||
i === col ? { ...d, events: [...d.events, ghostEvent] } : d
|
||||
);
|
||||
|
||||
return { ...week, slotMap, days };
|
||||
}
|
||||
|
||||
renderPillLayer(week) {
|
||||
ZStack(() => {
|
||||
const maxSlots = Math.max(0, ...week.slotMap.map(s => s.length));
|
||||
|
||||
for (let row = 0; row < Math.min(maxSlots, this.maxVisible); row++) {
|
||||
this.collectSpans(week, row).forEach(span => this.renderPill(span, week, row))
|
||||
}
|
||||
|
||||
// Overflow labels
|
||||
week.days.forEach((dayData, col) => {
|
||||
const overflow = Math.max(0, dayData.events.length - this.maxVisible);
|
||||
if (overflow === 0) return;
|
||||
|
||||
p(`+${overflow} more`)
|
||||
.margin(0)
|
||||
.fontSize(0.63, em)
|
||||
.fontWeight("600")
|
||||
.color("var(--headertext)")
|
||||
.opacity(0.5)
|
||||
.position("absolute")
|
||||
.bottom(this.overflowLabelHeight, em)
|
||||
.left((col / 7) * 100, pct)
|
||||
.width(100 / 7, pct)
|
||||
.paddingHorizontal(0.55, em)
|
||||
.zIndex(2)
|
||||
})
|
||||
})
|
||||
.position("absolute")
|
||||
.top(0).left(0).right(0).bottom(0)
|
||||
.pointerEvents("none")
|
||||
}
|
||||
|
||||
renderPill({ startCol, endCol, event }, week, row) {
|
||||
const isGhost = event.id === '__ghost__';
|
||||
const color = isGhost ? 'var(--quillred)' : calendarUtil.getCalendarColor(this.calendars, event.calendars.find(id => this.calendars.some(c => c.id === id)) ?? event.calendars[0]);
|
||||
const leftPct = (startCol / 7) * 100;
|
||||
const widthPct = ((endCol - startCol + 1) / 7) * 100;
|
||||
const topEm = this.headerHeight + row * (this.pillHeight + this.pillGap);
|
||||
|
||||
// const isSingleDay = startCol === endCol &&
|
||||
// calendarUtil.startOfDay(event.time_start).getTime() === calendarUtil.startOfDay(event.time_end).getTime();
|
||||
// const isTimedSingle = isSingleDay && !event.all_day;
|
||||
const isTimedSingle = !event.all_day; // wasn't rendering circle on multi-day timed events
|
||||
|
||||
const clippedLeft = startCol === 0 &&
|
||||
calendarUtil.startOfDay(event.time_start) < calendarUtil.startOfDay(week.days[0].day);
|
||||
const clippedRight = endCol === 6 &&
|
||||
calendarUtil.endOfDay(event.time_end) > calendarUtil.endOfDay(week.days[6].day);
|
||||
|
||||
const colW = 100 / 7;
|
||||
const leftInset = clippedLeft ? 0 : 0.02 * colW;
|
||||
const rightInset = clippedRight ? 0 : 0.02 * colW;
|
||||
const brLeft = clippedLeft ? 0 : 4;
|
||||
const brRight = clippedRight ? 0 : 4;
|
||||
|
||||
HStack(() => {
|
||||
if (isTimedSingle) {
|
||||
// Dot + time + title
|
||||
VStack(() => {})
|
||||
.width(0.42, em)
|
||||
.height(0.42, em)
|
||||
.borderRadius(50, pct)
|
||||
.background("white")
|
||||
.flexShrink(0)
|
||||
.marginRight(0.3, em)
|
||||
p(calendarUtil.formatTimeShort(event.time_start) + " " + (event.title || "Untitled"))
|
||||
.margin(0)
|
||||
.fontSize(0.68, em)
|
||||
.fontWeight("500")
|
||||
.color("white")
|
||||
.whiteSpace("nowrap")
|
||||
.overflow("hidden")
|
||||
} else {
|
||||
p(event.title || "Untitled")
|
||||
.margin(0)
|
||||
.fontSize(0.68, em)
|
||||
.fontWeight("600")
|
||||
.color("white")
|
||||
.whiteSpace("nowrap")
|
||||
.overflow("hidden")
|
||||
}
|
||||
})
|
||||
.position("absolute")
|
||||
.top(topEm, em)
|
||||
.left(leftPct + leftInset, pct)
|
||||
.width(widthPct - leftInset - rightInset, pct)
|
||||
.height(this.pillHeight, em)
|
||||
.paddingHorizontal(0.5, em)
|
||||
.background(color)
|
||||
.borderTopLeftRadius(`${brLeft}px`)
|
||||
.borderBottomLeftRadius(`${brLeft}px`)
|
||||
.borderTopRightRadius(`${brRight}px`)
|
||||
.borderBottomRightRadius(`${brRight}px`)
|
||||
.alignItems("center")
|
||||
.overflow("hidden")
|
||||
.boxSizing("border-box")
|
||||
.zIndex(1)
|
||||
.pointerEvents(isGhost ? "none" : "auto")
|
||||
.cursor(isGhost ? "default" : "pointer")
|
||||
.opacity(isGhost ? 0.75 : 1)
|
||||
.onClick((done) => {
|
||||
if (!isGhost && done && this.onEventClick) this.onEventClick(event)
|
||||
})
|
||||
.onHover(function(hovering) {
|
||||
if (!isGhost) this.opacity(hovering ? 0.82 : 1)
|
||||
})
|
||||
}
|
||||
|
||||
collectSpans(week, row) {
|
||||
const spans = [];
|
||||
let current = null;
|
||||
|
||||
for (let col = 0; col < 7; col++) {
|
||||
const slot = (week.slotMap[col] || [])[row] ?? null;
|
||||
|
||||
if (slot && current && slot.event === current.event) {
|
||||
current.endCol = col;
|
||||
} else {
|
||||
if (current) spans.push(current);
|
||||
current = slot ? { startCol: col, endCol: col, event: slot.event } : null;
|
||||
}
|
||||
}
|
||||
if (current) spans.push(current);
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
register(DesktopMonthGrid)
|
||||
Reference in New Issue
Block a user