From 8ec5f52872dc7f24ce49f7e19ff6c158363c68e3 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 3 Nov 2025 21:30:50 +0100 Subject: [PATCH] Adds I-prefix to all interfaces --- .claude/settings.local.json | 4 +- src/configuration/CalendarConfig.ts | 179 +++++ src/configuration/ConfigManager.ts | 55 ++ src/configuration/DateViewSettings.ts | 11 + src/configuration/GridSettings.ts | 16 + src/configuration/ICalendarConfig.ts | 30 + src/configuration/TimeFormatConfig.ts | 10 + src/configuration/WorkWeekSettings.ts | 9 + src/core/CalendarConfig.ts | 436 ----------- src/core/EventBus.ts | 8 +- src/elements/SwpEventElement.ts | 26 +- src/index.ts | 25 +- src/managers/AllDayManager.ts | 58 +- src/managers/CalendarManager.ts | 8 +- src/managers/ConfigManager.ts | 174 ----- src/managers/DragDropManager.ts | 66 +- src/managers/EdgeScrollManager.ts | 458 +++++------ src/managers/EventFilterManager.ts | 10 +- src/managers/EventLayoutCoordinator.ts | 58 +- src/managers/EventManager.ts | 24 +- src/managers/EventStackManager.ts | 34 +- src/managers/HeaderManager.ts | 20 +- src/managers/ResizeHandleManager.ts | 10 +- src/managers/ViewManager.ts | 8 +- src/managers/WorkHoursManager.ts | 48 +- src/renderers/AllDayEventRenderer.ts | 16 +- src/renderers/ColumnRenderer.ts | 14 +- src/renderers/DateHeaderRenderer.ts | 10 +- src/renderers/EventRenderer.ts | 54 +- src/renderers/EventRendererManager.ts | 24 +- src/renderers/GridRenderer.ts | 14 +- src/repositories/IEventRepository.ts | 6 +- src/repositories/MockEventRepository.ts | 8 +- src/types/CalendarTypes.ts | 10 +- src/types/DragDropTypes.ts | 18 +- src/types/EventTypes.ts | 64 +- src/types/ManagerTypes.ts | 42 +- src/utils/AllDayLayoutEngine.ts | 282 +++---- src/utils/ColumnDetectionUtils.ts | 234 +++--- src/utils/DateService.ts | 994 ++++++++++++------------ src/utils/PositionUtils.ts | 10 +- src/utils/TimeFormatter.ts | 6 +- wwwroot/data/calendar-config.json | 87 +++ wwwroot/index.html | 2 +- 44 files changed, 1731 insertions(+), 1949 deletions(-) create mode 100644 src/configuration/CalendarConfig.ts create mode 100644 src/configuration/ConfigManager.ts create mode 100644 src/configuration/DateViewSettings.ts create mode 100644 src/configuration/GridSettings.ts create mode 100644 src/configuration/ICalendarConfig.ts create mode 100644 src/configuration/TimeFormatConfig.ts create mode 100644 src/configuration/WorkWeekSettings.ts delete mode 100644 src/core/CalendarConfig.ts delete mode 100644 src/managers/ConfigManager.ts create mode 100644 wwwroot/data/calendar-config.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3a2fac9..096895c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,9 @@ "Bash(rm:*)", "Bash(npm install:*)", "Bash(npm test)", - "Bash(cat:*)" + "Bash(cat:*)", + "Bash(npm run test:run:*)", + "Bash(npx tsc)" ], "deny": [] } diff --git a/src/configuration/CalendarConfig.ts b/src/configuration/CalendarConfig.ts new file mode 100644 index 0000000..a9c9517 --- /dev/null +++ b/src/configuration/CalendarConfig.ts @@ -0,0 +1,179 @@ +import { ICalendarConfig } from './ICalendarConfig'; +import { IGridSettings } from './GridSettings'; +import { IDateViewSettings } from './DateViewSettings'; +import { ITimeFormatConfig } from './TimeFormatConfig'; +import { IWorkWeekSettings } from './WorkWeekSettings'; + +/** + * All-day event layout constants + */ +export const ALL_DAY_CONSTANTS = { + EVENT_HEIGHT: 22, + EVENT_GAP: 2, + CONTAINER_PADDING: 4, + MAX_COLLAPSED_ROWS: 4, + get SINGLE_ROW_HEIGHT() { + return this.EVENT_HEIGHT + this.EVENT_GAP; // 28px + } +} as const; + +/** + * Work week presets + */ +export const WORK_WEEK_PRESETS: { [key: string]: IWorkWeekSettings } = { + 'standard': { + id: 'standard', + workDays: [1, 2, 3, 4, 5], + totalDays: 5, + firstWorkDay: 1 + }, + 'compressed': { + id: 'compressed', + workDays: [1, 2, 3, 4], + totalDays: 4, + firstWorkDay: 1 + }, + 'midweek': { + id: 'midweek', + workDays: [3, 4, 5], + totalDays: 3, + firstWorkDay: 3 + }, + 'weekend': { + id: 'weekend', + workDays: [6, 7], + totalDays: 2, + firstWorkDay: 6 + }, + 'fullweek': { + id: 'fullweek', + workDays: [1, 2, 3, 4, 5, 6, 7], + totalDays: 7, + firstWorkDay: 1 + } +}; + +/** + * Configuration - DTO container for all configuration + * Pure data object loaded from JSON via ConfigManager + */ +export class Configuration { + private static _instance: Configuration | null = null; + + public config: ICalendarConfig; + public gridSettings: IGridSettings; + public dateViewSettings: IDateViewSettings; + public timeFormatConfig: ITimeFormatConfig; + public currentWorkWeek: string; + public selectedDate: Date; + + constructor( + config: ICalendarConfig, + gridSettings: IGridSettings, + dateViewSettings: IDateViewSettings, + timeFormatConfig: ITimeFormatConfig, + currentWorkWeek: string, + selectedDate: Date = new Date() + ) { + this.config = config; + this.gridSettings = gridSettings; + this.dateViewSettings = dateViewSettings; + this.timeFormatConfig = timeFormatConfig; + this.currentWorkWeek = currentWorkWeek; + this.selectedDate = selectedDate; + + // Store as singleton instance for web components + Configuration._instance = this; + } + + /** + * Get the current Configuration instance + * Used by web components that can't use dependency injection + */ + public static getInstance(): Configuration { + if (!Configuration._instance) { + throw new Error('Configuration has not been initialized. Call ConfigManager.load() first.'); + } + return Configuration._instance; + } + + // Computed properties + get minuteHeight(): number { + return this.gridSettings.hourHeight / 60; + } + + get totalHours(): number { + return this.gridSettings.dayEndHour - this.gridSettings.dayStartHour; + } + + get totalMinutes(): number { + return this.totalHours * 60; + } + + get slotsPerHour(): number { + return 60 / this.gridSettings.snapInterval; + } + + get totalSlots(): number { + return this.totalHours * this.slotsPerHour; + } + + get slotHeight(): number { + return this.gridSettings.hourHeight / this.slotsPerHour; + } + + // Backward compatibility getters + getGridSettings(): IGridSettings { + return this.gridSettings; + } + + getDateViewSettings(): IDateViewSettings { + return this.dateViewSettings; + } + + getWorkWeekSettings(): IWorkWeekSettings { + return WORK_WEEK_PRESETS[this.currentWorkWeek] || WORK_WEEK_PRESETS['standard']; + } + + getCurrentWorkWeek(): string { + return this.currentWorkWeek; + } + + getTimezone(): string { + return this.timeFormatConfig.timezone; + } + + getLocale(): string { + return this.timeFormatConfig.locale; + } + + getTimeFormatSettings(): ITimeFormatConfig { + return this.timeFormatConfig; + } + + is24HourFormat(): boolean { + return this.timeFormatConfig.use24HourFormat; + } + + getDateFormat(): 'locale' | 'technical' { + return this.timeFormatConfig.dateFormat; + } + + setWorkWeek(workWeekId: string): void { + if (WORK_WEEK_PRESETS[workWeekId]) { + this.currentWorkWeek = workWeekId; + this.dateViewSettings.weekDays = WORK_WEEK_PRESETS[workWeekId].totalDays; + } + } + + setSelectedDate(date: Date): void { + this.selectedDate = date; + } + + isValidSnapInterval(interval: number): boolean { + return [5, 10, 15, 30, 60].includes(interval); + } +} + +// Backward compatibility alias +export { Configuration as CalendarConfig }; diff --git a/src/configuration/ConfigManager.ts b/src/configuration/ConfigManager.ts new file mode 100644 index 0000000..517bcb0 --- /dev/null +++ b/src/configuration/ConfigManager.ts @@ -0,0 +1,55 @@ +import { Configuration } from './CalendarConfig'; +import { ICalendarConfig } from './ICalendarConfig'; +import { TimeFormatter } from '../utils/TimeFormatter'; + +/** + * ConfigManager - Static configuration loader + * Loads JSON and creates Configuration instance + */ +export class ConfigManager { + /** + * Load configuration from JSON and create Configuration instance + */ + static async load(): Promise { + const response = await fetch('/wwwroot/data/calendar-config.json'); + if (!response.ok) { + throw new Error(`Failed to load config: ${response.statusText}`); + } + + const data = await response.json(); + + // Build main config + const mainConfig: ICalendarConfig = { + scrollbarWidth: data.scrollbar.width, + scrollbarColor: data.scrollbar.color, + scrollbarTrackColor: data.scrollbar.trackColor, + scrollbarHoverColor: data.scrollbar.hoverColor, + scrollbarBorderRadius: data.scrollbar.borderRadius, + allowDrag: data.interaction.allowDrag, + allowResize: data.interaction.allowResize, + allowCreate: data.interaction.allowCreate, + apiEndpoint: data.api.endpoint, + dateFormat: data.api.dateFormat, + timeFormat: data.api.timeFormat, + enableSearch: data.features.enableSearch, + enableTouch: data.features.enableTouch, + defaultEventDuration: data.eventDefaults.defaultEventDuration, + minEventDuration: data.gridSettings.snapInterval, + maxEventDuration: data.eventDefaults.maxEventDuration + }; + + // Create Configuration instance + const config = new Configuration( + mainConfig, + data.gridSettings, + data.dateViewSettings, + data.timeFormatConfig, + data.currentWorkWeek + ); + + // Configure TimeFormatter + TimeFormatter.configure(config.timeFormatConfig); + + return config; + } +} diff --git a/src/configuration/DateViewSettings.ts b/src/configuration/DateViewSettings.ts new file mode 100644 index 0000000..ae9e1ea --- /dev/null +++ b/src/configuration/DateViewSettings.ts @@ -0,0 +1,11 @@ +import { ViewPeriod } from '../types/CalendarTypes'; + +/** + * View settings for date-based calendar mode + */ +export interface IDateViewSettings { + period: ViewPeriod; + weekDays: number; + firstDayOfWeek: number; + showAllDay: boolean; +} diff --git a/src/configuration/GridSettings.ts b/src/configuration/GridSettings.ts new file mode 100644 index 0000000..283de63 --- /dev/null +++ b/src/configuration/GridSettings.ts @@ -0,0 +1,16 @@ +/** + * Grid display settings interface + */ +export interface IGridSettings { + dayStartHour: number; + dayEndHour: number; + workStartHour: number; + workEndHour: number; + hourHeight: number; + snapInterval: number; + fitToWidth: boolean; + scrollToHour: number | null; + gridStartThresholdMinutes: number; + showCurrentTime: boolean; + showWorkHours: boolean; +} diff --git a/src/configuration/ICalendarConfig.ts b/src/configuration/ICalendarConfig.ts new file mode 100644 index 0000000..aa291e4 --- /dev/null +++ b/src/configuration/ICalendarConfig.ts @@ -0,0 +1,30 @@ +/** + * Main calendar configuration interface + */ +export interface ICalendarConfig { + // Scrollbar styling + scrollbarWidth: number; + scrollbarColor: string; + scrollbarTrackColor: string; + scrollbarHoverColor: string; + scrollbarBorderRadius: number; + + // Interaction settings + allowDrag: boolean; + allowResize: boolean; + allowCreate: boolean; + + // API settings + apiEndpoint: string; + dateFormat: string; + timeFormat: string; + + // Feature flags + enableSearch: boolean; + enableTouch: boolean; + + // Event defaults + defaultEventDuration: number; + minEventDuration: number; + maxEventDuration: number; +} diff --git a/src/configuration/TimeFormatConfig.ts b/src/configuration/TimeFormatConfig.ts new file mode 100644 index 0000000..2bb9207 --- /dev/null +++ b/src/configuration/TimeFormatConfig.ts @@ -0,0 +1,10 @@ +/** + * Time format configuration settings + */ +export interface ITimeFormatConfig { + timezone: string; + use24HourFormat: boolean; + locale: string; + dateFormat: 'locale' | 'technical'; + showSeconds: boolean; +} diff --git a/src/configuration/WorkWeekSettings.ts b/src/configuration/WorkWeekSettings.ts new file mode 100644 index 0000000..7c01b99 --- /dev/null +++ b/src/configuration/WorkWeekSettings.ts @@ -0,0 +1,9 @@ +/** + * Work week configuration settings + */ +export interface IWorkWeekSettings { + id: string; + workDays: number[]; + totalDays: number; + firstWorkDay: number; +} diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts deleted file mode 100644 index 309e0c2..0000000 --- a/src/core/CalendarConfig.ts +++ /dev/null @@ -1,436 +0,0 @@ -// Calendar configuration management -// Pure static configuration class - no dependencies, no events - -import { ICalendarConfig, ViewPeriod } from '../types/CalendarTypes'; -import { TimeFormatter, TimeFormatSettings } from '../utils/TimeFormatter'; - -/** - * All-day event layout constants - */ -export const ALL_DAY_CONSTANTS = { - EVENT_HEIGHT: 22, // Height of single all-day event - EVENT_GAP: 2, // Gap between stacked events - CONTAINER_PADDING: 4, // Container padding (top + bottom) - MAX_COLLAPSED_ROWS: 4, // Show 4 rows when collapsed (3 events + 1 indicator row) - get SINGLE_ROW_HEIGHT() { - return this.EVENT_HEIGHT + this.EVENT_GAP; // 28px - } -} as const; - -/** - * Layout and timing settings for the calendar grid - */ -interface GridSettings { - // Time boundaries - dayStartHour: number; - dayEndHour: number; - workStartHour: number; - workEndHour: number; - - // Layout settings - hourHeight: number; - snapInterval: number; - fitToWidth: boolean; - scrollToHour: number | null; - - // Event grouping settings - gridStartThresholdMinutes: number; // ±N minutes for events to share grid columns - - // Display options - showCurrentTime: boolean; - showWorkHours: boolean; -} - -/** - * View settings for date-based calendar mode - */ -interface DateViewSettings { - period: ViewPeriod; // day/week/month - weekDays: number; // Number of days to show in week view - firstDayOfWeek: number; // 0=Sunday, 1=Monday - showAllDay: boolean; // Show all-day event row -} - -/** - * Work week configuration settings - */ -interface WorkWeekSettings { - id: string; - workDays: number[]; // ISO 8601: [1,2,3,4,5] for mon-fri (1=Mon, 7=Sun) - totalDays: number; // 5 - firstWorkDay: number; // ISO: 1 = Monday, 7 = Sunday -} - -/** - * Time format configuration settings - */ -interface TimeFormatConfig { - timezone: string; - use24HourFormat: boolean; - locale: string; - dateFormat: 'locale' | 'technical'; - showSeconds: boolean; -} - -/** - * Calendar configuration management - Pure static config - */ -export class CalendarConfig { - private static config: ICalendarConfig = { - // Scrollbar styling - scrollbarWidth: 16, // Width of scrollbar in pixels - scrollbarColor: '#666', // Scrollbar thumb color - scrollbarTrackColor: '#f0f0f0', // Scrollbar track color - scrollbarHoverColor: '#b53f7aff', // Scrollbar thumb hover color - scrollbarBorderRadius: 6, // Border radius for scrollbar thumb - - // Interaction settings - allowDrag: true, - allowResize: true, - allowCreate: true, - - // API settings - apiEndpoint: '/api/events', - dateFormat: 'YYYY-MM-DD', - timeFormat: 'HH:mm', - - // Feature flags - enableSearch: true, - enableTouch: true, - - // Event defaults - defaultEventDuration: 60, // Minutes - minEventDuration: 15, // Will be same as snapInterval - maxEventDuration: 480 // 8 hours - }; - - private static selectedDate: Date | null = new Date(); - private static currentWorkWeek: string = 'standard'; - - // Grid display settings - private static gridSettings: GridSettings = { - hourHeight: 60, - dayStartHour: 0, - dayEndHour: 24, - workStartHour: 8, - workEndHour: 17, - snapInterval: 15, - gridStartThresholdMinutes: 30, // Events starting within ±15 min share grid columns - showCurrentTime: true, - showWorkHours: true, - fitToWidth: false, - scrollToHour: 8 - }; - - // Date view settings - private static dateViewSettings: DateViewSettings = { - period: 'week', - weekDays: 7, - firstDayOfWeek: 1, - showAllDay: true - }; - - // Time format settings - default to Denmark with technical format - private static timeFormatConfig: TimeFormatConfig = { - timezone: 'Europe/Copenhagen', - use24HourFormat: true, - locale: 'da-DK', - dateFormat: 'technical', - showSeconds: false - }; - - /** - * Initialize configuration - called once at startup - */ - static initialize(): void { - // Set computed values - CalendarConfig.config.minEventDuration = CalendarConfig.gridSettings.snapInterval; - - // Initialize TimeFormatter with default settings - TimeFormatter.configure(CalendarConfig.timeFormatConfig); - - // Load from data attributes - CalendarConfig.loadFromDOM(); - } - - - /** - * Load configuration from DOM data attributes - */ - private static loadFromDOM(): void { - const calendar = document.querySelector('swp-calendar') as HTMLElement; - if (!calendar) return; - - // Read data attributes - const attrs = calendar.dataset; - - // Update date view settings - if (attrs.view) CalendarConfig.dateViewSettings.period = attrs.view as ViewPeriod; - if (attrs.weekDays) CalendarConfig.dateViewSettings.weekDays = parseInt(attrs.weekDays); - - // Update grid settings - if (attrs.snapInterval) CalendarConfig.gridSettings.snapInterval = parseInt(attrs.snapInterval); - if (attrs.dayStartHour) CalendarConfig.gridSettings.dayStartHour = parseInt(attrs.dayStartHour); - if (attrs.dayEndHour) CalendarConfig.gridSettings.dayEndHour = parseInt(attrs.dayEndHour); - if (attrs.hourHeight) CalendarConfig.gridSettings.hourHeight = parseInt(attrs.hourHeight); - if (attrs.fitToWidth !== undefined) CalendarConfig.gridSettings.fitToWidth = attrs.fitToWidth === 'true'; - - // Update computed values - CalendarConfig.config.minEventDuration = CalendarConfig.gridSettings.snapInterval; - } - - /** - * Get a config value - */ - static get(key: K): ICalendarConfig[K] { - return CalendarConfig.config[key]; - } - - /** - * Set a config value (no events - use ConfigManager for updates with events) - */ - static set(key: K, value: ICalendarConfig[K]): void { - CalendarConfig.config[key] = value; - } - - /** - * Update multiple config values (no events - use ConfigManager for updates with events) - */ - static update(updates: Partial): void { - Object.entries(updates).forEach(([key, value]) => { - CalendarConfig.set(key as keyof ICalendarConfig, value); - }); - } - - /** - * Get all config - */ - static getAll(): ICalendarConfig { - return { ...CalendarConfig.config }; - } - - /** - * Calculate derived values - */ - - static get minuteHeight(): number { - return CalendarConfig.gridSettings.hourHeight / 60; - } - - static get totalHours(): number { - return CalendarConfig.gridSettings.dayEndHour - CalendarConfig.gridSettings.dayStartHour; - } - - static get totalMinutes(): number { - return CalendarConfig.totalHours * 60; - } - - static get slotsPerHour(): number { - return 60 / CalendarConfig.gridSettings.snapInterval; - } - - static get totalSlots(): number { - return CalendarConfig.totalHours * CalendarConfig.slotsPerHour; - } - - static get slotHeight(): number { - return CalendarConfig.gridSettings.hourHeight / CalendarConfig.slotsPerHour; - } - - /** - * Validate snap interval - */ - static isValidSnapInterval(interval: number): boolean { - return [5, 10, 15, 30, 60].includes(interval); - } - - /** - * Get grid display settings - */ - static getGridSettings(): GridSettings { - return { ...CalendarConfig.gridSettings }; - } - - /** - * Update grid display settings (no events - use ConfigManager for updates with events) - */ - static updateGridSettings(updates: Partial): void { - CalendarConfig.gridSettings = { ...CalendarConfig.gridSettings, ...updates }; - - // Update computed values - if (updates.snapInterval) { - CalendarConfig.config.minEventDuration = updates.snapInterval; - } - } - - /** - * Get date view settings - */ - static getDateViewSettings(): DateViewSettings { - return { ...CalendarConfig.dateViewSettings }; - } - - /** - * Get selected date - */ - static getSelectedDate(): Date | null { - return CalendarConfig.selectedDate; - } - - /** - * Set selected date - * Note: Does not emit events - caller is responsible for event emission - */ - static setSelectedDate(date: Date): void { - CalendarConfig.selectedDate = date; - } - - /** - * Get work week presets - */ - private static getWorkWeekPresets(): { [key: string]: WorkWeekSettings } { - return { - 'standard': { - id: 'standard', - workDays: [1,2,3,4,5], // Monday-Friday (ISO) - totalDays: 5, - firstWorkDay: 1 - }, - 'compressed': { - id: 'compressed', - workDays: [1,2,3,4], // Monday-Thursday (ISO) - totalDays: 4, - firstWorkDay: 1 - }, - 'midweek': { - id: 'midweek', - workDays: [3,4,5], // Wednesday-Friday (ISO) - totalDays: 3, - firstWorkDay: 3 - }, - 'weekend': { - id: 'weekend', - workDays: [6,7], // Saturday-Sunday (ISO) - totalDays: 2, - firstWorkDay: 6 - }, - 'fullweek': { - id: 'fullweek', - workDays: [1,2,3,4,5,6,7], // Monday-Sunday (ISO) - totalDays: 7, - firstWorkDay: 1 - } - }; - } - - /** - * Get current work week settings - */ - static getWorkWeekSettings(): WorkWeekSettings { - const presets = CalendarConfig.getWorkWeekPresets(); - return presets[CalendarConfig.currentWorkWeek] || presets['standard']; - } - - /** - * Set work week preset - * Note: Does not emit events - caller is responsible for event emission - */ - static setWorkWeek(workWeekId: string): void { - const presets = CalendarConfig.getWorkWeekPresets(); - if (presets[workWeekId]) { - CalendarConfig.currentWorkWeek = workWeekId; - - // Update dateViewSettings to match work week - CalendarConfig.dateViewSettings.weekDays = presets[workWeekId].totalDays; - } - } - - /** - * Get current work week ID - */ - static getCurrentWorkWeek(): string { - return CalendarConfig.currentWorkWeek; - } - - /** - * Get time format settings - */ - static getTimeFormatSettings(): TimeFormatConfig { - return { ...CalendarConfig.timeFormatConfig }; - } - - /** - * Get configured timezone - */ - static getTimezone(): string { - return CalendarConfig.timeFormatConfig.timezone; - } - - /** - * Get configured locale - */ - static getLocale(): string { - return CalendarConfig.timeFormatConfig.locale; - } - - /** - * Check if using 24-hour format - */ - static is24HourFormat(): boolean { - return CalendarConfig.timeFormatConfig.use24HourFormat; - } - - /** - * Get current date format - */ - static getDateFormat(): 'locale' | 'technical' { - return CalendarConfig.timeFormatConfig.dateFormat; - } - - /** - * Load configuration from JSON - */ - static loadFromJSON(json: string): void { - try { - const data = JSON.parse(json); - if (data.gridSettings) CalendarConfig.updateGridSettings(data.gridSettings); - if (data.dateViewSettings) CalendarConfig.dateViewSettings = { ...CalendarConfig.dateViewSettings, ...data.dateViewSettings }; - if (data.timeFormatConfig) { - CalendarConfig.timeFormatConfig = { ...CalendarConfig.timeFormatConfig, ...data.timeFormatConfig }; - TimeFormatter.configure(CalendarConfig.timeFormatConfig); - } - } catch (error) { - console.error('Failed to load config from JSON:', error); - } - } - - // ======================================================================== - // Instance method wrappers for backward compatibility - // These allow injected CalendarConfig to work with existing code - // ======================================================================== - - get(key: keyof ICalendarConfig) { return CalendarConfig.get(key); } - set(key: keyof ICalendarConfig, value: any) { return CalendarConfig.set(key, value); } - update(updates: Partial) { return CalendarConfig.update(updates); } - getAll() { return CalendarConfig.getAll(); } - get minuteHeight() { return CalendarConfig.minuteHeight; } - get totalHours() { return CalendarConfig.totalHours; } - get totalMinutes() { return CalendarConfig.totalMinutes; } - get slotsPerHour() { return CalendarConfig.slotsPerHour; } - get totalSlots() { return CalendarConfig.totalSlots; } - get slotHeight() { return CalendarConfig.slotHeight; } - isValidSnapInterval(interval: number) { return CalendarConfig.isValidSnapInterval(interval); } - getGridSettings() { return CalendarConfig.getGridSettings(); } - updateGridSettings(updates: Partial) { return CalendarConfig.updateGridSettings(updates); } - getDateViewSettings() { return CalendarConfig.getDateViewSettings(); } - getSelectedDate() { return CalendarConfig.getSelectedDate(); } - setSelectedDate(date: Date) { return CalendarConfig.setSelectedDate(date); } - getWorkWeekSettings() { return CalendarConfig.getWorkWeekSettings(); } - setWorkWeek(workWeekId: string) { return CalendarConfig.setWorkWeek(workWeekId); } - getCurrentWorkWeek() { return CalendarConfig.getCurrentWorkWeek(); } - getTimeFormatSettings() { return CalendarConfig.getTimeFormatSettings(); } - getTimezone() { return CalendarConfig.getTimezone(); } - getLocale() { return CalendarConfig.getLocale(); } - is24HourFormat() { return CalendarConfig.is24HourFormat(); } - getDateFormat() { return CalendarConfig.getDateFormat(); } -} \ No newline at end of file diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts index 02a02eb..d58a75a 100644 --- a/src/core/EventBus.ts +++ b/src/core/EventBus.ts @@ -1,14 +1,14 @@ // Core EventBus using pure DOM CustomEvents -import { EventLogEntry, ListenerEntry, IEventBus } from '../types/CalendarTypes'; +import { IEventLogEntry, IListenerEntry, IEventBus } from '../types/CalendarTypes'; /** * Central event dispatcher for calendar using DOM CustomEvents * Provides logging and debugging capabilities */ export class EventBus implements IEventBus { - private eventLog: EventLogEntry[] = []; + private eventLog: IEventLogEntry[] = []; private debug: boolean = false; - private listeners: Set = new Set(); + private listeners: Set = new Set(); // Log configuration for different categories private logConfig: { [key: string]: boolean } = { @@ -161,7 +161,7 @@ export class EventBus implements IEventBus { /** * Get event history */ - getEventLog(eventType?: string): EventLogEntry[] { + getEventLog(eventType?: string): IEventLogEntry[] { if (eventType) { return this.eventLog.filter(e => e.type === eventType); } diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index 02ec85e..cc5936c 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -1,5 +1,5 @@ -import { CalendarEvent } from '../types/CalendarTypes'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { ICalendarEvent } from '../types/CalendarTypes'; +import { Configuration } from '../configuration/CalendarConfig'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; import { DateService } from '../utils/DateService'; @@ -9,12 +9,12 @@ import { DateService } from '../utils/DateService'; */ export abstract class BaseSwpEventElement extends HTMLElement { protected dateService: DateService; - protected config: CalendarConfig; + protected config: Configuration; constructor() { super(); - // TODO: Find better solution for web component DI - this.config = new CalendarConfig(); + // Get singleton instance for web components (can't use DI) + this.config = Configuration.getInstance(); this.dateService = new DateService(this.config); } @@ -256,11 +256,11 @@ export class SwpEventElement extends BaseSwpEventElement { // ============================================ /** - * Create SwpEventElement from CalendarEvent + * Create SwpEventElement from ICalendarEvent */ - public static fromCalendarEvent(event: CalendarEvent): SwpEventElement { + public static fromCalendarEvent(event: ICalendarEvent): SwpEventElement { const element = document.createElement('swp-event') as SwpEventElement; - const config = new CalendarConfig(); + const config = Configuration.getInstance(); const dateService = new DateService(config); element.dataset.eventId = event.id; @@ -274,9 +274,9 @@ export class SwpEventElement extends BaseSwpEventElement { } /** - * Extract CalendarEvent from DOM element + * Extract ICalendarEvent from DOM element */ - public static extractCalendarEventFromElement(element: HTMLElement): CalendarEvent { + public static extractCalendarEventFromElement(element: HTMLElement): ICalendarEvent { return { id: element.dataset.eventId || '', title: element.dataset.title || '', @@ -331,11 +331,11 @@ export class SwpAllDayEventElement extends BaseSwpEventElement { } /** - * Create from CalendarEvent + * Create from ICalendarEvent */ - public static fromCalendarEvent(event: CalendarEvent): SwpAllDayEventElement { + public static fromCalendarEvent(event: ICalendarEvent): SwpAllDayEventElement { const element = document.createElement('swp-allday-event') as SwpAllDayEventElement; - const config = new CalendarConfig(); + const config = Configuration.getInstance(); const dateService = new DateService(config); element.dataset.eventId = event.id; diff --git a/src/index.ts b/src/index.ts index 123cfff..2874e0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ // Main entry point for Calendar Plantempus import { Container } from '@novadi/core'; import { eventBus } from './core/EventBus'; -import { CalendarConfig } from './core/CalendarConfig'; +import { ConfigManager } from './configuration/ConfigManager'; +import { Configuration } from './configuration/CalendarConfig'; import { URLManager } from './utils/URLManager'; import { IEventBus } from './types/CalendarTypes'; @@ -19,7 +20,6 @@ import { ResizeHandleManager } from './managers/ResizeHandleManager'; import { EdgeScrollManager } from './managers/EdgeScrollManager'; import { DragHoverManager } from './managers/DragHoverManager'; import { HeaderManager } from './managers/HeaderManager'; -import { ConfigManager } from './managers/ConfigManager'; // Import repositories import { IEventRepository } from './repositories/IEventRepository'; @@ -27,7 +27,7 @@ import { MockEventRepository } from './repositories/MockEventRepository'; // Import renderers import { DateHeaderRenderer, type IHeaderRenderer } from './renderers/DateHeaderRenderer'; -import { DateColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer'; +import { DateColumnRenderer, type IColumnRenderer } from './renderers/ColumnRenderer'; import { DateEventRenderer, type IEventRenderer } from './renderers/EventRenderer'; import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { GridRenderer } from './renderers/GridRenderer'; @@ -70,8 +70,8 @@ async function handleDeepLinking(eventManager: EventManager, urlManager: URLMana */ async function initializeCalendar(): Promise { try { - // Initialize static calendar configuration - CalendarConfig.initialize(); + // Load configuration from JSON + const config = await ConfigManager.load(); // Create NovaDI container const container = new Container(); @@ -80,21 +80,18 @@ async function initializeCalendar(): Promise { // Enable debug mode for development eventBus.setDebug(true); - // Register CalendarConfig as singleton instance (static class, not instantiated) - builder.registerInstance(CalendarConfig).as(); - - // Register ConfigManager for event-driven config updates - builder.registerType(ConfigManager).as(); - // Bind core services as instances builder.registerInstance(eventBus).as(); + // Register configuration instance + builder.registerInstance(config).as(); + // Register repositories builder.registerType(MockEventRepository).as(); // Register renderers builder.registerType(DateHeaderRenderer).as(); - builder.registerType(DateColumnRenderer).as(); + builder.registerType(DateColumnRenderer).as(); builder.registerType(DateEventRenderer).as(); // Register core services and utilities @@ -130,7 +127,6 @@ async function initializeCalendar(): Promise { // Get managers from container const eb = app.resolveType(); - const configManager = app.resolveType(); const calendarManager = app.resolveType(); const eventManager = app.resolveType(); const resizeHandleManager = app.resolveType(); @@ -143,9 +139,6 @@ async function initializeCalendar(): Promise { const allDayManager = app.resolveType(); const urlManager = app.resolveType(); - // Initialize CSS variables before any rendering - configManager.initialize(); - // Initialize managers await calendarManager.initialize?.(); await resizeHandleManager.initialize?.(); diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 73df2b0..fc82de7 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -1,21 +1,21 @@ // All-day row height management and animations import { eventBus } from '../core/EventBus'; -import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; +import { ALL_DAY_CONSTANTS } from '../configuration/CalendarConfig'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; -import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine'; -import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; -import { CalendarEvent } from '../types/CalendarTypes'; +import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine'; +import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; +import { ICalendarEvent } from '../types/CalendarTypes'; import { SwpAllDayEventElement } from '../elements/SwpEventElement'; import { - DragMouseEnterHeaderEventPayload, - DragStartEventPayload, - DragMoveEventPayload, - DragEndEventPayload, - DragColumnChangeEventPayload, - HeaderReadyEventPayload + IDragMouseEnterHeaderEventPayload, + IDragStartEventPayload, + IDragMoveEventPayload, + IDragEndEventPayload, + IDragColumnChangeEventPayload, + IHeaderReadyEventPayload } from '../types/EventTypes'; -import { DragOffset, MousePosition } from '../types/DragDropTypes'; +import { IDragOffset, IMousePosition } from '../types/DragDropTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { EventManager } from './EventManager'; import { differenceInCalendarDays } from 'date-fns'; @@ -33,10 +33,10 @@ export class AllDayManager { private layoutEngine: AllDayLayoutEngine | null = null; // State tracking for differential updates - private currentLayouts: EventLayout[] = []; - private currentAllDayEvents: CalendarEvent[] = []; - private currentWeekDates: ColumnBounds[] = []; - private newLayouts: EventLayout[] = []; + private currentLayouts: IEventLayout[] = []; + private currentAllDayEvents: ICalendarEvent[] = []; + private currentWeekDates: IColumnBounds[] = []; + private newLayouts: IEventLayout[] = []; // Expand/collapse state private isExpanded: boolean = false; @@ -62,7 +62,7 @@ export class AllDayManager { */ private setupEventListeners(): void { eventBus.on('drag:mouseenter-header', (event) => { - const payload = (event as CustomEvent).detail; + const payload = (event as CustomEvent).detail; if (payload.draggedClone.hasAttribute('data-allday')) return; @@ -87,7 +87,7 @@ export class AllDayManager { // Listen for drag operations on all-day events eventBus.on('drag:start', (event) => { - let payload: DragStartEventPayload = (event as CustomEvent).detail; + let payload: IDragStartEventPayload = (event as CustomEvent).detail; if (!payload.draggedClone?.hasAttribute('data-allday')) { return; @@ -97,7 +97,7 @@ export class AllDayManager { }); eventBus.on('drag:column-change', (event) => { - let payload: DragColumnChangeEventPayload = (event as CustomEvent).detail; + let payload: IDragColumnChangeEventPayload = (event as CustomEvent).detail; if (!payload.draggedClone?.hasAttribute('data-allday')) { return; @@ -107,7 +107,7 @@ export class AllDayManager { }); eventBus.on('drag:end', (event) => { - let draggedElement: DragEndEventPayload = (event as CustomEvent).detail; + let draggedElement: IDragEndEventPayload = (event as CustomEvent).detail; if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore. return; @@ -128,12 +128,12 @@ export class AllDayManager { // Listen for header ready - when dates are populated with period data eventBus.on('header:ready', (event: Event) => { - let headerReadyEventPayload = (event as CustomEvent).detail; + let headerReadyEventPayload = (event as CustomEvent).detail; let startDate = new Date(headerReadyEventPayload.headerElements.at(0)!.date); let endDate = new Date(headerReadyEventPayload.headerElements.at(-1)!.date); - let events: CalendarEvent[] = this.eventManager.getEventsForPeriod(startDate, endDate); + let events: ICalendarEvent[] = this.eventManager.getEventsForPeriod(startDate, endDate); // Filter for all-day events const allDayEvents = events.filter(event => event.allDay); @@ -302,7 +302,7 @@ export class AllDayManager { * Calculate layout for ALL all-day events using AllDayLayoutEngine * This is the correct method that processes all events together for proper overlap detection */ - private calculateAllDayEventsLayout(events: CalendarEvent[], dayHeaders: ColumnBounds[]): EventLayout[] { + private calculateAllDayEventsLayout(events: ICalendarEvent[], dayHeaders: IColumnBounds[]): IEventLayout[] { // Store current state this.currentAllDayEvents = events; @@ -316,12 +316,12 @@ export class AllDayManager { } - private handleConvertToAllDay(payload: DragMouseEnterHeaderEventPayload): void { + private handleConvertToAllDay(payload: IDragMouseEnterHeaderEventPayload): void { let allDayContainer = this.getAllDayContainer(); if (!allDayContainer) return; - // Create SwpAllDayEventElement from CalendarEvent + // Create SwpAllDayEventElement from ICalendarEvent const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent); // Apply grid positioning @@ -345,7 +345,7 @@ export class AllDayManager { /** * Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER */ - private handleColumnChange(dragColumnChangeEventPayload: DragColumnChangeEventPayload): void { + private handleColumnChange(dragColumnChangeEventPayload: IDragColumnChangeEventPayload): void { let allDayContainer = this.getAllDayContainer(); if (!allDayContainer) return; @@ -380,7 +380,7 @@ export class AllDayManager { } - private handleDragEnd(dragEndEvent: DragEndEventPayload): void { + private handleDragEnd(dragEndEvent: IDragEndEventPayload): void { const getEventDurationDays = (start: string | undefined, end: string | undefined): number => { @@ -433,7 +433,7 @@ export class AllDayManager { dragEndEvent.draggedClone.dataset.start = this.dateService.toUTC(newStartDate); dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate); - const droppedEvent: CalendarEvent = { + const droppedEvent: ICalendarEvent = { id: eventId, title: dragEndEvent.draggedClone.dataset.title || '', start: newStartDate, @@ -557,9 +557,9 @@ export class AllDayManager { }); } /** - * Count number of events in a specific column using ColumnBounds + * Count number of events in a specific column using IColumnBounds */ - private countEventsInColumn(columnBounds: ColumnBounds): number { + private countEventsInColumn(columnBounds: IColumnBounds): number { let columnIndex = columnBounds.index; let count = 0; diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 5ecaf8e..68fc38c 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -1,5 +1,5 @@ import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { CalendarView, IEventBus } from '../types/CalendarTypes'; import { EventManager } from './EventManager'; import { GridManager } from './GridManager'; @@ -15,7 +15,7 @@ export class CalendarManager { private gridManager: GridManager; private eventRenderer: EventRenderingService; private scrollManager: ScrollManager; - private config: CalendarConfig; + private config: Configuration; private currentView: CalendarView = 'week'; private currentDate: Date = new Date(); private isInitialized: boolean = false; @@ -26,7 +26,7 @@ export class CalendarManager { gridManager: GridManager, eventRenderingService: EventRenderingService, scrollManager: ScrollManager, - config: CalendarConfig + config: Configuration ) { this.eventBus = eventBus; this.eventManager = eventManager; @@ -232,4 +232,4 @@ export class CalendarManager { }); } -} \ No newline at end of file +} diff --git a/src/managers/ConfigManager.ts b/src/managers/ConfigManager.ts deleted file mode 100644 index ab19129..0000000 --- a/src/managers/ConfigManager.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Configuration manager - handles config updates with event emission -// Uses static CalendarConfig internally but adds event-driven updates - -import { IEventBus, ICalendarConfig } from '../types/CalendarTypes'; -import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarConfig } from '../core/CalendarConfig'; - -/** - * Grid display settings interface (re-export from CalendarConfig) - */ -interface GridSettings { - dayStartHour: number; - dayEndHour: number; - workStartHour: number; - workEndHour: number; - hourHeight: number; - snapInterval: number; - fitToWidth: boolean; - scrollToHour: number | null; - gridStartThresholdMinutes: number; - showCurrentTime: boolean; - showWorkHours: boolean; -} - -/** - * ConfigManager - Handles configuration updates with event emission - * Wraps static CalendarConfig with event-driven functionality for DI system - * Also manages CSS custom properties that reflect config values - */ -export class ConfigManager { - constructor(private eventBus: IEventBus) {} - - /** - * Initialize CSS variables on startup - * Must be called after DOM is ready but before any rendering - */ - public initialize(): void { - this.updateCSSVariables(); - } - - /** - * Set a config value and emit event - */ - set(key: K, value: ICalendarConfig[K]): void { - const oldValue = CalendarConfig.get(key); - CalendarConfig.set(key, value); - - // Update CSS variables to reflect config change - this.updateCSSVariables(); - - // Emit config update event - this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key, - value, - oldValue - }); - } - - /** - * Update multiple config values and emit event - */ - update(updates: Partial): void { - Object.entries(updates).forEach(([key, value]) => { - this.set(key as keyof ICalendarConfig, value); - }); - } - - /** - * Update grid display settings and emit event - */ - updateGridSettings(updates: Partial): void { - CalendarConfig.updateGridSettings(updates); - - // Update CSS variables to reflect config change - this.updateCSSVariables(); - - // Emit event after update - this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'gridSettings', - value: CalendarConfig.getGridSettings() - }); - } - - /** - * Set selected date and emit event - */ - setSelectedDate(date: Date): void { - const oldDate = CalendarConfig.getSelectedDate(); - CalendarConfig.setSelectedDate(date); - - // Emit date change event if it actually changed - if (!oldDate || oldDate.getTime() !== date.getTime()) { - this.eventBus.emit(CoreEvents.DATE_CHANGED, { - date, - oldDate - }); - } - } - - /** - * Set work week and emit event - */ - setWorkWeek(workWeekId: string): void { - const oldWorkWeek = CalendarConfig.getCurrentWorkWeek(); - CalendarConfig.setWorkWeek(workWeekId); - - // Update CSS variables to reflect config change - this.updateCSSVariables(); - - // Emit event if changed - if (oldWorkWeek !== workWeekId) { - this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, { - key: 'workWeek', - value: workWeekId, - oldValue: oldWorkWeek - }); - } - } - - /** - * Update all CSS custom properties based on current config - * This keeps the DOM in sync with config values - */ - private updateCSSVariables(): void { - const root = document.documentElement; - const gridSettings = CalendarConfig.getGridSettings(); - const calendar = document.querySelector('swp-calendar') as HTMLElement; - - // Set time-related CSS variables - root.style.setProperty('--header-height', '80px'); // Fixed header height - root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); - root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`); - root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString()); - root.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString()); - root.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString()); - root.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString()); - root.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString()); - - // Set column count based on view - const columnCount = this.calculateColumnCount(); - root.style.setProperty('--grid-columns', columnCount.toString()); - - // Set column width based on fitToWidth setting - if (gridSettings.fitToWidth) { - root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space - } else { - root.style.setProperty('--day-column-min-width', '250px'); // Default min-width for horizontal scroll mode - } - - // Set fitToWidth data attribute for CSS targeting - if (calendar) { - calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString()); - } - } - - /** - * Calculate number of columns based on view - */ - private calculateColumnCount(): number { - const dateSettings = CalendarConfig.getDateViewSettings(); - const workWeekSettings = CalendarConfig.getWorkWeekSettings(); - - switch (dateSettings.period) { - case 'day': - return 1; - case 'week': - return workWeekSettings.totalDays; - case 'month': - return workWeekSettings.totalDays; // Use work week for month view too - default: - return workWeekSettings.totalDays; - } - } -} diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 2e51f96..933c816 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -134,33 +134,33 @@ import { IEventBus } from '../types/CalendarTypes'; import { PositionUtils } from '../utils/PositionUtils'; -import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; +import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement'; import { - DragStartEventPayload, - DragMoveEventPayload, - DragEndEventPayload, - DragMouseEnterHeaderEventPayload, - DragMouseLeaveHeaderEventPayload, - DragMouseEnterColumnEventPayload, - DragColumnChangeEventPayload + IDragStartEventPayload, + IDragMoveEventPayload, + IDragEndEventPayload, + IDragMouseEnterHeaderEventPayload, + IDragMouseLeaveHeaderEventPayload, + IDragMouseEnterColumnEventPayload, + IDragColumnChangeEventPayload } from '../types/EventTypes'; -import { MousePosition } from '../types/DragDropTypes'; +import { IMousePosition } from '../types/DragDropTypes'; import { CoreEvents } from '../constants/CoreEvents'; export class DragDropManager { private eventBus: IEventBus; // Mouse tracking with optimized state - private mouseDownPosition: MousePosition = { x: 0, y: 0 }; - private currentMousePosition: MousePosition = { x: 0, y: 0 }; - private mouseOffset: MousePosition = { x: 0, y: 0 }; + private mouseDownPosition: IMousePosition = { x: 0, y: 0 }; + private currentMousePosition: IMousePosition = { x: 0, y: 0 }; + private mouseOffset: IMousePosition = { x: 0, y: 0 }; // Drag state private originalElement!: HTMLElement | null; private draggedClone!: HTMLElement | null; - private currentColumn: ColumnBounds | null = null; - private previousColumn: ColumnBounds | null = null; + private currentColumn: IColumnBounds | null = null; + private previousColumn: IColumnBounds | null = null; private isDragStarted = false; // Movement threshold to distinguish click from drag @@ -176,7 +176,7 @@ export class DragDropManager { private dragAnimationId: number | null = null; private targetY = 0; private currentY = 0; - private targetColumn: ColumnBounds | null = null; + private targetColumn: IColumnBounds | null = null; private positionUtils: PositionUtils; constructor(eventBus: IEventBus, positionUtils: PositionUtils) { @@ -336,7 +336,7 @@ export class DragDropManager { * Try to initialize drag based on movement threshold * Returns true if drag was initialized, false if not enough movement */ - private initializeDrag(currentPosition: MousePosition): boolean { + private initializeDrag(currentPosition: IMousePosition): boolean { const deltaX = Math.abs(currentPosition.x - this.mouseDownPosition.x); const deltaY = Math.abs(currentPosition.y - this.mouseDownPosition.y); const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY); @@ -362,7 +362,7 @@ export class DragDropManager { this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); this.draggedClone = originalElement.createClone(); - const dragStartPayload: DragStartEventPayload = { + const dragStartPayload: IDragStartEventPayload = { originalElement: this.originalElement!, draggedClone: this.draggedClone, mousePosition: this.mouseDownPosition, @@ -375,7 +375,7 @@ export class DragDropManager { } - private continueDrag(currentPosition: MousePosition): void { + private continueDrag(currentPosition: IMousePosition): void { if (!this.draggedClone!.hasAttribute("data-allday")) { // Calculate raw position from mouse (no snapping) @@ -405,7 +405,7 @@ export class DragDropManager { /** * Detect column change and emit event */ - private detectColumnChange(currentPosition: MousePosition): void { + private detectColumnChange(currentPosition: IMousePosition): void { const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); if (newColumn == null) return; @@ -413,7 +413,7 @@ export class DragDropManager { this.previousColumn = this.currentColumn; this.currentColumn = newColumn; - const dragColumnChangePayload: DragColumnChangeEventPayload = { + const dragColumnChangePayload: IDragColumnChangeEventPayload = { originalElement: this.originalElement!, draggedClone: this.draggedClone!, previousColumn: this.previousColumn, @@ -434,7 +434,7 @@ export class DragDropManager { // Only emit drag:end if drag was actually started if (this.isDragStarted) { - const mousePosition: MousePosition = { x: event.clientX, y: event.clientY }; + const mousePosition: IMousePosition = { x: event.clientX, y: event.clientY }; // Snap to grid on mouse up (like ResizeHandleManager) const column = ColumnDetectionUtils.getColumnBounds(mousePosition); @@ -455,7 +455,7 @@ export class DragDropManager { if (!dropTarget) throw "dropTarget is null"; - const dragEndPayload: DragEndEventPayload = { + const dragEndPayload: IDragEndEventPayload = { originalElement: this.originalElement, draggedClone: this.draggedClone, mousePosition, @@ -530,7 +530,7 @@ export class DragDropManager { /** * Optimized snap position calculation using PositionUtils */ - private calculateSnapPosition(mouseY: number, column: ColumnBounds): number { + private calculateSnapPosition(mouseY: number, column: IColumnBounds): number { // Calculate where the event top would be (accounting for mouse offset) const eventTopY = mouseY - this.mouseOffset.y; @@ -560,7 +560,7 @@ export class DragDropManager { this.currentY += step; // Emit drag:move event with current draggedClone reference - const dragMovePayload: DragMoveEventPayload = { + const dragMovePayload: IDragMoveEventPayload = { originalElement: this.originalElement!, draggedClone: this.draggedClone, // Always uses current reference mousePosition: this.currentMousePosition, // Use current mouse position! @@ -576,7 +576,7 @@ export class DragDropManager { this.currentY = this.targetY; // Emit final position - const dragMovePayload: DragMoveEventPayload = { + const dragMovePayload: IDragMoveEventPayload = { originalElement: this.originalElement!, draggedClone: this.draggedClone, mousePosition: this.currentMousePosition, // Use current mouse position! @@ -633,7 +633,7 @@ export class DragDropManager { /** * Detect drop target - whether dropped in swp-day-column or swp-day-header */ - private detectDropTarget(position: MousePosition): 'swp-day-column' | 'swp-day-header' | null { + private detectDropTarget(position: IMousePosition): 'swp-day-column' | 'swp-day-header' | null { // Traverse up the DOM tree to find the target container let currentElement = this.draggedClone; @@ -659,13 +659,13 @@ export class DragDropManager { return; } - const position: MousePosition = { x: event.clientX, y: event.clientY }; + const position: IMousePosition = { x: event.clientX, y: event.clientY }; const targetColumn = ColumnDetectionUtils.getColumnBounds(position); if (targetColumn) { const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone); - const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = { + const dragMouseEnterPayload: IDragMouseEnterHeaderEventPayload = { targetColumn: targetColumn, mousePosition: position, originalElement: this.originalElement, @@ -689,7 +689,7 @@ export class DragDropManager { return; } - const position: MousePosition = { x: event.clientX, y: event.clientY }; + const position: IMousePosition = { x: event.clientX, y: event.clientY }; const targetColumn = ColumnDetectionUtils.getColumnBounds(position); if (!targetColumn) { @@ -699,10 +699,10 @@ export class DragDropManager { // Calculate snapped Y position const snappedY = this.calculateSnapPosition(position.y, targetColumn); - // Extract CalendarEvent from the dragged clone + // Extract ICalendarEvent from the dragged clone const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone); - const dragMouseEnterPayload: DragMouseEnterColumnEventPayload = { + const dragMouseEnterPayload: IDragMouseEnterColumnEventPayload = { targetColumn: targetColumn, mousePosition: position, snappedY: snappedY, @@ -727,14 +727,14 @@ export class DragDropManager { return; } - const position: MousePosition = { x: event.clientX, y: event.clientY }; + const position: IMousePosition = { x: event.clientX, y: event.clientY }; const targetColumn = ColumnDetectionUtils.getColumnBounds(position); if (!targetColumn) { return; } - const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = { + const dragMouseLeavePayload: IDragMouseLeaveHeaderEventPayload = { targetDate: targetColumn.date, mousePosition: position, originalElement: this.originalElement, diff --git a/src/managers/EdgeScrollManager.ts b/src/managers/EdgeScrollManager.ts index 54be817..a9b45ab 100644 --- a/src/managers/EdgeScrollManager.ts +++ b/src/managers/EdgeScrollManager.ts @@ -1,230 +1,230 @@ -/** - * EdgeScrollManager - Auto-scroll when dragging near edges - * Uses time-based scrolling with 2-zone system for variable speed - */ - -import { IEventBus } from '../types/CalendarTypes'; -import { DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes'; - -export class EdgeScrollManager { - private scrollableContent: HTMLElement | null = null; - private timeGrid: HTMLElement | null = null; - private draggedClone: HTMLElement | null = null; - private scrollRAF: number | null = null; - private mouseY = 0; - private isDragging = false; - private isScrolling = false; // Track if edge-scroll is active - private lastTs = 0; - private rect: DOMRect | null = null; - private initialScrollTop = 0; - private scrollListener: ((e: Event) => void) | null = null; - - // Constants - fixed values as per requirements - private readonly OUTER_ZONE = 100; // px from edge (slow zone) - private readonly INNER_ZONE = 50; // px from edge (fast zone) - private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone - private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone - - constructor(private eventBus: IEventBus) { - this.init(); - } - - private init(): void { - // Wait for DOM to be ready - setTimeout(() => { - this.scrollableContent = document.querySelector('swp-scrollable-content'); - this.timeGrid = document.querySelector('swp-time-grid'); - - if (this.scrollableContent) { - // Disable smooth scroll for instant auto-scroll - this.scrollableContent.style.scrollBehavior = 'auto'; - - // Add scroll listener to detect actual scrolling - this.scrollListener = this.handleScroll.bind(this); - this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true }); - } - }, 100); - - // Listen to mousemove directly from document to always get mouse coords - document.body.addEventListener('mousemove', (e: MouseEvent) => { - if (this.isDragging) { - this.mouseY = e.clientY; - } - }); - - this.subscribeToEvents(); - } - - private subscribeToEvents(): void { - - // Listen to drag events from DragDropManager - this.eventBus.on('drag:start', (event: Event) => { - const payload = (event as CustomEvent).detail; - this.draggedClone = payload.draggedClone; - this.startDrag(); - }); - - this.eventBus.on('drag:end', () => this.stopDrag()); - this.eventBus.on('drag:cancelled', () => this.stopDrag()); - - // Stop scrolling when event converts to/from all-day - this.eventBus.on('drag:mouseenter-header', () => { - console.log('🔄 EdgeScrollManager: Event converting to all-day - stopping scroll'); - this.stopDrag(); - }); - - this.eventBus.on('drag:mouseenter-column', () => { - this.startDrag(); - }); - } - - private startDrag(): void { - console.log('🎬 EdgeScrollManager: Starting drag'); - this.isDragging = true; - this.isScrolling = false; // Reset scroll state - this.lastTs = performance.now(); - - // Save initial scroll position - if (this.scrollableContent) { - this.initialScrollTop = this.scrollableContent.scrollTop; - } - - if (this.scrollRAF === null) { - this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); - } - } - - private stopDrag(): void { - this.isDragging = false; - - // Emit stopped event if we were scrolling - if (this.isScrolling) { - this.isScrolling = false; - console.log('🛑 EdgeScrollManager: Edge-scroll stopped (drag ended)'); - this.eventBus.emit('edgescroll:stopped', {}); - } - - if (this.scrollRAF !== null) { - cancelAnimationFrame(this.scrollRAF); - this.scrollRAF = null; - } - this.rect = null; - this.lastTs = 0; - this.initialScrollTop = 0; - } - - private handleScroll(): void { - if (!this.isDragging || !this.scrollableContent) return; - - const currentScrollTop = this.scrollableContent.scrollTop; - const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop); - - // Only emit started event if we've actually scrolled more than 1px - if (scrollDelta > 1 && !this.isScrolling) { - this.isScrolling = true; - console.log('💾 EdgeScrollManager: Edge-scroll started (actual scroll detected)', { - initialScrollTop: this.initialScrollTop, - currentScrollTop, - scrollDelta - }); - this.eventBus.emit('edgescroll:started', {}); - } - } - - private scrollTick(ts: number): void { - const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0; - this.lastTs = ts; - - if (!this.scrollableContent) { - this.stopDrag(); - return; - } - - // Cache rect for performance (only measure once per frame) - if (!this.rect) { - this.rect = this.scrollableContent.getBoundingClientRect(); - } - - let vy = 0; - if (this.isDragging) { - const distTop = this.mouseY - this.rect.top; - const distBot = this.rect.bottom - this.mouseY; - - // Check top edge - if (distTop < this.INNER_ZONE) { - vy = -this.FAST_SPEED_PXS; - } else if (distTop < this.OUTER_ZONE) { - vy = -this.SLOW_SPEED_PXS; - } - // Check bottom edge - else if (distBot < this.INNER_ZONE) { - vy = this.FAST_SPEED_PXS; - } else if (distBot < this.OUTER_ZONE) { - vy = this.SLOW_SPEED_PXS; - } - } - - if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedClone) { - // Check if we can scroll in the requested direction - const currentScrollTop = this.scrollableContent.scrollTop; - const scrollableHeight = this.scrollableContent.clientHeight; - const timeGridHeight = this.timeGrid.clientHeight; - - // Get dragged element position and height - const cloneRect = this.draggedClone.getBoundingClientRect(); - const cloneBottom = cloneRect.bottom; - const timeGridRect = this.timeGrid.getBoundingClientRect(); - const timeGridBottom = timeGridRect.bottom; - - // Check boundaries - const atTop = currentScrollTop <= 0 && vy < 0; - const atBottom = (cloneBottom >= timeGridBottom) && vy > 0; - - console.log('📊 Scroll check:', { - currentScrollTop, - scrollableHeight, - timeGridHeight, - cloneBottom, - timeGridBottom, - atTop, - atBottom, - vy - }); - - if (atTop || atBottom) { - // At boundary - stop scrolling - if (this.isScrolling) { - this.isScrolling = false; - this.initialScrollTop = this.scrollableContent.scrollTop; - console.log('🛑 EdgeScrollManager: Edge-scroll stopped (reached boundary)'); - this.eventBus.emit('edgescroll:stopped', {}); - } - - // Continue RAF loop to detect when mouse moves away from boundary - if (this.isDragging) { - this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); - } - } else { - // Not at boundary - apply scroll - this.scrollableContent.scrollTop += vy * dt; - this.rect = null; // Invalidate cache for next frame - this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); - } - } else { - // Mouse moved away from edge - stop scrolling - if (this.isScrolling) { - this.isScrolling = false; - this.initialScrollTop = this.scrollableContent.scrollTop; // Reset for next scroll - console.log('🛑 EdgeScrollManager: Edge-scroll stopped (mouse left edge)'); - this.eventBus.emit('edgescroll:stopped', {}); - } - - // Continue RAF loop even if not scrolling, to detect edge entry - if (this.isDragging) { - this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); - } else { - this.stopDrag(); - } - } - } +/** + * EdgeScrollManager - Auto-scroll when dragging near edges + * Uses time-based scrolling with 2-zone system for variable speed + */ + +import { IEventBus } from '../types/CalendarTypes'; +import { IDragMoveEventPayload, IDragStartEventPayload } from '../types/EventTypes'; + +export class EdgeScrollManager { + private scrollableContent: HTMLElement | null = null; + private timeGrid: HTMLElement | null = null; + private draggedClone: HTMLElement | null = null; + private scrollRAF: number | null = null; + private mouseY = 0; + private isDragging = false; + private isScrolling = false; // Track if edge-scroll is active + private lastTs = 0; + private rect: DOMRect | null = null; + private initialScrollTop = 0; + private scrollListener: ((e: Event) => void) | null = null; + + // Constants - fixed values as per requirements + private readonly OUTER_ZONE = 100; // px from edge (slow zone) + private readonly INNER_ZONE = 50; // px from edge (fast zone) + private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone + private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone + + constructor(private eventBus: IEventBus) { + this.init(); + } + + private init(): void { + // Wait for DOM to be ready + setTimeout(() => { + this.scrollableContent = document.querySelector('swp-scrollable-content'); + this.timeGrid = document.querySelector('swp-time-grid'); + + if (this.scrollableContent) { + // Disable smooth scroll for instant auto-scroll + this.scrollableContent.style.scrollBehavior = 'auto'; + + // Add scroll listener to detect actual scrolling + this.scrollListener = this.handleScroll.bind(this); + this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true }); + } + }, 100); + + // Listen to mousemove directly from document to always get mouse coords + document.body.addEventListener('mousemove', (e: MouseEvent) => { + if (this.isDragging) { + this.mouseY = e.clientY; + } + }); + + this.subscribeToEvents(); + } + + private subscribeToEvents(): void { + + // Listen to drag events from DragDropManager + this.eventBus.on('drag:start', (event: Event) => { + const payload = (event as CustomEvent).detail; + this.draggedClone = payload.draggedClone; + this.startDrag(); + }); + + this.eventBus.on('drag:end', () => this.stopDrag()); + this.eventBus.on('drag:cancelled', () => this.stopDrag()); + + // Stop scrolling when event converts to/from all-day + this.eventBus.on('drag:mouseenter-header', () => { + console.log('🔄 EdgeScrollManager: Event converting to all-day - stopping scroll'); + this.stopDrag(); + }); + + this.eventBus.on('drag:mouseenter-column', () => { + this.startDrag(); + }); + } + + private startDrag(): void { + console.log('🎬 EdgeScrollManager: Starting drag'); + this.isDragging = true; + this.isScrolling = false; // Reset scroll state + this.lastTs = performance.now(); + + // Save initial scroll position + if (this.scrollableContent) { + this.initialScrollTop = this.scrollableContent.scrollTop; + } + + if (this.scrollRAF === null) { + this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); + } + } + + private stopDrag(): void { + this.isDragging = false; + + // Emit stopped event if we were scrolling + if (this.isScrolling) { + this.isScrolling = false; + console.log('🛑 EdgeScrollManager: Edge-scroll stopped (drag ended)'); + this.eventBus.emit('edgescroll:stopped', {}); + } + + if (this.scrollRAF !== null) { + cancelAnimationFrame(this.scrollRAF); + this.scrollRAF = null; + } + this.rect = null; + this.lastTs = 0; + this.initialScrollTop = 0; + } + + private handleScroll(): void { + if (!this.isDragging || !this.scrollableContent) return; + + const currentScrollTop = this.scrollableContent.scrollTop; + const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop); + + // Only emit started event if we've actually scrolled more than 1px + if (scrollDelta > 1 && !this.isScrolling) { + this.isScrolling = true; + console.log('💾 EdgeScrollManager: Edge-scroll started (actual scroll detected)', { + initialScrollTop: this.initialScrollTop, + currentScrollTop, + scrollDelta + }); + this.eventBus.emit('edgescroll:started', {}); + } + } + + private scrollTick(ts: number): void { + const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0; + this.lastTs = ts; + + if (!this.scrollableContent) { + this.stopDrag(); + return; + } + + // Cache rect for performance (only measure once per frame) + if (!this.rect) { + this.rect = this.scrollableContent.getBoundingClientRect(); + } + + let vy = 0; + if (this.isDragging) { + const distTop = this.mouseY - this.rect.top; + const distBot = this.rect.bottom - this.mouseY; + + // Check top edge + if (distTop < this.INNER_ZONE) { + vy = -this.FAST_SPEED_PXS; + } else if (distTop < this.OUTER_ZONE) { + vy = -this.SLOW_SPEED_PXS; + } + // Check bottom edge + else if (distBot < this.INNER_ZONE) { + vy = this.FAST_SPEED_PXS; + } else if (distBot < this.OUTER_ZONE) { + vy = this.SLOW_SPEED_PXS; + } + } + + if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedClone) { + // Check if we can scroll in the requested direction + const currentScrollTop = this.scrollableContent.scrollTop; + const scrollableHeight = this.scrollableContent.clientHeight; + const timeGridHeight = this.timeGrid.clientHeight; + + // Get dragged element position and height + const cloneRect = this.draggedClone.getBoundingClientRect(); + const cloneBottom = cloneRect.bottom; + const timeGridRect = this.timeGrid.getBoundingClientRect(); + const timeGridBottom = timeGridRect.bottom; + + // Check boundaries + const atTop = currentScrollTop <= 0 && vy < 0; + const atBottom = (cloneBottom >= timeGridBottom) && vy > 0; + + console.log('📊 Scroll check:', { + currentScrollTop, + scrollableHeight, + timeGridHeight, + cloneBottom, + timeGridBottom, + atTop, + atBottom, + vy + }); + + if (atTop || atBottom) { + // At boundary - stop scrolling + if (this.isScrolling) { + this.isScrolling = false; + this.initialScrollTop = this.scrollableContent.scrollTop; + console.log('🛑 EdgeScrollManager: Edge-scroll stopped (reached boundary)'); + this.eventBus.emit('edgescroll:stopped', {}); + } + + // Continue RAF loop to detect when mouse moves away from boundary + if (this.isDragging) { + this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); + } + } else { + // Not at boundary - apply scroll + this.scrollableContent.scrollTop += vy * dt; + this.rect = null; // Invalidate cache for next frame + this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); + } + } else { + // Mouse moved away from edge - stop scrolling + if (this.isScrolling) { + this.isScrolling = false; + this.initialScrollTop = this.scrollableContent.scrollTop; // Reset for next scroll + console.log('🛑 EdgeScrollManager: Edge-scroll stopped (mouse left edge)'); + this.eventBus.emit('edgescroll:stopped', {}); + } + + // Continue RAF loop even if not scrolling, to detect edge entry + if (this.isDragging) { + this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); + } else { + this.stopDrag(); + } + } + } } \ No newline at end of file diff --git a/src/managers/EventFilterManager.ts b/src/managers/EventFilterManager.ts index 71663af..1f6777a 100644 --- a/src/managers/EventFilterManager.ts +++ b/src/managers/EventFilterManager.ts @@ -5,24 +5,24 @@ import { eventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarEvent } from '../types/CalendarTypes'; +import { ICalendarEvent } from '../types/CalendarTypes'; // Import Fuse.js from npm import Fuse from 'fuse.js'; interface FuseResult { - item: CalendarEvent; + item: ICalendarEvent; refIndex: number; score?: number; } export class EventFilterManager { private searchInput: HTMLInputElement | null = null; - private allEvents: CalendarEvent[] = []; + private allEvents: ICalendarEvent[] = []; private matchingEventIds: Set = new Set(); private isFilterActive: boolean = false; private frameRequest: number | null = null; - private fuse: Fuse | null = null; + private fuse: Fuse | null = null; constructor() { // Wait for DOM to be ready before initializing @@ -77,7 +77,7 @@ export class EventFilterManager { }); } - private updateEventsList(events: CalendarEvent[]): void { + private updateEventsList(events: ICalendarEvent[]): void { this.allEvents = events; // Initialize Fuse with the new events list diff --git a/src/managers/EventLayoutCoordinator.ts b/src/managers/EventLayoutCoordinator.ts index 55f565c..6f18d6f 100644 --- a/src/managers/EventLayoutCoordinator.ts +++ b/src/managers/EventLayoutCoordinator.ts @@ -5,35 +5,35 @@ * Calculates stack levels, groups events, and determines rendering strategy. */ -import { CalendarEvent } from '../types/CalendarTypes'; -import { EventStackManager, EventGroup, StackLink } from './EventStackManager'; +import { ICalendarEvent } from '../types/CalendarTypes'; +import { EventStackManager, IEventGroup, IStackLink } from './EventStackManager'; import { PositionUtils } from '../utils/PositionUtils'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; -export interface GridGroupLayout { - events: CalendarEvent[]; +export interface IGridGroupLayout { + events: ICalendarEvent[]; stackLevel: number; position: { top: number }; - columns: CalendarEvent[][]; // Events grouped by column (events in same array share a column) + columns: ICalendarEvent[][]; // Events grouped by column (events in same array share a column) } -export interface StackedEventLayout { - event: CalendarEvent; - stackLink: StackLink; +export interface IStackedEventLayout { + event: ICalendarEvent; + stackLink: IStackLink; position: { top: number; height: number }; } -export interface ColumnLayout { - gridGroups: GridGroupLayout[]; - stackedEvents: StackedEventLayout[]; +export interface IColumnLayout { + gridGroups: IGridGroupLayout[]; + stackedEvents: IStackedEventLayout[]; } export class EventLayoutCoordinator { private stackManager: EventStackManager; - private config: CalendarConfig; + private config: Configuration; private positionUtils: PositionUtils; - constructor(stackManager: EventStackManager, config: CalendarConfig, positionUtils: PositionUtils) { + constructor(stackManager: EventStackManager, config: Configuration, positionUtils: PositionUtils) { this.stackManager = stackManager; this.config = config; this.positionUtils = positionUtils; @@ -42,14 +42,14 @@ export class EventLayoutCoordinator { /** * Calculate complete layout for a column of events (recursive approach) */ - public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout { + public calculateColumnLayout(columnEvents: ICalendarEvent[]): IColumnLayout { if (columnEvents.length === 0) { return { gridGroups: [], stackedEvents: [] }; } - const gridGroupLayouts: GridGroupLayout[] = []; - const stackedEventLayouts: StackedEventLayout[] = []; - const renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> = []; + const gridGroupLayouts: IGridGroupLayout[] = []; + const stackedEventLayouts: IStackedEventLayout[] = []; + const renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }> = []; let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime()); // Process events recursively @@ -66,7 +66,7 @@ export class EventLayoutCoordinator { const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes); // Decide: should this group be GRID or STACK? - const group: EventGroup = { + const group: IEventGroup = { events: gridCandidates, containerType: 'NONE', startTime: firstEvent.start @@ -129,8 +129,8 @@ export class EventLayoutCoordinator { * Calculate stack level for a grid group based on already rendered events */ private calculateGridGroupStackLevelFromRendered( - gridEvents: CalendarEvent[], - renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> + gridEvents: ICalendarEvent[], + renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }> ): number { // Find highest stack level of any rendered event that overlaps with this grid let maxOverlappingLevel = -1; @@ -150,8 +150,8 @@ export class EventLayoutCoordinator { * Calculate stack level for a single stacked event based on already rendered events */ private calculateStackLevelFromRendered( - event: CalendarEvent, - renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> + event: ICalendarEvent, + renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }> ): number { // Find highest stack level of any rendered event that overlaps with this event let maxOverlappingLevel = -1; @@ -173,7 +173,7 @@ export class EventLayoutCoordinator { * @param thresholdMinutes - Threshold in minutes * @returns true if events conflict */ - private detectConflict(event1: CalendarEvent, event2: CalendarEvent, thresholdMinutes: number): boolean { + private detectConflict(event1: ICalendarEvent, event2: ICalendarEvent, thresholdMinutes: number): boolean { // Check 1: Start-to-start conflict (starts within threshold) const startToStartDiff = Math.abs(event1.start.getTime() - event2.start.getTime()) / (1000 * 60); if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(event1, event2)) { @@ -206,10 +206,10 @@ export class EventLayoutCoordinator { * @returns Array of all events in the conflict chain */ private expandGridCandidates( - firstEvent: CalendarEvent, - remaining: CalendarEvent[], + firstEvent: ICalendarEvent, + remaining: ICalendarEvent[], thresholdMinutes: number - ): CalendarEvent[] { + ): ICalendarEvent[] { const gridCandidates = [firstEvent]; let candidatesChanged = true; @@ -246,11 +246,11 @@ export class EventLayoutCoordinator { * @param events - Events in the grid group (should already be sorted by start time) * @returns Array of columns, where each column is an array of events */ - private allocateColumns(events: CalendarEvent[]): CalendarEvent[][] { + private allocateColumns(events: ICalendarEvent[]): ICalendarEvent[][] { if (events.length === 0) return []; if (events.length === 1) return [[events[0]]]; - const columns: CalendarEvent[][] = []; + const columns: ICalendarEvent[][] = []; // For each event, try to place it in an existing column where it doesn't overlap for (const event of events) { diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index e357c54..52ceefd 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -1,6 +1,6 @@ -import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; +import { IEventBus, ICalendarEvent } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { DateService } from '../utils/DateService'; import { IEventRepository } from '../repositories/IEventRepository'; @@ -10,15 +10,15 @@ import { IEventRepository } from '../repositories/IEventRepository'; */ export class EventManager { - private events: CalendarEvent[] = []; + private events: ICalendarEvent[] = []; private dateService: DateService; - private config: CalendarConfig; + private config: Configuration; private repository: IEventRepository; constructor( private eventBus: IEventBus, dateService: DateService, - config: CalendarConfig, + config: Configuration, repository: IEventRepository ) { this.dateService = dateService; @@ -42,14 +42,14 @@ export class EventManager { /** * Get events with optional copying for performance */ - public getEvents(copy: boolean = false): CalendarEvent[] { + public getEvents(copy: boolean = false): ICalendarEvent[] { return copy ? [...this.events] : this.events; } /** * Optimized event lookup with early return */ - public getEventById(id: string): CalendarEvent | undefined { + public getEventById(id: string): ICalendarEvent | undefined { // Use find for better performance than filter + first return this.events.find(event => event.id === id); } @@ -59,7 +59,7 @@ export class EventManager { * @param id Event ID to find * @returns Event with navigation info or null if not found */ - public getEventForNavigation(id: string): { event: CalendarEvent; eventDate: Date } | null { + public getEventForNavigation(id: string): { event: ICalendarEvent; eventDate: Date } | null { const event = this.getEventById(id); if (!event) { return null; @@ -113,7 +113,7 @@ export class EventManager { /** * Get events that overlap with a given time period */ - public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { + public getEventsForPeriod(startDate: Date, endDate: Date): ICalendarEvent[] { // Event overlaps period if it starts before period ends AND ends after period starts return this.events.filter(event => { return event.start <= endDate && event.end >= startDate; @@ -123,8 +123,8 @@ export class EventManager { /** * Create a new event and add it to the calendar */ - public addEvent(event: Omit): CalendarEvent { - const newEvent: CalendarEvent = { + public addEvent(event: Omit): ICalendarEvent { + const newEvent: ICalendarEvent = { ...event, id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` }; @@ -141,7 +141,7 @@ export class EventManager { /** * Update an existing event */ - public updateEvent(id: string, updates: Partial): CalendarEvent | null { + public updateEvent(id: string, updates: Partial): ICalendarEvent | null { const eventIndex = this.events.findIndex(event => event.id === id); if (eventIndex === -1) return null; diff --git a/src/managers/EventStackManager.ts b/src/managers/EventStackManager.ts index da04ead..7c701de 100644 --- a/src/managers/EventStackManager.ts +++ b/src/managers/EventStackManager.ts @@ -13,26 +13,26 @@ * @see stacking-visualization.html for visual examples */ -import { CalendarEvent } from '../types/CalendarTypes'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { ICalendarEvent } from '../types/CalendarTypes'; +import { Configuration } from '../configuration/CalendarConfig'; -export interface StackLink { +export interface IStackLink { prev?: string; // Event ID of previous event in stack next?: string; // Event ID of next event in stack stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.) } -export interface EventGroup { - events: CalendarEvent[]; +export interface IEventGroup { + events: ICalendarEvent[]; containerType: 'NONE' | 'GRID' | 'STACKING'; startTime: Date; } export class EventStackManager { private static readonly STACK_OFFSET_PX = 15; - private config: CalendarConfig; + private config: Configuration; - constructor(config: CalendarConfig) { + constructor(config: Configuration) { this.config = config; } @@ -47,7 +47,7 @@ export class EventStackManager { * 1. They start within ±threshold minutes of each other (start-to-start) * 2. One event starts within threshold minutes before another ends (end-to-start conflict) */ - public groupEventsByStartTime(events: CalendarEvent[]): EventGroup[] { + public groupEventsByStartTime(events: ICalendarEvent[]): IEventGroup[] { if (events.length === 0) return []; // Get threshold from config @@ -57,7 +57,7 @@ export class EventStackManager { // Sort events by start time const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); - const groups: EventGroup[] = []; + const groups: IEventGroup[] = []; for (const event of sorted) { // Find existing group that this event conflicts with @@ -112,7 +112,7 @@ export class EventStackManager { * even if they overlap each other. This provides better visual indication that * events start at the same time. */ - public decideContainerType(group: EventGroup): 'NONE' | 'GRID' | 'STACKING' { + public decideContainerType(group: IEventGroup): 'NONE' | 'GRID' | 'STACKING' { if (group.events.length === 1) { return 'NONE'; } @@ -127,7 +127,7 @@ export class EventStackManager { /** * Check if two events overlap in time */ - public doEventsOverlap(event1: CalendarEvent, event2: CalendarEvent): boolean { + public doEventsOverlap(event1: ICalendarEvent, event2: ICalendarEvent): boolean { return event1.start < event2.end && event1.end > event2.start; } @@ -139,8 +139,8 @@ export class EventStackManager { /** * Create optimized stack links (events share levels when possible) */ - public createOptimizedStackLinks(events: CalendarEvent[]): Map { - const stackLinks = new Map(); + public createOptimizedStackLinks(events: ICalendarEvent[]): Map { + const stackLinks = new Map(); if (events.length === 0) return stackLinks; @@ -218,14 +218,14 @@ export class EventStackManager { /** * Serialize stack link to JSON string */ - public serializeStackLink(stackLink: StackLink): string { + public serializeStackLink(stackLink: IStackLink): string { return JSON.stringify(stackLink); } /** * Deserialize JSON string to stack link */ - public deserializeStackLink(json: string): StackLink | null { + public deserializeStackLink(json: string): IStackLink | null { try { return JSON.parse(json); } catch (e) { @@ -236,14 +236,14 @@ export class EventStackManager { /** * Apply stack link to DOM element */ - public applyStackLinkToElement(element: HTMLElement, stackLink: StackLink): void { + public applyStackLinkToElement(element: HTMLElement, stackLink: IStackLink): void { element.dataset.stackLink = this.serializeStackLink(stackLink); } /** * Get stack link from DOM element */ - public getStackLinkFromElement(element: HTMLElement): StackLink | null { + public getStackLinkFromElement(element: HTMLElement): IStackLink | null { const data = element.dataset.stackLink; if (!data) return null; return this.deserializeStackLink(data); diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index e7a0580..c7c702d 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -1,8 +1,8 @@ import { eventBus } from '../core/EventBus'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; -import { IHeaderRenderer, HeaderRenderContext } from '../renderers/DateHeaderRenderer'; -import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; +import { IHeaderRenderer, IHeaderRenderContext } from '../renderers/DateHeaderRenderer'; +import { IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IHeaderReadyEventPayload } from '../types/EventTypes'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; /** @@ -12,9 +12,9 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; */ export class HeaderManager { private headerRenderer: IHeaderRenderer; - private config: CalendarConfig; + private config: Configuration; - constructor(headerRenderer: IHeaderRenderer, config: CalendarConfig) { + constructor(headerRenderer: IHeaderRenderer, config: Configuration) { this.headerRenderer = headerRenderer; this.config = config; @@ -44,7 +44,7 @@ export class HeaderManager { */ private handleDragMouseEnterHeader(event: Event): void { const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } = - (event as CustomEvent).detail; + (event as CustomEvent).detail; console.log('🎯 HeaderManager: Received drag:mouseenter-header', { targetDate, @@ -58,7 +58,7 @@ export class HeaderManager { */ private handleDragMouseLeaveHeader(event: Event): void { const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = - (event as CustomEvent).detail; + (event as CustomEvent).detail; console.log('🚪 HeaderManager: Received drag:mouseleave-header', { targetDate, @@ -109,7 +109,7 @@ export class HeaderManager { calendarHeader.innerHTML = ''; // Render new header content using injected renderer - const context: HeaderRenderContext = { + const context: IHeaderRenderContext = { currentWeek: currentDate, config: this.config }; @@ -120,9 +120,9 @@ export class HeaderManager { this.setupHeaderDragListeners(); // Notify other managers that header is ready with period data - const payload: HeaderReadyEventPayload = { + const payload: IHeaderReadyEventPayload = { headerElements: ColumnDetectionUtils.getHeaderColumns(), }; eventBus.emit('header:ready', payload); } -} \ No newline at end of file +} diff --git a/src/managers/ResizeHandleManager.ts b/src/managers/ResizeHandleManager.ts index e0b3f0a..95c6b3a 100644 --- a/src/managers/ResizeHandleManager.ts +++ b/src/managers/ResizeHandleManager.ts @@ -1,7 +1,7 @@ import { eventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; -import { CalendarConfig } from '../core/CalendarConfig'; -import { ResizeEndEventPayload } from '../types/EventTypes'; +import { Configuration } from '../configuration/CalendarConfig'; +import { IResizeEndEventPayload } from '../types/EventTypes'; type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; @@ -29,9 +29,9 @@ export class ResizeHandleManager { private unsubscribers: Array<() => void> = []; private pointerCaptured = false; private prevZ?: string; - private config: CalendarConfig; + private config: Configuration; - constructor(config: CalendarConfig) { + constructor(config: Configuration) { this.config = config; const grid = this.config.getGridSettings(); this.hourHeightPx = grid.hourHeight; @@ -237,7 +237,7 @@ export class ResizeHandleManager { // Emit resize:end event for re-stacking const eventId = this.targetEl.dataset.eventId || ''; - const resizeEndPayload: ResizeEndEventPayload = { + const resizeEndPayload: IResizeEndEventPayload = { eventId, element: this.targetEl, finalHeight diff --git a/src/managers/ViewManager.ts b/src/managers/ViewManager.ts index a90523c..596564c 100644 --- a/src/managers/ViewManager.ts +++ b/src/managers/ViewManager.ts @@ -1,15 +1,15 @@ import { CalendarView, IEventBus } from '../types/CalendarTypes'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; export class ViewManager { private eventBus: IEventBus; - private config: CalendarConfig; + private config: Configuration; private currentView: CalendarView = 'week'; private buttonListeners: Map = new Map(); - constructor(eventBus: IEventBus, config: CalendarConfig) { + constructor(eventBus: IEventBus, config: Configuration) { this.eventBus = eventBus; this.config = config; this.setupEventListeners(); @@ -143,4 +143,4 @@ export class ViewManager { } -} \ No newline at end of file +} diff --git a/src/managers/WorkHoursManager.ts b/src/managers/WorkHoursManager.ts index a76cfec..1091b5b 100644 --- a/src/managers/WorkHoursManager.ts +++ b/src/managers/WorkHoursManager.ts @@ -1,13 +1,13 @@ // Work hours management for per-column scheduling import { DateService } from '../utils/DateService'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; /** * Work hours for a specific day */ -export interface DayWorkHours { +export interface IDayWorkHours { start: number; // Hour (0-23) end: number; // Hour (0-23) } @@ -15,18 +15,18 @@ export interface DayWorkHours { /** * Work schedule configuration */ -export interface WorkScheduleConfig { +export interface IWorkScheduleConfig { weeklyDefault: { - monday: DayWorkHours | 'off'; - tuesday: DayWorkHours | 'off'; - wednesday: DayWorkHours | 'off'; - thursday: DayWorkHours | 'off'; - friday: DayWorkHours | 'off'; - saturday: DayWorkHours | 'off'; - sunday: DayWorkHours | 'off'; + monday: IDayWorkHours | 'off'; + tuesday: IDayWorkHours | 'off'; + wednesday: IDayWorkHours | 'off'; + thursday: IDayWorkHours | 'off'; + friday: IDayWorkHours | 'off'; + saturday: IDayWorkHours | 'off'; + sunday: IDayWorkHours | 'off'; }; dateOverrides: { - [dateString: string]: DayWorkHours | 'off'; // YYYY-MM-DD format + [dateString: string]: IDayWorkHours | 'off'; // YYYY-MM-DD format }; } @@ -35,11 +35,11 @@ export interface WorkScheduleConfig { */ export class WorkHoursManager { private dateService: DateService; - private config: CalendarConfig; + private config: Configuration; private positionUtils: PositionUtils; - private workSchedule: WorkScheduleConfig; + private workSchedule: IWorkScheduleConfig; - constructor(dateService: DateService, config: CalendarConfig, positionUtils: PositionUtils) { + constructor(dateService: DateService, config: Configuration, positionUtils: PositionUtils) { this.dateService = dateService; this.config = config; this.positionUtils = positionUtils; @@ -66,7 +66,7 @@ export class WorkHoursManager { /** * Get work hours for a specific date */ - getWorkHoursForDate(date: Date): DayWorkHours | 'off' { + getWorkHoursForDate(date: Date): IDayWorkHours | 'off' { const dateString = this.dateService.formatISODate(date); // Check for date-specific override first @@ -82,8 +82,8 @@ export class WorkHoursManager { /** * Get work hours for multiple dates (used by GridManager) */ - getWorkHoursForDateRange(dates: Date[]): Map { - const workHoursMap = new Map(); + getWorkHoursForDateRange(dates: Date[]): Map { + const workHoursMap = new Map(); dates.forEach(date => { const dateString = this.dateService.formatISODate(date); @@ -97,7 +97,7 @@ export class WorkHoursManager { /** * Calculate CSS custom properties for non-work hour overlays using PositionUtils */ - calculateNonWorkHoursStyle(workHours: DayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null { + calculateNonWorkHoursStyle(workHours: IDayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null { if (workHours === 'off') { return null; // Full day will be colored via CSS background } @@ -121,7 +121,7 @@ export class WorkHoursManager { /** * Calculate CSS custom properties for work hours overlay using PositionUtils */ - calculateWorkHoursStyle(workHours: DayWorkHours | 'off'): { top: number; height: number } | null { + calculateWorkHoursStyle(workHours: IDayWorkHours | 'off'): { top: number; height: number } | null { if (workHours === 'off') { return null; } @@ -139,24 +139,24 @@ export class WorkHoursManager { /** * Load work schedule from JSON (future implementation) */ - async loadWorkSchedule(jsonData: WorkScheduleConfig): Promise { + async loadWorkSchedule(jsonData: IWorkScheduleConfig): Promise { this.workSchedule = jsonData; } /** * Get current work schedule configuration */ - getWorkSchedule(): WorkScheduleConfig { + getWorkSchedule(): IWorkScheduleConfig { return this.workSchedule; } /** * Convert Date to day name key */ - private getDayName(date: Date): keyof WorkScheduleConfig['weeklyDefault'] { - const dayNames: (keyof WorkScheduleConfig['weeklyDefault'])[] = [ + private getDayName(date: Date): keyof IWorkScheduleConfig['weeklyDefault'] { + const dayNames: (keyof IWorkScheduleConfig['weeklyDefault'])[] = [ 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' ]; return dayNames[date.getDay()]; } -} \ No newline at end of file +} diff --git a/src/renderers/AllDayEventRenderer.ts b/src/renderers/AllDayEventRenderer.ts index 7c6b8e3..60916eb 100644 --- a/src/renderers/AllDayEventRenderer.ts +++ b/src/renderers/AllDayEventRenderer.ts @@ -1,9 +1,9 @@ -import { CalendarEvent } from '../types/CalendarTypes'; +import { ICalendarEvent } from '../types/CalendarTypes'; import { SwpAllDayEventElement } from '../elements/SwpEventElement'; -import { EventLayout } from '../utils/AllDayLayoutEngine'; -import { ColumnBounds } from '../utils/ColumnDetectionUtils'; +import { IEventLayout } from '../utils/AllDayLayoutEngine'; +import { IColumnBounds } from '../utils/ColumnDetectionUtils'; import { EventManager } from '../managers/EventManager'; -import { DragStartEventPayload } from '../types/EventTypes'; +import { IDragStartEventPayload } from '../types/EventTypes'; import { IEventRenderer } from './EventRenderer'; export class AllDayEventRenderer { @@ -38,7 +38,7 @@ export class AllDayEventRenderer { /** * Handle drag start for all-day events */ - public handleDragStart(payload: DragStartEventPayload): void { + public handleDragStart(payload: IDragStartEventPayload): void { this.originalEvent = payload.originalElement;; this.draggedClone = payload.draggedClone; @@ -70,8 +70,8 @@ export class AllDayEventRenderer { * Render an all-day event with pre-calculated layout */ private renderAllDayEventWithLayout( - event: CalendarEvent, - layout: EventLayout + event: ICalendarEvent, + layout: IEventLayout ) { const container = this.getContainer(); if (!container) return null; @@ -109,7 +109,7 @@ export class AllDayEventRenderer { /** * Render all-day events for specific period using AllDayEventRenderer */ - public renderAllDayEventsForPeriod(eventLayouts: EventLayout[]): void { + public renderAllDayEventsForPeriod(eventLayouts: IEventLayout[]): void { this.clearAllDayEvents(); eventLayouts.forEach(layout => { diff --git a/src/renderers/ColumnRenderer.ts b/src/renderers/ColumnRenderer.ts index ba82248..61226f8 100644 --- a/src/renderers/ColumnRenderer.ts +++ b/src/renderers/ColumnRenderer.ts @@ -1,28 +1,28 @@ // Column rendering strategy interface and implementations -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { DateService } from '../utils/DateService'; import { WorkHoursManager } from '../managers/WorkHoursManager'; /** * Interface for column rendering strategies */ -export interface ColumnRenderer { - render(columnContainer: HTMLElement, context: ColumnRenderContext): void; +export interface IColumnRenderer { + render(columnContainer: HTMLElement, context: IColumnRenderContext): void; } /** * Context for column rendering */ -export interface ColumnRenderContext { +export interface IColumnRenderContext { currentWeek: Date; - config: CalendarConfig; + config: Configuration; } /** * Date-based column renderer (original functionality) */ -export class DateColumnRenderer implements ColumnRenderer { +export class DateColumnRenderer implements IColumnRenderer { private dateService: DateService; private workHoursManager: WorkHoursManager; @@ -34,7 +34,7 @@ export class DateColumnRenderer implements ColumnRenderer { this.workHoursManager = workHoursManager; } - render(columnContainer: HTMLElement, context: ColumnRenderContext): void { + render(columnContainer: HTMLElement, context: IColumnRenderContext): void { const { currentWeek, config } = context; const workWeekSettings = config.getWorkWeekSettings(); diff --git a/src/renderers/DateHeaderRenderer.ts b/src/renderers/DateHeaderRenderer.ts index ff18396..027354d 100644 --- a/src/renderers/DateHeaderRenderer.ts +++ b/src/renderers/DateHeaderRenderer.ts @@ -1,22 +1,22 @@ // Header rendering strategy interface and implementations -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { DateService } from '../utils/DateService'; /** * Interface for header rendering strategies */ export interface IHeaderRenderer { - render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; + render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void; } /** * Context for header rendering */ -export interface HeaderRenderContext { +export interface IHeaderRenderContext { currentWeek: Date; - config: CalendarConfig; + config: Configuration; } /** @@ -25,7 +25,7 @@ export interface HeaderRenderContext { export class DateHeaderRenderer implements IHeaderRenderer { private dateService!: DateService; - render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { + render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void { const { currentWeek, config } = context; // FIRST: Always create all-day container as part of standard header structure diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index f56515d..4e7b2a9 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -1,29 +1,29 @@ // Event rendering strategy interface and implementations -import { CalendarEvent } from '../types/CalendarTypes'; -import { CalendarConfig } from '../core/CalendarConfig'; +import { ICalendarEvent } from '../types/CalendarTypes'; +import { Configuration } from '../configuration/CalendarConfig'; import { SwpEventElement } from '../elements/SwpEventElement'; import { PositionUtils } from '../utils/PositionUtils'; -import { ColumnBounds } from '../utils/ColumnDetectionUtils'; -import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload, DragMouseEnterColumnEventPayload } from '../types/EventTypes'; +import { IColumnBounds } from '../utils/ColumnDetectionUtils'; +import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPayload, IDragMouseEnterColumnEventPayload } from '../types/EventTypes'; import { DateService } from '../utils/DateService'; import { EventStackManager } from '../managers/EventStackManager'; -import { EventLayoutCoordinator, GridGroupLayout, StackedEventLayout } from '../managers/EventLayoutCoordinator'; +import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator'; /** * Interface for event rendering strategies */ export interface IEventRenderer { - renderEvents(events: CalendarEvent[], container: HTMLElement): void; + renderEvents(events: ICalendarEvent[], container: HTMLElement): void; clearEvents(container?: HTMLElement): void; - handleDragStart?(payload: DragStartEventPayload): void; - handleDragMove?(payload: DragMoveEventPayload): void; + handleDragStart?(payload: IDragStartEventPayload): void; + handleDragMove?(payload: IDragMoveEventPayload): void; handleDragAutoScroll?(eventId: string, snappedY: number): void; - handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void; + handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void; handleEventClick?(eventId: string, originalElement: HTMLElement): void; - handleColumnChange?(payload: DragColumnChangeEventPayload): void; + handleColumnChange?(payload: IDragColumnChangeEventPayload): void; handleNavigationCompleted?(): void; - handleConvertAllDayToTimed?(payload: DragMouseEnterColumnEventPayload): void; + handleConvertAllDayToTimed?(payload: IDragMouseEnterColumnEventPayload): void; } /** @@ -34,7 +34,7 @@ export class DateEventRenderer implements IEventRenderer { private dateService: DateService; private stackManager: EventStackManager; private layoutCoordinator: EventLayoutCoordinator; - private config: CalendarConfig; + private config: Configuration; private positionUtils: PositionUtils; private draggedClone: HTMLElement | null = null; private originalEvent: HTMLElement | null = null; @@ -43,7 +43,7 @@ export class DateEventRenderer implements IEventRenderer { dateService: DateService, stackManager: EventStackManager, layoutCoordinator: EventLayoutCoordinator, - config: CalendarConfig, + config: Configuration, positionUtils: PositionUtils ) { this.dateService = dateService; @@ -63,7 +63,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Handle drag start event */ - public handleDragStart(payload: DragStartEventPayload): void { + public handleDragStart(payload: IDragStartEventPayload): void { this.originalEvent = payload.originalElement;; @@ -98,7 +98,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Handle drag move event */ - public handleDragMove(payload: DragMoveEventPayload): void { + public handleDragMove(payload: IDragMoveEventPayload): void { const swpEvent = payload.draggedClone as SwpEventElement; const columnDate = this.dateService.parseISO(payload.columnBounds!!.date); @@ -108,7 +108,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Handle column change during drag */ - public handleColumnChange(payload: DragColumnChangeEventPayload): void { + public handleColumnChange(payload: IDragColumnChangeEventPayload): void { const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer'); if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) { @@ -125,7 +125,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Handle conversion of all-day event to timed event */ - public handleConvertAllDayToTimed(payload: DragMouseEnterColumnEventPayload): void { + public handleConvertAllDayToTimed(payload: IDragMouseEnterColumnEventPayload): void { console.log('🎯 DateEventRenderer: Converting all-day to timed event', { eventId: payload.calendarEvent.id, @@ -165,7 +165,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Handle drag end event */ - public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void { + public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void { if (!draggedClone || !originalElement) { console.warn('Missing draggedClone or originalElement'); return; @@ -209,7 +209,7 @@ export class DateEventRenderer implements IEventRenderer { } - renderEvents(events: CalendarEvent[], container: HTMLElement): void { + renderEvents(events: ICalendarEvent[], container: HTMLElement): void { // Filter out all-day events - they should be handled by AllDayEventRenderer const timedEvents = events.filter(event => !event.allDay); @@ -229,7 +229,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Render events in a column using combined stacking + grid algorithm */ - private renderColumnEvents(columnEvents: CalendarEvent[], eventsLayer: HTMLElement): void { + private renderColumnEvents(columnEvents: ICalendarEvent[], eventsLayer: HTMLElement): void { if (columnEvents.length === 0) return; // Get layout from coordinator @@ -251,7 +251,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Render events in a grid container (side-by-side with column sharing) */ - private renderGridGroup(gridGroup: GridGroupLayout, eventsLayer: HTMLElement): void { + private renderGridGroup(gridGroup: IGridGroupLayout, eventsLayer: HTMLElement): void { const groupElement = document.createElement('swp-event-group'); // Add grid column class based on number of columns (not events) @@ -275,7 +275,7 @@ export class DateEventRenderer implements IEventRenderer { // Render each column const earliestEvent = gridGroup.events[0]; - gridGroup.columns.forEach(columnEvents => { + gridGroup.columns.forEach((columnEvents: ICalendarEvent[]) => { const columnContainer = this.renderGridColumn(columnEvents, earliestEvent.start); groupElement.appendChild(columnContainer); }); @@ -287,7 +287,7 @@ export class DateEventRenderer implements IEventRenderer { * Render a single column within a grid group * Column may contain multiple events that don't overlap */ - private renderGridColumn(columnEvents: CalendarEvent[], containerStart: Date): HTMLElement { + private renderGridColumn(columnEvents: ICalendarEvent[], containerStart: Date): HTMLElement { const columnContainer = document.createElement('div'); columnContainer.style.position = 'relative'; @@ -302,7 +302,7 @@ export class DateEventRenderer implements IEventRenderer { /** * Render event within a grid container (absolute positioning within column) */ - private renderEventInGrid(event: CalendarEvent, containerStart: Date): HTMLElement { + private renderEventInGrid(event: ICalendarEvent, containerStart: Date): HTMLElement { const element = SwpEventElement.fromCalendarEvent(event); // Calculate event height @@ -326,7 +326,7 @@ export class DateEventRenderer implements IEventRenderer { } - private renderEvent(event: CalendarEvent): HTMLElement { + private renderEvent(event: ICalendarEvent): HTMLElement { const element = SwpEventElement.fromCalendarEvent(event); // Apply positioning (moved from SwpEventElement.applyPositioning) @@ -340,7 +340,7 @@ export class DateEventRenderer implements IEventRenderer { return element; } - protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } { + protected calculateEventPosition(event: ICalendarEvent): { top: number; height: number } { // Delegate to PositionUtils for centralized position calculation return this.positionUtils.calculateEventPosition(event.start, event.end); } @@ -366,7 +366,7 @@ export class DateEventRenderer implements IEventRenderer { return Array.from(columns) as HTMLElement[]; } - protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] { + protected getEventsForColumn(column: HTMLElement, events: ICalendarEvent[]): ICalendarEvent[] { const columnDate = column.dataset.date; if (!columnDate) { return []; diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 8a34a7e..49260c7 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -1,12 +1,12 @@ import { EventBus } from '../core/EventBus'; -import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes'; +import { IEventBus, ICalendarEvent, IRenderContext } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { EventManager } from '../managers/EventManager'; import { IEventRenderer } from './EventRenderer'; import { SwpEventElement } from '../elements/SwpEventElement'; -import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragMouseEnterColumnEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload, ResizeEndEventPayload } from '../types/EventTypes'; +import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IHeaderReadyEventPayload, IResizeEndEventPayload } from '../types/EventTypes'; import { DateService } from '../utils/DateService'; -import { ColumnBounds } from '../utils/ColumnDetectionUtils'; +import { IColumnBounds } from '../utils/ColumnDetectionUtils'; /** * EventRenderingService - Render events i DOM med positionering using Strategy Pattern * Håndterer event positioning og overlap detection @@ -36,7 +36,7 @@ export class EventRenderingService { /** * Render events in a specific container for a given period */ - public renderEvents(context: RenderContext): void { + public renderEvents(context: IRenderContext): void { // Clear existing events in the specific container first this.strategy.clearEvents(context.container); @@ -133,7 +133,7 @@ export class EventRenderingService { private setupDragStartListener(): void { this.eventBus.on('drag:start', (event: Event) => { - const dragStartPayload = (event as CustomEvent).detail; + const dragStartPayload = (event as CustomEvent).detail; if (dragStartPayload.originalElement.hasAttribute('data-allday')) { return; @@ -147,7 +147,7 @@ export class EventRenderingService { private setupDragMoveListener(): void { this.eventBus.on('drag:move', (event: Event) => { - let dragEvent = (event as CustomEvent).detail; + let dragEvent = (event as CustomEvent).detail; if (dragEvent.draggedClone.hasAttribute('data-allday')) { return; @@ -161,7 +161,7 @@ export class EventRenderingService { private setupDragEndListener(): void { this.eventBus.on('drag:end', (event: Event) => { - const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent).detail; + const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent).detail; const finalColumn = finalPosition.column; const finalY = finalPosition.snappedY; const eventId = draggedElement.dataset.eventId || ''; @@ -207,7 +207,7 @@ export class EventRenderingService { private setupDragColumnChangeListener(): void { this.eventBus.on('drag:column-change', (event: Event) => { - let columnChangeEvent = (event as CustomEvent).detail; + let columnChangeEvent = (event as CustomEvent).detail; // Filter: Only handle events where clone is NOT an all-day event (normal timed events) if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) { @@ -223,7 +223,7 @@ export class EventRenderingService { private setupDragMouseLeaveHeaderListener(): void { this.dragMouseLeaveHeaderListener = (event: Event) => { - const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; + const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; if (cloneElement) cloneElement.style.display = ''; @@ -241,7 +241,7 @@ export class EventRenderingService { private setupDragMouseEnterColumnListener(): void { this.eventBus.on('drag:mouseenter-column', (event: Event) => { - const payload = (event as CustomEvent).detail; + const payload = (event as CustomEvent).detail; // Only handle if clone is an all-day event if (!payload.draggedClone.hasAttribute('data-allday')) { @@ -263,7 +263,7 @@ export class EventRenderingService { private setupResizeEndListener(): void { this.eventBus.on('resize:end', (event: Event) => { - const { eventId, element } = (event as CustomEvent).detail; + const { eventId, element } = (event as CustomEvent).detail; // Update event data in EventManager with new end time from resized element const swpEvent = element as SwpEventElement; @@ -306,7 +306,7 @@ export class EventRenderingService { /** * Re-render affected columns after drag to recalculate stacking/grouping */ - private reRenderAffectedColumns(sourceColumn: ColumnBounds | null, targetColumn: ColumnBounds | null): void { + private reRenderAffectedColumns(sourceColumn: IColumnBounds | null, targetColumn: IColumnBounds | null): void { const columnsToRender = new Set(); // Add source column if exists diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 1fe47b8..d070f97 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -1,6 +1,6 @@ -import { CalendarConfig } from '../core/CalendarConfig'; +import { Configuration } from '../configuration/CalendarConfig'; import { CalendarView } from '../types/CalendarTypes'; -import { ColumnRenderer, ColumnRenderContext } from './ColumnRenderer'; +import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; @@ -82,13 +82,13 @@ export class GridRenderer { private cachedGridContainer: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; private dateService: DateService; - private columnRenderer: ColumnRenderer; - private config: CalendarConfig; + private columnRenderer: IColumnRenderer; + private config: Configuration; constructor( - columnRenderer: ColumnRenderer, + columnRenderer: IColumnRenderer, dateService: DateService, - config: CalendarConfig + config: Configuration ) { this.dateService = dateService; this.columnRenderer = columnRenderer; @@ -255,7 +255,7 @@ export class GridRenderer { currentDate: Date, view: CalendarView ): void { - const context: ColumnRenderContext = { + const context: IColumnRenderContext = { currentWeek: currentDate, // ColumnRenderer expects currentWeek property config: this.config }; diff --git a/src/repositories/IEventRepository.ts b/src/repositories/IEventRepository.ts index df5d13b..d73949f 100644 --- a/src/repositories/IEventRepository.ts +++ b/src/repositories/IEventRepository.ts @@ -1,4 +1,4 @@ -import { CalendarEvent } from '../types/CalendarTypes'; +import { ICalendarEvent } from '../types/CalendarTypes'; /** * IEventRepository - Interface for event data loading @@ -13,8 +13,8 @@ import { CalendarEvent } from '../types/CalendarTypes'; export interface IEventRepository { /** * Load all calendar events from the data source - * @returns Promise resolving to array of CalendarEvent objects + * @returns Promise resolving to array of ICalendarEvent objects * @throws Error if loading fails */ - loadEvents(): Promise; + loadEvents(): Promise; } diff --git a/src/repositories/MockEventRepository.ts b/src/repositories/MockEventRepository.ts index 528ef79..662f661 100644 --- a/src/repositories/MockEventRepository.ts +++ b/src/repositories/MockEventRepository.ts @@ -1,4 +1,4 @@ -import { CalendarEvent } from '../types/CalendarTypes'; +import { ICalendarEvent } from '../types/CalendarTypes'; import { IEventRepository } from './IEventRepository'; interface RawEventData { @@ -23,7 +23,7 @@ interface RawEventData { export class MockEventRepository implements IEventRepository { private readonly dataUrl = 'data/mock-events.json'; - public async loadEvents(): Promise { + public async loadEvents(): Promise { try { const response = await fetch(this.dataUrl); @@ -40,8 +40,8 @@ export class MockEventRepository implements IEventRepository { } } - private processCalendarData(data: RawEventData[]): CalendarEvent[] { - return data.map((event): CalendarEvent => ({ + private processCalendarData(data: RawEventData[]): ICalendarEvent[] { + return data.map((event): ICalendarEvent => ({ ...event, start: new Date(event.start), end: new Date(event.end), diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts index bba326b..77dbd8c 100644 --- a/src/types/CalendarTypes.ts +++ b/src/types/CalendarTypes.ts @@ -8,13 +8,13 @@ export type CalendarView = ViewPeriod; export type SyncStatus = 'synced' | 'pending' | 'error'; -export interface RenderContext { +export interface IRenderContext { container: HTMLElement; startDate: Date; endDate: Date; } -export interface CalendarEvent { +export interface ICalendarEvent { id: string; title: string; start: Date; @@ -55,13 +55,13 @@ export interface ICalendarConfig { maxEventDuration: number; // Minutes } -export interface EventLogEntry { +export interface IEventLogEntry { type: string; detail: unknown; timestamp: number; } -export interface ListenerEntry { +export interface IListenerEntry { eventType: string; handler: EventListener; options?: AddEventListenerOptions; @@ -72,6 +72,6 @@ export interface IEventBus { once(eventType: string, handler: EventListener): () => void; off(eventType: string, handler: EventListener): void; emit(eventType: string, detail?: unknown): boolean; - getEventLog(eventType?: string): EventLogEntry[]; + getEventLog(eventType?: string): IEventLogEntry[]; setDebug(enabled: boolean): void; } \ No newline at end of file diff --git a/src/types/DragDropTypes.ts b/src/types/DragDropTypes.ts index 1297d83..fe9ce7b 100644 --- a/src/types/DragDropTypes.ts +++ b/src/types/DragDropTypes.ts @@ -2,46 +2,46 @@ * Type definitions for drag and drop functionality */ -export interface MousePosition { +export interface IMousePosition { x: number; y: number; clientX?: number; clientY?: number; } -export interface DragOffset { +export interface IDragOffset { x: number; y: number; offsetX?: number; offsetY?: number; } -export interface DragState { +export interface IDragState { isDragging: boolean; draggedElement: HTMLElement | null; draggedClone: HTMLElement | null; eventId: string | null; startColumn: string | null; currentColumn: string | null; - mouseOffset: DragOffset; + mouseOffset: IDragOffset; } -export interface DragEndPosition { +export interface IDragEndPosition { column: string; y: number; snappedY: number; time?: Date; } -export interface StackLinkData { +export interface IStackLinkData { prev?: string; next?: string; isFirst?: boolean; isLast?: boolean; } -export interface DragEventHandlers { - handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void; - handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void; +export interface IDragEventHandlers { + handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: IDragOffset, column: string): void; + handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: IDragOffset): void; handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void; } \ No newline at end of file diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index 2a9a5aa..45daa17 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -2,8 +2,8 @@ * Type definitions for calendar events and drag operations */ -import { ColumnBounds } from "../utils/ColumnDetectionUtils"; -import { CalendarEvent } from "./CalendarTypes"; +import { IColumnBounds } from "../utils/ColumnDetectionUtils"; +import { ICalendarEvent } from "./CalendarTypes"; /** * Drag Event Payload Interfaces @@ -11,89 +11,89 @@ import { CalendarEvent } from "./CalendarTypes"; */ // Common position interface -export interface MousePosition { +export interface IMousePosition { x: number; y: number; } // Drag start event payload -export interface DragStartEventPayload { +export interface IDragStartEventPayload { originalElement: HTMLElement; draggedClone: HTMLElement | null; - mousePosition: MousePosition; - mouseOffset: MousePosition; - columnBounds: ColumnBounds | null; + mousePosition: IMousePosition; + mouseOffset: IMousePosition; + columnBounds: IColumnBounds | null; } // Drag move event payload -export interface DragMoveEventPayload { +export interface IDragMoveEventPayload { originalElement: HTMLElement; draggedClone: HTMLElement; - mousePosition: MousePosition; - mouseOffset: MousePosition; - columnBounds: ColumnBounds | null; + mousePosition: IMousePosition; + mouseOffset: IMousePosition; + columnBounds: IColumnBounds | null; snappedY: number; } // Drag end event payload -export interface DragEndEventPayload { +export interface IDragEndEventPayload { originalElement: HTMLElement; draggedClone: HTMLElement | null; - mousePosition: MousePosition; - sourceColumn: ColumnBounds; + mousePosition: IMousePosition; + sourceColumn: IColumnBounds; finalPosition: { - column: ColumnBounds | null; // Where drag ended + column: IColumnBounds | null; // Where drag ended snappedY: number; }; target: 'swp-day-column' | 'swp-day-header' | null; } // Drag mouse enter header event payload -export interface DragMouseEnterHeaderEventPayload { - targetColumn: ColumnBounds; - mousePosition: MousePosition; +export interface IDragMouseEnterHeaderEventPayload { + targetColumn: IColumnBounds; + mousePosition: IMousePosition; originalElement: HTMLElement | null; draggedClone: HTMLElement; - calendarEvent: CalendarEvent; + calendarEvent: ICalendarEvent; replaceClone: (newClone: HTMLElement) => void; } // Drag mouse leave header event payload -export interface DragMouseLeaveHeaderEventPayload { +export interface IDragMouseLeaveHeaderEventPayload { targetDate: string | null; - mousePosition: MousePosition; + mousePosition: IMousePosition; originalElement: HTMLElement| null; draggedClone: HTMLElement| null; } // Drag mouse enter column event payload -export interface DragMouseEnterColumnEventPayload { - targetColumn: ColumnBounds; - mousePosition: MousePosition; +export interface IDragMouseEnterColumnEventPayload { + targetColumn: IColumnBounds; + mousePosition: IMousePosition; snappedY: number; originalElement: HTMLElement | null; draggedClone: HTMLElement; - calendarEvent: CalendarEvent; + calendarEvent: ICalendarEvent; replaceClone: (newClone: HTMLElement) => void; } // Drag column change event payload -export interface DragColumnChangeEventPayload { +export interface IDragColumnChangeEventPayload { originalElement: HTMLElement; draggedClone: HTMLElement; - previousColumn: ColumnBounds | null; - newColumn: ColumnBounds; - mousePosition: MousePosition; + previousColumn: IColumnBounds | null; + newColumn: IColumnBounds; + mousePosition: IMousePosition; } // Header ready event payload -export interface HeaderReadyEventPayload { - headerElements: ColumnBounds[]; +export interface IHeaderReadyEventPayload { + headerElements: IColumnBounds[]; } // Resize end event payload -export interface ResizeEndEventPayload { +export interface IResizeEndEventPayload { eventId: string; element: HTMLElement; finalHeight: number; diff --git a/src/types/ManagerTypes.ts b/src/types/ManagerTypes.ts index 33f7b8d..ca2fe64 100644 --- a/src/types/ManagerTypes.ts +++ b/src/types/ManagerTypes.ts @@ -1,19 +1,19 @@ -import { IEventBus, CalendarEvent, CalendarView } from './CalendarTypes'; +import { IEventBus, ICalendarEvent, CalendarView } from './CalendarTypes'; /** * Complete type definition for all managers returned by ManagerFactory */ -export interface CalendarManagers { - eventManager: EventManager; - eventRenderer: EventRenderingService; - gridManager: GridManager; - scrollManager: ScrollManager; +export interface ICalendarManagers { + eventManager: IEventManager; + eventRenderer: IEventRenderingService; + gridManager: IGridManager; + scrollManager: IScrollManager; navigationManager: unknown; // Avoid interface conflicts - viewManager: ViewManager; - calendarManager: CalendarManager; + viewManager: IViewManager; + calendarManager: ICalendarManager; dragDropManager: unknown; // Avoid interface conflicts allDayManager: unknown; // Avoid interface conflicts - resizeHandleManager: ResizeHandleManager; + resizeHandleManager: IResizeHandleManager; edgeScrollManager: unknown; // Avoid interface conflicts dragHoverManager: unknown; // Avoid interface conflicts headerManager: unknown; // Avoid interface conflicts @@ -27,50 +27,50 @@ interface IManager { refresh?(): void; } -export interface EventManager extends IManager { +export interface IEventManager extends IManager { loadData(): Promise; - getEvents(): CalendarEvent[]; - getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[]; + getEvents(): ICalendarEvent[]; + getEventsForPeriod(startDate: Date, endDate: Date): ICalendarEvent[]; navigateToEvent(eventId: string): boolean; } -export interface EventRenderingService extends IManager { +export interface IEventRenderingService extends IManager { // EventRenderingService doesn't have a render method in current implementation } -export interface GridManager extends IManager { +export interface IGridManager extends IManager { render(): Promise; getDisplayDates(): Date[]; } -export interface ScrollManager extends IManager { +export interface IScrollManager extends IManager { scrollTo(scrollTop: number): void; scrollToHour(hour: number): void; } // Use a more flexible interface that matches actual implementation -export interface NavigationManager extends IManager { +export interface INavigationManager extends IManager { [key: string]: unknown; // Allow any properties from actual implementation } -export interface ViewManager extends IManager { +export interface IViewManager extends IManager { // ViewManager doesn't have setView in current implementation getCurrentView?(): CalendarView; } -export interface CalendarManager extends IManager { +export interface ICalendarManager extends IManager { setView(view: CalendarView): void; setCurrentDate(date: Date): void; } -export interface DragDropManager extends IManager { +export interface IDragDropManager extends IManager { // DragDropManager has different interface in current implementation } -export interface AllDayManager extends IManager { +export interface IAllDayManager extends IManager { [key: string]: unknown; // Allow any properties from actual implementation } -export interface ResizeHandleManager extends IManager { +export interface IResizeHandleManager extends IManager { // ResizeHandleManager handles hover effects for resize handles } diff --git a/src/utils/AllDayLayoutEngine.ts b/src/utils/AllDayLayoutEngine.ts index ac8bad8..a43f8e3 100644 --- a/src/utils/AllDayLayoutEngine.ts +++ b/src/utils/AllDayLayoutEngine.ts @@ -1,142 +1,142 @@ -import { CalendarEvent } from '../types/CalendarTypes'; - -export interface EventLayout { - calenderEvent: CalendarEvent; - gridArea: string; // "row-start / col-start / row-end / col-end" - startColumn: number; - endColumn: number; - row: number; - columnSpan: number; -} - -export class AllDayLayoutEngine { - private weekDates: string[]; - private tracks: boolean[][]; - - constructor(weekDates: string[]) { - this.weekDates = weekDates; - this.tracks = []; - } - - /** - * Calculate layout for all events using clean day-based logic - */ - public calculateLayout(events: CalendarEvent[]): EventLayout[] { - - let layouts: EventLayout[] = []; - // Reset tracks for new calculation - this.tracks = [new Array(this.weekDates.length).fill(false)]; - - // Filter to only visible events - const visibleEvents = events.filter(event => this.isEventVisible(event)); - - // Process events in input order (no sorting) - for (const event of visibleEvents) { - const startDay = this.getEventStartDay(event); - const endDay = this.getEventEndDay(event); - - if (startDay > 0 && endDay > 0) { - const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks - - // Mark days as occupied - for (let day = startDay - 1; day <= endDay - 1; day++) { - this.tracks[track][day] = true; - } - - const layout: EventLayout = { - calenderEvent: event, - gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`, - startColumn: startDay, - endColumn: endDay, - row: track + 1, - columnSpan: endDay - startDay + 1 - }; - layouts.push(layout); - - } - } - - return layouts; - } - - /** - * Find available track for event spanning from startDay to endDay (0-based indices) - */ - private findAvailableTrack(startDay: number, endDay: number): number { - for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) { - if (this.isTrackAvailable(trackIndex, startDay, endDay)) { - return trackIndex; - } - } - - // Create new track if none available - this.tracks.push(new Array(this.weekDates.length).fill(false)); - return this.tracks.length - 1; - } - - /** - * Check if track is available for the given day range (0-based indices) - */ - private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean { - for (let day = startDay; day <= endDay; day++) { - if (this.tracks[trackIndex][day]) { - return false; - } - } - return true; - } - - /** - * Get start day index for event (1-based, 0 if not visible) - */ - private getEventStartDay(event: CalendarEvent): number { - const eventStartDate = this.formatDate(event.start); - const firstVisibleDate = this.weekDates[0]; - - // If event starts before visible range, clip to first visible day - const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate; - - const dayIndex = this.weekDates.indexOf(clippedStartDate); - return dayIndex >= 0 ? dayIndex + 1 : 0; - } - - /** - * Get end day index for event (1-based, 0 if not visible) - */ - private getEventEndDay(event: CalendarEvent): number { - const eventEndDate = this.formatDate(event.end); - const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; - - // If event ends after visible range, clip to last visible day - const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate; - - const dayIndex = this.weekDates.indexOf(clippedEndDate); - return dayIndex >= 0 ? dayIndex + 1 : 0; - } - - /** - * Check if event is visible in the current date range - */ - private isEventVisible(event: CalendarEvent): boolean { - if (this.weekDates.length === 0) return false; - - const eventStartDate = this.formatDate(event.start); - const eventEndDate = this.formatDate(event.end); - const firstVisibleDate = this.weekDates[0]; - const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; - - // Event overlaps if it doesn't end before visible range starts - // AND doesn't start after visible range ends - return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate); - } - - /** - * Format date to YYYY-MM-DD string using local date - */ - private formatDate(date: Date): string { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - } +import { ICalendarEvent } from '../types/CalendarTypes'; + +export interface IEventLayout { + calenderEvent: ICalendarEvent; + gridArea: string; // "row-start / col-start / row-end / col-end" + startColumn: number; + endColumn: number; + row: number; + columnSpan: number; +} + +export class AllDayLayoutEngine { + private weekDates: string[]; + private tracks: boolean[][]; + + constructor(weekDates: string[]) { + this.weekDates = weekDates; + this.tracks = []; + } + + /** + * Calculate layout for all events using clean day-based logic + */ + public calculateLayout(events: ICalendarEvent[]): IEventLayout[] { + + let layouts: IEventLayout[] = []; + // Reset tracks for new calculation + this.tracks = [new Array(this.weekDates.length).fill(false)]; + + // Filter to only visible events + const visibleEvents = events.filter(event => this.isEventVisible(event)); + + // Process events in input order (no sorting) + for (const event of visibleEvents) { + const startDay = this.getEventStartDay(event); + const endDay = this.getEventEndDay(event); + + if (startDay > 0 && endDay > 0) { + const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks + + // Mark days as occupied + for (let day = startDay - 1; day <= endDay - 1; day++) { + this.tracks[track][day] = true; + } + + const layout: IEventLayout = { + calenderEvent: event, + gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`, + startColumn: startDay, + endColumn: endDay, + row: track + 1, + columnSpan: endDay - startDay + 1 + }; + layouts.push(layout); + + } + } + + return layouts; + } + + /** + * Find available track for event spanning from startDay to endDay (0-based indices) + */ + private findAvailableTrack(startDay: number, endDay: number): number { + for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) { + if (this.isTrackAvailable(trackIndex, startDay, endDay)) { + return trackIndex; + } + } + + // Create new track if none available + this.tracks.push(new Array(this.weekDates.length).fill(false)); + return this.tracks.length - 1; + } + + /** + * Check if track is available for the given day range (0-based indices) + */ + private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean { + for (let day = startDay; day <= endDay; day++) { + if (this.tracks[trackIndex][day]) { + return false; + } + } + return true; + } + + /** + * Get start day index for event (1-based, 0 if not visible) + */ + private getEventStartDay(event: ICalendarEvent): number { + const eventStartDate = this.formatDate(event.start); + const firstVisibleDate = this.weekDates[0]; + + // If event starts before visible range, clip to first visible day + const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate; + + const dayIndex = this.weekDates.indexOf(clippedStartDate); + return dayIndex >= 0 ? dayIndex + 1 : 0; + } + + /** + * Get end day index for event (1-based, 0 if not visible) + */ + private getEventEndDay(event: ICalendarEvent): number { + const eventEndDate = this.formatDate(event.end); + const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; + + // If event ends after visible range, clip to last visible day + const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate; + + const dayIndex = this.weekDates.indexOf(clippedEndDate); + return dayIndex >= 0 ? dayIndex + 1 : 0; + } + + /** + * Check if event is visible in the current date range + */ + private isEventVisible(event: ICalendarEvent): boolean { + if (this.weekDates.length === 0) return false; + + const eventStartDate = this.formatDate(event.start); + const eventEndDate = this.formatDate(event.end); + const firstVisibleDate = this.weekDates[0]; + const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; + + // Event overlaps if it doesn't end before visible range starts + // AND doesn't start after visible range ends + return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate); + } + + /** + * Format date to YYYY-MM-DD string using local date + */ + private formatDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } } \ No newline at end of file diff --git a/src/utils/ColumnDetectionUtils.ts b/src/utils/ColumnDetectionUtils.ts index 1024dd3..148015a 100644 --- a/src/utils/ColumnDetectionUtils.ts +++ b/src/utils/ColumnDetectionUtils.ts @@ -1,118 +1,118 @@ -/** - * ColumnDetectionUtils - Shared utility for column detection and caching - * Used by both DragDropManager and AllDayManager for consistent column detection - */ - -import { MousePosition } from "../types/DragDropTypes"; - - -export interface ColumnBounds { - date: string; - left: number; - right: number; - boundingClientRect: DOMRect, - element : HTMLElement, - index: number -} - -export class ColumnDetectionUtils { - private static columnBoundsCache: ColumnBounds[] = []; - - /** - * Update column bounds cache for coordinate-based column detection - */ - public static updateColumnBoundsCache(): void { - // Reset cache - this.columnBoundsCache = []; - - // Find alle kolonner - const columns = document.querySelectorAll('swp-day-column'); - let index = 1; - // Cache hver kolonnes x-grænser - columns.forEach(column => { - const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; - - if (date) { - this.columnBoundsCache.push({ - boundingClientRect : rect, - element: column as HTMLElement, - date, - left: rect.left, - right: rect.right, - index: index++ - }); - } - }); - - // Sorter efter x-position (fra venstre til højre) - this.columnBoundsCache.sort((a, b) => a.left - b.left); - } - - /** - * Get column date from X coordinate using cached bounds - */ - public static getColumnBounds(position: MousePosition): ColumnBounds | null{ - if (this.columnBoundsCache.length === 0) { - this.updateColumnBoundsCache(); - } - - // Find den kolonne hvor x-koordinaten er indenfor grænserne - let column = this.columnBoundsCache.find(col => - position.x >= col.left && position.x <= col.right - ); - if (column) - return column; - - return null; - } - - /** - * Get column bounds by Date - */ - public static getColumnBoundsByDate(date: Date): ColumnBounds | null { - if (this.columnBoundsCache.length === 0) { - this.updateColumnBoundsCache(); - } - - // Convert Date to YYYY-MM-DD format - let dateString = date.toISOString().split('T')[0]; - - // Find column that matches the date - let column = this.columnBoundsCache.find(col => col.date === dateString); - return column || null; - } - - - public static getColumns(): ColumnBounds[] { - return [...this.columnBoundsCache]; - } - public static getHeaderColumns(): ColumnBounds[] { - - let dayHeaders: ColumnBounds[] = []; - - const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header'); - let index = 1; - // Cache hver kolonnes x-grænser - dayColumns.forEach(column => { - const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; - - if (date) { - dayHeaders.push({ - boundingClientRect : rect, - element: column as HTMLElement, - date, - left: rect.left, - right: rect.right, - index: index++ - }); - } - }); - - // Sorter efter x-position (fra venstre til højre) - dayHeaders.sort((a, b) => a.left - b.left); - return dayHeaders; - - } +/** + * ColumnDetectionUtils - Shared utility for column detection and caching + * Used by both DragDropManager and AllDayManager for consistent column detection + */ + +import { IMousePosition } from "../types/DragDropTypes"; + + +export interface IColumnBounds { + date: string; + left: number; + right: number; + boundingClientRect: DOMRect, + element : HTMLElement, + index: number +} + +export class ColumnDetectionUtils { + private static columnBoundsCache: IColumnBounds[] = []; + + /** + * Update column bounds cache for coordinate-based column detection + */ + public static updateColumnBoundsCache(): void { + // Reset cache + this.columnBoundsCache = []; + + // Find alle kolonner + const columns = document.querySelectorAll('swp-day-column'); + let index = 1; + // Cache hver kolonnes x-grænser + columns.forEach(column => { + const rect = column.getBoundingClientRect(); + const date = (column as HTMLElement).dataset.date; + + if (date) { + this.columnBoundsCache.push({ + boundingClientRect : rect, + element: column as HTMLElement, + date, + left: rect.left, + right: rect.right, + index: index++ + }); + } + }); + + // Sorter efter x-position (fra venstre til højre) + this.columnBoundsCache.sort((a, b) => a.left - b.left); + } + + /** + * Get column date from X coordinate using cached bounds + */ + public static getColumnBounds(position: IMousePosition): IColumnBounds | null{ + if (this.columnBoundsCache.length === 0) { + this.updateColumnBoundsCache(); + } + + // Find den kolonne hvor x-koordinaten er indenfor grænserne + let column = this.columnBoundsCache.find(col => + position.x >= col.left && position.x <= col.right + ); + if (column) + return column; + + return null; + } + + /** + * Get column bounds by Date + */ + public static getColumnBoundsByDate(date: Date): IColumnBounds | null { + if (this.columnBoundsCache.length === 0) { + this.updateColumnBoundsCache(); + } + + // Convert Date to YYYY-MM-DD format + let dateString = date.toISOString().split('T')[0]; + + // Find column that matches the date + let column = this.columnBoundsCache.find(col => col.date === dateString); + return column || null; + } + + + public static getColumns(): IColumnBounds[] { + return [...this.columnBoundsCache]; + } + public static getHeaderColumns(): IColumnBounds[] { + + let dayHeaders: IColumnBounds[] = []; + + const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header'); + let index = 1; + // Cache hver kolonnes x-grænser + dayColumns.forEach(column => { + const rect = column.getBoundingClientRect(); + const date = (column as HTMLElement).dataset.date; + + if (date) { + dayHeaders.push({ + boundingClientRect : rect, + element: column as HTMLElement, + date, + left: rect.left, + right: rect.right, + index: index++ + }); + } + }); + + // Sorter efter x-position (fra venstre til højre) + dayHeaders.sort((a, b) => a.left - b.left); + return dayHeaders; + + } } \ No newline at end of file diff --git a/src/utils/DateService.ts b/src/utils/DateService.ts index 5059955..14723c7 100644 --- a/src/utils/DateService.ts +++ b/src/utils/DateService.ts @@ -1,498 +1,498 @@ -/** - * DateService - Unified date/time service using date-fns - * Handles all date operations, timezone conversions, and formatting - */ - -import { - format, - parse, - addMinutes, - differenceInMinutes, - startOfDay, - endOfDay, - setHours, - setMinutes as setMins, - getHours, - getMinutes, - parseISO, - isValid, - addDays, - startOfWeek, - endOfWeek, - addWeeks, - addMonths, - isSameDay, - getISOWeek -} from 'date-fns'; -import { - toZonedTime, - fromZonedTime, - formatInTimeZone -} from 'date-fns-tz'; -import { CalendarConfig } from '../core/CalendarConfig'; - -export class DateService { - private timezone: string; - - constructor(config: CalendarConfig) { - this.timezone = config.getTimezone(); - } - - // ============================================ - // CORE CONVERSIONS - // ============================================ - - /** - * Convert local date to UTC ISO string - * @param localDate - Date in local timezone - * @returns ISO string in UTC (with 'Z' suffix) - */ - public toUTC(localDate: Date): string { - return fromZonedTime(localDate, this.timezone).toISOString(); - } - - /** - * Convert UTC ISO string to local date - * @param utcString - ISO string in UTC - * @returns Date in local timezone - */ - public fromUTC(utcString: string): Date { - return toZonedTime(parseISO(utcString), this.timezone); - } - - // ============================================ - // FORMATTING - // ============================================ - - /** - * Format time as HH:mm or HH:mm:ss - * @param date - Date to format - * @param showSeconds - Include seconds in output - * @returns Formatted time string - */ - public formatTime(date: Date, showSeconds = false): string { - const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm'; - return format(date, pattern); - } - - /** - * Format time range as "HH:mm - HH:mm" - * @param start - Start date - * @param end - End date - * @returns Formatted time range - */ - public formatTimeRange(start: Date, end: Date): string { - return `${this.formatTime(start)} - ${this.formatTime(end)}`; - } - - /** - * Format date and time in technical format: yyyy-MM-dd HH:mm:ss - * @param date - Date to format - * @returns Technical datetime string - */ - public formatTechnicalDateTime(date: Date): string { - return format(date, 'yyyy-MM-dd HH:mm:ss'); - } - - /** - * Format date as yyyy-MM-dd - * @param date - Date to format - * @returns ISO date string - */ - public formatDate(date: Date): string { - return format(date, 'yyyy-MM-dd'); - } - - /** - * Format date as "Month Year" (e.g., "January 2025") - * @param date - Date to format - * @param locale - Locale for month name (default: 'en-US') - * @returns Formatted month and year - */ - public formatMonthYear(date: Date, locale: string = 'en-US'): string { - return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' }); - } - - /** - * Format date as ISO string (same as formatDate for compatibility) - * @param date - Date to format - * @returns ISO date string - */ - public formatISODate(date: Date): string { - return this.formatDate(date); - } - - /** - * Format time in 12-hour format with AM/PM - * @param date - Date to format - * @returns Time string in 12-hour format (e.g., "2:30 PM") - */ - public formatTime12(date: Date): string { - const hours = getHours(date); - const minutes = getMinutes(date); - const period = hours >= 12 ? 'PM' : 'AM'; - const displayHours = hours % 12 || 12; - - return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; - } - - /** - * Get day name for a date - * @param date - Date to get day name for - * @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday') - * @param locale - Locale for day name (default: 'da-DK') - * @returns Day name - */ - public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string { - const formatter = new Intl.DateTimeFormat(locale, { - weekday: format - }); - return formatter.format(date); - } - - /** - * Format a date range with customizable options - * @param start - Start date - * @param end - End date - * @param options - Formatting options - * @returns Formatted date range string - */ - public formatDateRange( - start: Date, - end: Date, - options: { - locale?: string; - month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; - day?: 'numeric' | '2-digit'; - year?: 'numeric' | '2-digit'; - } = {} - ): string { - const { locale = 'en-US', month = 'short', day = 'numeric' } = options; - - const startYear = start.getFullYear(); - const endYear = end.getFullYear(); - - const formatter = new Intl.DateTimeFormat(locale, { - month, - day, - year: startYear !== endYear ? 'numeric' : undefined - }); - - // @ts-ignore - formatRange is available in modern browsers - if (typeof formatter.formatRange === 'function') { - // @ts-ignore - return formatter.formatRange(start, end); - } - - return `${formatter.format(start)} - ${formatter.format(end)}`; - } - - // ============================================ - // TIME CALCULATIONS - // ============================================ - - /** - * Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight - * @param timeString - Time in format HH:mm or HH:mm:ss - * @returns Total minutes since midnight - */ - public timeToMinutes(timeString: string): number { - const parts = timeString.split(':').map(Number); - const hours = parts[0] || 0; - const minutes = parts[1] || 0; - return hours * 60 + minutes; - } - - /** - * Convert total minutes since midnight to time string HH:mm - * @param totalMinutes - Minutes since midnight - * @returns Time string in format HH:mm - */ - public minutesToTime(totalMinutes: number): string { - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - const date = setMins(setHours(new Date(), hours), minutes); - return format(date, 'HH:mm'); - } - - /** - * Format time from total minutes (alias for minutesToTime) - * @param totalMinutes - Minutes since midnight - * @returns Time string in format HH:mm - */ - public formatTimeFromMinutes(totalMinutes: number): string { - return this.minutesToTime(totalMinutes); - } - - /** - * Get minutes since midnight for a given date - * @param date - Date to calculate from - * @returns Minutes since midnight - */ - public getMinutesSinceMidnight(date: Date): number { - return getHours(date) * 60 + getMinutes(date); - } - - /** - * Calculate duration in minutes between two dates - * @param start - Start date or ISO string - * @param end - End date or ISO string - * @returns Duration in minutes - */ - public getDurationMinutes(start: Date | string, end: Date | string): number { - const startDate = typeof start === 'string' ? parseISO(start) : start; - const endDate = typeof end === 'string' ? parseISO(end) : end; - return differenceInMinutes(endDate, startDate); - } - - // ============================================ - // WEEK OPERATIONS - // ============================================ - - /** - * Get start and end of week (Monday to Sunday) - * @param date - Reference date - * @returns Object with start and end dates - */ - public getWeekBounds(date: Date): { start: Date; end: Date } { - return { - start: startOfWeek(date, { weekStartsOn: 1 }), // Monday - end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday - }; - } - - /** - * Add weeks to a date - * @param date - Base date - * @param weeks - Number of weeks to add (can be negative) - * @returns New date - */ - public addWeeks(date: Date, weeks: number): Date { - return addWeeks(date, weeks); - } - - /** - * Add months to a date - * @param date - Base date - * @param months - Number of months to add (can be negative) - * @returns New date - */ - public addMonths(date: Date, months: number): Date { - return addMonths(date, months); - } - - /** - * Get ISO week number (1-53) - * @param date - Date to get week number for - * @returns ISO week number - */ - public getWeekNumber(date: Date): number { - return getISOWeek(date); - } - - /** - * Get all dates in a full week (7 days starting from given date) - * @param weekStart - Start date of the week - * @returns Array of 7 dates - */ - public getFullWeekDates(weekStart: Date): Date[] { - const dates: Date[] = []; - for (let i = 0; i < 7; i++) { - dates.push(this.addDays(weekStart, i)); - } - return dates; - } - - /** - * Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7) - * @param weekStart - Any date in the week - * @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday) - * @returns Array of dates for the specified work days - */ - public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] { - const dates: Date[] = []; - - // Get Monday of the week - const weekBounds = this.getWeekBounds(weekStart); - const mondayOfWeek = this.startOfDay(weekBounds.start); - - // Calculate dates for each work day using ISO numbering - workDays.forEach(isoDay => { - const date = new Date(mondayOfWeek); - // ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days - const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; - date.setDate(mondayOfWeek.getDate() + daysFromMonday); - dates.push(date); - }); - - return dates; - } - - // ============================================ - // GRID HELPERS - // ============================================ - - /** - * Create a date at a specific time (minutes since midnight) - * @param baseDate - Base date (date component) - * @param totalMinutes - Minutes since midnight - * @returns New date with specified time - */ - public createDateAtTime(baseDate: Date, totalMinutes: number): Date { - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - return setMins(setHours(startOfDay(baseDate), hours), minutes); - } - - /** - * Snap date to nearest interval - * @param date - Date to snap - * @param intervalMinutes - Snap interval in minutes - * @returns Snapped date - */ - public snapToInterval(date: Date, intervalMinutes: number): Date { - const minutes = this.getMinutesSinceMidnight(date); - const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes; - return this.createDateAtTime(date, snappedMinutes); - } - - // ============================================ - // UTILITY METHODS - // ============================================ - - /** - * Check if two dates are the same day - * @param date1 - First date - * @param date2 - Second date - * @returns True if same day - */ - public isSameDay(date1: Date, date2: Date): boolean { - return isSameDay(date1, date2); - } - - /** - * Get start of day - * @param date - Date - * @returns Start of day (00:00:00) - */ - public startOfDay(date: Date): Date { - return startOfDay(date); - } - - /** - * Get end of day - * @param date - Date - * @returns End of day (23:59:59.999) - */ - public endOfDay(date: Date): Date { - return endOfDay(date); - } - - /** - * Add days to a date - * @param date - Base date - * @param days - Number of days to add (can be negative) - * @returns New date - */ - public addDays(date: Date, days: number): Date { - return addDays(date, days); - } - - /** - * Add minutes to a date - * @param date - Base date - * @param minutes - Number of minutes to add (can be negative) - * @returns New date - */ - public addMinutes(date: Date, minutes: number): Date { - return addMinutes(date, minutes); - } - - /** - * Parse ISO string to date - * @param isoString - ISO date string - * @returns Parsed date - */ - public parseISO(isoString: string): Date { - return parseISO(isoString); - } - - /** - * Check if date is valid - * @param date - Date to check - * @returns True if valid - */ - public isValid(date: Date): boolean { - return isValid(date); - } - - /** - * Validate date range (start must be before or equal to end) - * @param start - Start date - * @param end - End date - * @returns True if valid range - */ - public isValidRange(start: Date, end: Date): boolean { - if (!this.isValid(start) || !this.isValid(end)) { - return false; - } - return start.getTime() <= end.getTime(); - } - - /** - * Check if date is within reasonable bounds (1900-2100) - * @param date - Date to check - * @returns True if within bounds - */ - public isWithinBounds(date: Date): boolean { - if (!this.isValid(date)) { - return false; - } - const year = date.getFullYear(); - return year >= 1900 && year <= 2100; - } - - /** - * Validate date with comprehensive checks - * @param date - Date to validate - * @param options - Validation options - * @returns Validation result with error message - */ - public validateDate( - date: Date, - options: { - requireFuture?: boolean; - requirePast?: boolean; - minDate?: Date; - maxDate?: Date; - } = {} - ): { valid: boolean; error?: string } { - if (!this.isValid(date)) { - return { valid: false, error: 'Invalid date' }; - } - - if (!this.isWithinBounds(date)) { - return { valid: false, error: 'Date out of bounds (1900-2100)' }; - } - - const now = new Date(); - - if (options.requireFuture && date <= now) { - return { valid: false, error: 'Date must be in the future' }; - } - - if (options.requirePast && date >= now) { - return { valid: false, error: 'Date must be in the past' }; - } - - if (options.minDate && date < options.minDate) { - return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` }; - } - - if (options.maxDate && date > options.maxDate) { - return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` }; - } - - return { valid: true }; - } +/** + * DateService - Unified date/time service using date-fns + * Handles all date operations, timezone conversions, and formatting + */ + +import { + format, + parse, + addMinutes, + differenceInMinutes, + startOfDay, + endOfDay, + setHours, + setMinutes as setMins, + getHours, + getMinutes, + parseISO, + isValid, + addDays, + startOfWeek, + endOfWeek, + addWeeks, + addMonths, + isSameDay, + getISOWeek +} from 'date-fns'; +import { + toZonedTime, + fromZonedTime, + formatInTimeZone +} from 'date-fns-tz'; +import { Configuration } from '../configuration/CalendarConfig'; + +export class DateService { + private timezone: string; + + constructor(config: Configuration) { + this.timezone = config.getTimezone(); + } + + // ============================================ + // CORE CONVERSIONS + // ============================================ + + /** + * Convert local date to UTC ISO string + * @param localDate - Date in local timezone + * @returns ISO string in UTC (with 'Z' suffix) + */ + public toUTC(localDate: Date): string { + return fromZonedTime(localDate, this.timezone).toISOString(); + } + + /** + * Convert UTC ISO string to local date + * @param utcString - ISO string in UTC + * @returns Date in local timezone + */ + public fromUTC(utcString: string): Date { + return toZonedTime(parseISO(utcString), this.timezone); + } + + // ============================================ + // FORMATTING + // ============================================ + + /** + * Format time as HH:mm or HH:mm:ss + * @param date - Date to format + * @param showSeconds - Include seconds in output + * @returns Formatted time string + */ + public formatTime(date: Date, showSeconds = false): string { + const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm'; + return format(date, pattern); + } + + /** + * Format time range as "HH:mm - HH:mm" + * @param start - Start date + * @param end - End date + * @returns Formatted time range + */ + public formatTimeRange(start: Date, end: Date): string { + return `${this.formatTime(start)} - ${this.formatTime(end)}`; + } + + /** + * Format date and time in technical format: yyyy-MM-dd HH:mm:ss + * @param date - Date to format + * @returns Technical datetime string + */ + public formatTechnicalDateTime(date: Date): string { + return format(date, 'yyyy-MM-dd HH:mm:ss'); + } + + /** + * Format date as yyyy-MM-dd + * @param date - Date to format + * @returns ISO date string + */ + public formatDate(date: Date): string { + return format(date, 'yyyy-MM-dd'); + } + + /** + * Format date as "Month Year" (e.g., "January 2025") + * @param date - Date to format + * @param locale - Locale for month name (default: 'en-US') + * @returns Formatted month and year + */ + public formatMonthYear(date: Date, locale: string = 'en-US'): string { + return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' }); + } + + /** + * Format date as ISO string (same as formatDate for compatibility) + * @param date - Date to format + * @returns ISO date string + */ + public formatISODate(date: Date): string { + return this.formatDate(date); + } + + /** + * Format time in 12-hour format with AM/PM + * @param date - Date to format + * @returns Time string in 12-hour format (e.g., "2:30 PM") + */ + public formatTime12(date: Date): string { + const hours = getHours(date); + const minutes = getMinutes(date); + const period = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + + return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`; + } + + /** + * Get day name for a date + * @param date - Date to get day name for + * @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday') + * @param locale - Locale for day name (default: 'da-DK') + * @returns Day name + */ + public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string { + const formatter = new Intl.DateTimeFormat(locale, { + weekday: format + }); + return formatter.format(date); + } + + /** + * Format a date range with customizable options + * @param start - Start date + * @param end - End date + * @param options - Formatting options + * @returns Formatted date range string + */ + public formatDateRange( + start: Date, + end: Date, + options: { + locale?: string; + month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'; + day?: 'numeric' | '2-digit'; + year?: 'numeric' | '2-digit'; + } = {} + ): string { + const { locale = 'en-US', month = 'short', day = 'numeric' } = options; + + const startYear = start.getFullYear(); + const endYear = end.getFullYear(); + + const formatter = new Intl.DateTimeFormat(locale, { + month, + day, + year: startYear !== endYear ? 'numeric' : undefined + }); + + // @ts-ignore - formatRange is available in modern browsers + if (typeof formatter.formatRange === 'function') { + // @ts-ignore + return formatter.formatRange(start, end); + } + + return `${formatter.format(start)} - ${formatter.format(end)}`; + } + + // ============================================ + // TIME CALCULATIONS + // ============================================ + + /** + * Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight + * @param timeString - Time in format HH:mm or HH:mm:ss + * @returns Total minutes since midnight + */ + public timeToMinutes(timeString: string): number { + const parts = timeString.split(':').map(Number); + const hours = parts[0] || 0; + const minutes = parts[1] || 0; + return hours * 60 + minutes; + } + + /** + * Convert total minutes since midnight to time string HH:mm + * @param totalMinutes - Minutes since midnight + * @returns Time string in format HH:mm + */ + public minutesToTime(totalMinutes: number): string { + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + const date = setMins(setHours(new Date(), hours), minutes); + return format(date, 'HH:mm'); + } + + /** + * Format time from total minutes (alias for minutesToTime) + * @param totalMinutes - Minutes since midnight + * @returns Time string in format HH:mm + */ + public formatTimeFromMinutes(totalMinutes: number): string { + return this.minutesToTime(totalMinutes); + } + + /** + * Get minutes since midnight for a given date + * @param date - Date to calculate from + * @returns Minutes since midnight + */ + public getMinutesSinceMidnight(date: Date): number { + return getHours(date) * 60 + getMinutes(date); + } + + /** + * Calculate duration in minutes between two dates + * @param start - Start date or ISO string + * @param end - End date or ISO string + * @returns Duration in minutes + */ + public getDurationMinutes(start: Date | string, end: Date | string): number { + const startDate = typeof start === 'string' ? parseISO(start) : start; + const endDate = typeof end === 'string' ? parseISO(end) : end; + return differenceInMinutes(endDate, startDate); + } + + // ============================================ + // WEEK OPERATIONS + // ============================================ + + /** + * Get start and end of week (Monday to Sunday) + * @param date - Reference date + * @returns Object with start and end dates + */ + public getWeekBounds(date: Date): { start: Date; end: Date } { + return { + start: startOfWeek(date, { weekStartsOn: 1 }), // Monday + end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday + }; + } + + /** + * Add weeks to a date + * @param date - Base date + * @param weeks - Number of weeks to add (can be negative) + * @returns New date + */ + public addWeeks(date: Date, weeks: number): Date { + return addWeeks(date, weeks); + } + + /** + * Add months to a date + * @param date - Base date + * @param months - Number of months to add (can be negative) + * @returns New date + */ + public addMonths(date: Date, months: number): Date { + return addMonths(date, months); + } + + /** + * Get ISO week number (1-53) + * @param date - Date to get week number for + * @returns ISO week number + */ + public getWeekNumber(date: Date): number { + return getISOWeek(date); + } + + /** + * Get all dates in a full week (7 days starting from given date) + * @param weekStart - Start date of the week + * @returns Array of 7 dates + */ + public getFullWeekDates(weekStart: Date): Date[] { + const dates: Date[] = []; + for (let i = 0; i < 7; i++) { + dates.push(this.addDays(weekStart, i)); + } + return dates; + } + + /** + * Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7) + * @param weekStart - Any date in the week + * @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday) + * @returns Array of dates for the specified work days + */ + public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] { + const dates: Date[] = []; + + // Get Monday of the week + const weekBounds = this.getWeekBounds(weekStart); + const mondayOfWeek = this.startOfDay(weekBounds.start); + + // Calculate dates for each work day using ISO numbering + workDays.forEach(isoDay => { + const date = new Date(mondayOfWeek); + // ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days + const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; + date.setDate(mondayOfWeek.getDate() + daysFromMonday); + dates.push(date); + }); + + return dates; + } + + // ============================================ + // GRID HELPERS + // ============================================ + + /** + * Create a date at a specific time (minutes since midnight) + * @param baseDate - Base date (date component) + * @param totalMinutes - Minutes since midnight + * @returns New date with specified time + */ + public createDateAtTime(baseDate: Date, totalMinutes: number): Date { + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return setMins(setHours(startOfDay(baseDate), hours), minutes); + } + + /** + * Snap date to nearest interval + * @param date - Date to snap + * @param intervalMinutes - Snap interval in minutes + * @returns Snapped date + */ + public snapToInterval(date: Date, intervalMinutes: number): Date { + const minutes = this.getMinutesSinceMidnight(date); + const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes; + return this.createDateAtTime(date, snappedMinutes); + } + + // ============================================ + // UTILITY METHODS + // ============================================ + + /** + * Check if two dates are the same day + * @param date1 - First date + * @param date2 - Second date + * @returns True if same day + */ + public isSameDay(date1: Date, date2: Date): boolean { + return isSameDay(date1, date2); + } + + /** + * Get start of day + * @param date - Date + * @returns Start of day (00:00:00) + */ + public startOfDay(date: Date): Date { + return startOfDay(date); + } + + /** + * Get end of day + * @param date - Date + * @returns End of day (23:59:59.999) + */ + public endOfDay(date: Date): Date { + return endOfDay(date); + } + + /** + * Add days to a date + * @param date - Base date + * @param days - Number of days to add (can be negative) + * @returns New date + */ + public addDays(date: Date, days: number): Date { + return addDays(date, days); + } + + /** + * Add minutes to a date + * @param date - Base date + * @param minutes - Number of minutes to add (can be negative) + * @returns New date + */ + public addMinutes(date: Date, minutes: number): Date { + return addMinutes(date, minutes); + } + + /** + * Parse ISO string to date + * @param isoString - ISO date string + * @returns Parsed date + */ + public parseISO(isoString: string): Date { + return parseISO(isoString); + } + + /** + * Check if date is valid + * @param date - Date to check + * @returns True if valid + */ + public isValid(date: Date): boolean { + return isValid(date); + } + + /** + * Validate date range (start must be before or equal to end) + * @param start - Start date + * @param end - End date + * @returns True if valid range + */ + public isValidRange(start: Date, end: Date): boolean { + if (!this.isValid(start) || !this.isValid(end)) { + return false; + } + return start.getTime() <= end.getTime(); + } + + /** + * Check if date is within reasonable bounds (1900-2100) + * @param date - Date to check + * @returns True if within bounds + */ + public isWithinBounds(date: Date): boolean { + if (!this.isValid(date)) { + return false; + } + const year = date.getFullYear(); + return year >= 1900 && year <= 2100; + } + + /** + * Validate date with comprehensive checks + * @param date - Date to validate + * @param options - Validation options + * @returns Validation result with error message + */ + public validateDate( + date: Date, + options: { + requireFuture?: boolean; + requirePast?: boolean; + minDate?: Date; + maxDate?: Date; + } = {} + ): { valid: boolean; error?: string } { + if (!this.isValid(date)) { + return { valid: false, error: 'Invalid date' }; + } + + if (!this.isWithinBounds(date)) { + return { valid: false, error: 'Date out of bounds (1900-2100)' }; + } + + const now = new Date(); + + if (options.requireFuture && date <= now) { + return { valid: false, error: 'Date must be in the future' }; + } + + if (options.requirePast && date >= now) { + return { valid: false, error: 'Date must be in the past' }; + } + + if (options.minDate && date < options.minDate) { + return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` }; + } + + if (options.maxDate && date > options.maxDate) { + return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` }; + } + + return { valid: true }; + } } \ No newline at end of file diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index a13ac76..526db61 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,5 +1,5 @@ -import { CalendarConfig } from '../core/CalendarConfig'; -import { ColumnBounds } from './ColumnDetectionUtils'; +import { Configuration } from '../configuration/CalendarConfig'; +import { IColumnBounds } from './ColumnDetectionUtils'; import { DateService } from './DateService'; import { TimeFormatter } from './TimeFormatter'; @@ -11,9 +11,9 @@ import { TimeFormatter } from './TimeFormatter'; */ export class PositionUtils { private dateService: DateService; - private config: CalendarConfig; + private config: Configuration; - constructor(dateService: DateService, config: CalendarConfig) { + constructor(dateService: DateService, config: Configuration) { this.dateService = dateService; this.config = config; } @@ -169,7 +169,7 @@ export class PositionUtils { /** * Beregn Y position fra mouse/touch koordinat */ - public getPositionFromCoordinate(clientY: number, column: ColumnBounds): number { + public getPositionFromCoordinate(clientY: number, column: IColumnBounds): number { const relativeY = clientY - column.boundingClientRect.top; diff --git a/src/utils/TimeFormatter.ts b/src/utils/TimeFormatter.ts index fe55171..d9d53a2 100644 --- a/src/utils/TimeFormatter.ts +++ b/src/utils/TimeFormatter.ts @@ -10,7 +10,7 @@ import { DateService } from './DateService'; -export interface TimeFormatSettings { +export interface ITimeFormatSettings { timezone: string; use24HourFormat: boolean; locale: string; @@ -19,7 +19,7 @@ export interface TimeFormatSettings { } export class TimeFormatter { - private static settings: TimeFormatSettings = { + private static settings: ITimeFormatSettings = { timezone: 'Europe/Copenhagen', // Default to Denmark use24HourFormat: true, // 24-hour format standard in Denmark locale: 'da-DK', // Danish locale @@ -44,7 +44,7 @@ export class TimeFormatter { /** * Configure time formatting settings */ - static configure(settings: Partial): void { + static configure(settings: Partial): void { TimeFormatter.settings = { ...TimeFormatter.settings, ...settings }; // Reset DateService to pick up new timezone TimeFormatter.dateService = null; diff --git a/wwwroot/data/calendar-config.json b/wwwroot/data/calendar-config.json new file mode 100644 index 0000000..e4bd5a1 --- /dev/null +++ b/wwwroot/data/calendar-config.json @@ -0,0 +1,87 @@ +{ + "gridSettings": { + "hourHeight": 80, + "dayStartHour": 6, + "dayEndHour": 22, + "workStartHour": 8, + "workEndHour": 17, + "snapInterval": 15, + "gridStartThresholdMinutes": 30, + "showCurrentTime": true, + "showWorkHours": true, + "fitToWidth": false, + "scrollToHour": 8 + }, + "dateViewSettings": { + "period": "week", + "weekDays": 7, + "firstDayOfWeek": 1, + "showAllDay": true + }, + "timeFormatConfig": { + "timezone": "Europe/Copenhagen", + "use24HourFormat": true, + "locale": "da-DK", + "dateFormat": "technical", + "showSeconds": false + }, + "workWeekPresets": { + "standard": { + "id": "standard", + "workDays": [1, 2, 3, 4, 5], + "totalDays": 5, + "firstWorkDay": 1 + }, + "compressed": { + "id": "compressed", + "workDays": [1, 2, 3, 4], + "totalDays": 4, + "firstWorkDay": 1 + }, + "midweek": { + "id": "midweek", + "workDays": [3, 4, 5], + "totalDays": 3, + "firstWorkDay": 3 + }, + "weekend": { + "id": "weekend", + "workDays": [6, 7], + "totalDays": 2, + "firstWorkDay": 6 + }, + "fullweek": { + "id": "fullweek", + "workDays": [1, 2, 3, 4, 5, 6, 7], + "totalDays": 7, + "firstWorkDay": 1 + } + }, + "currentWorkWeek": "standard", + "scrollbar": { + "width": 16, + "color": "#666", + "trackColor": "#f0f0f0", + "hoverColor": "#b53f7aff", + "borderRadius": 6 + }, + "interaction": { + "allowDrag": true, + "allowResize": true, + "allowCreate": true + }, + "api": { + "endpoint": "/api/events", + "dateFormat": "YYYY-MM-DD", + "timeFormat": "HH:mm" + }, + "features": { + "enableSearch": true, + "enableTouch": true + }, + "eventDefaults": { + "defaultEventDuration": 60, + "minEventDuration": 15, + "maxEventDuration": 480 + } +} diff --git a/wwwroot/index.html b/wwwroot/index.html index c4f7ffc..d1a1629 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -15,7 +15,7 @@
- +