Refactors calendar architecture for month view

Prepares the calendar component for month view implementation
by introducing a strategy pattern for view management,
splitting configuration settings, and consolidating events
into a core set. It also removes dead code and enforces type safety,
improving overall code quality and maintainability.

Addresses critical issues identified in the code review,
laying the groundwork for efficient feature addition.
This commit is contained in:
Janus Knudsen 2025-08-20 19:42:13 +02:00
parent 7d513600d8
commit 3ddc6352f2
17 changed files with 1347 additions and 428 deletions

View file

@ -0,0 +1,84 @@
/**
* CoreEvents - Consolidated essential events for the calendar
* Reduces complexity from 102+ events to ~20 core events
*/
export const CoreEvents = {
// Lifecycle events (3)
INITIALIZED: 'core:initialized',
READY: 'core:ready',
DESTROYED: 'core:destroyed',
// View events (3)
VIEW_CHANGED: 'view:changed',
VIEW_RENDERED: 'view:rendered',
WORKWEEK_CHANGED: 'workweek:changed',
// Navigation events (3)
DATE_CHANGED: 'nav:date-changed',
PERIOD_CHANGED: 'nav:period-changed',
WEEK_CHANGED: 'nav:week-changed',
// Data events (4)
DATA_LOADING: 'data:loading',
DATA_LOADED: 'data:loaded',
DATA_ERROR: 'data:error',
EVENTS_FILTERED: 'data:events-filtered',
// Grid events (3)
GRID_RENDERED: 'grid:rendered',
GRID_CLICKED: 'grid:clicked',
CELL_SELECTED: 'grid:cell-selected',
// Event management (4)
EVENT_CREATED: 'event:created',
EVENT_UPDATED: 'event:updated',
EVENT_DELETED: 'event:deleted',
EVENT_SELECTED: 'event:selected',
// System events (2)
ERROR: 'system:error',
REFRESH_REQUESTED: 'system:refresh'
} as const;
// Type for the event values
export type CoreEventType = typeof CoreEvents[keyof typeof CoreEvents];
/**
* Migration map from old EventTypes to CoreEvents
* This helps transition existing code gradually
*/
export const EVENT_MIGRATION_MAP: Record<string, string> = {
// Lifecycle
'calendar:initialized': CoreEvents.INITIALIZED,
'calendar:ready': CoreEvents.READY,
// View
'calendar:viewchanged': CoreEvents.VIEW_CHANGED,
'calendar:viewrendered': CoreEvents.VIEW_RENDERED,
'calendar:workweekchanged': CoreEvents.WORKWEEK_CHANGED,
// Navigation
'calendar:datechanged': CoreEvents.DATE_CHANGED,
'calendar:periodchange': CoreEvents.PERIOD_CHANGED,
'calendar:weekchanged': CoreEvents.WEEK_CHANGED,
// Data
'calendar:datafetchstart': CoreEvents.DATA_LOADING,
'calendar:datafetchsuccess': CoreEvents.DATA_LOADED,
'calendar:datafetcherror': CoreEvents.DATA_ERROR,
'calendar:eventsloaded': CoreEvents.DATA_LOADED,
// Grid
'calendar:gridrendered': CoreEvents.GRID_RENDERED,
'calendar:gridclick': CoreEvents.GRID_CLICKED,
// Event management
'calendar:eventcreated': CoreEvents.EVENT_CREATED,
'calendar:eventupdated': CoreEvents.EVENT_UPDATED,
'calendar:eventdeleted': CoreEvents.EVENT_DELETED,
'calendar:eventselected': CoreEvents.EVENT_SELECTED,
// System
'calendar:error': CoreEvents.ERROR,
'calendar:refreshrequested': CoreEvents.REFRESH_REQUESTED
};

View file

@ -1,7 +1,6 @@
import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { calendarConfig } from '../core/CalendarConfig';
/**
@ -96,7 +95,7 @@ export class EventManager {
}
private syncEvents(): void {
// Events are now synced via StateEvents.DATA_LOADED during initialization
// Events are synced during initialization
// This method maintained for internal state management only
console.log(`EventManager: Internal sync - ${this.events.length} events in memory`);
}

View file

@ -3,9 +3,9 @@
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { DateCalculator } from '../utils/DateCalculator';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { AllDayEvent } from '../types/EventTypes';
import { GridRenderer } from '../renderers/GridRenderer';
import { GridStyleManager } from '../renderers/GridStyleManager';
@ -25,7 +25,7 @@ export class GridManager {
private container: HTMLElement | null = null;
private grid: HTMLElement | null = null;
private currentWeek: Date | null = null;
private allDayEvents: any[] = []; // Store all-day events for current week
private allDayEvents: AllDayEvent[] = []; // Store all-day events for current week
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
private gridRenderer: GridRenderer;
private styleManager: GridStyleManager;
@ -111,17 +111,6 @@ export class GridManager {
this.updateAllDayEvents(detail.events);
});
// Handle data loaded for resource mode
eventBus.on(StateEvents.DATA_LOADED, (e: Event) => {
const detail = (e as CustomEvent).detail;
console.log(`GridManager: Received DATA_LOADED`);
if (detail.data && detail.data.calendarMode === 'resource') {
// Resource data will be passed in the state event
// For now just update grid styles
this.styleManager.updateGridStyles(this.resourceData);
}
});
// Handle grid clicks
this.setupGridInteractions();
@ -176,7 +165,7 @@ export class GridManager {
/**
* Update all-day events data and re-render if needed
*/
private updateAllDayEvents(events: any[]): void {
private updateAllDayEvents(events: AllDayEvent[]): void {
if (!this.currentWeek) return;
// Filter all-day events for current week

View file

@ -3,7 +3,6 @@
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
/**
* Manages scrolling functionality for the calendar using native scrollbars

View file

@ -10,6 +10,8 @@ import { EventTypes } from '../constants/EventTypes';
export class ViewManager {
private eventBus: IEventBus;
private currentView: CalendarView = 'week';
private eventCleanup: (() => void)[] = [];
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
@ -17,19 +19,26 @@ export class ViewManager {
}
private setupEventListeners(): void {
this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => {
this.initializeView();
});
// Track event bus listeners for cleanup
this.eventCleanup.push(
this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => {
this.initializeView();
})
);
this.eventBus.on(EventTypes.VIEW_CHANGE_REQUESTED, (event: Event) => {
const customEvent = event as CustomEvent;
const { currentView } = customEvent.detail;
this.changeView(currentView);
});
this.eventCleanup.push(
this.eventBus.on(EventTypes.VIEW_CHANGE_REQUESTED, (event: Event) => {
const customEvent = event as CustomEvent;
const { currentView } = customEvent.detail;
this.changeView(currentView);
})
);
this.eventBus.on(EventTypes.DATE_CHANGED, () => {
this.refreshCurrentView();
});
this.eventCleanup.push(
this.eventBus.on(EventTypes.DATE_CHANGED, () => {
this.refreshCurrentView();
})
);
// Setup view button handlers
this.setupViewButtonHandlers();
@ -42,26 +51,30 @@ export class ViewManager {
private setupViewButtonHandlers(): void {
const viewButtons = document.querySelectorAll('swp-view-button[data-view]');
viewButtons.forEach(button => {
button.addEventListener('click', (event) => {
const handler = (event: Event) => {
event.preventDefault();
const view = button.getAttribute('data-view') as CalendarView;
if (view && this.isValidView(view)) {
this.changeView(view);
}
});
};
button.addEventListener('click', handler);
this.buttonListeners.set(button, handler);
});
}
private setupWorkweekButtonHandlers(): void {
const workweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
workweekButtons.forEach(button => {
button.addEventListener('click', (event) => {
const handler = (event: Event) => {
event.preventDefault();
const workweekId = button.getAttribute('data-workweek');
if (workweekId) {
this.changeWorkweek(workweekId);
}
});
};
button.addEventListener('click', handler);
this.buttonListeners.set(button, handler);
});
}
@ -149,6 +162,14 @@ export class ViewManager {
}
public destroy(): void {
// Event listeners bliver automatisk fjernet af EventBus
// Clean up event bus listeners
this.eventCleanup.forEach(cleanup => cleanup());
this.eventCleanup = [];
// Clean up button listeners
this.buttonListeners.forEach((handler, button) => {
button.removeEventListener('click', handler);
});
this.buttonListeners.clear();
}
}

View file

@ -1,7 +1,6 @@
import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from '../managers/EventManager';

View file

@ -3,6 +3,7 @@ import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from './HeaderRenderer';
import { ColumnRenderContext } from './ColumnRenderer';
import { AllDayEvent } from '../types/EventTypes';
/**
* GridRenderer - Handles DOM rendering for the calendar grid
* Separated from GridManager to follow Single Responsibility Principle
@ -21,7 +22,7 @@ export class GridRenderer {
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
allDayEvents: AllDayEvent[]
): void {
console.log('GridRenderer: renderGrid called', {
hasGrid: !!grid,
@ -89,7 +90,7 @@ export class GridRenderer {
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
allDayEvents: AllDayEvent[]
): void {
const gridContainer = document.createElement('swp-grid-container');
@ -124,7 +125,7 @@ export class GridRenderer {
calendarHeader: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
allDayEvents: AllDayEvent[]
): void {
const calendarType = this.config.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
@ -167,7 +168,7 @@ export class GridRenderer {
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
allDayEvents: AllDayEvent[]
): void {
const calendarHeader = grid.querySelector('swp-calendar-header');
if (!calendarHeader) return;

View file

@ -1,170 +0,0 @@
// Calendar state management types
/**
* Calendar initialization and runtime states
* Represents the progression from startup to ready state
*/
export enum CalendarState {
UNINITIALIZED = 'uninitialized',
INITIALIZING = 'initializing',
CONFIG_LOADED = 'config_loaded',
DATA_LOADING = 'data_loading',
DATA_LOADED = 'data_loaded',
RENDERING = 'rendering',
RENDERED = 'rendered',
READY = 'ready',
ERROR = 'error'
}
/**
* State-driven events with clear progression and timing
*/
export const StateEvents = {
// Core lifecycle events
CALENDAR_STATE_CHANGED: 'calendar:state:changed',
// Configuration phase
CONFIG_LOADING_STARTED: 'calendar:config:loading:started',
CONFIG_LOADED: 'calendar:config:loaded',
CONFIG_FAILED: 'calendar:config:failed',
// Data loading phase (can run parallel with rendering setup)
DATA_LOADING_STARTED: 'calendar:data:loading:started',
DATA_LOADED: 'calendar:data:loaded',
DATA_FAILED: 'calendar:data:failed',
// Rendering phase
RENDERING_STARTED: 'calendar:rendering:started',
DOM_STRUCTURE_READY: 'calendar:dom:structure:ready',
GRID_RENDERED: 'calendar:grid:rendered',
EVENTS_RENDERED: 'calendar:events:rendered',
RENDERING_COMPLETE: 'calendar:rendering:complete',
// System ready
CALENDAR_READY: 'calendar:ready',
// Error handling
CALENDAR_ERROR: 'calendar:error',
RECOVERY_ATTEMPTED: 'calendar:recovery:attempted',
RECOVERY_SUCCESS: 'calendar:recovery:success',
RECOVERY_FAILED: 'calendar:recovery:failed',
// User interaction events (unchanged)
VIEW_CHANGE_REQUESTED: 'calendar:view:change:requested',
VIEW_CHANGED: 'calendar:view:changed',
NAVIGATION_REQUESTED: 'calendar:navigation:requested',
} as const;
/**
* Standardized event payload structure
*/
export interface CalendarEvent {
type: string;
component: string;
timestamp: number;
data?: any;
error?: Error;
metadata?: {
duration?: number;
dependencies?: string[];
phase?: string;
retryCount?: number;
};
}
/**
* State change event payload
*/
export interface StateChangeEvent extends CalendarEvent {
type: typeof StateEvents.CALENDAR_STATE_CHANGED;
data: {
from: CalendarState;
to: CalendarState;
transitionValid: boolean;
};
}
/**
* Error event payload
*/
export interface ErrorEvent extends CalendarEvent {
type: typeof StateEvents.CALENDAR_ERROR;
error: Error;
data: {
failedComponent: string;
currentState: CalendarState;
canRecover: boolean;
};
}
/**
* Data loaded event payload
*/
export interface DataLoadedEvent extends CalendarEvent {
type: typeof StateEvents.DATA_LOADED;
data: {
eventCount: number;
calendarMode: 'date' | 'resource';
period: {
start: string;
end: string;
};
};
}
/**
* Grid rendered event payload
*/
export interface GridRenderedEvent extends CalendarEvent {
type: typeof StateEvents.GRID_RENDERED;
data: {
columnCount: number;
rowCount?: number;
gridMode: 'date' | 'resource';
domElementsCreated: string[];
};
}
/**
* Valid state transitions map
* Defines which state transitions are allowed
*/
export const VALID_STATE_TRANSITIONS: Record<CalendarState, CalendarState[]> = {
[CalendarState.UNINITIALIZED]: [CalendarState.INITIALIZING, CalendarState.ERROR],
[CalendarState.INITIALIZING]: [CalendarState.CONFIG_LOADED, CalendarState.ERROR],
[CalendarState.CONFIG_LOADED]: [CalendarState.DATA_LOADING, CalendarState.RENDERING, CalendarState.ERROR],
[CalendarState.DATA_LOADING]: [CalendarState.DATA_LOADED, CalendarState.ERROR],
[CalendarState.DATA_LOADED]: [CalendarState.RENDERING, CalendarState.RENDERED, CalendarState.ERROR],
[CalendarState.RENDERING]: [CalendarState.RENDERED, CalendarState.ERROR],
[CalendarState.RENDERED]: [CalendarState.READY, CalendarState.ERROR],
[CalendarState.READY]: [CalendarState.DATA_LOADING, CalendarState.ERROR], // Allow refresh
[CalendarState.ERROR]: [CalendarState.INITIALIZING, CalendarState.CONFIG_LOADED] // Recovery paths
};
/**
* State phases for logical grouping
*/
export enum InitializationPhase {
STARTUP = 'startup',
CONFIGURATION = 'configuration',
DATA_AND_DOM = 'data-and-dom',
EVENT_RENDERING = 'event-rendering',
FINALIZATION = 'finalization',
ERROR_RECOVERY = 'error-recovery'
}
/**
* Map states to their initialization phases
*/
export const STATE_TO_PHASE: Record<CalendarState, InitializationPhase> = {
[CalendarState.UNINITIALIZED]: InitializationPhase.STARTUP,
[CalendarState.INITIALIZING]: InitializationPhase.STARTUP,
[CalendarState.CONFIG_LOADED]: InitializationPhase.CONFIGURATION,
[CalendarState.DATA_LOADING]: InitializationPhase.DATA_AND_DOM,
[CalendarState.DATA_LOADED]: InitializationPhase.DATA_AND_DOM,
[CalendarState.RENDERING]: InitializationPhase.DATA_AND_DOM,
[CalendarState.RENDERED]: InitializationPhase.EVENT_RENDERING,
[CalendarState.READY]: InitializationPhase.FINALIZATION,
[CalendarState.ERROR]: InitializationPhase.ERROR_RECOVERY
};

33
src/types/EventTypes.ts Normal file
View file

@ -0,0 +1,33 @@
/**
* Type definitions for calendar events
*/
export interface AllDayEvent {
id: string;
title: string;
start: Date | string;
end: Date | string;
allDay: true;
color?: string;
metadata?: {
color?: string;
category?: string;
location?: string;
};
}
export interface TimeEvent {
id: string;
title: string;
start: Date | string;
end: Date | string;
allDay?: false;
color?: string;
metadata?: {
color?: string;
category?: string;
location?: string;
};
}
export type CalendarEventData = AllDayEvent | TimeEvent;

View file

@ -12,12 +12,26 @@ export class DateCalculator {
this.config = config;
}
/**
* Validate that a date is valid
* @param date - Date to validate
* @param methodName - Name of calling method for error messages
* @throws Error if date is invalid
*/
private validateDate(date: Date, methodName: string): void {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
throw new Error(`${methodName}: Invalid date provided - ${date}`);
}
}
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @returns Array of dates for the configured work days
*/
getWorkWeekDates(weekStart: Date): Date[] {
this.validateDate(weekStart, 'getWorkWeekDates');
const dates: Date[] = [];
const workWeekSettings = this.config.getWorkWeekSettings();
@ -42,6 +56,8 @@ export class DateCalculator {
* @returns The Monday of the ISO week
*/
getISOWeekStart(date: Date): Date {
this.validateDate(date, 'getISOWeekStart');
const monday = new Date(date);
const currentDay = monday.getDay();
const daysToSubtract = currentDay === 0 ? 6 : currentDay - 1;
@ -57,6 +73,8 @@ export class DateCalculator {
* @returns The end date of the ISO week (Sunday)
*/
getWeekEnd(date: Date): Date {
this.validateDate(date, 'getWeekEnd');
const weekStart = this.getISOWeekStart(date);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);

View file

@ -1,218 +0,0 @@
/**
* PositionUtils - Utility functions for converting between pixels and minutes/hours in the calendar
* This module provides essential conversion functions for positioning events and calculating dimensions
*/
import { calendarConfig } from '../core/CalendarConfig.js';
export class PositionUtils {
/**
* Convert minutes to pixels based on the current time scale
* @param {number} minutes - Number of minutes to convert
* @returns {number} Pixel value
*/
static minutesToPixels(minutes) {
return minutes * calendarConfig.minuteHeight;
}
/**
* Convert pixels to minutes based on the current time scale
* @param {number} pixels - Number of pixels to convert
* @returns {number} Minutes value
*/
static pixelsToMinutes(pixels) {
return pixels / calendarConfig.minuteHeight;
}
/**
* Convert a time string (HH:MM) to minutes from start of day
* @param {string} timeString - Time in format "HH:MM"
* @returns {number} Minutes from start of day
*/
static timeStringToMinutes(timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
return hours * 60 + minutes;
}
/**
* Convert minutes from start of day to time string (HH:MM)
* @param {number} minutes - Minutes from start of day
* @returns {string} Time in format "HH:MM"
*/
static minutesToTimeString(minutes) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
}
/**
* Calculate the pixel position for a given time
* @param {string|number} time - Time as string "HH:MM" or minutes from start of day
* @returns {number} Pixel position from top of calendar
*/
static getPixelPositionForTime(time) {
const startHour = calendarConfig.get('dayStartHour');
let minutes;
if (typeof time === 'string') {
minutes = this.timeStringToMinutes(time);
} else {
minutes = time;
}
// Subtract start hour offset
const adjustedMinutes = minutes - (startHour * 60);
return this.minutesToPixels(adjustedMinutes);
}
/**
* Calculate the time for a given pixel position
* @param {number} pixelPosition - Pixel position from top of calendar
* @returns {number} Minutes from start of day
*/
static getTimeForPixelPosition(pixelPosition) {
const startHour = calendarConfig.get('dayStartHour');
const minutes = this.pixelsToMinutes(pixelPosition);
// Add start hour offset
return minutes + (startHour * 60);
}
/**
* Calculate event height based on duration
* @param {number} durationMinutes - Duration in minutes
* @returns {number} Height in pixels
*/
static getEventHeight(durationMinutes) {
return this.minutesToPixels(durationMinutes);
}
/**
* Calculate event duration based on height
* @param {number} heightPixels - Height in pixels
* @returns {number} Duration in minutes
*/
static getEventDuration(heightPixels) {
return this.pixelsToMinutes(heightPixels);
}
/**
* Get the pixel position for a specific day column
* @param {number} dayIndex - Day index (0 = Monday, 6 = Sunday)
* @returns {number} Pixel position from left
*/
static getDayColumnPosition(dayIndex) {
// These values should be calculated based on actual calendar layout
const timeAxisWidth = 60; // Default time axis width
const scrollableContent = document.querySelector('swp-scrollable-content');
const dayColumnWidth = scrollableContent ?
(scrollableContent.clientWidth) / calendarConfig.get('weekDays') :
120; // Default day column width
return timeAxisWidth + (dayIndex * dayColumnWidth);
}
/**
* Get the day index for a given pixel position
* @param {number} pixelPosition - Pixel position from left
* @returns {number} Day index (0-6) or -1 if outside day columns
*/
static getDayIndexForPosition(pixelPosition) {
const timeAxisWidth = 60; // Default time axis width
const scrollableContent = document.querySelector('swp-scrollable-content');
const dayColumnWidth = scrollableContent ?
(scrollableContent.clientWidth) / calendarConfig.get('weekDays') :
120; // Default day column width
if (pixelPosition < timeAxisWidth) {
return -1; // In time axis area
}
const dayPosition = pixelPosition - timeAxisWidth;
const dayIndex = Math.floor(dayPosition / dayColumnWidth);
return dayIndex >= 0 && dayIndex < calendarConfig.get('weekDays') ? dayIndex : -1;
}
/**
* Calculate the bounds for an event element
* @param {Object} eventData - Event data with startTime, endTime, and day
* @returns {Object} Bounds object with top, left, width, height
*/
static getEventBounds(eventData) {
const startMinutes = typeof eventData.startTime === 'string'
? this.timeStringToMinutes(eventData.startTime)
: eventData.startTime;
const endMinutes = typeof eventData.endTime === 'string'
? this.timeStringToMinutes(eventData.endTime)
: eventData.endTime;
const duration = endMinutes - startMinutes;
const scrollableContent = document.querySelector('swp-scrollable-content');
const timeAxisWidth = 60; // Default time axis width
const dayColumnWidth = scrollableContent ?
(scrollableContent.clientWidth) / calendarConfig.get('weekDays') :
120; // Default day column width
return {
top: this.getPixelPositionForTime(startMinutes),
left: this.getDayColumnPosition(eventData.day),
width: dayColumnWidth,
height: this.getEventHeight(duration)
};
}
/**
* Check if a pixel position is within the visible time range
* @param {number} pixelPosition - Pixel position from top
* @returns {boolean} True if within visible range
*/
static isWithinVisibleTimeRange(pixelPosition) {
const startHour = calendarConfig.get('dayStartHour');
const endHour = calendarConfig.get('dayEndHour');
const minutes = this.getTimeForPixelPosition(pixelPosition);
const hours = minutes / 60;
return hours >= startHour && hours <= endHour;
}
/**
* Clamp a pixel position to the visible time range
* @param {number} pixelPosition - Pixel position from top
* @returns {number} Clamped pixel position
*/
static clampToVisibleTimeRange(pixelPosition) {
const startHour = calendarConfig.get('dayStartHour');
const endHour = calendarConfig.get('dayEndHour');
const minPosition = this.getPixelPositionForTime(startHour * 60);
const maxPosition = this.getPixelPositionForTime(endHour * 60);
return Math.max(minPosition, Math.min(maxPosition, pixelPosition));
}
/**
* Get the total height of the calendar content area
* @returns {number} Total height in pixels
*/
static getTotalCalendarHeight() {
return calendarConfig.get('hourHeight') * calendarConfig.totalHours;
}
/**
* Round a pixel position to the nearest time interval
* @param {number} pixelPosition - Pixel position to round
* @param {number} intervalMinutes - Interval in minutes (default: 15)
* @returns {number} Rounded pixel position
*/
static roundToTimeInterval(pixelPosition, intervalMinutes = 15) {
const minutes = this.getTimeForPixelPosition(pixelPosition);
const roundedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
return this.getPixelPositionForTime(roundedMinutes);
}
}