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:
parent
7d513600d8
commit
3ddc6352f2
17 changed files with 1347 additions and 428 deletions
84
src/constants/CoreEvents.ts
Normal file
84
src/constants/CoreEvents.ts
Normal 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
|
||||
};
|
||||
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
33
src/types/EventTypes.ts
Normal 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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue