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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,230 +1,230 @@
/**
* EdgeScrollManager - Auto-scroll when dragging near edges
* Uses time-based scrolling with 2-zone system for variable speed
*/
import { IEventBus } from '../types/CalendarTypes';
import { DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
export class EdgeScrollManager {
private scrollableContent: HTMLElement | null = null;
private timeGrid: HTMLElement | null = null;
private draggedClone: HTMLElement | null = null;
private scrollRAF: number | null = null;
private mouseY = 0;
private isDragging = false;
private isScrolling = false; // Track if edge-scroll is active
private lastTs = 0;
private rect: DOMRect | null = null;
private initialScrollTop = 0;
private scrollListener: ((e: Event) => void) | null = null;
// Constants - fixed values as per requirements
private readonly OUTER_ZONE = 100; // px from edge (slow zone)
private readonly INNER_ZONE = 50; // px from edge (fast zone)
private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone
constructor(private eventBus: IEventBus) {
this.init();
}
private init(): void {
// Wait for DOM to be ready
setTimeout(() => {
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.timeGrid = document.querySelector('swp-time-grid');
if (this.scrollableContent) {
// Disable smooth scroll for instant auto-scroll
this.scrollableContent.style.scrollBehavior = 'auto';
// Add scroll listener to detect actual scrolling
this.scrollListener = this.handleScroll.bind(this);
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
}
}, 100);
// Listen to mousemove directly from document to always get mouse coords
document.body.addEventListener('mousemove', (e: MouseEvent) => {
if (this.isDragging) {
this.mouseY = e.clientY;
}
});
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen to drag events from DragDropManager
this.eventBus.on('drag:start', (event: Event) => {
const payload = (event as CustomEvent).detail;
this.draggedClone = payload.draggedClone;
this.startDrag();
});
this.eventBus.on('drag:end', () => this.stopDrag());
this.eventBus.on('drag:cancelled', () => this.stopDrag());
// Stop scrolling when event converts to/from all-day
this.eventBus.on('drag:mouseenter-header', () => {
console.log('🔄 EdgeScrollManager: Event converting to all-day - stopping scroll');
this.stopDrag();
});
this.eventBus.on('drag:mouseenter-column', () => {
this.startDrag();
});
}
private startDrag(): void {
console.log('🎬 EdgeScrollManager: Starting drag');
this.isDragging = true;
this.isScrolling = false; // Reset scroll state
this.lastTs = performance.now();
// Save initial scroll position
if (this.scrollableContent) {
this.initialScrollTop = this.scrollableContent.scrollTop;
}
if (this.scrollRAF === null) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
}
private stopDrag(): void {
this.isDragging = false;
// Emit stopped event if we were scrolling
if (this.isScrolling) {
this.isScrolling = false;
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (drag ended)');
this.eventBus.emit('edgescroll:stopped', {});
}
if (this.scrollRAF !== null) {
cancelAnimationFrame(this.scrollRAF);
this.scrollRAF = null;
}
this.rect = null;
this.lastTs = 0;
this.initialScrollTop = 0;
}
private handleScroll(): void {
if (!this.isDragging || !this.scrollableContent) return;
const currentScrollTop = this.scrollableContent.scrollTop;
const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop);
// Only emit started event if we've actually scrolled more than 1px
if (scrollDelta > 1 && !this.isScrolling) {
this.isScrolling = true;
console.log('💾 EdgeScrollManager: Edge-scroll started (actual scroll detected)', {
initialScrollTop: this.initialScrollTop,
currentScrollTop,
scrollDelta
});
this.eventBus.emit('edgescroll:started', {});
}
}
private scrollTick(ts: number): void {
const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
this.lastTs = ts;
if (!this.scrollableContent) {
this.stopDrag();
return;
}
// Cache rect for performance (only measure once per frame)
if (!this.rect) {
this.rect = this.scrollableContent.getBoundingClientRect();
}
let vy = 0;
if (this.isDragging) {
const distTop = this.mouseY - this.rect.top;
const distBot = this.rect.bottom - this.mouseY;
// Check top edge
if (distTop < this.INNER_ZONE) {
vy = -this.FAST_SPEED_PXS;
} else if (distTop < this.OUTER_ZONE) {
vy = -this.SLOW_SPEED_PXS;
}
// Check bottom edge
else if (distBot < this.INNER_ZONE) {
vy = this.FAST_SPEED_PXS;
} else if (distBot < this.OUTER_ZONE) {
vy = this.SLOW_SPEED_PXS;
}
}
if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedClone) {
// Check if we can scroll in the requested direction
const currentScrollTop = this.scrollableContent.scrollTop;
const scrollableHeight = this.scrollableContent.clientHeight;
const timeGridHeight = this.timeGrid.clientHeight;
// Get dragged element position and height
const cloneRect = this.draggedClone.getBoundingClientRect();
const cloneBottom = cloneRect.bottom;
const timeGridRect = this.timeGrid.getBoundingClientRect();
const timeGridBottom = timeGridRect.bottom;
// Check boundaries
const atTop = currentScrollTop <= 0 && vy < 0;
const atBottom = (cloneBottom >= timeGridBottom) && vy > 0;
console.log('📊 Scroll check:', {
currentScrollTop,
scrollableHeight,
timeGridHeight,
cloneBottom,
timeGridBottom,
atTop,
atBottom,
vy
});
if (atTop || atBottom) {
// At boundary - stop scrolling
if (this.isScrolling) {
this.isScrolling = false;
this.initialScrollTop = this.scrollableContent.scrollTop;
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (reached boundary)');
this.eventBus.emit('edgescroll:stopped', {});
}
// Continue RAF loop to detect when mouse moves away from boundary
if (this.isDragging) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
} else {
// Not at boundary - apply scroll
this.scrollableContent.scrollTop += vy * dt;
this.rect = null; // Invalidate cache for next frame
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
} else {
// Mouse moved away from edge - stop scrolling
if (this.isScrolling) {
this.isScrolling = false;
this.initialScrollTop = this.scrollableContent.scrollTop; // Reset for next scroll
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (mouse left edge)');
this.eventBus.emit('edgescroll:stopped', {});
}
// Continue RAF loop even if not scrolling, to detect edge entry
if (this.isDragging) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
} else {
this.stopDrag();
}
}
}
/**
* EdgeScrollManager - Auto-scroll when dragging near edges
* Uses time-based scrolling with 2-zone system for variable speed
*/
import { IEventBus } from '../types/CalendarTypes';
import { IDragMoveEventPayload, IDragStartEventPayload } from '../types/EventTypes';
export class EdgeScrollManager {
private scrollableContent: HTMLElement | null = null;
private timeGrid: HTMLElement | null = null;
private draggedClone: HTMLElement | null = null;
private scrollRAF: number | null = null;
private mouseY = 0;
private isDragging = false;
private isScrolling = false; // Track if edge-scroll is active
private lastTs = 0;
private rect: DOMRect | null = null;
private initialScrollTop = 0;
private scrollListener: ((e: Event) => void) | null = null;
// Constants - fixed values as per requirements
private readonly OUTER_ZONE = 100; // px from edge (slow zone)
private readonly INNER_ZONE = 50; // px from edge (fast zone)
private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone
constructor(private eventBus: IEventBus) {
this.init();
}
private init(): void {
// Wait for DOM to be ready
setTimeout(() => {
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.timeGrid = document.querySelector('swp-time-grid');
if (this.scrollableContent) {
// Disable smooth scroll for instant auto-scroll
this.scrollableContent.style.scrollBehavior = 'auto';
// Add scroll listener to detect actual scrolling
this.scrollListener = this.handleScroll.bind(this);
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
}
}, 100);
// Listen to mousemove directly from document to always get mouse coords
document.body.addEventListener('mousemove', (e: MouseEvent) => {
if (this.isDragging) {
this.mouseY = e.clientY;
}
});
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen to drag events from DragDropManager
this.eventBus.on('drag:start', (event: Event) => {
const payload = (event as CustomEvent).detail;
this.draggedClone = payload.draggedClone;
this.startDrag();
});
this.eventBus.on('drag:end', () => this.stopDrag());
this.eventBus.on('drag:cancelled', () => this.stopDrag());
// Stop scrolling when event converts to/from all-day
this.eventBus.on('drag:mouseenter-header', () => {
console.log('🔄 EdgeScrollManager: Event converting to all-day - stopping scroll');
this.stopDrag();
});
this.eventBus.on('drag:mouseenter-column', () => {
this.startDrag();
});
}
private startDrag(): void {
console.log('🎬 EdgeScrollManager: Starting drag');
this.isDragging = true;
this.isScrolling = false; // Reset scroll state
this.lastTs = performance.now();
// Save initial scroll position
if (this.scrollableContent) {
this.initialScrollTop = this.scrollableContent.scrollTop;
}
if (this.scrollRAF === null) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
}
private stopDrag(): void {
this.isDragging = false;
// Emit stopped event if we were scrolling
if (this.isScrolling) {
this.isScrolling = false;
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (drag ended)');
this.eventBus.emit('edgescroll:stopped', {});
}
if (this.scrollRAF !== null) {
cancelAnimationFrame(this.scrollRAF);
this.scrollRAF = null;
}
this.rect = null;
this.lastTs = 0;
this.initialScrollTop = 0;
}
private handleScroll(): void {
if (!this.isDragging || !this.scrollableContent) return;
const currentScrollTop = this.scrollableContent.scrollTop;
const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop);
// Only emit started event if we've actually scrolled more than 1px
if (scrollDelta > 1 && !this.isScrolling) {
this.isScrolling = true;
console.log('💾 EdgeScrollManager: Edge-scroll started (actual scroll detected)', {
initialScrollTop: this.initialScrollTop,
currentScrollTop,
scrollDelta
});
this.eventBus.emit('edgescroll:started', {});
}
}
private scrollTick(ts: number): void {
const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
this.lastTs = ts;
if (!this.scrollableContent) {
this.stopDrag();
return;
}
// Cache rect for performance (only measure once per frame)
if (!this.rect) {
this.rect = this.scrollableContent.getBoundingClientRect();
}
let vy = 0;
if (this.isDragging) {
const distTop = this.mouseY - this.rect.top;
const distBot = this.rect.bottom - this.mouseY;
// Check top edge
if (distTop < this.INNER_ZONE) {
vy = -this.FAST_SPEED_PXS;
} else if (distTop < this.OUTER_ZONE) {
vy = -this.SLOW_SPEED_PXS;
}
// Check bottom edge
else if (distBot < this.INNER_ZONE) {
vy = this.FAST_SPEED_PXS;
} else if (distBot < this.OUTER_ZONE) {
vy = this.SLOW_SPEED_PXS;
}
}
if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedClone) {
// Check if we can scroll in the requested direction
const currentScrollTop = this.scrollableContent.scrollTop;
const scrollableHeight = this.scrollableContent.clientHeight;
const timeGridHeight = this.timeGrid.clientHeight;
// Get dragged element position and height
const cloneRect = this.draggedClone.getBoundingClientRect();
const cloneBottom = cloneRect.bottom;
const timeGridRect = this.timeGrid.getBoundingClientRect();
const timeGridBottom = timeGridRect.bottom;
// Check boundaries
const atTop = currentScrollTop <= 0 && vy < 0;
const atBottom = (cloneBottom >= timeGridBottom) && vy > 0;
console.log('📊 Scroll check:', {
currentScrollTop,
scrollableHeight,
timeGridHeight,
cloneBottom,
timeGridBottom,
atTop,
atBottom,
vy
});
if (atTop || atBottom) {
// At boundary - stop scrolling
if (this.isScrolling) {
this.isScrolling = false;
this.initialScrollTop = this.scrollableContent.scrollTop;
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (reached boundary)');
this.eventBus.emit('edgescroll:stopped', {});
}
// Continue RAF loop to detect when mouse moves away from boundary
if (this.isDragging) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
} else {
// Not at boundary - apply scroll
this.scrollableContent.scrollTop += vy * dt;
this.rect = null; // Invalidate cache for next frame
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
} else {
// Mouse moved away from edge - stop scrolling
if (this.isScrolling) {
this.isScrolling = false;
this.initialScrollTop = this.scrollableContent.scrollTop; // Reset for next scroll
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (mouse left edge)');
this.eventBus.emit('edgescroll:stopped', {});
}
// Continue RAF loop even if not scrolling, to detect edge entry
if (this.isDragging) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
} else {
this.stopDrag();
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,142 +1,142 @@
import { CalendarEvent } from '../types/CalendarTypes';
export interface EventLayout {
calenderEvent: CalendarEvent;
gridArea: string; // "row-start / col-start / row-end / col-end"
startColumn: number;
endColumn: number;
row: number;
columnSpan: number;
}
export class AllDayLayoutEngine {
private weekDates: string[];
private tracks: boolean[][];
constructor(weekDates: string[]) {
this.weekDates = weekDates;
this.tracks = [];
}
/**
* Calculate layout for all events using clean day-based logic
*/
public calculateLayout(events: CalendarEvent[]): EventLayout[] {
let layouts: EventLayout[] = [];
// Reset tracks for new calculation
this.tracks = [new Array(this.weekDates.length).fill(false)];
// Filter to only visible events
const visibleEvents = events.filter(event => this.isEventVisible(event));
// Process events in input order (no sorting)
for (const event of visibleEvents) {
const startDay = this.getEventStartDay(event);
const endDay = this.getEventEndDay(event);
if (startDay > 0 && endDay > 0) {
const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks
// Mark days as occupied
for (let day = startDay - 1; day <= endDay - 1; day++) {
this.tracks[track][day] = true;
}
const layout: EventLayout = {
calenderEvent: event,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay,
endColumn: endDay,
row: track + 1,
columnSpan: endDay - startDay + 1
};
layouts.push(layout);
}
}
return layouts;
}
/**
* Find available track for event spanning from startDay to endDay (0-based indices)
*/
private findAvailableTrack(startDay: number, endDay: number): number {
for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) {
if (this.isTrackAvailable(trackIndex, startDay, endDay)) {
return trackIndex;
}
}
// Create new track if none available
this.tracks.push(new Array(this.weekDates.length).fill(false));
return this.tracks.length - 1;
}
/**
* Check if track is available for the given day range (0-based indices)
*/
private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
for (let day = startDay; day <= endDay; day++) {
if (this.tracks[trackIndex][day]) {
return false;
}
}
return true;
}
/**
* Get start day index for event (1-based, 0 if not visible)
*/
private getEventStartDay(event: CalendarEvent): number {
const eventStartDate = this.formatDate(event.start);
const firstVisibleDate = this.weekDates[0];
// If event starts before visible range, clip to first visible day
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
const dayIndex = this.weekDates.indexOf(clippedStartDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Get end day index for event (1-based, 0 if not visible)
*/
private getEventEndDay(event: CalendarEvent): number {
const eventEndDate = this.formatDate(event.end);
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// If event ends after visible range, clip to last visible day
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
const dayIndex = this.weekDates.indexOf(clippedEndDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Check if event is visible in the current date range
*/
private isEventVisible(event: CalendarEvent): boolean {
if (this.weekDates.length === 0) return false;
const eventStartDate = this.formatDate(event.start);
const eventEndDate = this.formatDate(event.end);
const firstVisibleDate = this.weekDates[0];
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// Event overlaps if it doesn't end before visible range starts
// AND doesn't start after visible range ends
return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);
}
/**
* Format date to YYYY-MM-DD string using local date
*/
private formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
import { ICalendarEvent } from '../types/CalendarTypes';
export interface IEventLayout {
calenderEvent: ICalendarEvent;
gridArea: string; // "row-start / col-start / row-end / col-end"
startColumn: number;
endColumn: number;
row: number;
columnSpan: number;
}
export class AllDayLayoutEngine {
private weekDates: string[];
private tracks: boolean[][];
constructor(weekDates: string[]) {
this.weekDates = weekDates;
this.tracks = [];
}
/**
* Calculate layout for all events using clean day-based logic
*/
public calculateLayout(events: ICalendarEvent[]): IEventLayout[] {
let layouts: IEventLayout[] = [];
// Reset tracks for new calculation
this.tracks = [new Array(this.weekDates.length).fill(false)];
// Filter to only visible events
const visibleEvents = events.filter(event => this.isEventVisible(event));
// Process events in input order (no sorting)
for (const event of visibleEvents) {
const startDay = this.getEventStartDay(event);
const endDay = this.getEventEndDay(event);
if (startDay > 0 && endDay > 0) {
const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks
// Mark days as occupied
for (let day = startDay - 1; day <= endDay - 1; day++) {
this.tracks[track][day] = true;
}
const layout: IEventLayout = {
calenderEvent: event,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay,
endColumn: endDay,
row: track + 1,
columnSpan: endDay - startDay + 1
};
layouts.push(layout);
}
}
return layouts;
}
/**
* Find available track for event spanning from startDay to endDay (0-based indices)
*/
private findAvailableTrack(startDay: number, endDay: number): number {
for (let trackIndex = 0; trackIndex < this.tracks.length; trackIndex++) {
if (this.isTrackAvailable(trackIndex, startDay, endDay)) {
return trackIndex;
}
}
// Create new track if none available
this.tracks.push(new Array(this.weekDates.length).fill(false));
return this.tracks.length - 1;
}
/**
* Check if track is available for the given day range (0-based indices)
*/
private isTrackAvailable(trackIndex: number, startDay: number, endDay: number): boolean {
for (let day = startDay; day <= endDay; day++) {
if (this.tracks[trackIndex][day]) {
return false;
}
}
return true;
}
/**
* Get start day index for event (1-based, 0 if not visible)
*/
private getEventStartDay(event: ICalendarEvent): number {
const eventStartDate = this.formatDate(event.start);
const firstVisibleDate = this.weekDates[0];
// If event starts before visible range, clip to first visible day
const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate;
const dayIndex = this.weekDates.indexOf(clippedStartDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Get end day index for event (1-based, 0 if not visible)
*/
private getEventEndDay(event: ICalendarEvent): number {
const eventEndDate = this.formatDate(event.end);
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// If event ends after visible range, clip to last visible day
const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate;
const dayIndex = this.weekDates.indexOf(clippedEndDate);
return dayIndex >= 0 ? dayIndex + 1 : 0;
}
/**
* Check if event is visible in the current date range
*/
private isEventVisible(event: ICalendarEvent): boolean {
if (this.weekDates.length === 0) return false;
const eventStartDate = this.formatDate(event.start);
const eventEndDate = this.formatDate(event.end);
const firstVisibleDate = this.weekDates[0];
const lastVisibleDate = this.weekDates[this.weekDates.length - 1];
// Event overlaps if it doesn't end before visible range starts
// AND doesn't start after visible range ends
return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);
}
/**
* Format date to YYYY-MM-DD string using local date
*/
private formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
}

View file

@ -1,118 +1,118 @@
/**
* ColumnDetectionUtils - Shared utility for column detection and caching
* Used by both DragDropManager and AllDayManager for consistent column detection
*/
import { MousePosition } from "../types/DragDropTypes";
export interface ColumnBounds {
date: string;
left: number;
right: number;
boundingClientRect: DOMRect,
element : HTMLElement,
index: number
}
export class ColumnDetectionUtils {
private static columnBoundsCache: ColumnBounds[] = [];
/**
* Update column bounds cache for coordinate-based column detection
*/
public static updateColumnBoundsCache(): void {
// Reset cache
this.columnBoundsCache = [];
// Find alle kolonner
const columns = document.querySelectorAll('swp-day-column');
let index = 1;
// Cache hver kolonnes x-grænser
columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
this.columnBoundsCache.push({
boundingClientRect : rect,
element: column as HTMLElement,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left);
}
/**
* Get column date from X coordinate using cached bounds
*/
public static getColumnBounds(position: MousePosition): ColumnBounds | null{
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Find den kolonne hvor x-koordinaten er indenfor grænserne
let column = this.columnBoundsCache.find(col =>
position.x >= col.left && position.x <= col.right
);
if (column)
return column;
return null;
}
/**
* Get column bounds by Date
*/
public static getColumnBoundsByDate(date: Date): ColumnBounds | null {
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Convert Date to YYYY-MM-DD format
let dateString = date.toISOString().split('T')[0];
// Find column that matches the date
let column = this.columnBoundsCache.find(col => col.date === dateString);
return column || null;
}
public static getColumns(): ColumnBounds[] {
return [...this.columnBoundsCache];
}
public static getHeaderColumns(): ColumnBounds[] {
let dayHeaders: ColumnBounds[] = [];
const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header');
let index = 1;
// Cache hver kolonnes x-grænser
dayColumns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
dayHeaders.push({
boundingClientRect : rect,
element: column as HTMLElement,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
dayHeaders.sort((a, b) => a.left - b.left);
return dayHeaders;
}
/**
* ColumnDetectionUtils - Shared utility for column detection and caching
* Used by both DragDropManager and AllDayManager for consistent column detection
*/
import { IMousePosition } from "../types/DragDropTypes";
export interface IColumnBounds {
date: string;
left: number;
right: number;
boundingClientRect: DOMRect,
element : HTMLElement,
index: number
}
export class ColumnDetectionUtils {
private static columnBoundsCache: IColumnBounds[] = [];
/**
* Update column bounds cache for coordinate-based column detection
*/
public static updateColumnBoundsCache(): void {
// Reset cache
this.columnBoundsCache = [];
// Find alle kolonner
const columns = document.querySelectorAll('swp-day-column');
let index = 1;
// Cache hver kolonnes x-grænser
columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
this.columnBoundsCache.push({
boundingClientRect : rect,
element: column as HTMLElement,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left);
}
/**
* Get column date from X coordinate using cached bounds
*/
public static getColumnBounds(position: IMousePosition): IColumnBounds | null{
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Find den kolonne hvor x-koordinaten er indenfor grænserne
let column = this.columnBoundsCache.find(col =>
position.x >= col.left && position.x <= col.right
);
if (column)
return column;
return null;
}
/**
* Get column bounds by Date
*/
public static getColumnBoundsByDate(date: Date): IColumnBounds | null {
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Convert Date to YYYY-MM-DD format
let dateString = date.toISOString().split('T')[0];
// Find column that matches the date
let column = this.columnBoundsCache.find(col => col.date === dateString);
return column || null;
}
public static getColumns(): IColumnBounds[] {
return [...this.columnBoundsCache];
}
public static getHeaderColumns(): IColumnBounds[] {
let dayHeaders: IColumnBounds[] = [];
const dayColumns = document.querySelectorAll('swp-calendar-header swp-day-header');
let index = 1;
// Cache hver kolonnes x-grænser
dayColumns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
dayHeaders.push({
boundingClientRect : rect,
element: column as HTMLElement,
date,
left: rect.left,
right: rect.right,
index: index++
});
}
});
// Sorter efter x-position (fra venstre til højre)
dayHeaders.sort((a, b) => a.left - b.left);
return dayHeaders;
}
}

View file

@ -1,498 +1,498 @@
/**
* DateService - Unified date/time service using date-fns
* Handles all date operations, timezone conversions, and formatting
*/
import {
format,
parse,
addMinutes,
differenceInMinutes,
startOfDay,
endOfDay,
setHours,
setMinutes as setMins,
getHours,
getMinutes,
parseISO,
isValid,
addDays,
startOfWeek,
endOfWeek,
addWeeks,
addMonths,
isSameDay,
getISOWeek
} from 'date-fns';
import {
toZonedTime,
fromZonedTime,
formatInTimeZone
} from 'date-fns-tz';
import { CalendarConfig } from '../core/CalendarConfig';
export class DateService {
private timezone: string;
constructor(config: CalendarConfig) {
this.timezone = config.getTimezone();
}
// ============================================
// CORE CONVERSIONS
// ============================================
/**
* Convert local date to UTC ISO string
* @param localDate - Date in local timezone
* @returns ISO string in UTC (with 'Z' suffix)
*/
public toUTC(localDate: Date): string {
return fromZonedTime(localDate, this.timezone).toISOString();
}
/**
* Convert UTC ISO string to local date
* @param utcString - ISO string in UTC
* @returns Date in local timezone
*/
public fromUTC(utcString: string): Date {
return toZonedTime(parseISO(utcString), this.timezone);
}
// ============================================
// FORMATTING
// ============================================
/**
* Format time as HH:mm or HH:mm:ss
* @param date - Date to format
* @param showSeconds - Include seconds in output
* @returns Formatted time string
*/
public formatTime(date: Date, showSeconds = false): string {
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
return format(date, pattern);
}
/**
* Format time range as "HH:mm - HH:mm"
* @param start - Start date
* @param end - End date
* @returns Formatted time range
*/
public formatTimeRange(start: Date, end: Date): string {
return `${this.formatTime(start)} - ${this.formatTime(end)}`;
}
/**
* Format date and time in technical format: yyyy-MM-dd HH:mm:ss
* @param date - Date to format
* @returns Technical datetime string
*/
public formatTechnicalDateTime(date: Date): string {
return format(date, 'yyyy-MM-dd HH:mm:ss');
}
/**
* Format date as yyyy-MM-dd
* @param date - Date to format
* @returns ISO date string
*/
public formatDate(date: Date): string {
return format(date, 'yyyy-MM-dd');
}
/**
* Format date as "Month Year" (e.g., "January 2025")
* @param date - Date to format
* @param locale - Locale for month name (default: 'en-US')
* @returns Formatted month and year
*/
public formatMonthYear(date: Date, locale: string = 'en-US'): string {
return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' });
}
/**
* Format date as ISO string (same as formatDate for compatibility)
* @param date - Date to format
* @returns ISO date string
*/
public formatISODate(date: Date): string {
return this.formatDate(date);
}
/**
* Format time in 12-hour format with AM/PM
* @param date - Date to format
* @returns Time string in 12-hour format (e.g., "2:30 PM")
*/
public formatTime12(date: Date): string {
const hours = getHours(date);
const minutes = getMinutes(date);
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Get day name for a date
* @param date - Date to get day name for
* @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday')
* @param locale - Locale for day name (default: 'da-DK')
* @returns Day name
*/
public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string {
const formatter = new Intl.DateTimeFormat(locale, {
weekday: format
});
return formatter.format(date);
}
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
public formatDateRange(
start: Date,
end: Date,
options: {
locale?: string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
year?: 'numeric' | '2-digit';
} = {}
): string {
const { locale = 'en-US', month = 'short', day = 'numeric' } = options;
const startYear = start.getFullYear();
const endYear = end.getFullYear();
const formatter = new Intl.DateTimeFormat(locale, {
month,
day,
year: startYear !== endYear ? 'numeric' : undefined
});
// @ts-ignore - formatRange is available in modern browsers
if (typeof formatter.formatRange === 'function') {
// @ts-ignore
return formatter.formatRange(start, end);
}
return `${formatter.format(start)} - ${formatter.format(end)}`;
}
// ============================================
// TIME CALCULATIONS
// ============================================
/**
* Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight
* @param timeString - Time in format HH:mm or HH:mm:ss
* @returns Total minutes since midnight
*/
public timeToMinutes(timeString: string): number {
const parts = timeString.split(':').map(Number);
const hours = parts[0] || 0;
const minutes = parts[1] || 0;
return hours * 60 + minutes;
}
/**
* Convert total minutes since midnight to time string HH:mm
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
public minutesToTime(totalMinutes: number): string {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const date = setMins(setHours(new Date(), hours), minutes);
return format(date, 'HH:mm');
}
/**
* Format time from total minutes (alias for minutesToTime)
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
public formatTimeFromMinutes(totalMinutes: number): string {
return this.minutesToTime(totalMinutes);
}
/**
* Get minutes since midnight for a given date
* @param date - Date to calculate from
* @returns Minutes since midnight
*/
public getMinutesSinceMidnight(date: Date): number {
return getHours(date) * 60 + getMinutes(date);
}
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
public getDurationMinutes(start: Date | string, end: Date | string): number {
const startDate = typeof start === 'string' ? parseISO(start) : start;
const endDate = typeof end === 'string' ? parseISO(end) : end;
return differenceInMinutes(endDate, startDate);
}
// ============================================
// WEEK OPERATIONS
// ============================================
/**
* Get start and end of week (Monday to Sunday)
* @param date - Reference date
* @returns Object with start and end dates
*/
public getWeekBounds(date: Date): { start: Date; end: Date } {
return {
start: startOfWeek(date, { weekStartsOn: 1 }), // Monday
end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday
};
}
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
public addWeeks(date: Date, weeks: number): Date {
return addWeeks(date, weeks);
}
/**
* Add months to a date
* @param date - Base date
* @param months - Number of months to add (can be negative)
* @returns New date
*/
public addMonths(date: Date, months: number): Date {
return addMonths(date, months);
}
/**
* Get ISO week number (1-53)
* @param date - Date to get week number for
* @returns ISO week number
*/
public getWeekNumber(date: Date): number {
return getISOWeek(date);
}
/**
* Get all dates in a full week (7 days starting from given date)
* @param weekStart - Start date of the week
* @returns Array of 7 dates
*/
public getFullWeekDates(weekStart: Date): Date[] {
const dates: Date[] = [];
for (let i = 0; i < 7; i++) {
dates.push(this.addDays(weekStart, i));
}
return dates;
}
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday)
* @returns Array of dates for the specified work days
*/
public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] {
const dates: Date[] = [];
// Get Monday of the week
const weekBounds = this.getWeekBounds(weekStart);
const mondayOfWeek = this.startOfDay(weekBounds.start);
// Calculate dates for each work day using ISO numbering
workDays.forEach(isoDay => {
const date = new Date(mondayOfWeek);
// ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
date.setDate(mondayOfWeek.getDate() + daysFromMonday);
dates.push(date);
});
return dates;
}
// ============================================
// GRID HELPERS
// ============================================
/**
* Create a date at a specific time (minutes since midnight)
* @param baseDate - Base date (date component)
* @param totalMinutes - Minutes since midnight
* @returns New date with specified time
*/
public createDateAtTime(baseDate: Date, totalMinutes: number): Date {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return setMins(setHours(startOfDay(baseDate), hours), minutes);
}
/**
* Snap date to nearest interval
* @param date - Date to snap
* @param intervalMinutes - Snap interval in minutes
* @returns Snapped date
*/
public snapToInterval(date: Date, intervalMinutes: number): Date {
const minutes = this.getMinutesSinceMidnight(date);
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
return this.createDateAtTime(date, snappedMinutes);
}
// ============================================
// UTILITY METHODS
// ============================================
/**
* Check if two dates are the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
public isSameDay(date1: Date, date2: Date): boolean {
return isSameDay(date1, date2);
}
/**
* Get start of day
* @param date - Date
* @returns Start of day (00:00:00)
*/
public startOfDay(date: Date): Date {
return startOfDay(date);
}
/**
* Get end of day
* @param date - Date
* @returns End of day (23:59:59.999)
*/
public endOfDay(date: Date): Date {
return endOfDay(date);
}
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
public addDays(date: Date, days: number): Date {
return addDays(date, days);
}
/**
* Add minutes to a date
* @param date - Base date
* @param minutes - Number of minutes to add (can be negative)
* @returns New date
*/
public addMinutes(date: Date, minutes: number): Date {
return addMinutes(date, minutes);
}
/**
* Parse ISO string to date
* @param isoString - ISO date string
* @returns Parsed date
*/
public parseISO(isoString: string): Date {
return parseISO(isoString);
}
/**
* Check if date is valid
* @param date - Date to check
* @returns True if valid
*/
public isValid(date: Date): boolean {
return isValid(date);
}
/**
* Validate date range (start must be before or equal to end)
* @param start - Start date
* @param end - End date
* @returns True if valid range
*/
public isValidRange(start: Date, end: Date): boolean {
if (!this.isValid(start) || !this.isValid(end)) {
return false;
}
return start.getTime() <= end.getTime();
}
/**
* Check if date is within reasonable bounds (1900-2100)
* @param date - Date to check
* @returns True if within bounds
*/
public isWithinBounds(date: Date): boolean {
if (!this.isValid(date)) {
return false;
}
const year = date.getFullYear();
return year >= 1900 && year <= 2100;
}
/**
* Validate date with comprehensive checks
* @param date - Date to validate
* @param options - Validation options
* @returns Validation result with error message
*/
public validateDate(
date: Date,
options: {
requireFuture?: boolean;
requirePast?: boolean;
minDate?: Date;
maxDate?: Date;
} = {}
): { valid: boolean; error?: string } {
if (!this.isValid(date)) {
return { valid: false, error: 'Invalid date' };
}
if (!this.isWithinBounds(date)) {
return { valid: false, error: 'Date out of bounds (1900-2100)' };
}
const now = new Date();
if (options.requireFuture && date <= now) {
return { valid: false, error: 'Date must be in the future' };
}
if (options.requirePast && date >= now) {
return { valid: false, error: 'Date must be in the past' };
}
if (options.minDate && date < options.minDate) {
return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` };
}
if (options.maxDate && date > options.maxDate) {
return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` };
}
return { valid: true };
}
/**
* DateService - Unified date/time service using date-fns
* Handles all date operations, timezone conversions, and formatting
*/
import {
format,
parse,
addMinutes,
differenceInMinutes,
startOfDay,
endOfDay,
setHours,
setMinutes as setMins,
getHours,
getMinutes,
parseISO,
isValid,
addDays,
startOfWeek,
endOfWeek,
addWeeks,
addMonths,
isSameDay,
getISOWeek
} from 'date-fns';
import {
toZonedTime,
fromZonedTime,
formatInTimeZone
} from 'date-fns-tz';
import { Configuration } from '../configuration/CalendarConfig';
export class DateService {
private timezone: string;
constructor(config: Configuration) {
this.timezone = config.getTimezone();
}
// ============================================
// CORE CONVERSIONS
// ============================================
/**
* Convert local date to UTC ISO string
* @param localDate - Date in local timezone
* @returns ISO string in UTC (with 'Z' suffix)
*/
public toUTC(localDate: Date): string {
return fromZonedTime(localDate, this.timezone).toISOString();
}
/**
* Convert UTC ISO string to local date
* @param utcString - ISO string in UTC
* @returns Date in local timezone
*/
public fromUTC(utcString: string): Date {
return toZonedTime(parseISO(utcString), this.timezone);
}
// ============================================
// FORMATTING
// ============================================
/**
* Format time as HH:mm or HH:mm:ss
* @param date - Date to format
* @param showSeconds - Include seconds in output
* @returns Formatted time string
*/
public formatTime(date: Date, showSeconds = false): string {
const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm';
return format(date, pattern);
}
/**
* Format time range as "HH:mm - HH:mm"
* @param start - Start date
* @param end - End date
* @returns Formatted time range
*/
public formatTimeRange(start: Date, end: Date): string {
return `${this.formatTime(start)} - ${this.formatTime(end)}`;
}
/**
* Format date and time in technical format: yyyy-MM-dd HH:mm:ss
* @param date - Date to format
* @returns Technical datetime string
*/
public formatTechnicalDateTime(date: Date): string {
return format(date, 'yyyy-MM-dd HH:mm:ss');
}
/**
* Format date as yyyy-MM-dd
* @param date - Date to format
* @returns ISO date string
*/
public formatDate(date: Date): string {
return format(date, 'yyyy-MM-dd');
}
/**
* Format date as "Month Year" (e.g., "January 2025")
* @param date - Date to format
* @param locale - Locale for month name (default: 'en-US')
* @returns Formatted month and year
*/
public formatMonthYear(date: Date, locale: string = 'en-US'): string {
return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' });
}
/**
* Format date as ISO string (same as formatDate for compatibility)
* @param date - Date to format
* @returns ISO date string
*/
public formatISODate(date: Date): string {
return this.formatDate(date);
}
/**
* Format time in 12-hour format with AM/PM
* @param date - Date to format
* @returns Time string in 12-hour format (e.g., "2:30 PM")
*/
public formatTime12(date: Date): string {
const hours = getHours(date);
const minutes = getMinutes(date);
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Get day name for a date
* @param date - Date to get day name for
* @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday')
* @param locale - Locale for day name (default: 'da-DK')
* @returns Day name
*/
public getDayName(date: Date, format: 'short' | 'long' = 'short', locale: string = 'da-DK'): string {
const formatter = new Intl.DateTimeFormat(locale, {
weekday: format
});
return formatter.format(date);
}
/**
* Format a date range with customizable options
* @param start - Start date
* @param end - End date
* @param options - Formatting options
* @returns Formatted date range string
*/
public formatDateRange(
start: Date,
end: Date,
options: {
locale?: string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day?: 'numeric' | '2-digit';
year?: 'numeric' | '2-digit';
} = {}
): string {
const { locale = 'en-US', month = 'short', day = 'numeric' } = options;
const startYear = start.getFullYear();
const endYear = end.getFullYear();
const formatter = new Intl.DateTimeFormat(locale, {
month,
day,
year: startYear !== endYear ? 'numeric' : undefined
});
// @ts-ignore - formatRange is available in modern browsers
if (typeof formatter.formatRange === 'function') {
// @ts-ignore
return formatter.formatRange(start, end);
}
return `${formatter.format(start)} - ${formatter.format(end)}`;
}
// ============================================
// TIME CALCULATIONS
// ============================================
/**
* Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight
* @param timeString - Time in format HH:mm or HH:mm:ss
* @returns Total minutes since midnight
*/
public timeToMinutes(timeString: string): number {
const parts = timeString.split(':').map(Number);
const hours = parts[0] || 0;
const minutes = parts[1] || 0;
return hours * 60 + minutes;
}
/**
* Convert total minutes since midnight to time string HH:mm
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
public minutesToTime(totalMinutes: number): string {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const date = setMins(setHours(new Date(), hours), minutes);
return format(date, 'HH:mm');
}
/**
* Format time from total minutes (alias for minutesToTime)
* @param totalMinutes - Minutes since midnight
* @returns Time string in format HH:mm
*/
public formatTimeFromMinutes(totalMinutes: number): string {
return this.minutesToTime(totalMinutes);
}
/**
* Get minutes since midnight for a given date
* @param date - Date to calculate from
* @returns Minutes since midnight
*/
public getMinutesSinceMidnight(date: Date): number {
return getHours(date) * 60 + getMinutes(date);
}
/**
* Calculate duration in minutes between two dates
* @param start - Start date or ISO string
* @param end - End date or ISO string
* @returns Duration in minutes
*/
public getDurationMinutes(start: Date | string, end: Date | string): number {
const startDate = typeof start === 'string' ? parseISO(start) : start;
const endDate = typeof end === 'string' ? parseISO(end) : end;
return differenceInMinutes(endDate, startDate);
}
// ============================================
// WEEK OPERATIONS
// ============================================
/**
* Get start and end of week (Monday to Sunday)
* @param date - Reference date
* @returns Object with start and end dates
*/
public getWeekBounds(date: Date): { start: Date; end: Date } {
return {
start: startOfWeek(date, { weekStartsOn: 1 }), // Monday
end: endOfWeek(date, { weekStartsOn: 1 }) // Sunday
};
}
/**
* Add weeks to a date
* @param date - Base date
* @param weeks - Number of weeks to add (can be negative)
* @returns New date
*/
public addWeeks(date: Date, weeks: number): Date {
return addWeeks(date, weeks);
}
/**
* Add months to a date
* @param date - Base date
* @param months - Number of months to add (can be negative)
* @returns New date
*/
public addMonths(date: Date, months: number): Date {
return addMonths(date, months);
}
/**
* Get ISO week number (1-53)
* @param date - Date to get week number for
* @returns ISO week number
*/
public getWeekNumber(date: Date): number {
return getISOWeek(date);
}
/**
* Get all dates in a full week (7 days starting from given date)
* @param weekStart - Start date of the week
* @returns Array of 7 dates
*/
public getFullWeekDates(weekStart: Date): Date[] {
const dates: Date[] = [];
for (let i = 0; i < 7; i++) {
dates.push(this.addDays(weekStart, i));
}
return dates;
}
/**
* Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7)
* @param weekStart - Any date in the week
* @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday)
* @returns Array of dates for the specified work days
*/
public getWorkWeekDates(weekStart: Date, workDays: number[]): Date[] {
const dates: Date[] = [];
// Get Monday of the week
const weekBounds = this.getWeekBounds(weekStart);
const mondayOfWeek = this.startOfDay(weekBounds.start);
// Calculate dates for each work day using ISO numbering
workDays.forEach(isoDay => {
const date = new Date(mondayOfWeek);
// ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days
const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1;
date.setDate(mondayOfWeek.getDate() + daysFromMonday);
dates.push(date);
});
return dates;
}
// ============================================
// GRID HELPERS
// ============================================
/**
* Create a date at a specific time (minutes since midnight)
* @param baseDate - Base date (date component)
* @param totalMinutes - Minutes since midnight
* @returns New date with specified time
*/
public createDateAtTime(baseDate: Date, totalMinutes: number): Date {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return setMins(setHours(startOfDay(baseDate), hours), minutes);
}
/**
* Snap date to nearest interval
* @param date - Date to snap
* @param intervalMinutes - Snap interval in minutes
* @returns Snapped date
*/
public snapToInterval(date: Date, intervalMinutes: number): Date {
const minutes = this.getMinutesSinceMidnight(date);
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
return this.createDateAtTime(date, snappedMinutes);
}
// ============================================
// UTILITY METHODS
// ============================================
/**
* Check if two dates are the same day
* @param date1 - First date
* @param date2 - Second date
* @returns True if same day
*/
public isSameDay(date1: Date, date2: Date): boolean {
return isSameDay(date1, date2);
}
/**
* Get start of day
* @param date - Date
* @returns Start of day (00:00:00)
*/
public startOfDay(date: Date): Date {
return startOfDay(date);
}
/**
* Get end of day
* @param date - Date
* @returns End of day (23:59:59.999)
*/
public endOfDay(date: Date): Date {
return endOfDay(date);
}
/**
* Add days to a date
* @param date - Base date
* @param days - Number of days to add (can be negative)
* @returns New date
*/
public addDays(date: Date, days: number): Date {
return addDays(date, days);
}
/**
* Add minutes to a date
* @param date - Base date
* @param minutes - Number of minutes to add (can be negative)
* @returns New date
*/
public addMinutes(date: Date, minutes: number): Date {
return addMinutes(date, minutes);
}
/**
* Parse ISO string to date
* @param isoString - ISO date string
* @returns Parsed date
*/
public parseISO(isoString: string): Date {
return parseISO(isoString);
}
/**
* Check if date is valid
* @param date - Date to check
* @returns True if valid
*/
public isValid(date: Date): boolean {
return isValid(date);
}
/**
* Validate date range (start must be before or equal to end)
* @param start - Start date
* @param end - End date
* @returns True if valid range
*/
public isValidRange(start: Date, end: Date): boolean {
if (!this.isValid(start) || !this.isValid(end)) {
return false;
}
return start.getTime() <= end.getTime();
}
/**
* Check if date is within reasonable bounds (1900-2100)
* @param date - Date to check
* @returns True if within bounds
*/
public isWithinBounds(date: Date): boolean {
if (!this.isValid(date)) {
return false;
}
const year = date.getFullYear();
return year >= 1900 && year <= 2100;
}
/**
* Validate date with comprehensive checks
* @param date - Date to validate
* @param options - Validation options
* @returns Validation result with error message
*/
public validateDate(
date: Date,
options: {
requireFuture?: boolean;
requirePast?: boolean;
minDate?: Date;
maxDate?: Date;
} = {}
): { valid: boolean; error?: string } {
if (!this.isValid(date)) {
return { valid: false, error: 'Invalid date' };
}
if (!this.isWithinBounds(date)) {
return { valid: false, error: 'Date out of bounds (1900-2100)' };
}
const now = new Date();
if (options.requireFuture && date <= now) {
return { valid: false, error: 'Date must be in the future' };
}
if (options.requirePast && date >= now) {
return { valid: false, error: 'Date must be in the past' };
}
if (options.minDate && date < options.minDate) {
return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` };
}
if (options.maxDate && date > options.maxDate) {
return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` };
}
return { valid: true };
}
}

View file

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

View file

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