feat: Rolling 5-week month view with week-by-week scroll
Month view now shows 5 weeks starting from the week containing currentDate (not fixed to month boundaries), enabling views like "mid-April to mid-May". Prev/Next buttons jump 4 weeks; mouse wheel scrolls 1 week at a time with 500ms debounce.
This commit is contained in:
@@ -93,10 +93,11 @@ function getViewRange() {
|
|||||||
let start, end;
|
let start, end;
|
||||||
|
|
||||||
if (state.currentView === 'month') {
|
if (state.currentView === 'month') {
|
||||||
start = new Date(d.getFullYear(), d.getMonth(), 1);
|
// Rolling view: 5 weeks from the start of currentDate's week
|
||||||
start.setDate(start.getDate() - dayOfWeek(start, weekStartDay) - 1);
|
start = weekStart(d, weekStartDay);
|
||||||
end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
start.setDate(start.getDate() - 1); // 1-day buffer
|
||||||
end.setDate(end.getDate() + (6 - dayOfWeek(end, weekStartDay)) + 1);
|
end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + 37); // 5 weeks + buffer
|
||||||
} else if (state.currentView === 'week') {
|
} else if (state.currentView === 'week') {
|
||||||
start = weekStart(d, weekStartDay);
|
start = weekStart(d, weekStartDay);
|
||||||
end = new Date(start);
|
end = new Date(start);
|
||||||
@@ -165,7 +166,17 @@ function updateTitle() {
|
|||||||
let title = '';
|
let title = '';
|
||||||
const M = t('months');
|
const M = t('months');
|
||||||
if (state.currentView === 'month') {
|
if (state.currentView === 'month') {
|
||||||
title = `${M[d.getMonth()]} ${d.getFullYear()}`;
|
// Show date range of the rolling 5-week window
|
||||||
|
const ws = weekStart(d, weekStartDay);
|
||||||
|
const we = new Date(ws); we.setDate(we.getDate() + 34); // last day of 5th week
|
||||||
|
const Ms = t('months_short');
|
||||||
|
if (ws.getFullYear() !== we.getFullYear()) {
|
||||||
|
title = `${Ms[ws.getMonth()]} ${ws.getFullYear()} – ${Ms[we.getMonth()]} ${we.getFullYear()}`;
|
||||||
|
} else if (ws.getMonth() !== we.getMonth()) {
|
||||||
|
title = `${Ms[ws.getMonth()]} – ${Ms[we.getMonth()]} ${we.getFullYear()}`;
|
||||||
|
} else {
|
||||||
|
title = `${M[ws.getMonth()]} ${ws.getFullYear()}`;
|
||||||
|
}
|
||||||
} else if (state.currentView === 'week') {
|
} else if (state.currentView === 'week') {
|
||||||
const mon = weekStart(d, weekStartDay);
|
const mon = weekStart(d, weekStartDay);
|
||||||
const sun = new Date(mon);
|
const sun = new Date(mon);
|
||||||
@@ -507,7 +518,9 @@ function renderCalendarList() {
|
|||||||
function navigate(dir) {
|
function navigate(dir) {
|
||||||
const d = state.currentDate;
|
const d = state.currentDate;
|
||||||
if (state.currentView === 'month') {
|
if (state.currentView === 'month') {
|
||||||
state.currentDate = new Date(d.getFullYear(), d.getMonth() + dir, 1);
|
// Buttons jump 4 weeks (one screenful)
|
||||||
|
state.currentDate = new Date(d);
|
||||||
|
state.currentDate.setDate(d.getDate() + dir * 28);
|
||||||
} else if (state.currentView === 'week') {
|
} else if (state.currentView === 'week') {
|
||||||
state.currentDate = new Date(d);
|
state.currentDate = new Date(d);
|
||||||
state.currentDate.setDate(d.getDate() + dir * 7);
|
state.currentDate.setDate(d.getDate() + dir * 7);
|
||||||
@@ -551,8 +564,8 @@ function bindTopbar() {
|
|||||||
const dir = e.deltaY > 0 ? 1 : -1;
|
const dir = e.deltaY > 0 ? 1 : -1;
|
||||||
if (state.currentView === 'agenda') return;
|
if (state.currentView === 'agenda') return;
|
||||||
if (state.currentView === 'month') {
|
if (state.currentView === 'month') {
|
||||||
const d = state.currentDate;
|
state.currentDate = new Date(state.currentDate);
|
||||||
state.currentDate = new Date(d.getFullYear(), d.getMonth() + dir, 1);
|
state.currentDate.setDate(state.currentDate.getDate() + dir * 7);
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
} else {
|
} else {
|
||||||
navigate(dir);
|
navigate(dir);
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { isToday, isPast, dayOfWeek, getISOWeekNumber } from '../utils.js';
|
import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../utils.js';
|
||||||
import { t } from '../i18n.js';
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
const LANE_H = 20; // px per lane (event height 18px + 2px gap)
|
const LANE_H = 20; // px per lane (event height 18px + 2px gap)
|
||||||
const MAX_LANES = 3; // max visible lanes per row
|
const MAX_LANES = 3; // max visible lanes per row
|
||||||
|
|
||||||
|
const NUM_ROWS = 5; // rolling view: always 5 weeks
|
||||||
|
|
||||||
export function renderMonth(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
|
export function renderMonth(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
|
||||||
const year = currentDate.getFullYear();
|
// "Primary month" = currentDate's month (used for muting other-month days)
|
||||||
const month = currentDate.getMonth();
|
const primaryMonth = currentDate.getMonth();
|
||||||
const DOW = weekStartDay === 'sunday' ? t('dow_sunday') : t('dow_monday');
|
const DOW = weekStartDay === 'sunday' ? t('dow_sunday') : t('dow_monday');
|
||||||
|
|
||||||
const firstDay = new Date(year, month, 1);
|
// Rolling grid: start at the week that contains currentDate
|
||||||
|
const gridStart = weekStart(currentDate, weekStartDay);
|
||||||
|
|
||||||
// Build 42-cell grid
|
// Build NUM_ROWS × 7 cells
|
||||||
const cells = [];
|
const cells = [];
|
||||||
const gridStart = new Date(firstDay);
|
|
||||||
const offset = dayOfWeek(firstDay, weekStartDay);
|
|
||||||
gridStart.setDate(gridStart.getDate() - offset);
|
|
||||||
const d = new Date(gridStart);
|
const d = new Date(gridStart);
|
||||||
for (let i = 0; i < 42; i++) {
|
for (let i = 0; i < NUM_ROWS * 7; i++) {
|
||||||
cells.push(new Date(d));
|
cells.push(new Date(d));
|
||||||
d.setDate(d.getDate() + 1);
|
d.setDate(d.getDate() + 1);
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
|
|||||||
|
|
||||||
// Build rows
|
// Build rows
|
||||||
let bodyHtml = '';
|
let bodyHtml = '';
|
||||||
for (let row = 0; row < 6; row++) {
|
for (let row = 0; row < NUM_ROWS; row++) {
|
||||||
const rowCells = cells.slice(row * 7, row * 7 + 7);
|
const rowCells = cells.slice(row * 7, row * 7 + 7);
|
||||||
const rowStart = new Date(rowCells[0]); rowStart.setHours(0, 0, 0, 0);
|
const rowStart = new Date(rowCells[0]); rowStart.setHours(0, 0, 0, 0);
|
||||||
const rowEnd = new Date(rowCells[6]); rowEnd.setHours(0, 0, 0, 0);
|
const rowEnd = new Date(rowCells[6]); rowEnd.setHours(0, 0, 0, 0);
|
||||||
@@ -119,7 +119,7 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
|
|||||||
let colsHtml = '';
|
let colsHtml = '';
|
||||||
rowCells.forEach(cell => {
|
rowCells.forEach(cell => {
|
||||||
const key = dateKey(cell);
|
const key = dateKey(cell);
|
||||||
const isOther = cell.getMonth() !== month;
|
const isOther = cell.getMonth() !== primaryMonth;
|
||||||
const todayCls = isToday(cell) ? 'today' : '';
|
const todayCls = isToday(cell) ? 'today' : '';
|
||||||
const otherCls = isOther ? 'other-month' : '';
|
const otherCls = isOther ? 'other-month' : '';
|
||||||
const numCls = isToday(cell) ? 'today' : '';
|
const numCls = isToday(cell) ? 'today' : '';
|
||||||
|
|||||||
Reference in New Issue
Block a user