import { addDays, startOfMonth, startOfWeekMonday } from '../services/dateUtils.js'; const DEFAULT_WEEKDAYS = ['pn', 'wt', 'śr', 'cz', 'pt', 'sb', 'nd']; const DEFAULT_MONTHS_LONG = [ 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień', ]; const DEFAULT_THEME = { selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)', selectedText: 'rgb(var(--text-emphasis-rgb))', selectedDot: 'rgb(var(--text-emphasis-rgb))', selectedShadow: '0 0 0 1px rgba(var(--text-emphasis-rgb),0.10)', bg: 'rgb(var(--app-bg-rgb))', border: 'transparent', text: 'rgb(var(--text-body-soft-rgb))', dimmedBg: 'transparent', dimText: 'rgb(var(--text-faint-rgb))', dimOpacity: 0.58, dot: 'rgb(var(--text-faint-rgb))', }; function dateKeyLocal(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } function keyToDate(key) { return new Date(`${key}T00:00:00`); } function normalizeMonth(date) { return startOfMonth(date instanceof Date ? date : new Date()); } function monthLabel(date, monthsLong) { return `${monthsLong[date.getMonth()]} ${date.getFullYear()}`; } function dayRange(aKey, bKey) { const a = keyToDate(aKey); const b = keyToDate(bKey); const [from, to] = a <= b ? [a, b] : [b, a]; const out = []; for (let d = new Date(from); d <= to; d = addDays(d, 1)) out.push(dateKeyLocal(d)); return out; } export function createSwipePopoverCalendarHTML({ idPrefix, weekdays = DEFAULT_WEEKDAYS, monthLabelTextClass = 'text-[10px] font-semibold leading-none tabular-nums whitespace-nowrap', }) { const weekdayHeader = `
${weekdays.map((d) => `
${d}
`).join('')}
`; return `
${weekdayHeader}
${weekdayHeader}
${weekdayHeader}
`; } export function initSwipePopoverCalendar({ idPrefix, selectionMode = 'single', monthsLong = DEFAULT_MONTHS_LONG, theme = DEFAULT_THEME, getMonthAnchor, setMonthAnchor, canNavigateToMonth, getSelectionKeys, onSelectionCommit, resolveDayState, panelHandlePx = null, panelHandleRatio = 0.045, panelHandleMin = 10, panelHandleMax = 16, }) { const monthLabelEl = document.getElementById(`${idPrefix}-month-label`); const gridEl = document.getElementById(`${idPrefix}-grid`); const prevGridEl = document.getElementById(`${idPrefix}-prev-grid`); const nextGridEl = document.getElementById(`${idPrefix}-next-grid`); const track = document.getElementById(`${idPrefix}-track`); const viewport = document.getElementById(`${idPrefix}-viewport`); if (!gridEl || !track || !viewport) { return { render: () => {}, reapplyLayout: () => {}, resetTrackPosition: () => {}, }; } const MOVE_THRESHOLD = 6; const SWIPE_THRESHOLD = 40; const ANIMATION_MS = 260; let panelWidth = 0; let panelInset = 0; let restOffset = 0; let animatingNav = false; let pendingRangeStart = null; let suppressClickUntil = 0; const panels = Array.from(track.querySelectorAll('.swc-panel')); const applyLayout = () => { const vw = viewport.clientWidth || viewport.getBoundingClientRect().width; if (!vw) return; const computedInset = panelHandlePx == null ? Math.round(vw * panelHandleRatio) : panelHandlePx; panelInset = Math.max(panelHandleMin, Math.min(panelHandleMax, computedInset)); panelWidth = vw; restOffset = -panelWidth; panels.forEach((panel) => { panel.style.width = `${panelWidth}px`; panel.style.boxSizing = 'border-box'; panel.style.paddingLeft = `${panelInset}px`; panel.style.paddingRight = `${panelInset}px`; }); track.style.transition = 'none'; track.style.transform = `translate3d(${restOffset}px, 0, 0)`; }; const resetTrackPosition = () => { track.style.transition = 'none'; track.style.transform = `translate3d(${restOffset}px, 0, 0)`; }; const setDragTranslate = (dx, ms) => { track.style.transition = ms ? `transform ${ms}ms ease` : 'none'; track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`; }; const snapBack = () => { setDragTranslate(0, ANIMATION_MS); }; const getNavigationTarget = (monthDelta) => { const anchor = normalizeMonth(getMonthAnchor()); return startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1)); }; const canNavigate = (monthDelta) => { if (typeof canNavigateToMonth !== 'function') return true; const anchor = normalizeMonth(getMonthAnchor()); const target = getNavigationTarget(monthDelta); return canNavigateToMonth(target, { currentMonth: anchor, monthDelta }) !== false; }; const getAllowedDragDx = (dx) => { if (dx === 0) return 0; return canNavigate(dx > 0 ? -1 : 1) ? dx : 0; }; const getSelectedSet = (previewSelection = null) => { const raw = previewSelection ?? getSelectionKeys(); if (Array.isArray(raw)) return new Set(raw); if (typeof raw === 'string' && raw) return new Set([raw]); return new Set(); }; const renderMonthGrid = (targetGrid, monthAnchor, selectedSet) => { if (!targetGrid) return; const first = startOfMonth(monthAnchor); const gridStart = startOfWeekMonday(first); const cells = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i)); targetGrid.innerHTML = cells.map((day) => { const dk = dateKeyLocal(day); const inCurrentMonth = day.getMonth() === first.getMonth(); const isSelected = selectedSet.has(dk); const resolved = (typeof resolveDayState === 'function' ? resolveDayState(day, { inCurrentMonth, isSelected }) : {}) || {}; const disabled = !!resolved.disabled; const dimmed = !!resolved.dimmed; const showDot = !!resolved.showDot; let bg; let borderColor; let text; let shadow = 'none'; let borderClass = 'border-0'; if (dimmed) { bg = theme.dimmedBg ?? DEFAULT_THEME.dimmedBg; borderColor = 'transparent'; text = theme.dimText || DEFAULT_THEME.dimText; } else { bg = theme.bg || DEFAULT_THEME.bg; borderColor = theme.border || DEFAULT_THEME.border; text = theme.text || DEFAULT_THEME.text; } if (isSelected) { borderColor = theme.selectedBorder || DEFAULT_THEME.selectedBorder; text = theme.selectedText || DEFAULT_THEME.selectedText; shadow = theme.selectedShadow || DEFAULT_THEME.selectedShadow; borderClass = 'border'; } const opacity = dimmed && !isSelected ? String(theme.dimOpacity ?? 0.58) : '1'; const dotColor = isSelected ? theme.selectedDot : theme.dot; const tag = disabled ? 'div' : 'button'; const attrs = disabled ? '' : `type="button" data-dk="${dk}"`; const dayClass = disabled ? '' : ' swc-day'; return ` <${tag} ${attrs} class="mx-auto flex h-[2.05rem] w-full min-w-0 max-w-full items-center justify-center rounded-full ${borderClass}${dayClass} text-xs font-medium leading-tight overflow-hidden" style="background:${bg}; border-color:${borderColor}; color:${text}; opacity:${opacity}; box-shadow:${shadow}; touch-action:pan-y;"> ${day.getDate()} ${showDot ? `` : ''} `; }).join(''); }; const render = (previewSelection = null) => { const anchor = normalizeMonth(getMonthAnchor()); const rangePreview = selectionMode === 'range' && pendingRangeStart ? [pendingRangeStart] : null; const selectedSet = getSelectedSet(previewSelection ?? rangePreview); if (monthLabelEl) monthLabelEl.textContent = monthLabel(anchor, monthsLong); renderMonthGrid(gridEl, anchor, selectedSet); renderMonthGrid(prevGridEl, new Date(anchor.getFullYear(), anchor.getMonth() - 1, 1), selectedSet); renderMonthGrid(nextGridEl, new Date(anchor.getFullYear(), anchor.getMonth() + 1, 1), selectedSet); applyLayout(); }; const commitNavigation = (monthDelta) => { if (!canNavigate(monthDelta)) { snapBack(); return; } animatingNav = true; const targetDx = monthDelta < 0 ? panelWidth : -panelWidth; setDragTranslate(targetDx, ANIMATION_MS); setTimeout(() => { setMonthAnchor(getNavigationTarget(monthDelta)); render(); resetTrackPosition(); animatingNav = false; }, ANIMATION_MS); }; if (selectionMode === 'range') { gridEl.addEventListener('click', (e) => { const btn = e.target.closest('.swc-day'); if (!btn) return; e.stopPropagation(); const selectedKey = btn.dataset.dk; if (!pendingRangeStart) { pendingRangeStart = selectedKey; if (typeof onSelectionCommit === 'function') onSelectionCommit([selectedKey]); render([selectedKey]); return; } const range = dayRange(pendingRangeStart, selectedKey); pendingRangeStart = null; if (typeof onSelectionCommit === 'function') onSelectionCommit(range); render(); }); } else { gridEl.addEventListener('click', (e) => { const btn = e.target.closest('.swc-day'); if (!btn) return; e.stopPropagation(); if (typeof onSelectionCommit === 'function') onSelectionCommit(btn.dataset.dk); render(); }); } let ptrId = null; let startX = 0; let startY = 0; let moved = false; let axis = null; let hasPointerCapture = false; viewport.addEventListener('pointerdown', (e) => { if (animatingNav || ptrId !== null) return; if (e.pointerType === 'mouse' && e.button !== 0) return; if (!panelWidth) applyLayout(); ptrId = e.pointerId; startX = e.clientX; startY = e.clientY; moved = false; axis = null; hasPointerCapture = false; }); viewport.addEventListener('pointermove', (e) => { if (e.pointerId !== ptrId || animatingNav) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (!moved && (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)) { moved = true; axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y'; } if (axis === 'x') { e.preventDefault(); suppressClickUntil = Date.now() + 450; if (!hasPointerCapture) { try { viewport.setPointerCapture(e.pointerId); hasPointerCapture = true; } catch (_) {} } setDragTranslate(getAllowedDragDx(dx), 0); } }); const endGesture = (e) => { if (e && e.pointerId !== ptrId) return; if (e && hasPointerCapture) { try { viewport.releasePointerCapture(e.pointerId); } catch (_) {} } hasPointerCapture = false; ptrId = null; if (!moved || axis !== 'x') return; const dx = e ? e.clientX - startX : 0; const monthDelta = dx > 0 ? -1 : 1; if (Math.abs(dx) >= SWIPE_THRESHOLD && canNavigate(monthDelta)) commitNavigation(monthDelta); else { snapBack(); } moved = false; axis = null; }; viewport.addEventListener('click', (e) => { if (Date.now() > suppressClickUntil) return; suppressClickUntil = 0; e.preventDefault(); e.stopPropagation(); }, true); window.addEventListener('pointerup', endGesture); window.addEventListener('pointercancel', endGesture); window.addEventListener('resize', applyLayout); const clearPendingRange = () => { pendingRangeStart = null; render(); }; return { render, reapplyLayout: applyLayout, resetTrackPosition, clearPendingRange }; }