// js/managers/GridManager.js import { eventBus } from '../core/EventBus.js'; import { calendarConfig } from '../core/CalendarConfig.js'; import { EventTypes } from '../types/EventTypes.js'; import { DateUtils } from '../utils/DateUtils.js'; /** * Manages the calendar grid structure */ export class GridManager { constructor() { this.container = null; this.timeAxis = null; this.weekHeader = null; this.timeGrid = null; this.dayColumns = null; this.currentWeek = null; this.init(); } init() { this.findElements(); this.subscribeToEvents(); } findElements() { this.container = document.querySelector('swp-calendar-container'); this.timeAxis = document.querySelector('swp-time-axis'); this.weekHeader = document.querySelector('swp-calendar-header'); this.timeGrid = document.querySelector('swp-time-grid'); this.scrollableContent = document.querySelector('swp-scrollable-content'); } subscribeToEvents() { // Re-render grid on config changes eventBus.on(EventTypes.CONFIG_UPDATE, (e) => { if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays'].includes(e.detail.key)) { this.render(); } }); // Re-render on view change eventBus.on(EventTypes.VIEW_CHANGE, () => { this.render(); }); // Re-render on period change eventBus.on(EventTypes.PERIOD_CHANGE, (e) => { this.currentWeek = e.detail.week; this.renderHeaders(); }); // Handle grid clicks this.setupGridInteractions(); } /** * Render the complete grid structure */ render() { this.renderTimeAxis(); this.renderHeaders(); this.renderGrid(); this.renderGridLines(); // Emit grid rendered event eventBus.emit(EventTypes.GRID_RENDERED); } /** * Render time axis (left side hours) */ renderTimeAxis() { if (!this.timeAxis) return; const startHour = calendarConfig.get('dayStartHour'); const endHour = calendarConfig.get('dayEndHour'); this.timeAxis.innerHTML = ''; for (let hour = startHour; hour <= endHour; hour++) { const marker = document.createElement('swp-hour-marker'); marker.textContent = this.formatHour(hour); marker.dataset.hour = hour; this.timeAxis.appendChild(marker); } } /** * Render week headers */ renderHeaders() { if (!this.weekHeader || !this.currentWeek) return; const view = calendarConfig.get('view'); const weekDays = calendarConfig.get('weekDays'); this.weekHeader.innerHTML = ''; if (view === 'week') { const dates = this.getWeekDates(this.currentWeek); const daysToShow = dates.slice(0, weekDays); daysToShow.forEach((date, index) => { const header = document.createElement('swp-day-header'); header.innerHTML = ` ${this.getDayName(date)} ${date.getDate()} `; header.dataset.date = this.formatDate(date); header.dataset.dayIndex = index; // Mark today if (this.isToday(date)) { header.dataset.today = 'true'; } this.weekHeader.appendChild(header); }); } } /** * Render the main grid structure */ renderGrid() { if (!this.timeGrid) return; // Clear existing columns let dayColumns = this.timeGrid.querySelector('swp-day-columns'); if (!dayColumns) { dayColumns = document.createElement('swp-day-columns'); this.timeGrid.appendChild(dayColumns); } dayColumns.innerHTML = ''; const view = calendarConfig.get('view'); const columnsCount = view === 'week' ? calendarConfig.get('weekDays') : 1; // Create columns for (let i = 0; i < columnsCount; i++) { const column = document.createElement('swp-day-column'); column.dataset.columnIndex = i; if (this.currentWeek) { const dates = this.getWeekDates(this.currentWeek); if (dates[i]) { column.dataset.date = this.formatDate(dates[i]); } } // Add events container const eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); dayColumns.appendChild(column); } this.dayColumns = dayColumns; this.updateGridStyles(); } /** * Render grid lines */ renderGridLines() { let gridLines = this.timeGrid.querySelector('swp-grid-lines'); if (!gridLines) { gridLines = document.createElement('swp-grid-lines'); this.timeGrid.insertBefore(gridLines, this.timeGrid.firstChild); } const totalHours = calendarConfig.totalHours; const hourHeight = calendarConfig.get('hourHeight'); // Set CSS variables this.timeGrid.style.setProperty('--total-hours', totalHours); this.timeGrid.style.setProperty('--hour-height', `${hourHeight}px`); // Grid lines are handled by CSS } /** * Update grid CSS variables */ updateGridStyles() { const root = document.documentElement; const config = calendarConfig.getAll(); // Set CSS variables root.style.setProperty('--hour-height', `${config.hourHeight}px`); root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`); root.style.setProperty('--snap-interval', config.snapInterval); root.style.setProperty('--day-start-hour', config.dayStartHour); root.style.setProperty('--day-end-hour', config.dayEndHour); root.style.setProperty('--work-start-hour', config.workStartHour); root.style.setProperty('--work-end-hour', config.workEndHour); // Set grid height const totalHeight = calendarConfig.totalHours * config.hourHeight; if (this.timeGrid) { this.timeGrid.style.height = `${totalHeight}px`; } } /** * Setup grid interaction handlers */ setupGridInteractions() { if (!this.timeGrid) return; // Click handler this.timeGrid.addEventListener('click', (e) => { // Ignore if clicking on an event if (e.target.closest('swp-event')) return; const column = e.target.closest('swp-day-column'); if (!column) return; const position = this.getClickPosition(e, column); eventBus.emit(EventTypes.GRID_CLICK, { date: column.dataset.date, time: position.time, minutes: position.minutes, columnIndex: parseInt(column.dataset.columnIndex) }); }); // Double click handler this.timeGrid.addEventListener('dblclick', (e) => { // Ignore if clicking on an event if (e.target.closest('swp-event')) return; const column = e.target.closest('swp-day-column'); if (!column) return; const position = this.getClickPosition(e, column); eventBus.emit(EventTypes.GRID_DBLCLICK, { date: column.dataset.date, time: position.time, minutes: position.minutes, columnIndex: parseInt(column.dataset.columnIndex) }); }); } /** * Get click position in grid */ getClickPosition(event, column) { const rect = column.getBoundingClientRect(); const y = event.clientY - rect.top + this.scrollableContent.scrollTop; const minuteHeight = calendarConfig.minuteHeight; const snapInterval = calendarConfig.get('snapInterval'); const dayStartHour = calendarConfig.get('dayStartHour'); // Calculate minutes from start of day let minutes = Math.floor(y / minuteHeight); // Snap to interval minutes = Math.round(minutes / snapInterval) * snapInterval; // Add day start offset const totalMinutes = (dayStartHour * 60) + minutes; return { minutes: totalMinutes, time: this.minutesToTime(totalMinutes), y: minutes * minuteHeight }; } /** * Utility methods */ formatHour(hour) { const period = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); return `${displayHour} ${period}`; } formatDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } getDayName(date) { const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return days[date.getDay()]; } getWeekDates(weekStart) { const dates = []; for (let i = 0; i < 7; i++) { const date = new Date(weekStart); date.setDate(weekStart.getDate() + i); dates.push(date); } return dates; } isToday(date) { const today = new Date(); return date.toDateString() === today.toDateString(); } minutesToTime(totalMinutes) { const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; const period = hours >= 12 ? 'PM' : 'AM'; const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours); return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`; } /** * Scroll to specific hour */ scrollToHour(hour) { if (!this.scrollableContent) return; const hourHeight = calendarConfig.get('hourHeight'); const dayStartHour = calendarConfig.get('dayStartHour'); const scrollTop = (hour - dayStartHour) * hourHeight; this.scrollableContent.scrollTop = scrollTop; } }