Refactors dependency injection and configuration management

Replaces global singleton configuration with dependency injection
Introduces more modular and testable approach to configuration
Removes direct references to calendarConfig in multiple components
Adds explicit configuration passing to constructors

Improves code maintainability and reduces global state dependencies
This commit is contained in:
Janus C. H. Knudsen 2025-10-30 23:47:30 +01:00
parent fb48e410ea
commit 8bbb2f05d3
30 changed files with 365 additions and 559 deletions

View file

@ -1,7 +1,7 @@
// All-day row height management and animations
import { eventBus } from '../core/EventBus';
import { ALL_DAY_CONSTANTS, calendarConfig } from '../core/CalendarConfig';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
@ -43,11 +43,14 @@ export class AllDayManager {
private actualRowCount: number = 0;
constructor(eventManager: EventManager) {
constructor(
eventManager: EventManager,
allDayEventRenderer: AllDayEventRenderer,
dateService: DateService
) {
this.eventManager = eventManager;
this.allDayEventRenderer = new AllDayEventRenderer();
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
this.allDayEventRenderer = allDayEventRenderer;
this.dateService = dateService;
// Sync CSS variable with TypeScript constant to ensure consistency
document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`);

View file

@ -1,5 +1,5 @@
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { EventManager } from './EventManager';
import { GridManager } from './GridManager';
@ -8,7 +8,6 @@ import { ScrollManager } from './ScrollManager';
/**
* CalendarManager - Main coordinator for all calendar managers
* Uses singleton calendarConfig for consistent configuration access
*/
export class CalendarManager {
private eventBus: IEventBus;
@ -16,6 +15,7 @@ export class CalendarManager {
private gridManager: GridManager;
private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager;
private config: CalendarConfig;
private currentView: CalendarView = 'week';
private currentDate: Date = new Date();
private isInitialized: boolean = false;
@ -25,14 +25,15 @@ export class CalendarManager {
eventManager: EventManager,
gridManager: GridManager,
eventRenderingService: EventRenderingService,
scrollManager: ScrollManager
scrollManager: ScrollManager,
config: CalendarConfig
) {
this.eventBus = eventBus;
this.eventManager = eventManager;
this.gridManager = gridManager;
this.eventRenderer = eventRenderingService;
this.scrollManager = scrollManager;
const timezone = calendarConfig.getTimezone?.();
this.config = config;
this.setupEventListeners();
}
@ -47,7 +48,7 @@ export class CalendarManager {
try {
// Debug: Check calendar type
const calendarType = calendarConfig.getCalendarMode();
const calendarType = this.config.getCalendarMode();
// Step 1: Load data
await this.eventManager.loadData();
@ -212,7 +213,7 @@ export class CalendarManager {
this.eventBus.emit('workweek:header-update', {
currentDate: this.currentDate,
currentView: this.currentView,
workweek: calendarConfig.getCurrentWorkWeek()
workweek: this.config.getCurrentWorkWeek()
});
}

View file

@ -4,7 +4,6 @@
*/
import { IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement';
@ -49,9 +48,11 @@ export class DragDropManager {
private targetY = 0;
private currentY = 0;
private targetColumn: ColumnBounds | null = null;
private positionUtils: PositionUtils;
constructor(eventBus: IEventBus) {
constructor(eventBus: IEventBus, positionUtils: PositionUtils) {
this.eventBus = eventBus;
this.positionUtils = positionUtils;
this.init();
}
@ -415,7 +416,7 @@ export class DragDropManager {
const eventTopY = mouseY - this.mouseOffset.y;
// Snap the event top position, not the mouse position
const snappedY = PositionUtils.getPositionFromCoordinate(eventTopY, column);
const snappedY = this.positionUtils.getPositionFromCoordinate(eventTopY, column);
return Math.max(0, snappedY);
}

View file

@ -8,7 +8,7 @@
import { CalendarEvent } from '../types/CalendarTypes';
import { EventStackManager, EventGroup, StackLink } from './EventStackManager';
import { PositionUtils } from '../utils/PositionUtils';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
export interface GridGroupLayout {
events: CalendarEvent[];
@ -30,9 +30,13 @@ export interface ColumnLayout {
export class EventLayoutCoordinator {
private stackManager: EventStackManager;
private config: CalendarConfig;
private positionUtils: PositionUtils;
constructor() {
this.stackManager = new EventStackManager();
constructor(stackManager: EventStackManager, config: CalendarConfig, positionUtils: PositionUtils) {
this.stackManager = stackManager;
this.config = config;
this.positionUtils = positionUtils;
}
/**
@ -55,7 +59,7 @@ export class EventLayoutCoordinator {
// Find events that could be in GRID with first event
// Use expanding search to find chains (A→B→C where each conflicts with next)
const gridSettings = calendarConfig.getGridSettings();
const gridSettings = this.config.getGridSettings();
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
// Use refactored method for expanding grid candidates
@ -78,7 +82,7 @@ export class EventLayoutCoordinator {
// Ensure we get the earliest event (explicit sort for robustness)
const earliestEvent = [...gridCandidates].sort((a, b) => a.start.getTime() - b.start.getTime())[0];
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
const position = this.positionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
const columns = this.allocateColumns(gridCandidates);
gridGroupLayouts.push({
@ -100,7 +104,7 @@ export class EventLayoutCoordinator {
renderedEventsWithLevels
);
const position = PositionUtils.calculateEventPosition(firstEvent.start, firstEvent.end);
const position = this.positionUtils.calculateEventPosition(firstEvent.start, firstEvent.end);
stackedEventLayouts.push({
event: firstEvent,
stackLink: { stackLevel },

View file

@ -1,6 +1,6 @@
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { DateService } from '../utils/DateService';
import { ResourceData } from '../types/ManagerTypes';
@ -20,16 +20,21 @@ interface RawEventData {
* Handles data loading with improved performance and caching
*/
export class EventManager {
private events: CalendarEvent[] = [];
private rawData: ResourceCalendarData | RawEventData[] | null = null;
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
private lastCacheKey: string = '';
private dateService: DateService;
private config: CalendarConfig;
constructor(private eventBus: IEventBus) {
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
constructor(
private eventBus: IEventBus,
dateService: DateService,
config: CalendarConfig
) {
this.dateService = dateService;
this.config = config;
}
/**
@ -50,7 +55,7 @@ export class EventManager {
* Optimized mock data loading with better resource handling
*/
private async loadMockData(): Promise<void> {
const calendarType = calendarConfig.getCalendarMode();
const calendarType = this.config.getCalendarMode();
const jsonFile = calendarType === 'resource'
? '/src/data/mock-resource-events.json'
: '/src/data/mock-events.json';

View file

@ -14,7 +14,7 @@
*/
import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
export interface StackLink {
prev?: string; // Event ID of previous event in stack
@ -30,6 +30,11 @@ export interface EventGroup {
export class EventStackManager {
private static readonly STACK_OFFSET_PX = 15;
private config: CalendarConfig;
constructor(config: CalendarConfig) {
this.config = config;
}
// ============================================
// PHASE 1: Start Time Grouping
@ -46,7 +51,7 @@ export class EventStackManager {
if (events.length === 0) return [];
// Get threshold from config
const gridSettings = calendarConfig.getGridSettings();
const gridSettings = this.config.getGridSettings();
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
// Sort events by start time

View file

@ -4,7 +4,6 @@
*/
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { GridRenderer } from '../renderers/GridRenderer';
@ -23,11 +22,14 @@ export class GridManager {
private styleManager: GridStyleManager;
private dateService: DateService;
constructor(gridRenderer: GridRenderer) {
// Inject GridRenderer via DI
constructor(
gridRenderer: GridRenderer,
styleManager: GridStyleManager,
dateService: DateService
) {
this.gridRenderer = gridRenderer;
this.styleManager = new GridStyleManager();
this.dateService = new DateService('Europe/Copenhagen');
this.styleManager = styleManager;
this.dateService = dateService;
this.init();
}

View file

@ -1,5 +1,5 @@
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { HeaderRenderer, HeaderRenderContext } from '../renderers/HeaderRenderer';
import { ResourceCalendarData } from '../types/CalendarTypes';
@ -13,9 +13,11 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
*/
export class HeaderManager {
private headerRenderer: HeaderRenderer;
private config: CalendarConfig;
constructor(headerRenderer: HeaderRenderer) {
constructor(headerRenderer: HeaderRenderer, config: CalendarConfig) {
this.headerRenderer = headerRenderer;
this.config = config;
// Bind handler methods for event listeners
this.handleDragMouseEnterHeader = this.handleDragMouseEnterHeader.bind(this);
@ -127,7 +129,7 @@ export class HeaderManager {
// Render new header content using injected renderer
const context: HeaderRenderContext = {
currentWeek: currentDate,
config: calendarConfig,
config: this.config,
resourceData: resourceData
};

View file

@ -4,7 +4,6 @@ import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents';
import { NavigationRenderer } from '../renderers/NavigationRenderer';
import { GridRenderer } from '../renderers/GridRenderer';
import { calendarConfig } from '../core/CalendarConfig';
/**
* NavigationManager handles calendar navigation (prev/next/today buttons)
@ -19,10 +18,16 @@ export class NavigationManager {
private targetWeek: Date;
private animationQueue: number = 0;
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService, gridRenderer: GridRenderer) {
constructor(
eventBus: IEventBus,
eventRenderer: EventRenderingService,
gridRenderer: GridRenderer,
dateService: DateService,
navigationRenderer: NavigationRenderer
) {
this.eventBus = eventBus;
this.dateService = new DateService('Europe/Copenhagen');
this.navigationRenderer = new NavigationRenderer(eventBus, eventRenderer);
this.dateService = dateService;
this.navigationRenderer = navigationRenderer;
this.gridRenderer = gridRenderer;
this.currentWeek = this.getISOWeekStart(new Date());
this.targetWeek = new Date(this.currentWeek);

View file

@ -1,6 +1,6 @@
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { ResizeEndEventPayload } from '../types/EventTypes';
type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void };
@ -29,9 +29,11 @@ export class ResizeHandleManager {
private unsubscribers: Array<() => void> = [];
private pointerCaptured = false;
private prevZ?: string;
private config: CalendarConfig;
constructor() {
const grid = calendarConfig.getGridSettings();
constructor(config: CalendarConfig) {
this.config = config;
const grid = this.config.getGridSettings();
this.hourHeightPx = grid.hourHeight;
this.snapMin = grid.snapInterval;
this.minDurationMin = this.snapMin; // Use snap interval as minimum duration

View file

@ -1,7 +1,6 @@
// Custom scroll management for calendar week container
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { PositionUtils } from '../utils/PositionUtils';
@ -14,8 +13,10 @@ export class ScrollManager {
private timeAxis: HTMLElement | null = null;
private calendarHeader: HTMLElement | null = null;
private resizeObserver: ResizeObserver | null = null;
private positionUtils: PositionUtils;
constructor() {
constructor(positionUtils: PositionUtils) {
this.positionUtils = positionUtils;
this.init();
}
@ -112,7 +113,7 @@ export class ScrollManager {
scrollToHour(hour: number): void {
// Create time string for the hour
const timeString = `${hour.toString().padStart(2, '0')}:00`;
const scrollTop = PositionUtils.timeToPixels(timeString);
const scrollTop = this.positionUtils.timeToPixels(timeString);
this.scrollTo(scrollTop);
}

View file

@ -1,6 +1,6 @@
import { EventBus } from '../core/EventBus';
import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
/**
@ -9,17 +9,19 @@ import { CoreEvents } from '../constants/CoreEvents';
*/
export class ViewManager {
private eventBus: IEventBus;
private config: CalendarConfig;
private currentView: CalendarView = 'week';
private buttonListeners: Map<Element, EventListener> = new Map();
// Cached DOM elements for performance
private cachedViewButtons: NodeListOf<Element> | null = null;
private cachedWorkweekButtons: NodeListOf<Element> | null = null;
private lastButtonCacheTime: number = 0;
private readonly CACHE_DURATION = 5000; // 5 seconds
constructor(eventBus: IEventBus) {
constructor(eventBus: IEventBus, config: CalendarConfig) {
this.eventBus = eventBus;
this.config = config;
this.setupEventListeners();
}
@ -140,13 +142,13 @@ export class ViewManager {
*/
private changeWorkweek(workweekId: string): void {
// Update the calendar config (does not emit events)
calendarConfig.setWorkWeek(workweekId);
this.config.setWorkWeek(workweekId);
// Update button states using cached elements
this.updateAllButtons();
// Emit workweek change event with full payload
const settings = calendarConfig.getWorkWeekSettings();
const settings = this.config.getWorkWeekSettings();
this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, {
workWeekId: workweekId,
settings: settings
@ -166,7 +168,7 @@ export class ViewManager {
this.updateButtonGroup(
this.getWorkweekButtons(),
'data-workweek',
calendarConfig.getCurrentWorkWeek()
this.config.getCurrentWorkWeek()
);
}

View file

@ -1,7 +1,7 @@
// Work hours management for per-column scheduling
import { DateService } from '../utils/DateService';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils';
/**
@ -35,11 +35,14 @@ export interface WorkScheduleConfig {
*/
export class WorkHoursManager {
private dateService: DateService;
private config: CalendarConfig;
private positionUtils: PositionUtils;
private workSchedule: WorkScheduleConfig;
constructor() {
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
constructor(dateService: DateService, config: CalendarConfig, positionUtils: PositionUtils) {
this.dateService = dateService;
this.config = config;
this.positionUtils = positionUtils;
// Default work schedule - will be loaded from JSON later
this.workSchedule = {
@ -98,8 +101,8 @@ export class WorkHoursManager {
if (workHours === 'off') {
return null; // Full day will be colored via CSS background
}
const gridSettings = calendarConfig.getGridSettings();
const gridSettings = this.config.getGridSettings();
const dayStartHour = gridSettings.dayStartHour;
const hourHeight = gridSettings.hourHeight;
@ -128,7 +131,7 @@ export class WorkHoursManager {
const endTime = `${workHours.end.toString().padStart(2, '0')}:00`;
// Use PositionUtils for consistent position calculation
const position = PositionUtils.calculateEventPosition(startTime, endTime);
const position = this.positionUtils.calculateEventPosition(startTime, endTime);
return { top: position.top, height: position.height };
}