const RsvCalendarPicker = (() => { function get_first_day_of_month(date) { const day = new Date(date.getFullYear(), date.getMonth(), 1).getDay(); return day === 0 ? 6 : day - 1; // Mon=0 … Sun=6 } function is_same_day(a, b) { return a.getUTCFullYear() === b.getUTCFullYear() && a.getUTCMonth() === b.getUTCMonth() && a.getUTCDate() === b.getUTCDate(); } function is_same_month(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth(); } function clear_class(root, cls) { root.querySelectorAll('.' + cls).forEach(el => el.classList.remove(cls)); } function set_cell(cell, date, outside) { cell.classList.toggle('dimm', outside); const iso = date.toISOString(); cell.setAttribute('datetime', iso); cell.children[0].id = iso; cell.children[0].setAttribute('datetime', iso); cell.children[1].textContent = date.getUTCDate(); cell.children[1].setAttribute('for', iso); } function render(state, date) { const year = date.getFullYear(); const month = date.getMonth(); const first = get_first_day_of_month(date); const in_cur = new Date(year, month + 1, 0).getDate(); const in_prev = new Date(year, month, 0).getDate(); const today = new Date(); const rows = state.body.querySelectorAll('tr'); clear_class(state.body, 'rsv-cal-cell-current'); clear_class(state.body, 'rsv-cal-cell-today'); let idx = 0; for (let d = in_prev - first + 1; d <= in_prev; d++, idx++) { const dt = new Date(Date.UTC(year, month - 1, d)); const cell = rows[0].children[idx]; set_cell(cell, dt, true); if (is_same_day(dt, today)) cell.classList.add('rsv-cal-cell-today'); } for (let i = 1; i <= in_cur; i++, idx++) { const dt = new Date(Date.UTC(year, month, i)); const cell = rows[Math.floor(idx / 7)].children[idx % 7]; set_cell(cell, dt, false); if (is_same_day(dt, date)) cell.querySelector('input').checked = true; if (is_same_day(dt, today)) cell.classList.add('rsv-cal-cell-today'); } for (let i = 1; idx < 42; i++, idx++) { const dt = new Date(Date.UTC(year, month + 1, i)); const cell = rows[Math.floor(idx / 7)].children[idx % 7]; set_cell(cell, dt, true); if (is_same_day(dt, today)) cell.classList.add('rsv-cal-cell-today'); } state.month_el.textContent = new Date(year, month).toLocaleString(navigator.language, { month: 'long' }) + ' ' + year; } function day_names() { // Generate short weekday names starting on Monday using the browser locale. return Array.from({ length: 7 }, (_, i) => new Date(2024, 0, 1 + i) // 2024-01-01 is a Monday .toLocaleDateString(navigator.language, { weekday: 'short' }) ); } const ARROW_L = ``; const ARROW_R = ``; function nav_btn(icon, handler) { const btn = document.createElement('button'); btn.type = 'button'; btn.innerHTML = icon; btn.classList.add('rsv-cal-btn-nav'); btn.addEventListener('click', handler); const wrap = document.createElement('div'); wrap.appendChild(btn); return wrap; } function build_header(state) { const month_el = document.createElement('span'); month_el.classList.add('rsv-cal-month'); state.month_el = month_el; const controls = document.createElement('div'); controls.classList.add('rsv-cal-controls'); controls.append( nav_btn(ARROW_L, () => state.set_date(new Date(state.date.getFullYear(), state.date.getMonth() - 1, state.date.getDate()))), month_el, nav_btn(ARROW_R, () => state.set_date(new Date(state.date.getFullYear(), state.date.getMonth() + 1, state.date.getDate()))) ); const ctrl_td = document.createElement('td'); ctrl_td.colSpan = 7; ctrl_td.appendChild(controls); const ctrl_row = document.createElement('tr'); ctrl_row.appendChild(ctrl_td); const names_row = document.createElement('tr'); day_names().forEach(name => { const th = document.createElement('th'); th.textContent = name; names_row.appendChild(th); }); const header = document.createElement('thead'); header.classList.add('rsv-cal-header'); header.append(ctrl_row, names_row); return header; } function build_body(name, on_select) { const tbody = document.createElement('tbody'); tbody.classList.add('rsv-cal-grid'); for (let y = 0; y < 6; y++) { const row = document.createElement('tr'); for (let x = 0; x < 7; x++) { const radio = document.createElement('input'); radio.type = 'radio'; radio.name = name + '.date'; radio.hidden = true; radio.addEventListener('change', on_select); const label = document.createElement('label'); label.setAttribute('unselectable', 'true'); const cell = document.createElement('td'); cell.classList.add('rsv-cal-cell'); cell.append(radio, label); row.appendChild(cell); } tbody.appendChild(row); } return tbody; } return { create(container, name) { const state = { date: null, month_el: null, body: null, container, set_date(date) { if (this.date !== null && is_same_day(date, this.date)) return; const month_changed = this.date === null || !is_same_month(date, this.date); if (month_changed) { const prev = this.body.querySelector('input[type="radio"]:checked'); if (prev) prev.checked = false; render(this, date); const next = this.body.querySelector(`input[id="${date.toISOString()}"]`); if (next) next.checked = true; } const first = get_first_day_of_month(date); const cell_idx = first + date.getDate() - 1; const rows = this.body.querySelectorAll('tr'); rows[Math.floor(cell_idx / 7)].children[cell_idx % 7].querySelector('input').checked = true; this.date = date; this.container.value = date; this.container.dispatchEvent( new InputEvent('change', { bubbles: true, cancelable: true, composed: true }) ); }, }; container.classList.add('rsv-calendar'); const table = document.createElement('table'); table.appendChild(build_header(state)); table.appendChild(build_body(name, e => state.set_date(new Date(e.target.getAttribute('datetime'))))); state.body = table.querySelector('tbody'); container.appendChild(table); state.set_date(new Date()); return state; }, }; })();