class RsvReservationSelector extends HTMLElement { static get observedAttributes() { return ['timetable-id', 'name', 'price-per-block']; } // ---- Attribute accessors ------------------------------------------------ get timetableId() { return parseInt(this.getAttribute('timetable-id')); } get inputName() { return this.getAttribute('name') ?? 'reservation'; } get pricePerBlock() { return parseFloat(this.getAttribute('price-per-block')) || 0; } // ---- Lifecycle ---------------------------------------------------------- connectedCallback() { this._slots = []; this.classList.add('rsv-timetable-selector'); this._build(); } attributeChangedCallback(_attr, oldVal, newVal) { if (oldVal === null || oldVal === newVal || !this.isConnected) return; this._build(); } // ---- Public API --------------------------------------------------------- getValue() { return { timetable_id: this.timetableId, timetable_reservations: this._slots.map(s => s.start_utc), }; } clear() { this.querySelectorAll('.rsv-slots-slot-selected').forEach(s => s.classList.remove('rsv-slots-slot-selected')); this._slots = []; this._commit(); } // ---- Private ------------------------------------------------------------ _build() { this._slots = []; this.replaceChildren(); const tid = document.createElement('input'); tid.type = 'hidden'; tid.name = `${this.inputName}.timetable_id`; tid.value = this.timetableId; this.appendChild(tid); const cal_el = document.createElement('div'); cal_el.classList.add('rsv-calendar'); // Create rsv-timeline with timetable-id set before appending so // connectedCallback sees the correct attribute on first render. const time_el = document.createElement('rsv-timeline'); time_el.setAttribute('timetable-id', this.timetableId); this.append(cal_el, time_el); this._calendar = RsvCalendarPicker.create(cal_el, this.inputName); // Date change: clear selection, then push new date to the timeline element. cal_el.addEventListener('change', () => { this.querySelectorAll('.rsv-slots-slot-selected').forEach(s => s.classList.remove('rsv-slots-slot-selected')); this._slots = []; this._commit(); time_el.date = this._calendar.date; }); // Slot toggle: read selected slots from timeline, then commit. time_el.addEventListener('input', e => { e.stopPropagation(); this._slots = Array.from(time_el.querySelectorAll('.rsv-slots-slot-selected')).map(s => ({ start_utc: s.dataset.start_utc, end_utc: s.dataset.end_utc, })); this._commit(); }); this._commit(); } _commit() { const name = this.inputName; this.querySelectorAll(`input[name="${name}.timetable_reservations[]"]`).forEach(i => i.remove()); let json = []; this._slots.forEach(slot => { const inp = document.createElement('input'); inp.type = 'hidden'; inp.name = `${name}.timetable_reservations[]`; inp.value = slot.start_utc; this.appendChild(inp); json.push(slot.start_utc); }); this.value = JSON.stringify({ "timetable_id": this.timetableId, "timetable_reservations": json }); this.dispatchEvent(new CustomEvent('rsv:slots-changed', { bubbles: true, detail: { name, slots: this._slots, price_per_block: this.pricePerBlock, value: this.getValue(), }, })); } } customElements.define('rsv-reservation-selector', RsvReservationSelector);