Files
Reservair/assets/js/elements/RsvCalendar.js
T
Martin Slachta 0d829845c4 initial
2026-06-11 19:03:29 +02:00

197 lines
7.1 KiB
JavaScript

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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"/></svg>`;
const ARROW_R = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"/></svg>`;
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;
},
};
})();