Adds I-prefix to all interfaces

This commit is contained in:
Janus C. H. Knudsen 2025-11-03 21:30:50 +01:00
parent 80aaab46f2
commit 8ec5f52872
44 changed files with 1731 additions and 1949 deletions

View file

@ -10,7 +10,9 @@
"Bash(rm:*)", "Bash(rm:*)",
"Bash(npm install:*)", "Bash(npm install:*)",
"Bash(npm test)", "Bash(npm test)",
"Bash(cat:*)" "Bash(cat:*)",
"Bash(npm run test:run:*)",
"Bash(npx tsc)"
], ],
"deny": [] "deny": []
} }

View file

@ -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 };

View file

@ -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<Configuration> {
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;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,10 @@
/**
* Time format configuration settings
*/
export interface ITimeFormatConfig {
timezone: string;
use24HourFormat: boolean;
locale: string;
dateFormat: 'locale' | 'technical';
showSeconds: boolean;
}

View file

@ -0,0 +1,9 @@
/**
* Work week configuration settings
*/
export interface IWorkWeekSettings {
id: string;
workDays: number[];
totalDays: number;
firstWorkDay: number;
}

View file

@ -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<K extends keyof ICalendarConfig>(key: K): ICalendarConfig[K] {
return CalendarConfig.config[key];
}
/**
* Set a config value (no events - use ConfigManager for updates with events)
*/
static set<K extends keyof ICalendarConfig>(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<ICalendarConfig>): 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<GridSettings>): 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<ICalendarConfig>) { 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<GridSettings>) { 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(); }
}

View file

@ -1,14 +1,14 @@
// Core EventBus using pure DOM CustomEvents // 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 * Central event dispatcher for calendar using DOM CustomEvents
* Provides logging and debugging capabilities * Provides logging and debugging capabilities
*/ */
export class EventBus implements IEventBus { export class EventBus implements IEventBus {
private eventLog: EventLogEntry[] = []; private eventLog: IEventLogEntry[] = [];
private debug: boolean = false; private debug: boolean = false;
private listeners: Set<ListenerEntry> = new Set(); private listeners: Set<IListenerEntry> = new Set();
// Log configuration for different categories // Log configuration for different categories
private logConfig: { [key: string]: boolean } = { private logConfig: { [key: string]: boolean } = {
@ -161,7 +161,7 @@ export class EventBus implements IEventBus {
/** /**
* Get event history * Get event history
*/ */
getEventLog(eventType?: string): EventLogEntry[] { getEventLog(eventType?: string): IEventLogEntry[] {
if (eventType) { if (eventType) {
return this.eventLog.filter(e => e.type === eventType); return this.eventLog.filter(e => e.type === eventType);
} }

View file

@ -1,5 +1,5 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { TimeFormatter } from '../utils/TimeFormatter'; import { TimeFormatter } from '../utils/TimeFormatter';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
@ -9,12 +9,12 @@ import { DateService } from '../utils/DateService';
*/ */
export abstract class BaseSwpEventElement extends HTMLElement { export abstract class BaseSwpEventElement extends HTMLElement {
protected dateService: DateService; protected dateService: DateService;
protected config: CalendarConfig; protected config: Configuration;
constructor() { constructor() {
super(); super();
// TODO: Find better solution for web component DI // Get singleton instance for web components (can't use DI)
this.config = new CalendarConfig(); this.config = Configuration.getInstance();
this.dateService = new DateService(this.config); 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 element = document.createElement('swp-event') as SwpEventElement;
const config = new CalendarConfig(); const config = Configuration.getInstance();
const dateService = new DateService(config); const dateService = new DateService(config);
element.dataset.eventId = event.id; 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 { return {
id: element.dataset.eventId || '', id: element.dataset.eventId || '',
title: element.dataset.title || '', 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 element = document.createElement('swp-allday-event') as SwpAllDayEventElement;
const config = new CalendarConfig(); const config = Configuration.getInstance();
const dateService = new DateService(config); const dateService = new DateService(config);
element.dataset.eventId = event.id; element.dataset.eventId = event.id;

View file

@ -1,7 +1,8 @@
// Main entry point for Calendar Plantempus // Main entry point for Calendar Plantempus
import { Container } from '@novadi/core'; import { Container } from '@novadi/core';
import { eventBus } from './core/EventBus'; 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 { URLManager } from './utils/URLManager';
import { IEventBus } from './types/CalendarTypes'; import { IEventBus } from './types/CalendarTypes';
@ -19,7 +20,6 @@ import { ResizeHandleManager } from './managers/ResizeHandleManager';
import { EdgeScrollManager } from './managers/EdgeScrollManager'; import { EdgeScrollManager } from './managers/EdgeScrollManager';
import { DragHoverManager } from './managers/DragHoverManager'; import { DragHoverManager } from './managers/DragHoverManager';
import { HeaderManager } from './managers/HeaderManager'; import { HeaderManager } from './managers/HeaderManager';
import { ConfigManager } from './managers/ConfigManager';
// Import repositories // Import repositories
import { IEventRepository } from './repositories/IEventRepository'; import { IEventRepository } from './repositories/IEventRepository';
@ -27,7 +27,7 @@ import { MockEventRepository } from './repositories/MockEventRepository';
// Import renderers // Import renderers
import { DateHeaderRenderer, type IHeaderRenderer } from './renderers/DateHeaderRenderer'; 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 { DateEventRenderer, type IEventRenderer } from './renderers/EventRenderer';
import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { AllDayEventRenderer } from './renderers/AllDayEventRenderer';
import { GridRenderer } from './renderers/GridRenderer'; import { GridRenderer } from './renderers/GridRenderer';
@ -70,8 +70,8 @@ async function handleDeepLinking(eventManager: EventManager, urlManager: URLMana
*/ */
async function initializeCalendar(): Promise<void> { async function initializeCalendar(): Promise<void> {
try { try {
// Initialize static calendar configuration // Load configuration from JSON
CalendarConfig.initialize(); const config = await ConfigManager.load();
// Create NovaDI container // Create NovaDI container
const container = new Container(); const container = new Container();
@ -80,21 +80,18 @@ async function initializeCalendar(): Promise<void> {
// Enable debug mode for development // Enable debug mode for development
eventBus.setDebug(true); eventBus.setDebug(true);
// Register CalendarConfig as singleton instance (static class, not instantiated)
builder.registerInstance(CalendarConfig).as<CalendarConfig>();
// Register ConfigManager for event-driven config updates
builder.registerType(ConfigManager).as<ConfigManager>();
// Bind core services as instances // Bind core services as instances
builder.registerInstance(eventBus).as<IEventBus>(); builder.registerInstance(eventBus).as<IEventBus>();
// Register configuration instance
builder.registerInstance(config).as<Configuration>();
// Register repositories // Register repositories
builder.registerType(MockEventRepository).as<IEventRepository>(); builder.registerType(MockEventRepository).as<IEventRepository>();
// Register renderers // Register renderers
builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>(); builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>();
builder.registerType(DateColumnRenderer).as<ColumnRenderer>(); builder.registerType(DateColumnRenderer).as<IColumnRenderer>();
builder.registerType(DateEventRenderer).as<IEventRenderer>(); builder.registerType(DateEventRenderer).as<IEventRenderer>();
// Register core services and utilities // Register core services and utilities
@ -130,7 +127,6 @@ async function initializeCalendar(): Promise<void> {
// Get managers from container // Get managers from container
const eb = app.resolveType<IEventBus>(); const eb = app.resolveType<IEventBus>();
const configManager = app.resolveType<ConfigManager>();
const calendarManager = app.resolveType<CalendarManager>(); const calendarManager = app.resolveType<CalendarManager>();
const eventManager = app.resolveType<EventManager>(); const eventManager = app.resolveType<EventManager>();
const resizeHandleManager = app.resolveType<ResizeHandleManager>(); const resizeHandleManager = app.resolveType<ResizeHandleManager>();
@ -143,9 +139,6 @@ async function initializeCalendar(): Promise<void> {
const allDayManager = app.resolveType<AllDayManager>(); const allDayManager = app.resolveType<AllDayManager>();
const urlManager = app.resolveType<URLManager>(); const urlManager = app.resolveType<URLManager>();
// Initialize CSS variables before any rendering
configManager.initialize();
// Initialize managers // Initialize managers
await calendarManager.initialize?.(); await calendarManager.initialize?.();
await resizeHandleManager.initialize?.(); await resizeHandleManager.initialize?.();

View file

@ -1,21 +1,21 @@
// All-day row height management and animations // All-day row height management and animations
import { eventBus } from '../core/EventBus'; 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 { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine'; import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement'; import { SwpAllDayEventElement } from '../elements/SwpEventElement';
import { import {
DragMouseEnterHeaderEventPayload, IDragMouseEnterHeaderEventPayload,
DragStartEventPayload, IDragStartEventPayload,
DragMoveEventPayload, IDragMoveEventPayload,
DragEndEventPayload, IDragEndEventPayload,
DragColumnChangeEventPayload, IDragColumnChangeEventPayload,
HeaderReadyEventPayload IHeaderReadyEventPayload
} from '../types/EventTypes'; } from '../types/EventTypes';
import { DragOffset, MousePosition } from '../types/DragDropTypes'; import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from './EventManager'; import { EventManager } from './EventManager';
import { differenceInCalendarDays } from 'date-fns'; import { differenceInCalendarDays } from 'date-fns';
@ -33,10 +33,10 @@ export class AllDayManager {
private layoutEngine: AllDayLayoutEngine | null = null; private layoutEngine: AllDayLayoutEngine | null = null;
// State tracking for differential updates // State tracking for differential updates
private currentLayouts: EventLayout[] = []; private currentLayouts: IEventLayout[] = [];
private currentAllDayEvents: CalendarEvent[] = []; private currentAllDayEvents: ICalendarEvent[] = [];
private currentWeekDates: ColumnBounds[] = []; private currentWeekDates: IColumnBounds[] = [];
private newLayouts: EventLayout[] = []; private newLayouts: IEventLayout[] = [];
// Expand/collapse state // Expand/collapse state
private isExpanded: boolean = false; private isExpanded: boolean = false;
@ -62,7 +62,7 @@ export class AllDayManager {
*/ */
private setupEventListeners(): void { private setupEventListeners(): void {
eventBus.on('drag:mouseenter-header', (event) => { eventBus.on('drag:mouseenter-header', (event) => {
const payload = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail; const payload = (event as CustomEvent<IDragMouseEnterHeaderEventPayload>).detail;
if (payload.draggedClone.hasAttribute('data-allday')) if (payload.draggedClone.hasAttribute('data-allday'))
return; return;
@ -87,7 +87,7 @@ export class AllDayManager {
// Listen for drag operations on all-day events // Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => { eventBus.on('drag:start', (event) => {
let payload: DragStartEventPayload = (event as CustomEvent<DragStartEventPayload>).detail; let payload: IDragStartEventPayload = (event as CustomEvent<IDragStartEventPayload>).detail;
if (!payload.draggedClone?.hasAttribute('data-allday')) { if (!payload.draggedClone?.hasAttribute('data-allday')) {
return; return;
@ -97,7 +97,7 @@ export class AllDayManager {
}); });
eventBus.on('drag:column-change', (event) => { eventBus.on('drag:column-change', (event) => {
let payload: DragColumnChangeEventPayload = (event as CustomEvent<DragColumnChangeEventPayload>).detail; let payload: IDragColumnChangeEventPayload = (event as CustomEvent<IDragColumnChangeEventPayload>).detail;
if (!payload.draggedClone?.hasAttribute('data-allday')) { if (!payload.draggedClone?.hasAttribute('data-allday')) {
return; return;
@ -107,7 +107,7 @@ export class AllDayManager {
}); });
eventBus.on('drag:end', (event) => { eventBus.on('drag:end', (event) => {
let draggedElement: DragEndEventPayload = (event as CustomEvent<DragEndEventPayload>).detail; let draggedElement: IDragEndEventPayload = (event as CustomEvent<IDragEndEventPayload>).detail;
if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore. if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
return; return;
@ -128,12 +128,12 @@ export class AllDayManager {
// Listen for header ready - when dates are populated with period data // Listen for header ready - when dates are populated with period data
eventBus.on('header:ready', (event: Event) => { eventBus.on('header:ready', (event: Event) => {
let headerReadyEventPayload = (event as CustomEvent<HeaderReadyEventPayload>).detail; let headerReadyEventPayload = (event as CustomEvent<IHeaderReadyEventPayload>).detail;
let startDate = new Date(headerReadyEventPayload.headerElements.at(0)!.date); let startDate = new Date(headerReadyEventPayload.headerElements.at(0)!.date);
let endDate = new Date(headerReadyEventPayload.headerElements.at(-1)!.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 // Filter for all-day events
const allDayEvents = events.filter(event => event.allDay); const allDayEvents = events.filter(event => event.allDay);
@ -302,7 +302,7 @@ export class AllDayManager {
* Calculate layout for ALL all-day events using AllDayLayoutEngine * Calculate layout for ALL all-day events using AllDayLayoutEngine
* This is the correct method that processes all events together for proper overlap detection * 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 // Store current state
this.currentAllDayEvents = events; this.currentAllDayEvents = events;
@ -316,12 +316,12 @@ export class AllDayManager {
} }
private handleConvertToAllDay(payload: DragMouseEnterHeaderEventPayload): void { private handleConvertToAllDay(payload: IDragMouseEnterHeaderEventPayload): void {
let allDayContainer = this.getAllDayContainer(); let allDayContainer = this.getAllDayContainer();
if (!allDayContainer) return; if (!allDayContainer) return;
// Create SwpAllDayEventElement from CalendarEvent // Create SwpAllDayEventElement from ICalendarEvent
const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent); const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent);
// Apply grid positioning // Apply grid positioning
@ -345,7 +345,7 @@ export class AllDayManager {
/** /**
* Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER * 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(); let allDayContainer = this.getAllDayContainer();
if (!allDayContainer) return; 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 => { 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.start = this.dateService.toUTC(newStartDate);
dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate); dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate);
const droppedEvent: CalendarEvent = { const droppedEvent: ICalendarEvent = {
id: eventId, id: eventId,
title: dragEndEvent.draggedClone.dataset.title || '', title: dragEndEvent.draggedClone.dataset.title || '',
start: newStartDate, 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 columnIndex = columnBounds.index;
let count = 0; let count = 0;

View file

@ -1,5 +1,5 @@
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { CalendarView, IEventBus } from '../types/CalendarTypes'; import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { EventManager } from './EventManager'; import { EventManager } from './EventManager';
import { GridManager } from './GridManager'; import { GridManager } from './GridManager';
@ -15,7 +15,7 @@ export class CalendarManager {
private gridManager: GridManager; private gridManager: GridManager;
private eventRenderer: EventRenderingService; private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager; private scrollManager: ScrollManager;
private config: CalendarConfig; private config: Configuration;
private currentView: CalendarView = 'week'; private currentView: CalendarView = 'week';
private currentDate: Date = new Date(); private currentDate: Date = new Date();
private isInitialized: boolean = false; private isInitialized: boolean = false;
@ -26,7 +26,7 @@ export class CalendarManager {
gridManager: GridManager, gridManager: GridManager,
eventRenderingService: EventRenderingService, eventRenderingService: EventRenderingService,
scrollManager: ScrollManager, scrollManager: ScrollManager,
config: CalendarConfig config: Configuration
) { ) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.eventManager = eventManager; this.eventManager = eventManager;

View file

@ -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<K extends keyof ICalendarConfig>(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<ICalendarConfig>): void {
Object.entries(updates).forEach(([key, value]) => {
this.set(key as keyof ICalendarConfig, value);
});
}
/**
* Update grid display settings and emit event
*/
updateGridSettings(updates: Partial<GridSettings>): 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;
}
}
}

View file

@ -134,33 +134,33 @@
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement';
import { import {
DragStartEventPayload, IDragStartEventPayload,
DragMoveEventPayload, IDragMoveEventPayload,
DragEndEventPayload, IDragEndEventPayload,
DragMouseEnterHeaderEventPayload, IDragMouseEnterHeaderEventPayload,
DragMouseLeaveHeaderEventPayload, IDragMouseLeaveHeaderEventPayload,
DragMouseEnterColumnEventPayload, IDragMouseEnterColumnEventPayload,
DragColumnChangeEventPayload IDragColumnChangeEventPayload
} from '../types/EventTypes'; } from '../types/EventTypes';
import { MousePosition } from '../types/DragDropTypes'; import { IMousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
export class DragDropManager { export class DragDropManager {
private eventBus: IEventBus; private eventBus: IEventBus;
// Mouse tracking with optimized state // Mouse tracking with optimized state
private mouseDownPosition: MousePosition = { x: 0, y: 0 }; private mouseDownPosition: IMousePosition = { x: 0, y: 0 };
private currentMousePosition: MousePosition = { x: 0, y: 0 }; private currentMousePosition: IMousePosition = { x: 0, y: 0 };
private mouseOffset: MousePosition = { x: 0, y: 0 }; private mouseOffset: IMousePosition = { x: 0, y: 0 };
// Drag state // Drag state
private originalElement!: HTMLElement | null; private originalElement!: HTMLElement | null;
private draggedClone!: HTMLElement | null; private draggedClone!: HTMLElement | null;
private currentColumn: ColumnBounds | null = null; private currentColumn: IColumnBounds | null = null;
private previousColumn: ColumnBounds | null = null; private previousColumn: IColumnBounds | null = null;
private isDragStarted = false; private isDragStarted = false;
// Movement threshold to distinguish click from drag // Movement threshold to distinguish click from drag
@ -176,7 +176,7 @@ export class DragDropManager {
private dragAnimationId: number | null = null; private dragAnimationId: number | null = null;
private targetY = 0; private targetY = 0;
private currentY = 0; private currentY = 0;
private targetColumn: ColumnBounds | null = null; private targetColumn: IColumnBounds | null = null;
private positionUtils: PositionUtils; private positionUtils: PositionUtils;
constructor(eventBus: IEventBus, positionUtils: PositionUtils) { constructor(eventBus: IEventBus, positionUtils: PositionUtils) {
@ -336,7 +336,7 @@ export class DragDropManager {
* Try to initialize drag based on movement threshold * Try to initialize drag based on movement threshold
* Returns true if drag was initialized, false if not enough movement * 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 deltaX = Math.abs(currentPosition.x - this.mouseDownPosition.x);
const deltaY = Math.abs(currentPosition.y - this.mouseDownPosition.y); const deltaY = Math.abs(currentPosition.y - this.mouseDownPosition.y);
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
@ -362,7 +362,7 @@ export class DragDropManager {
this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
this.draggedClone = originalElement.createClone(); this.draggedClone = originalElement.createClone();
const dragStartPayload: DragStartEventPayload = { const dragStartPayload: IDragStartEventPayload = {
originalElement: this.originalElement!, originalElement: this.originalElement!,
draggedClone: this.draggedClone, draggedClone: this.draggedClone,
mousePosition: this.mouseDownPosition, 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")) { if (!this.draggedClone!.hasAttribute("data-allday")) {
// Calculate raw position from mouse (no snapping) // Calculate raw position from mouse (no snapping)
@ -405,7 +405,7 @@ export class DragDropManager {
/** /**
* Detect column change and emit event * Detect column change and emit event
*/ */
private detectColumnChange(currentPosition: MousePosition): void { private detectColumnChange(currentPosition: IMousePosition): void {
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
if (newColumn == null) return; if (newColumn == null) return;
@ -413,7 +413,7 @@ export class DragDropManager {
this.previousColumn = this.currentColumn; this.previousColumn = this.currentColumn;
this.currentColumn = newColumn; this.currentColumn = newColumn;
const dragColumnChangePayload: DragColumnChangeEventPayload = { const dragColumnChangePayload: IDragColumnChangeEventPayload = {
originalElement: this.originalElement!, originalElement: this.originalElement!,
draggedClone: this.draggedClone!, draggedClone: this.draggedClone!,
previousColumn: this.previousColumn, previousColumn: this.previousColumn,
@ -434,7 +434,7 @@ export class DragDropManager {
// Only emit drag:end if drag was actually started // Only emit drag:end if drag was actually started
if (this.isDragStarted) { 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) // Snap to grid on mouse up (like ResizeHandleManager)
const column = ColumnDetectionUtils.getColumnBounds(mousePosition); const column = ColumnDetectionUtils.getColumnBounds(mousePosition);
@ -455,7 +455,7 @@ export class DragDropManager {
if (!dropTarget) if (!dropTarget)
throw "dropTarget is null"; throw "dropTarget is null";
const dragEndPayload: DragEndEventPayload = { const dragEndPayload: IDragEndEventPayload = {
originalElement: this.originalElement, originalElement: this.originalElement,
draggedClone: this.draggedClone, draggedClone: this.draggedClone,
mousePosition, mousePosition,
@ -530,7 +530,7 @@ export class DragDropManager {
/** /**
* Optimized snap position calculation using PositionUtils * 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) // Calculate where the event top would be (accounting for mouse offset)
const eventTopY = mouseY - this.mouseOffset.y; const eventTopY = mouseY - this.mouseOffset.y;
@ -560,7 +560,7 @@ export class DragDropManager {
this.currentY += step; this.currentY += step;
// Emit drag:move event with current draggedClone reference // Emit drag:move event with current draggedClone reference
const dragMovePayload: DragMoveEventPayload = { const dragMovePayload: IDragMoveEventPayload = {
originalElement: this.originalElement!, originalElement: this.originalElement!,
draggedClone: this.draggedClone, // Always uses current reference draggedClone: this.draggedClone, // Always uses current reference
mousePosition: this.currentMousePosition, // Use current mouse position! mousePosition: this.currentMousePosition, // Use current mouse position!
@ -576,7 +576,7 @@ export class DragDropManager {
this.currentY = this.targetY; this.currentY = this.targetY;
// Emit final position // Emit final position
const dragMovePayload: DragMoveEventPayload = { const dragMovePayload: IDragMoveEventPayload = {
originalElement: this.originalElement!, originalElement: this.originalElement!,
draggedClone: this.draggedClone, draggedClone: this.draggedClone,
mousePosition: this.currentMousePosition, // Use current mouse position! 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 * 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 // Traverse up the DOM tree to find the target container
let currentElement = this.draggedClone; let currentElement = this.draggedClone;
@ -659,13 +659,13 @@ export class DragDropManager {
return; return;
} }
const position: MousePosition = { x: event.clientX, y: event.clientY }; const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position); const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (targetColumn) { if (targetColumn) {
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone); const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = { const dragMouseEnterPayload: IDragMouseEnterHeaderEventPayload = {
targetColumn: targetColumn, targetColumn: targetColumn,
mousePosition: position, mousePosition: position,
originalElement: this.originalElement, originalElement: this.originalElement,
@ -689,7 +689,7 @@ export class DragDropManager {
return; return;
} }
const position: MousePosition = { x: event.clientX, y: event.clientY }; const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position); const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) { if (!targetColumn) {
@ -699,10 +699,10 @@ export class DragDropManager {
// Calculate snapped Y position // Calculate snapped Y position
const snappedY = this.calculateSnapPosition(position.y, targetColumn); 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 calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
const dragMouseEnterPayload: DragMouseEnterColumnEventPayload = { const dragMouseEnterPayload: IDragMouseEnterColumnEventPayload = {
targetColumn: targetColumn, targetColumn: targetColumn,
mousePosition: position, mousePosition: position,
snappedY: snappedY, snappedY: snappedY,
@ -727,14 +727,14 @@ export class DragDropManager {
return; return;
} }
const position: MousePosition = { x: event.clientX, y: event.clientY }; const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position); const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) { if (!targetColumn) {
return; return;
} }
const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = { const dragMouseLeavePayload: IDragMouseLeaveHeaderEventPayload = {
targetDate: targetColumn.date, targetDate: targetColumn.date,
mousePosition: position, mousePosition: position,
originalElement: this.originalElement, originalElement: this.originalElement,

View file

@ -4,7 +4,7 @@
*/ */
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
import { DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes'; import { IDragMoveEventPayload, IDragStartEventPayload } from '../types/EventTypes';
export class EdgeScrollManager { export class EdgeScrollManager {
private scrollableContent: HTMLElement | null = null; private scrollableContent: HTMLElement | null = null;

View file

@ -5,24 +5,24 @@
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
// Import Fuse.js from npm // Import Fuse.js from npm
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
interface FuseResult { interface FuseResult {
item: CalendarEvent; item: ICalendarEvent;
refIndex: number; refIndex: number;
score?: number; score?: number;
} }
export class EventFilterManager { export class EventFilterManager {
private searchInput: HTMLInputElement | null = null; private searchInput: HTMLInputElement | null = null;
private allEvents: CalendarEvent[] = []; private allEvents: ICalendarEvent[] = [];
private matchingEventIds: Set<string> = new Set(); private matchingEventIds: Set<string> = new Set();
private isFilterActive: boolean = false; private isFilterActive: boolean = false;
private frameRequest: number | null = null; private frameRequest: number | null = null;
private fuse: Fuse<CalendarEvent> | null = null; private fuse: Fuse<ICalendarEvent> | null = null;
constructor() { constructor() {
// Wait for DOM to be ready before initializing // 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; this.allEvents = events;
// Initialize Fuse with the new events list // Initialize Fuse with the new events list

View file

@ -5,35 +5,35 @@
* Calculates stack levels, groups events, and determines rendering strategy. * Calculates stack levels, groups events, and determines rendering strategy.
*/ */
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { EventStackManager, EventGroup, StackLink } from './EventStackManager'; import { EventStackManager, IEventGroup, IStackLink } from './EventStackManager';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
export interface GridGroupLayout { export interface IGridGroupLayout {
events: CalendarEvent[]; events: ICalendarEvent[];
stackLevel: number; stackLevel: number;
position: { top: 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 { export interface IStackedEventLayout {
event: CalendarEvent; event: ICalendarEvent;
stackLink: StackLink; stackLink: IStackLink;
position: { top: number; height: number }; position: { top: number; height: number };
} }
export interface ColumnLayout { export interface IColumnLayout {
gridGroups: GridGroupLayout[]; gridGroups: IGridGroupLayout[];
stackedEvents: StackedEventLayout[]; stackedEvents: IStackedEventLayout[];
} }
export class EventLayoutCoordinator { export class EventLayoutCoordinator {
private stackManager: EventStackManager; private stackManager: EventStackManager;
private config: CalendarConfig; private config: Configuration;
private positionUtils: PositionUtils; private positionUtils: PositionUtils;
constructor(stackManager: EventStackManager, config: CalendarConfig, positionUtils: PositionUtils) { constructor(stackManager: EventStackManager, config: Configuration, positionUtils: PositionUtils) {
this.stackManager = stackManager; this.stackManager = stackManager;
this.config = config; this.config = config;
this.positionUtils = positionUtils; this.positionUtils = positionUtils;
@ -42,14 +42,14 @@ export class EventLayoutCoordinator {
/** /**
* Calculate complete layout for a column of events (recursive approach) * Calculate complete layout for a column of events (recursive approach)
*/ */
public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout { public calculateColumnLayout(columnEvents: ICalendarEvent[]): IColumnLayout {
if (columnEvents.length === 0) { if (columnEvents.length === 0) {
return { gridGroups: [], stackedEvents: [] }; return { gridGroups: [], stackedEvents: [] };
} }
const gridGroupLayouts: GridGroupLayout[] = []; const gridGroupLayouts: IGridGroupLayout[] = [];
const stackedEventLayouts: StackedEventLayout[] = []; const stackedEventLayouts: IStackedEventLayout[] = [];
const renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> = []; const renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }> = [];
let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime()); let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
// Process events recursively // Process events recursively
@ -66,7 +66,7 @@ export class EventLayoutCoordinator {
const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes); const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes);
// Decide: should this group be GRID or STACK? // Decide: should this group be GRID or STACK?
const group: EventGroup = { const group: IEventGroup = {
events: gridCandidates, events: gridCandidates,
containerType: 'NONE', containerType: 'NONE',
startTime: firstEvent.start startTime: firstEvent.start
@ -129,8 +129,8 @@ export class EventLayoutCoordinator {
* Calculate stack level for a grid group based on already rendered events * Calculate stack level for a grid group based on already rendered events
*/ */
private calculateGridGroupStackLevelFromRendered( private calculateGridGroupStackLevelFromRendered(
gridEvents: CalendarEvent[], gridEvents: ICalendarEvent[],
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }>
): number { ): number {
// Find highest stack level of any rendered event that overlaps with this grid // Find highest stack level of any rendered event that overlaps with this grid
let maxOverlappingLevel = -1; let maxOverlappingLevel = -1;
@ -150,8 +150,8 @@ export class EventLayoutCoordinator {
* Calculate stack level for a single stacked event based on already rendered events * Calculate stack level for a single stacked event based on already rendered events
*/ */
private calculateStackLevelFromRendered( private calculateStackLevelFromRendered(
event: CalendarEvent, event: ICalendarEvent,
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }>
): number { ): number {
// Find highest stack level of any rendered event that overlaps with this event // Find highest stack level of any rendered event that overlaps with this event
let maxOverlappingLevel = -1; let maxOverlappingLevel = -1;
@ -173,7 +173,7 @@ export class EventLayoutCoordinator {
* @param thresholdMinutes - Threshold in minutes * @param thresholdMinutes - Threshold in minutes
* @returns true if events conflict * @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) // Check 1: Start-to-start conflict (starts within threshold)
const startToStartDiff = Math.abs(event1.start.getTime() - event2.start.getTime()) / (1000 * 60); const startToStartDiff = Math.abs(event1.start.getTime() - event2.start.getTime()) / (1000 * 60);
if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(event1, event2)) { if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(event1, event2)) {
@ -206,10 +206,10 @@ export class EventLayoutCoordinator {
* @returns Array of all events in the conflict chain * @returns Array of all events in the conflict chain
*/ */
private expandGridCandidates( private expandGridCandidates(
firstEvent: CalendarEvent, firstEvent: ICalendarEvent,
remaining: CalendarEvent[], remaining: ICalendarEvent[],
thresholdMinutes: number thresholdMinutes: number
): CalendarEvent[] { ): ICalendarEvent[] {
const gridCandidates = [firstEvent]; const gridCandidates = [firstEvent];
let candidatesChanged = true; let candidatesChanged = true;
@ -246,11 +246,11 @@ export class EventLayoutCoordinator {
* @param events - Events in the grid group (should already be sorted by start time) * @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 * @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 === 0) return [];
if (events.length === 1) return [[events[0]]]; 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 each event, try to place it in an existing column where it doesn't overlap
for (const event of events) { for (const event of events) {

View file

@ -1,6 +1,6 @@
import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; import { IEventBus, ICalendarEvent } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { IEventRepository } from '../repositories/IEventRepository'; import { IEventRepository } from '../repositories/IEventRepository';
@ -10,15 +10,15 @@ import { IEventRepository } from '../repositories/IEventRepository';
*/ */
export class EventManager { export class EventManager {
private events: CalendarEvent[] = []; private events: ICalendarEvent[] = [];
private dateService: DateService; private dateService: DateService;
private config: CalendarConfig; private config: Configuration;
private repository: IEventRepository; private repository: IEventRepository;
constructor( constructor(
private eventBus: IEventBus, private eventBus: IEventBus,
dateService: DateService, dateService: DateService,
config: CalendarConfig, config: Configuration,
repository: IEventRepository repository: IEventRepository
) { ) {
this.dateService = dateService; this.dateService = dateService;
@ -42,14 +42,14 @@ export class EventManager {
/** /**
* Get events with optional copying for performance * 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; return copy ? [...this.events] : this.events;
} }
/** /**
* Optimized event lookup with early return * 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 // Use find for better performance than filter + first
return this.events.find(event => event.id === id); return this.events.find(event => event.id === id);
} }
@ -59,7 +59,7 @@ export class EventManager {
* @param id Event ID to find * @param id Event ID to find
* @returns Event with navigation info or null if not found * @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); const event = this.getEventById(id);
if (!event) { if (!event) {
return null; return null;
@ -113,7 +113,7 @@ export class EventManager {
/** /**
* Get events that overlap with a given time period * 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 // Event overlaps period if it starts before period ends AND ends after period starts
return this.events.filter(event => { return this.events.filter(event => {
return event.start <= endDate && event.end >= startDate; return event.start <= endDate && event.end >= startDate;
@ -123,8 +123,8 @@ export class EventManager {
/** /**
* Create a new event and add it to the calendar * Create a new event and add it to the calendar
*/ */
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent { public addEvent(event: Omit<ICalendarEvent, 'id'>): ICalendarEvent {
const newEvent: CalendarEvent = { const newEvent: ICalendarEvent = {
...event, ...event,
id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}; };
@ -141,7 +141,7 @@ export class EventManager {
/** /**
* Update an existing event * Update an existing event
*/ */
public updateEvent(id: string, updates: Partial<CalendarEvent>): CalendarEvent | null { public updateEvent(id: string, updates: Partial<ICalendarEvent>): ICalendarEvent | null {
const eventIndex = this.events.findIndex(event => event.id === id); const eventIndex = this.events.findIndex(event => event.id === id);
if (eventIndex === -1) return null; if (eventIndex === -1) return null;

View file

@ -13,26 +13,26 @@
* @see stacking-visualization.html for visual examples * @see stacking-visualization.html for visual examples
*/ */
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
export interface StackLink { export interface IStackLink {
prev?: string; // Event ID of previous event in stack prev?: string; // Event ID of previous event in stack
next?: string; // Event ID of next event in stack next?: string; // Event ID of next event in stack
stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.) stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.)
} }
export interface EventGroup { export interface IEventGroup {
events: CalendarEvent[]; events: ICalendarEvent[];
containerType: 'NONE' | 'GRID' | 'STACKING'; containerType: 'NONE' | 'GRID' | 'STACKING';
startTime: Date; startTime: Date;
} }
export class EventStackManager { export class EventStackManager {
private static readonly STACK_OFFSET_PX = 15; private static readonly STACK_OFFSET_PX = 15;
private config: CalendarConfig; private config: Configuration;
constructor(config: CalendarConfig) { constructor(config: Configuration) {
this.config = config; this.config = config;
} }
@ -47,7 +47,7 @@ export class EventStackManager {
* 1. They start within ±threshold minutes of each other (start-to-start) * 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) * 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 []; if (events.length === 0) return [];
// Get threshold from config // Get threshold from config
@ -57,7 +57,7 @@ export class EventStackManager {
// Sort events by start time // Sort events by start time
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime()); const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
const groups: EventGroup[] = []; const groups: IEventGroup[] = [];
for (const event of sorted) { for (const event of sorted) {
// Find existing group that this event conflicts with // 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 * even if they overlap each other. This provides better visual indication that
* events start at the same time. * 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) { if (group.events.length === 1) {
return 'NONE'; return 'NONE';
} }
@ -127,7 +127,7 @@ export class EventStackManager {
/** /**
* Check if two events overlap in time * 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; 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) * Create optimized stack links (events share levels when possible)
*/ */
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> { public createOptimizedStackLinks(events: ICalendarEvent[]): Map<string, IStackLink> {
const stackLinks = new Map<string, StackLink>(); const stackLinks = new Map<string, IStackLink>();
if (events.length === 0) return stackLinks; if (events.length === 0) return stackLinks;
@ -218,14 +218,14 @@ export class EventStackManager {
/** /**
* Serialize stack link to JSON string * Serialize stack link to JSON string
*/ */
public serializeStackLink(stackLink: StackLink): string { public serializeStackLink(stackLink: IStackLink): string {
return JSON.stringify(stackLink); return JSON.stringify(stackLink);
} }
/** /**
* Deserialize JSON string to stack link * Deserialize JSON string to stack link
*/ */
public deserializeStackLink(json: string): StackLink | null { public deserializeStackLink(json: string): IStackLink | null {
try { try {
return JSON.parse(json); return JSON.parse(json);
} catch (e) { } catch (e) {
@ -236,14 +236,14 @@ export class EventStackManager {
/** /**
* Apply stack link to DOM element * 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); element.dataset.stackLink = this.serializeStackLink(stackLink);
} }
/** /**
* Get stack link from DOM element * Get stack link from DOM element
*/ */
public getStackLinkFromElement(element: HTMLElement): StackLink | null { public getStackLinkFromElement(element: HTMLElement): IStackLink | null {
const data = element.dataset.stackLink; const data = element.dataset.stackLink;
if (!data) return null; if (!data) return null;
return this.deserializeStackLink(data); return this.deserializeStackLink(data);

View file

@ -1,8 +1,8 @@
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { IHeaderRenderer, HeaderRenderContext } from '../renderers/DateHeaderRenderer'; import { IHeaderRenderer, IHeaderRenderContext } from '../renderers/DateHeaderRenderer';
import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; import { IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IHeaderReadyEventPayload } from '../types/EventTypes';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
/** /**
@ -12,9 +12,9 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
*/ */
export class HeaderManager { export class HeaderManager {
private headerRenderer: IHeaderRenderer; private headerRenderer: IHeaderRenderer;
private config: CalendarConfig; private config: Configuration;
constructor(headerRenderer: IHeaderRenderer, config: CalendarConfig) { constructor(headerRenderer: IHeaderRenderer, config: Configuration) {
this.headerRenderer = headerRenderer; this.headerRenderer = headerRenderer;
this.config = config; this.config = config;
@ -44,7 +44,7 @@ export class HeaderManager {
*/ */
private handleDragMouseEnterHeader(event: Event): void { private handleDragMouseEnterHeader(event: Event): void {
const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } = const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } =
(event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail; (event as CustomEvent<IDragMouseEnterHeaderEventPayload>).detail;
console.log('🎯 HeaderManager: Received drag:mouseenter-header', { console.log('🎯 HeaderManager: Received drag:mouseenter-header', {
targetDate, targetDate,
@ -58,7 +58,7 @@ export class HeaderManager {
*/ */
private handleDragMouseLeaveHeader(event: Event): void { private handleDragMouseLeaveHeader(event: Event): void {
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } =
(event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail; (event as CustomEvent<IDragMouseLeaveHeaderEventPayload>).detail;
console.log('🚪 HeaderManager: Received drag:mouseleave-header', { console.log('🚪 HeaderManager: Received drag:mouseleave-header', {
targetDate, targetDate,
@ -109,7 +109,7 @@ export class HeaderManager {
calendarHeader.innerHTML = ''; calendarHeader.innerHTML = '';
// Render new header content using injected renderer // Render new header content using injected renderer
const context: HeaderRenderContext = { const context: IHeaderRenderContext = {
currentWeek: currentDate, currentWeek: currentDate,
config: this.config config: this.config
}; };
@ -120,7 +120,7 @@ export class HeaderManager {
this.setupHeaderDragListeners(); this.setupHeaderDragListeners();
// Notify other managers that header is ready with period data // Notify other managers that header is ready with period data
const payload: HeaderReadyEventPayload = { const payload: IHeaderReadyEventPayload = {
headerElements: ColumnDetectionUtils.getHeaderColumns(), headerElements: ColumnDetectionUtils.getHeaderColumns(),
}; };
eventBus.emit('header:ready', payload); eventBus.emit('header:ready', payload);

View file

@ -1,7 +1,7 @@
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { ResizeEndEventPayload } from '../types/EventTypes'; import { IResizeEndEventPayload } from '../types/EventTypes';
type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void };
@ -29,9 +29,9 @@ export class ResizeHandleManager {
private unsubscribers: Array<() => void> = []; private unsubscribers: Array<() => void> = [];
private pointerCaptured = false; private pointerCaptured = false;
private prevZ?: string; private prevZ?: string;
private config: CalendarConfig; private config: Configuration;
constructor(config: CalendarConfig) { constructor(config: Configuration) {
this.config = config; this.config = config;
const grid = this.config.getGridSettings(); const grid = this.config.getGridSettings();
this.hourHeightPx = grid.hourHeight; this.hourHeightPx = grid.hourHeight;
@ -237,7 +237,7 @@ export class ResizeHandleManager {
// Emit resize:end event for re-stacking // Emit resize:end event for re-stacking
const eventId = this.targetEl.dataset.eventId || ''; const eventId = this.targetEl.dataset.eventId || '';
const resizeEndPayload: ResizeEndEventPayload = { const resizeEndPayload: IResizeEndEventPayload = {
eventId, eventId,
element: this.targetEl, element: this.targetEl,
finalHeight finalHeight

View file

@ -1,15 +1,15 @@
import { CalendarView, IEventBus } from '../types/CalendarTypes'; import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
export class ViewManager { export class ViewManager {
private eventBus: IEventBus; private eventBus: IEventBus;
private config: CalendarConfig; private config: Configuration;
private currentView: CalendarView = 'week'; private currentView: CalendarView = 'week';
private buttonListeners: Map<Element, EventListener> = new Map(); private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus, config: CalendarConfig) { constructor(eventBus: IEventBus, config: Configuration) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.config = config; this.config = config;
this.setupEventListeners(); this.setupEventListeners();

View file

@ -1,13 +1,13 @@
// Work hours management for per-column scheduling // Work hours management for per-column scheduling
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
/** /**
* Work hours for a specific day * Work hours for a specific day
*/ */
export interface DayWorkHours { export interface IDayWorkHours {
start: number; // Hour (0-23) start: number; // Hour (0-23)
end: number; // Hour (0-23) end: number; // Hour (0-23)
} }
@ -15,18 +15,18 @@ export interface DayWorkHours {
/** /**
* Work schedule configuration * Work schedule configuration
*/ */
export interface WorkScheduleConfig { export interface IWorkScheduleConfig {
weeklyDefault: { weeklyDefault: {
monday: DayWorkHours | 'off'; monday: IDayWorkHours | 'off';
tuesday: DayWorkHours | 'off'; tuesday: IDayWorkHours | 'off';
wednesday: DayWorkHours | 'off'; wednesday: IDayWorkHours | 'off';
thursday: DayWorkHours | 'off'; thursday: IDayWorkHours | 'off';
friday: DayWorkHours | 'off'; friday: IDayWorkHours | 'off';
saturday: DayWorkHours | 'off'; saturday: IDayWorkHours | 'off';
sunday: DayWorkHours | 'off'; sunday: IDayWorkHours | 'off';
}; };
dateOverrides: { 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 { export class WorkHoursManager {
private dateService: DateService; private dateService: DateService;
private config: CalendarConfig; private config: Configuration;
private positionUtils: PositionUtils; 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.dateService = dateService;
this.config = config; this.config = config;
this.positionUtils = positionUtils; this.positionUtils = positionUtils;
@ -66,7 +66,7 @@ export class WorkHoursManager {
/** /**
* Get work hours for a specific date * Get work hours for a specific date
*/ */
getWorkHoursForDate(date: Date): DayWorkHours | 'off' { getWorkHoursForDate(date: Date): IDayWorkHours | 'off' {
const dateString = this.dateService.formatISODate(date); const dateString = this.dateService.formatISODate(date);
// Check for date-specific override first // Check for date-specific override first
@ -82,8 +82,8 @@ export class WorkHoursManager {
/** /**
* Get work hours for multiple dates (used by GridManager) * Get work hours for multiple dates (used by GridManager)
*/ */
getWorkHoursForDateRange(dates: Date[]): Map<string, DayWorkHours | 'off'> { getWorkHoursForDateRange(dates: Date[]): Map<string, IDayWorkHours | 'off'> {
const workHoursMap = new Map<string, DayWorkHours | 'off'>(); const workHoursMap = new Map<string, IDayWorkHours | 'off'>();
dates.forEach(date => { dates.forEach(date => {
const dateString = this.dateService.formatISODate(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 * 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') { if (workHours === 'off') {
return null; // Full day will be colored via CSS background 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 * 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') { if (workHours === 'off') {
return null; return null;
} }
@ -139,22 +139,22 @@ export class WorkHoursManager {
/** /**
* Load work schedule from JSON (future implementation) * Load work schedule from JSON (future implementation)
*/ */
async loadWorkSchedule(jsonData: WorkScheduleConfig): Promise<void> { async loadWorkSchedule(jsonData: IWorkScheduleConfig): Promise<void> {
this.workSchedule = jsonData; this.workSchedule = jsonData;
} }
/** /**
* Get current work schedule configuration * Get current work schedule configuration
*/ */
getWorkSchedule(): WorkScheduleConfig { getWorkSchedule(): IWorkScheduleConfig {
return this.workSchedule; return this.workSchedule;
} }
/** /**
* Convert Date to day name key * Convert Date to day name key
*/ */
private getDayName(date: Date): keyof WorkScheduleConfig['weeklyDefault'] { private getDayName(date: Date): keyof IWorkScheduleConfig['weeklyDefault'] {
const dayNames: (keyof WorkScheduleConfig['weeklyDefault'])[] = [ const dayNames: (keyof IWorkScheduleConfig['weeklyDefault'])[] = [
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'
]; ];
return dayNames[date.getDay()]; return dayNames[date.getDay()];

View file

@ -1,9 +1,9 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement'; import { SwpAllDayEventElement } from '../elements/SwpEventElement';
import { EventLayout } from '../utils/AllDayLayoutEngine'; import { IEventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { IColumnBounds } from '../utils/ColumnDetectionUtils';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { DragStartEventPayload } from '../types/EventTypes'; import { IDragStartEventPayload } from '../types/EventTypes';
import { IEventRenderer } from './EventRenderer'; import { IEventRenderer } from './EventRenderer';
export class AllDayEventRenderer { export class AllDayEventRenderer {
@ -38,7 +38,7 @@ export class AllDayEventRenderer {
/** /**
* Handle drag start for all-day events * Handle drag start for all-day events
*/ */
public handleDragStart(payload: DragStartEventPayload): void { public handleDragStart(payload: IDragStartEventPayload): void {
this.originalEvent = payload.originalElement;; this.originalEvent = payload.originalElement;;
this.draggedClone = payload.draggedClone; this.draggedClone = payload.draggedClone;
@ -70,8 +70,8 @@ export class AllDayEventRenderer {
* Render an all-day event with pre-calculated layout * Render an all-day event with pre-calculated layout
*/ */
private renderAllDayEventWithLayout( private renderAllDayEventWithLayout(
event: CalendarEvent, event: ICalendarEvent,
layout: EventLayout layout: IEventLayout
) { ) {
const container = this.getContainer(); const container = this.getContainer();
if (!container) return null; if (!container) return null;
@ -109,7 +109,7 @@ export class AllDayEventRenderer {
/** /**
* Render all-day events for specific period using AllDayEventRenderer * Render all-day events for specific period using AllDayEventRenderer
*/ */
public renderAllDayEventsForPeriod(eventLayouts: EventLayout[]): void { public renderAllDayEventsForPeriod(eventLayouts: IEventLayout[]): void {
this.clearAllDayEvents(); this.clearAllDayEvents();
eventLayouts.forEach(layout => { eventLayouts.forEach(layout => {

View file

@ -1,28 +1,28 @@
// Column rendering strategy interface and implementations // Column rendering strategy interface and implementations
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { WorkHoursManager } from '../managers/WorkHoursManager'; import { WorkHoursManager } from '../managers/WorkHoursManager';
/** /**
* Interface for column rendering strategies * Interface for column rendering strategies
*/ */
export interface ColumnRenderer { export interface IColumnRenderer {
render(columnContainer: HTMLElement, context: ColumnRenderContext): void; render(columnContainer: HTMLElement, context: IColumnRenderContext): void;
} }
/** /**
* Context for column rendering * Context for column rendering
*/ */
export interface ColumnRenderContext { export interface IColumnRenderContext {
currentWeek: Date; currentWeek: Date;
config: CalendarConfig; config: Configuration;
} }
/** /**
* Date-based column renderer (original functionality) * Date-based column renderer (original functionality)
*/ */
export class DateColumnRenderer implements ColumnRenderer { export class DateColumnRenderer implements IColumnRenderer {
private dateService: DateService; private dateService: DateService;
private workHoursManager: WorkHoursManager; private workHoursManager: WorkHoursManager;
@ -34,7 +34,7 @@ export class DateColumnRenderer implements ColumnRenderer {
this.workHoursManager = workHoursManager; this.workHoursManager = workHoursManager;
} }
render(columnContainer: HTMLElement, context: ColumnRenderContext): void { render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
const { currentWeek, config } = context; const { currentWeek, config } = context;
const workWeekSettings = config.getWorkWeekSettings(); const workWeekSettings = config.getWorkWeekSettings();

View file

@ -1,22 +1,22 @@
// Header rendering strategy interface and implementations // Header rendering strategy interface and implementations
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
/** /**
* Interface for header rendering strategies * Interface for header rendering strategies
*/ */
export interface IHeaderRenderer { export interface IHeaderRenderer {
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void;
} }
/** /**
* Context for header rendering * Context for header rendering
*/ */
export interface HeaderRenderContext { export interface IHeaderRenderContext {
currentWeek: Date; currentWeek: Date;
config: CalendarConfig; config: Configuration;
} }
/** /**
@ -25,7 +25,7 @@ export interface HeaderRenderContext {
export class DateHeaderRenderer implements IHeaderRenderer { export class DateHeaderRenderer implements IHeaderRenderer {
private dateService!: DateService; private dateService!: DateService;
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { render(calendarHeader: HTMLElement, context: IHeaderRenderContext): void {
const { currentWeek, config } = context; const { currentWeek, config } = context;
// FIRST: Always create all-day container as part of standard header structure // FIRST: Always create all-day container as part of standard header structure

View file

@ -1,29 +1,29 @@
// Event rendering strategy interface and implementations // Event rendering strategy interface and implementations
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { IColumnBounds } from '../utils/ColumnDetectionUtils';
import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload, DragMouseEnterColumnEventPayload } from '../types/EventTypes'; import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPayload, IDragMouseEnterColumnEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { EventStackManager } from '../managers/EventStackManager'; import { EventStackManager } from '../managers/EventStackManager';
import { EventLayoutCoordinator, GridGroupLayout, StackedEventLayout } from '../managers/EventLayoutCoordinator'; import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
/** /**
* Interface for event rendering strategies * Interface for event rendering strategies
*/ */
export interface IEventRenderer { export interface IEventRenderer {
renderEvents(events: CalendarEvent[], container: HTMLElement): void; renderEvents(events: ICalendarEvent[], container: HTMLElement): void;
clearEvents(container?: HTMLElement): void; clearEvents(container?: HTMLElement): void;
handleDragStart?(payload: DragStartEventPayload): void; handleDragStart?(payload: IDragStartEventPayload): void;
handleDragMove?(payload: DragMoveEventPayload): void; handleDragMove?(payload: IDragMoveEventPayload): void;
handleDragAutoScroll?(eventId: string, snappedY: number): 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; handleEventClick?(eventId: string, originalElement: HTMLElement): void;
handleColumnChange?(payload: DragColumnChangeEventPayload): void; handleColumnChange?(payload: IDragColumnChangeEventPayload): void;
handleNavigationCompleted?(): void; handleNavigationCompleted?(): void;
handleConvertAllDayToTimed?(payload: DragMouseEnterColumnEventPayload): void; handleConvertAllDayToTimed?(payload: IDragMouseEnterColumnEventPayload): void;
} }
/** /**
@ -34,7 +34,7 @@ export class DateEventRenderer implements IEventRenderer {
private dateService: DateService; private dateService: DateService;
private stackManager: EventStackManager; private stackManager: EventStackManager;
private layoutCoordinator: EventLayoutCoordinator; private layoutCoordinator: EventLayoutCoordinator;
private config: CalendarConfig; private config: Configuration;
private positionUtils: PositionUtils; private positionUtils: PositionUtils;
private draggedClone: HTMLElement | null = null; private draggedClone: HTMLElement | null = null;
private originalEvent: HTMLElement | null = null; private originalEvent: HTMLElement | null = null;
@ -43,7 +43,7 @@ export class DateEventRenderer implements IEventRenderer {
dateService: DateService, dateService: DateService,
stackManager: EventStackManager, stackManager: EventStackManager,
layoutCoordinator: EventLayoutCoordinator, layoutCoordinator: EventLayoutCoordinator,
config: CalendarConfig, config: Configuration,
positionUtils: PositionUtils positionUtils: PositionUtils
) { ) {
this.dateService = dateService; this.dateService = dateService;
@ -63,7 +63,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle drag start event * Handle drag start event
*/ */
public handleDragStart(payload: DragStartEventPayload): void { public handleDragStart(payload: IDragStartEventPayload): void {
this.originalEvent = payload.originalElement;; this.originalEvent = payload.originalElement;;
@ -98,7 +98,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle drag move event * Handle drag move event
*/ */
public handleDragMove(payload: DragMoveEventPayload): void { public handleDragMove(payload: IDragMoveEventPayload): void {
const swpEvent = payload.draggedClone as SwpEventElement; const swpEvent = payload.draggedClone as SwpEventElement;
const columnDate = this.dateService.parseISO(payload.columnBounds!!.date); const columnDate = this.dateService.parseISO(payload.columnBounds!!.date);
@ -108,7 +108,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle column change during drag * Handle column change during drag
*/ */
public handleColumnChange(payload: DragColumnChangeEventPayload): void { public handleColumnChange(payload: IDragColumnChangeEventPayload): void {
const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer'); const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer');
if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) { if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) {
@ -125,7 +125,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle conversion of all-day event to timed event * 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', { console.log('🎯 DateEventRenderer: Converting all-day to timed event', {
eventId: payload.calendarEvent.id, eventId: payload.calendarEvent.id,
@ -165,7 +165,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle drag end event * 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) { if (!draggedClone || !originalElement) {
console.warn('Missing draggedClone or originalElement'); console.warn('Missing draggedClone or originalElement');
return; 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 // Filter out all-day events - they should be handled by AllDayEventRenderer
const timedEvents = events.filter(event => !event.allDay); 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 * 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; if (columnEvents.length === 0) return;
// Get layout from coordinator // 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) * 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'); const groupElement = document.createElement('swp-event-group');
// Add grid column class based on number of columns (not events) // Add grid column class based on number of columns (not events)
@ -275,7 +275,7 @@ export class DateEventRenderer implements IEventRenderer {
// Render each column // Render each column
const earliestEvent = gridGroup.events[0]; const earliestEvent = gridGroup.events[0];
gridGroup.columns.forEach(columnEvents => { gridGroup.columns.forEach((columnEvents: ICalendarEvent[]) => {
const columnContainer = this.renderGridColumn(columnEvents, earliestEvent.start); const columnContainer = this.renderGridColumn(columnEvents, earliestEvent.start);
groupElement.appendChild(columnContainer); groupElement.appendChild(columnContainer);
}); });
@ -287,7 +287,7 @@ export class DateEventRenderer implements IEventRenderer {
* Render a single column within a grid group * Render a single column within a grid group
* Column may contain multiple events that don't overlap * 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'); const columnContainer = document.createElement('div');
columnContainer.style.position = 'relative'; columnContainer.style.position = 'relative';
@ -302,7 +302,7 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Render event within a grid container (absolute positioning within column) * 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); const element = SwpEventElement.fromCalendarEvent(event);
// Calculate event height // 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); const element = SwpEventElement.fromCalendarEvent(event);
// Apply positioning (moved from SwpEventElement.applyPositioning) // Apply positioning (moved from SwpEventElement.applyPositioning)
@ -340,7 +340,7 @@ export class DateEventRenderer implements IEventRenderer {
return element; 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 // Delegate to PositionUtils for centralized position calculation
return this.positionUtils.calculateEventPosition(event.start, event.end); return this.positionUtils.calculateEventPosition(event.start, event.end);
} }
@ -366,7 +366,7 @@ export class DateEventRenderer implements IEventRenderer {
return Array.from(columns) as HTMLElement[]; 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; const columnDate = column.dataset.date;
if (!columnDate) { if (!columnDate) {
return []; return [];

View file

@ -1,12 +1,12 @@
import { EventBus } from '../core/EventBus'; 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 { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { IEventRenderer } from './EventRenderer'; import { IEventRenderer } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement'; 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 { DateService } from '../utils/DateService';
import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { IColumnBounds } from '../utils/ColumnDetectionUtils';
/** /**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection * Håndterer event positioning og overlap detection
@ -36,7 +36,7 @@ export class EventRenderingService {
/** /**
* Render events in a specific container for a given period * 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 // Clear existing events in the specific container first
this.strategy.clearEvents(context.container); this.strategy.clearEvents(context.container);
@ -133,7 +133,7 @@ export class EventRenderingService {
private setupDragStartListener(): void { private setupDragStartListener(): void {
this.eventBus.on('drag:start', (event: Event) => { this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail; const dragStartPayload = (event as CustomEvent<IDragStartEventPayload>).detail;
if (dragStartPayload.originalElement.hasAttribute('data-allday')) { if (dragStartPayload.originalElement.hasAttribute('data-allday')) {
return; return;
@ -147,7 +147,7 @@ export class EventRenderingService {
private setupDragMoveListener(): void { private setupDragMoveListener(): void {
this.eventBus.on('drag:move', (event: Event) => { this.eventBus.on('drag:move', (event: Event) => {
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail; let dragEvent = (event as CustomEvent<IDragMoveEventPayload>).detail;
if (dragEvent.draggedClone.hasAttribute('data-allday')) { if (dragEvent.draggedClone.hasAttribute('data-allday')) {
return; return;
@ -161,7 +161,7 @@ export class EventRenderingService {
private setupDragEndListener(): void { private setupDragEndListener(): void {
this.eventBus.on('drag:end', (event: Event) => { this.eventBus.on('drag:end', (event: Event) => {
const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail; const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
const finalColumn = finalPosition.column; const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY; const finalY = finalPosition.snappedY;
const eventId = draggedElement.dataset.eventId || ''; const eventId = draggedElement.dataset.eventId || '';
@ -207,7 +207,7 @@ export class EventRenderingService {
private setupDragColumnChangeListener(): void { private setupDragColumnChangeListener(): void {
this.eventBus.on('drag:column-change', (event: Event) => { this.eventBus.on('drag:column-change', (event: Event) => {
let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail; let columnChangeEvent = (event as CustomEvent<IDragColumnChangeEventPayload>).detail;
// Filter: Only handle events where clone is NOT an all-day event (normal timed events) // Filter: Only handle events where clone is NOT an all-day event (normal timed events)
if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) { if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) {
@ -223,7 +223,7 @@ export class EventRenderingService {
private setupDragMouseLeaveHeaderListener(): void { private setupDragMouseLeaveHeaderListener(): void {
this.dragMouseLeaveHeaderListener = (event: Event) => { this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail; const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<IDragMouseLeaveHeaderEventPayload>).detail;
if (cloneElement) if (cloneElement)
cloneElement.style.display = ''; cloneElement.style.display = '';
@ -241,7 +241,7 @@ export class EventRenderingService {
private setupDragMouseEnterColumnListener(): void { private setupDragMouseEnterColumnListener(): void {
this.eventBus.on('drag:mouseenter-column', (event: Event) => { this.eventBus.on('drag:mouseenter-column', (event: Event) => {
const payload = (event as CustomEvent<DragMouseEnterColumnEventPayload>).detail; const payload = (event as CustomEvent<IDragMouseEnterColumnEventPayload>).detail;
// Only handle if clone is an all-day event // Only handle if clone is an all-day event
if (!payload.draggedClone.hasAttribute('data-allday')) { if (!payload.draggedClone.hasAttribute('data-allday')) {
@ -263,7 +263,7 @@ export class EventRenderingService {
private setupResizeEndListener(): void { private setupResizeEndListener(): void {
this.eventBus.on('resize:end', (event: Event) => { this.eventBus.on('resize:end', (event: Event) => {
const { eventId, element } = (event as CustomEvent<ResizeEndEventPayload>).detail; const { eventId, element } = (event as CustomEvent<IResizeEndEventPayload>).detail;
// Update event data in EventManager with new end time from resized element // Update event data in EventManager with new end time from resized element
const swpEvent = element as SwpEventElement; const swpEvent = element as SwpEventElement;
@ -306,7 +306,7 @@ export class EventRenderingService {
/** /**
* Re-render affected columns after drag to recalculate stacking/grouping * 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<string>(); const columnsToRender = new Set<string>();
// Add source column if exists // Add source column if exists

View file

@ -1,6 +1,6 @@
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { CalendarView } from '../types/CalendarTypes'; import { CalendarView } from '../types/CalendarTypes';
import { ColumnRenderer, ColumnRenderContext } from './ColumnRenderer'; import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
@ -82,13 +82,13 @@ export class GridRenderer {
private cachedGridContainer: HTMLElement | null = null; private cachedGridContainer: HTMLElement | null = null;
private cachedTimeAxis: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null;
private dateService: DateService; private dateService: DateService;
private columnRenderer: ColumnRenderer; private columnRenderer: IColumnRenderer;
private config: CalendarConfig; private config: Configuration;
constructor( constructor(
columnRenderer: ColumnRenderer, columnRenderer: IColumnRenderer,
dateService: DateService, dateService: DateService,
config: CalendarConfig config: Configuration
) { ) {
this.dateService = dateService; this.dateService = dateService;
this.columnRenderer = columnRenderer; this.columnRenderer = columnRenderer;
@ -255,7 +255,7 @@ export class GridRenderer {
currentDate: Date, currentDate: Date,
view: CalendarView view: CalendarView
): void { ): void {
const context: ColumnRenderContext = { const context: IColumnRenderContext = {
currentWeek: currentDate, // ColumnRenderer expects currentWeek property currentWeek: currentDate, // ColumnRenderer expects currentWeek property
config: this.config config: this.config
}; };

View file

@ -1,4 +1,4 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
/** /**
* IEventRepository - Interface for event data loading * IEventRepository - Interface for event data loading
@ -13,8 +13,8 @@ import { CalendarEvent } from '../types/CalendarTypes';
export interface IEventRepository { export interface IEventRepository {
/** /**
* Load all calendar events from the data source * 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 * @throws Error if loading fails
*/ */
loadEvents(): Promise<CalendarEvent[]>; loadEvents(): Promise<ICalendarEvent[]>;
} }

View file

@ -1,4 +1,4 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { IEventRepository } from './IEventRepository'; import { IEventRepository } from './IEventRepository';
interface RawEventData { interface RawEventData {
@ -23,7 +23,7 @@ interface RawEventData {
export class MockEventRepository implements IEventRepository { export class MockEventRepository implements IEventRepository {
private readonly dataUrl = 'data/mock-events.json'; private readonly dataUrl = 'data/mock-events.json';
public async loadEvents(): Promise<CalendarEvent[]> { public async loadEvents(): Promise<ICalendarEvent[]> {
try { try {
const response = await fetch(this.dataUrl); const response = await fetch(this.dataUrl);
@ -40,8 +40,8 @@ export class MockEventRepository implements IEventRepository {
} }
} }
private processCalendarData(data: RawEventData[]): CalendarEvent[] { private processCalendarData(data: RawEventData[]): ICalendarEvent[] {
return data.map((event): CalendarEvent => ({ return data.map((event): ICalendarEvent => ({
...event, ...event,
start: new Date(event.start), start: new Date(event.start),
end: new Date(event.end), end: new Date(event.end),

View file

@ -8,13 +8,13 @@ export type CalendarView = ViewPeriod;
export type SyncStatus = 'synced' | 'pending' | 'error'; export type SyncStatus = 'synced' | 'pending' | 'error';
export interface RenderContext { export interface IRenderContext {
container: HTMLElement; container: HTMLElement;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
} }
export interface CalendarEvent { export interface ICalendarEvent {
id: string; id: string;
title: string; title: string;
start: Date; start: Date;
@ -55,13 +55,13 @@ export interface ICalendarConfig {
maxEventDuration: number; // Minutes maxEventDuration: number; // Minutes
} }
export interface EventLogEntry { export interface IEventLogEntry {
type: string; type: string;
detail: unknown; detail: unknown;
timestamp: number; timestamp: number;
} }
export interface ListenerEntry { export interface IListenerEntry {
eventType: string; eventType: string;
handler: EventListener; handler: EventListener;
options?: AddEventListenerOptions; options?: AddEventListenerOptions;
@ -72,6 +72,6 @@ export interface IEventBus {
once(eventType: string, handler: EventListener): () => void; once(eventType: string, handler: EventListener): () => void;
off(eventType: string, handler: EventListener): void; off(eventType: string, handler: EventListener): void;
emit(eventType: string, detail?: unknown): boolean; emit(eventType: string, detail?: unknown): boolean;
getEventLog(eventType?: string): EventLogEntry[]; getEventLog(eventType?: string): IEventLogEntry[];
setDebug(enabled: boolean): void; setDebug(enabled: boolean): void;
} }

View file

@ -2,46 +2,46 @@
* Type definitions for drag and drop functionality * Type definitions for drag and drop functionality
*/ */
export interface MousePosition { export interface IMousePosition {
x: number; x: number;
y: number; y: number;
clientX?: number; clientX?: number;
clientY?: number; clientY?: number;
} }
export interface DragOffset { export interface IDragOffset {
x: number; x: number;
y: number; y: number;
offsetX?: number; offsetX?: number;
offsetY?: number; offsetY?: number;
} }
export interface DragState { export interface IDragState {
isDragging: boolean; isDragging: boolean;
draggedElement: HTMLElement | null; draggedElement: HTMLElement | null;
draggedClone: HTMLElement | null; draggedClone: HTMLElement | null;
eventId: string | null; eventId: string | null;
startColumn: string | null; startColumn: string | null;
currentColumn: string | null; currentColumn: string | null;
mouseOffset: DragOffset; mouseOffset: IDragOffset;
} }
export interface DragEndPosition { export interface IDragEndPosition {
column: string; column: string;
y: number; y: number;
snappedY: number; snappedY: number;
time?: Date; time?: Date;
} }
export interface StackLinkData { export interface IStackLinkData {
prev?: string; prev?: string;
next?: string; next?: string;
isFirst?: boolean; isFirst?: boolean;
isLast?: boolean; isLast?: boolean;
} }
export interface DragEventHandlers { export interface IDragEventHandlers {
handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void; handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: IDragOffset, column: string): void;
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void; handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: IDragOffset): void;
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void; handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
} }

View file

@ -2,8 +2,8 @@
* Type definitions for calendar events and drag operations * Type definitions for calendar events and drag operations
*/ */
import { ColumnBounds } from "../utils/ColumnDetectionUtils"; import { IColumnBounds } from "../utils/ColumnDetectionUtils";
import { CalendarEvent } from "./CalendarTypes"; import { ICalendarEvent } from "./CalendarTypes";
/** /**
* Drag Event Payload Interfaces * Drag Event Payload Interfaces
@ -11,89 +11,89 @@ import { CalendarEvent } from "./CalendarTypes";
*/ */
// Common position interface // Common position interface
export interface MousePosition { export interface IMousePosition {
x: number; x: number;
y: number; y: number;
} }
// Drag start event payload // Drag start event payload
export interface DragStartEventPayload { export interface IDragStartEventPayload {
originalElement: HTMLElement; originalElement: HTMLElement;
draggedClone: HTMLElement | null; draggedClone: HTMLElement | null;
mousePosition: MousePosition; mousePosition: IMousePosition;
mouseOffset: MousePosition; mouseOffset: IMousePosition;
columnBounds: ColumnBounds | null; columnBounds: IColumnBounds | null;
} }
// Drag move event payload // Drag move event payload
export interface DragMoveEventPayload { export interface IDragMoveEventPayload {
originalElement: HTMLElement; originalElement: HTMLElement;
draggedClone: HTMLElement; draggedClone: HTMLElement;
mousePosition: MousePosition; mousePosition: IMousePosition;
mouseOffset: MousePosition; mouseOffset: IMousePosition;
columnBounds: ColumnBounds | null; columnBounds: IColumnBounds | null;
snappedY: number; snappedY: number;
} }
// Drag end event payload // Drag end event payload
export interface DragEndEventPayload { export interface IDragEndEventPayload {
originalElement: HTMLElement; originalElement: HTMLElement;
draggedClone: HTMLElement | null; draggedClone: HTMLElement | null;
mousePosition: MousePosition; mousePosition: IMousePosition;
sourceColumn: ColumnBounds; sourceColumn: IColumnBounds;
finalPosition: { finalPosition: {
column: ColumnBounds | null; // Where drag ended column: IColumnBounds | null; // Where drag ended
snappedY: number; snappedY: number;
}; };
target: 'swp-day-column' | 'swp-day-header' | null; target: 'swp-day-column' | 'swp-day-header' | null;
} }
// Drag mouse enter header event payload // Drag mouse enter header event payload
export interface DragMouseEnterHeaderEventPayload { export interface IDragMouseEnterHeaderEventPayload {
targetColumn: ColumnBounds; targetColumn: IColumnBounds;
mousePosition: MousePosition; mousePosition: IMousePosition;
originalElement: HTMLElement | null; originalElement: HTMLElement | null;
draggedClone: HTMLElement; draggedClone: HTMLElement;
calendarEvent: CalendarEvent; calendarEvent: ICalendarEvent;
replaceClone: (newClone: HTMLElement) => void; replaceClone: (newClone: HTMLElement) => void;
} }
// Drag mouse leave header event payload // Drag mouse leave header event payload
export interface DragMouseLeaveHeaderEventPayload { export interface IDragMouseLeaveHeaderEventPayload {
targetDate: string | null; targetDate: string | null;
mousePosition: MousePosition; mousePosition: IMousePosition;
originalElement: HTMLElement| null; originalElement: HTMLElement| null;
draggedClone: HTMLElement| null; draggedClone: HTMLElement| null;
} }
// Drag mouse enter column event payload // Drag mouse enter column event payload
export interface DragMouseEnterColumnEventPayload { export interface IDragMouseEnterColumnEventPayload {
targetColumn: ColumnBounds; targetColumn: IColumnBounds;
mousePosition: MousePosition; mousePosition: IMousePosition;
snappedY: number; snappedY: number;
originalElement: HTMLElement | null; originalElement: HTMLElement | null;
draggedClone: HTMLElement; draggedClone: HTMLElement;
calendarEvent: CalendarEvent; calendarEvent: ICalendarEvent;
replaceClone: (newClone: HTMLElement) => void; replaceClone: (newClone: HTMLElement) => void;
} }
// Drag column change event payload // Drag column change event payload
export interface DragColumnChangeEventPayload { export interface IDragColumnChangeEventPayload {
originalElement: HTMLElement; originalElement: HTMLElement;
draggedClone: HTMLElement; draggedClone: HTMLElement;
previousColumn: ColumnBounds | null; previousColumn: IColumnBounds | null;
newColumn: ColumnBounds; newColumn: IColumnBounds;
mousePosition: MousePosition; mousePosition: IMousePosition;
} }
// Header ready event payload // Header ready event payload
export interface HeaderReadyEventPayload { export interface IHeaderReadyEventPayload {
headerElements: ColumnBounds[]; headerElements: IColumnBounds[];
} }
// Resize end event payload // Resize end event payload
export interface ResizeEndEventPayload { export interface IResizeEndEventPayload {
eventId: string; eventId: string;
element: HTMLElement; element: HTMLElement;
finalHeight: number; finalHeight: number;

View file

@ -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 * Complete type definition for all managers returned by ManagerFactory
*/ */
export interface CalendarManagers { export interface ICalendarManagers {
eventManager: EventManager; eventManager: IEventManager;
eventRenderer: EventRenderingService; eventRenderer: IEventRenderingService;
gridManager: GridManager; gridManager: IGridManager;
scrollManager: ScrollManager; scrollManager: IScrollManager;
navigationManager: unknown; // Avoid interface conflicts navigationManager: unknown; // Avoid interface conflicts
viewManager: ViewManager; viewManager: IViewManager;
calendarManager: CalendarManager; calendarManager: ICalendarManager;
dragDropManager: unknown; // Avoid interface conflicts dragDropManager: unknown; // Avoid interface conflicts
allDayManager: unknown; // Avoid interface conflicts allDayManager: unknown; // Avoid interface conflicts
resizeHandleManager: ResizeHandleManager; resizeHandleManager: IResizeHandleManager;
edgeScrollManager: unknown; // Avoid interface conflicts edgeScrollManager: unknown; // Avoid interface conflicts
dragHoverManager: unknown; // Avoid interface conflicts dragHoverManager: unknown; // Avoid interface conflicts
headerManager: unknown; // Avoid interface conflicts headerManager: unknown; // Avoid interface conflicts
@ -27,50 +27,50 @@ interface IManager {
refresh?(): void; refresh?(): void;
} }
export interface EventManager extends IManager { export interface IEventManager extends IManager {
loadData(): Promise<void>; loadData(): Promise<void>;
getEvents(): CalendarEvent[]; getEvents(): ICalendarEvent[];
getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[]; getEventsForPeriod(startDate: Date, endDate: Date): ICalendarEvent[];
navigateToEvent(eventId: string): boolean; navigateToEvent(eventId: string): boolean;
} }
export interface EventRenderingService extends IManager { export interface IEventRenderingService extends IManager {
// EventRenderingService doesn't have a render method in current implementation // EventRenderingService doesn't have a render method in current implementation
} }
export interface GridManager extends IManager { export interface IGridManager extends IManager {
render(): Promise<void>; render(): Promise<void>;
getDisplayDates(): Date[]; getDisplayDates(): Date[];
} }
export interface ScrollManager extends IManager { export interface IScrollManager extends IManager {
scrollTo(scrollTop: number): void; scrollTo(scrollTop: number): void;
scrollToHour(hour: number): void; scrollToHour(hour: number): void;
} }
// Use a more flexible interface that matches actual implementation // 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 [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 // ViewManager doesn't have setView in current implementation
getCurrentView?(): CalendarView; getCurrentView?(): CalendarView;
} }
export interface CalendarManager extends IManager { export interface ICalendarManager extends IManager {
setView(view: CalendarView): void; setView(view: CalendarView): void;
setCurrentDate(date: Date): void; setCurrentDate(date: Date): void;
} }
export interface DragDropManager extends IManager { export interface IDragDropManager extends IManager {
// DragDropManager has different interface in current implementation // 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 [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 // ResizeHandleManager handles hover effects for resize handles
} }

View file

@ -1,7 +1,7 @@
import { CalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
export interface EventLayout { export interface IEventLayout {
calenderEvent: CalendarEvent; calenderEvent: ICalendarEvent;
gridArea: string; // "row-start / col-start / row-end / col-end" gridArea: string; // "row-start / col-start / row-end / col-end"
startColumn: number; startColumn: number;
endColumn: number; endColumn: number;
@ -21,9 +21,9 @@ export class AllDayLayoutEngine {
/** /**
* Calculate layout for all events using clean day-based logic * Calculate layout for all events using clean day-based logic
*/ */
public calculateLayout(events: CalendarEvent[]): EventLayout[] { public calculateLayout(events: ICalendarEvent[]): IEventLayout[] {
let layouts: EventLayout[] = []; let layouts: IEventLayout[] = [];
// Reset tracks for new calculation // Reset tracks for new calculation
this.tracks = [new Array(this.weekDates.length).fill(false)]; this.tracks = [new Array(this.weekDates.length).fill(false)];
@ -43,7 +43,7 @@ export class AllDayLayoutEngine {
this.tracks[track][day] = true; this.tracks[track][day] = true;
} }
const layout: EventLayout = { const layout: IEventLayout = {
calenderEvent: event, calenderEvent: event,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`, gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay, startColumn: startDay,
@ -89,7 +89,7 @@ export class AllDayLayoutEngine {
/** /**
* Get start day index for event (1-based, 0 if not visible) * Get start day index for event (1-based, 0 if not visible)
*/ */
private getEventStartDay(event: CalendarEvent): number { private getEventStartDay(event: ICalendarEvent): number {
const eventStartDate = this.formatDate(event.start); const eventStartDate = this.formatDate(event.start);
const firstVisibleDate = this.weekDates[0]; const firstVisibleDate = this.weekDates[0];
@ -103,7 +103,7 @@ export class AllDayLayoutEngine {
/** /**
* Get end day index for event (1-based, 0 if not visible) * Get end day index for event (1-based, 0 if not visible)
*/ */
private getEventEndDay(event: CalendarEvent): number { private getEventEndDay(event: ICalendarEvent): number {
const eventEndDate = this.formatDate(event.end); const eventEndDate = this.formatDate(event.end);
const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
@ -117,7 +117,7 @@ export class AllDayLayoutEngine {
/** /**
* Check if event is visible in the current date range * Check if event is visible in the current date range
*/ */
private isEventVisible(event: CalendarEvent): boolean { private isEventVisible(event: ICalendarEvent): boolean {
if (this.weekDates.length === 0) return false; if (this.weekDates.length === 0) return false;
const eventStartDate = this.formatDate(event.start); const eventStartDate = this.formatDate(event.start);

View file

@ -3,10 +3,10 @@
* Used by both DragDropManager and AllDayManager for consistent column detection * Used by both DragDropManager and AllDayManager for consistent column detection
*/ */
import { MousePosition } from "../types/DragDropTypes"; import { IMousePosition } from "../types/DragDropTypes";
export interface ColumnBounds { export interface IColumnBounds {
date: string; date: string;
left: number; left: number;
right: number; right: number;
@ -16,7 +16,7 @@ export interface ColumnBounds {
} }
export class ColumnDetectionUtils { export class ColumnDetectionUtils {
private static columnBoundsCache: ColumnBounds[] = []; private static columnBoundsCache: IColumnBounds[] = [];
/** /**
* Update column bounds cache for coordinate-based column detection * Update column bounds cache for coordinate-based column detection
@ -52,7 +52,7 @@ export class ColumnDetectionUtils {
/** /**
* Get column date from X coordinate using cached bounds * Get column date from X coordinate using cached bounds
*/ */
public static getColumnBounds(position: MousePosition): ColumnBounds | null{ public static getColumnBounds(position: IMousePosition): IColumnBounds | null{
if (this.columnBoundsCache.length === 0) { if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
} }
@ -70,7 +70,7 @@ export class ColumnDetectionUtils {
/** /**
* Get column bounds by Date * Get column bounds by Date
*/ */
public static getColumnBoundsByDate(date: Date): ColumnBounds | null { public static getColumnBoundsByDate(date: Date): IColumnBounds | null {
if (this.columnBoundsCache.length === 0) { if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
} }
@ -84,12 +84,12 @@ export class ColumnDetectionUtils {
} }
public static getColumns(): ColumnBounds[] { public static getColumns(): IColumnBounds[] {
return [...this.columnBoundsCache]; return [...this.columnBoundsCache];
} }
public static getHeaderColumns(): ColumnBounds[] { public static getHeaderColumns(): IColumnBounds[] {
let dayHeaders: ColumnBounds[] = []; let dayHeaders: IColumnBounds[] = [];
const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header'); const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header');
let index = 1; let index = 1;

View file

@ -29,12 +29,12 @@ import {
fromZonedTime, fromZonedTime,
formatInTimeZone formatInTimeZone
} from 'date-fns-tz'; } from 'date-fns-tz';
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
export class DateService { export class DateService {
private timezone: string; private timezone: string;
constructor(config: CalendarConfig) { constructor(config: Configuration) {
this.timezone = config.getTimezone(); this.timezone = config.getTimezone();
} }

View file

@ -1,5 +1,5 @@
import { CalendarConfig } from '../core/CalendarConfig'; import { Configuration } from '../configuration/CalendarConfig';
import { ColumnBounds } from './ColumnDetectionUtils'; import { IColumnBounds } from './ColumnDetectionUtils';
import { DateService } from './DateService'; import { DateService } from './DateService';
import { TimeFormatter } from './TimeFormatter'; import { TimeFormatter } from './TimeFormatter';
@ -11,9 +11,9 @@ import { TimeFormatter } from './TimeFormatter';
*/ */
export class PositionUtils { export class PositionUtils {
private dateService: DateService; private dateService: DateService;
private config: CalendarConfig; private config: Configuration;
constructor(dateService: DateService, config: CalendarConfig) { constructor(dateService: DateService, config: Configuration) {
this.dateService = dateService; this.dateService = dateService;
this.config = config; this.config = config;
} }
@ -169,7 +169,7 @@ export class PositionUtils {
/** /**
* Beregn Y position fra mouse/touch koordinat * 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; const relativeY = clientY - column.boundingClientRect.top;

View file

@ -10,7 +10,7 @@
import { DateService } from './DateService'; import { DateService } from './DateService';
export interface TimeFormatSettings { export interface ITimeFormatSettings {
timezone: string; timezone: string;
use24HourFormat: boolean; use24HourFormat: boolean;
locale: string; locale: string;
@ -19,7 +19,7 @@ export interface TimeFormatSettings {
} }
export class TimeFormatter { export class TimeFormatter {
private static settings: TimeFormatSettings = { private static settings: ITimeFormatSettings = {
timezone: 'Europe/Copenhagen', // Default to Denmark timezone: 'Europe/Copenhagen', // Default to Denmark
use24HourFormat: true, // 24-hour format standard in Denmark use24HourFormat: true, // 24-hour format standard in Denmark
locale: 'da-DK', // Danish locale locale: 'da-DK', // Danish locale
@ -44,7 +44,7 @@ export class TimeFormatter {
/** /**
* Configure time formatting settings * Configure time formatting settings
*/ */
static configure(settings: Partial<TimeFormatSettings>): void { static configure(settings: Partial<ITimeFormatSettings>): void {
TimeFormatter.settings = { ...TimeFormatter.settings, ...settings }; TimeFormatter.settings = { ...TimeFormatter.settings, ...settings };
// Reset DateService to pick up new timezone // Reset DateService to pick up new timezone
TimeFormatter.dateService = null; TimeFormatter.dateService = null;

View file

@ -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
}
}

View file

@ -15,7 +15,7 @@
</head> </head>
<body> <body>
<div class="calendar-wrapper"> <div class="calendar-wrapper">
<swp-calendar data-view="week" data-week-days="7" data-snap-interval="15" data-day-start-hour="6" data-day-end-hour="22" data-hour-height="80"> <swp-calendar>
<!-- Navigation Bar --> <!-- Navigation Bar -->
<swp-calendar-nav> <swp-calendar-nav>
<swp-nav-group> <swp-nav-group>