// Custom date-range picker. The two native (#date-start / // #date-end) stay in the DOM (hidden) as the source of truth — all filter code // keeps reading/writing them. The calendar selects into a temporary state; the // filter is applied (and the popup closed) only when the user presses «Применить». // Both endpoints are chosen by clicking days; «Сбросить» clears the in-progress // selection. const MONTHS = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; const WEEKDAYS = ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс']; function parseISO(iso: string): Date | null { return iso ? new Date(iso + 'T00:00:00') : null; } function toISO(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function fmt(iso: string): string { const p = iso.split('-'); // Short 2-digit year so the full range fits the compact "Период" field. return p.length === 3 ? `${p[2]}.${p[1]}.${p[0].slice(2)}` : iso; } export interface DateRange { /** Refresh the field text (+ open calendar) from the hidden inputs. */ sync(): void; } export function createDateRange(): DateRange { const startInput = document.getElementById('date-start') as HTMLInputElement; const endInput = document.getElementById('date-end') as HTMLInputElement; const field = document.getElementById('dr-field') as HTMLElement; const text = document.getElementById('dr-text') as HTMLElement; const clearBtn = document.getElementById('dr-clear') as HTMLElement; const cal = document.getElementById('dr-cal') as HTMLElement; let viewY = new Date().getFullYear(); let viewM = new Date().getMonth(); // In-progress selection (committed to the inputs only when both ends are set). let selStart = ''; let selEnd = ''; function updateText(): void { const s = startInput.value; const e = endInput.value; if (s && e) text.textContent = `${fmt(s)} — ${fmt(e)}`; else if (s) text.textContent = `с ${fmt(s)}`; else if (e) text.textContent = `по ${fmt(e)}`; else text.textContent = 'Период'; clearBtn.style.display = s || e ? '' : 'none'; field.classList.toggle('has-value', !!(s || e)); } function render(): void { const s = parseISO(selStart); const e = parseISO(selEnd); const parts: string[] = [ `
` + `${MONTHS[viewM]} ${viewY}` + `
`, '
', ]; for (const w of WEEKDAYS) parts.push(`${w}`); const lead = (new Date(viewY, viewM, 1).getDay() + 6) % 7; // Monday-first const days = new Date(viewY, viewM + 1, 0).getDate(); for (let i = 0; i < lead; i++) parts.push(''); for (let d = 1; d <= days; d++) { const cur = new Date(viewY, viewM, d); const iso = toISO(cur); let cls = 'dr-day'; if (iso === selStart) cls += ' s'; if (iso === selEnd) cls += ' e'; if (s && e && cur > s && cur < e) cls += ' rng'; parts.push(``); } parts.push('
'); const ready = !!(selStart && selEnd); parts.push( '
' + `` + `` + '
' ); cal.innerHTML = parts.join(''); } // Write the completed selection to the hidden inputs → triggers the filter reload. function applyRange(): void { startInput.value = selStart; endInput.value = selEnd; startInput.dispatchEvent(new Event('change', { bubbles: true })); endInput.dispatchEvent(new Event('change', { bubbles: true })); updateText(); } function pick(iso: string): void { if (!selStart || (selStart && selEnd)) { // start a fresh selection — first endpoint only selStart = iso; selEnd = ''; } else if (iso >= selStart) { // second endpoint — complete the range, but wait for «Применить» selEnd = iso; } else { // clicked before the start — move the start (still one endpoint) selStart = iso; } render(); } function open(): void { selStart = startInput.value; selEnd = endInput.value; const base = parseISO(selStart) || new Date(); viewY = base.getFullYear(); viewM = base.getMonth(); render(); cal.hidden = false; } function close(): void { cal.hidden = true; } field.addEventListener('click', (ev) => { if ((ev.target as HTMLElement).closest('.dr-clear')) return; if (cal.hidden) open(); else close(); }); field.addEventListener('keydown', (ev) => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); if (cal.hidden) open(); else close(); } }); clearBtn.addEventListener('click', (ev) => { ev.stopPropagation(); selStart = ''; selEnd = ''; startInput.value = ''; endInput.value = ''; startInput.dispatchEvent(new Event('change', { bubbles: true })); endInput.dispatchEvent(new Event('change', { bubbles: true })); updateText(); close(); }); cal.addEventListener('click', (ev) => { // Keep the click inside the calendar so re-rendering a clicked day doesn't // detach the target and trip the outside-click close below. ev.stopPropagation(); const target = ev.target as HTMLElement; const nav = target.closest('.dr-nav'); if (nav) { viewM += Number(nav.dataset.nav); if (viewM < 0) { viewM = 11; viewY--; } else if (viewM > 11) { viewM = 0; viewY++; } render(); return; } const act = target.closest('[data-act]'); if (act) { if (act.dataset.act === 'apply') { if (selStart && selEnd) { applyRange(); close(); } } else if (act.dataset.act === 'reset') { selStart = ''; selEnd = ''; render(); } return; } const day = target.closest('.dr-day'); if (day && day.dataset.iso) pick(day.dataset.iso); }); document.addEventListener('click', (ev) => { if (!(ev.target as HTMLElement).closest('#daterange')) close(); }); updateText(); return { sync(): void { updateText(); if (!cal.hidden) { selStart = startInput.value; selEnd = endInput.value; render(); } }, }; }