1294a177ae
This work was done with Claude. Added bundling of CSS & JS with WebPack. This also means minimization. --------- Co-authored-by: Martin Slachta <martin.slachta@outlook.com> Reviewed-on: #1
197 lines
7.1 KiB
JavaScript
197 lines
7.1 KiB
JavaScript
export 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;
|
|
},
|
|
};
|
|
})();
|