Refactor GridManager with new DateColumnDataSource

Introduces DateColumnDataSource to centralize date column generation logic
Simplifies GridManager by delegating date calculations to dedicated data source
Enhances flexibility for different calendar views and date rendering strategies

Improves separation of concerns and makes calendar view management more modular
This commit is contained in:
Janus C. H. Knudsen 2025-11-13 23:35:29 +01:00
parent 284c85b2f8
commit 75a2d4913e
6 changed files with 229 additions and 157 deletions

View file

@ -0,0 +1,120 @@
import { IColumnDataSource } from '../types/ColumnDataSource';
import { DateService } from '../utils/DateService';
import { Configuration } from '../configurations/CalendarConfig';
import { CalendarView } from '../types/CalendarTypes';
/**
* DateColumnDataSource - Provides date-based columns
*
* Calculates which dates to display based on:
* - Current date
* - Current view (day/week/month)
* - Workweek settings
*/
export class DateColumnDataSource implements IColumnDataSource {
private dateService: DateService;
private config: Configuration;
private currentDate: Date;
private currentView: CalendarView;
constructor(
dateService: DateService,
config: Configuration,
currentDate: Date,
currentView: CalendarView
) {
this.dateService = dateService;
this.config = config;
this.currentDate = currentDate;
this.currentView = currentView;
}
/**
* Get columns (dates) to display
*/
public getColumns(): Date[] {
switch (this.currentView) {
case 'week':
return this.getWeekDates();
case 'month':
return this.getMonthDates();
case 'day':
return [this.currentDate];
default:
return this.getWeekDates();
}
}
/**
* Get type of datasource
*/
public getType(): 'date' | 'resource' {
return 'date';
}
/**
* Update current date
*/
public setCurrentDate(date: Date): void {
this.currentDate = date;
}
/**
* Update current view
*/
public setCurrentView(view: CalendarView): void {
this.currentView = view;
}
/**
* Get dates for week view based on workweek settings
*/
private getWeekDates(): Date[] {
const weekStart = this.getISOWeekStart(this.currentDate);
const workWeekSettings = this.config.getWorkWeekSettings();
return this.dateService.getWorkWeekDates(weekStart, workWeekSettings.workDays);
}
/**
* Get all dates in current month
*/
private getMonthDates(): Date[] {
const dates: Date[] = [];
const monthStart = this.getMonthStart(this.currentDate);
const monthEnd = this.getMonthEnd(this.currentDate);
const totalDays = Math.ceil((monthEnd.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24)) + 1;
for (let i = 0; i < totalDays; i++) {
dates.push(this.dateService.addDays(monthStart, i));
}
return dates;
}
/**
* Get ISO week start (Monday)
*/
private getISOWeekStart(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.startOfDay(weekBounds.start);
}
/**
* Get month start
*/
private getMonthStart(date: Date): Date {
const year = date.getFullYear();
const month = date.getMonth();
return this.dateService.startOfDay(new Date(year, month, 1));
}
/**
* Get month end
*/
private getMonthEnd(date: Date): Date {
const nextMonth = this.dateService.addMonths(date, 1);
const firstOfNextMonth = this.getMonthStart(nextMonth);
return this.dateService.endOfDay(this.dateService.addDays(firstOfNextMonth, -1));
}
}

View file

@ -8,6 +8,9 @@ import { CoreEvents } from '../constants/CoreEvents';
import { CalendarView } from '../types/CalendarTypes'; import { CalendarView } from '../types/CalendarTypes';
import { GridRenderer } from '../renderers/GridRenderer'; import { GridRenderer } from '../renderers/GridRenderer';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { DateColumnDataSource } from '../datasources/DateColumnDataSource';
import { Configuration } from '../configurations/CalendarConfig';
import { EventManager } from './EventManager';
/** /**
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer * Simplified GridManager focused on coordination, delegates rendering to GridRenderer
@ -18,13 +21,21 @@ export class GridManager {
private currentView: CalendarView = 'week'; private currentView: CalendarView = 'week';
private gridRenderer: GridRenderer; private gridRenderer: GridRenderer;
private dateService: DateService; private dateService: DateService;
private config: Configuration;
private dataSource: DateColumnDataSource;
private eventManager: EventManager;
constructor( constructor(
gridRenderer: GridRenderer, gridRenderer: GridRenderer,
dateService: DateService dateService: DateService,
config: Configuration,
eventManager: EventManager
) { ) {
this.gridRenderer = gridRenderer; this.gridRenderer = gridRenderer;
this.dateService = dateService; this.dateService = dateService;
this.config = config;
this.eventManager = eventManager;
this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView);
this.init(); this.init();
} }
@ -33,22 +44,6 @@ export class GridManager {
this.subscribeToEvents(); this.subscribeToEvents();
} }
/**
* Get the start of the ISO week (Monday) for a given date
*/
private getISOWeekStart(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.startOfDay(weekBounds.start);
}
/**
* Get the end of the ISO week (Sunday) for a given date
*/
private getWeekEnd(date: Date): Date {
const weekBounds = this.dateService.getWeekBounds(date);
return this.dateService.endOfDay(weekBounds.end);
}
private findElements(): void { private findElements(): void {
this.container = document.querySelector('swp-calendar-container'); this.container = document.querySelector('swp-calendar-container');
} }
@ -58,14 +53,18 @@ export class GridManager {
eventBus.on(CoreEvents.VIEW_CHANGED, (e: Event) => { eventBus.on(CoreEvents.VIEW_CHANGED, (e: Event) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
this.currentView = detail.currentView; this.currentView = detail.currentView;
this.dataSource.setCurrentView(this.currentView);
this.render(); this.render();
}); });
// Listen for navigation events from NavigationButtons // Listen for navigation events from NavigationManager
// NavigationManager has already created new grid with animation
// GridManager only needs to update state, NOT re-render
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => { eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
this.currentDate = detail.newDate; this.currentDate = detail.newDate;
this.render(); this.dataSource.setCurrentDate(this.currentDate);
// Do NOT call render() - NavigationManager already created new grid
}); });
// Listen for config changes that affect rendering // Listen for config changes that affect rendering
@ -88,109 +87,28 @@ export class GridManager {
return; return;
} }
// Delegate to GridRenderer with current view context // Get dates from datasource - single source of truth
const dates = this.dataSource.getColumns();
// Get events for the period from EventManager
const startDate = dates[0];
const endDate = dates[dates.length - 1];
const events = await this.eventManager.getEventsForPeriod(startDate, endDate);
// Delegate to GridRenderer with dates and events
this.gridRenderer.renderGrid( this.gridRenderer.renderGrid(
this.container, this.container,
this.currentDate this.currentDate,
this.currentView,
dates,
events
); );
// Get display dates for current view
const dates = this.getDisplayDates();
// Get layout config based on current view
const layoutConfig = this.getLayoutConfig();
// Emit grid rendered event // Emit grid rendered event
eventBus.emit(CoreEvents.GRID_RENDERED, { eventBus.emit(CoreEvents.GRID_RENDERED, {
container: this.container, container: this.container,
currentDate: this.currentDate, currentDate: this.currentDate,
dates: dates, dates: dates
layoutConfig: layoutConfig,
columnCount: layoutConfig.columnCount
}); });
} }
/**
* Get current view's display dates
*/
public getDisplayDates(): Date[] {
switch (this.currentView) {
case 'week':
const weekStart = this.getISOWeekStart(this.currentDate);
return this.dateService.getFullWeekDates(weekStart);
case 'month':
return this.getMonthDates(this.currentDate);
case 'day':
return [this.currentDate];
default:
const defaultWeekStart = this.getISOWeekStart(this.currentDate);
return this.dateService.getFullWeekDates(defaultWeekStart);
}
}
/**
* Get layout config for current view
*/
private getLayoutConfig(): { columnCount: number; type: string } {
switch (this.currentView) {
case 'week':
return {
columnCount: 7,
type: 'week'
};
case 'month':
return {
columnCount: 7,
type: 'month'
};
case 'day':
return {
columnCount: 1,
type: 'day'
};
default:
return {
columnCount: 7,
type: 'week'
};
}
}
/**
* Helper method to get month start
*/
private getMonthStart(date: Date): Date {
const year = date.getFullYear();
const month = date.getMonth();
return this.dateService.startOfDay(new Date(year, month, 1));
}
/**
* Helper method to get month end
*/
private getMonthEnd(date: Date): Date {
const nextMonth = this.dateService.addMonths(date, 1);
const firstOfNextMonth = this.getMonthStart(nextMonth);
return this.dateService.endOfDay(this.dateService.addDays(firstOfNextMonth, -1));
}
/**
* Helper method to get all dates in a month
*/
private getMonthDates(date: Date): Date[] {
const dates: Date[] = [];
const monthStart = this.getMonthStart(date);
const monthEnd = this.getMonthEnd(date);
const totalDays = Math.ceil((monthEnd.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24)) + 1;
for (let i = 0; i < totalDays; i++) {
dates.push(this.dateService.addDays(monthStart, i));
}
return dates;
}
} }

View file

@ -1,16 +1,20 @@
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus, CalendarView } from '../types/CalendarTypes';
import { EventRenderingService } from '../renderers/EventRendererManager'; import { EventRenderingService } from '../renderers/EventRendererManager';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { WeekInfoRenderer } from '../renderers/WeekInfoRenderer'; import { WeekInfoRenderer } from '../renderers/WeekInfoRenderer';
import { GridRenderer } from '../renderers/GridRenderer'; import { GridRenderer } from '../renderers/GridRenderer';
import { INavButtonClickedEventPayload } from '../types/EventTypes'; import { INavButtonClickedEventPayload } from '../types/EventTypes';
import { DateColumnDataSource } from '../datasources/DateColumnDataSource';
import { Configuration } from '../configurations/CalendarConfig';
export class NavigationManager { export class NavigationManager {
private eventBus: IEventBus; private eventBus: IEventBus;
private weekInfoRenderer: WeekInfoRenderer; private weekInfoRenderer: WeekInfoRenderer;
private gridRenderer: GridRenderer; private gridRenderer: GridRenderer;
private dateService: DateService; private dateService: DateService;
private config: Configuration;
private dataSource: DateColumnDataSource;
private currentWeek: Date; private currentWeek: Date;
private targetWeek: Date; private targetWeek: Date;
private animationQueue: number = 0; private animationQueue: number = 0;
@ -20,14 +24,17 @@ export class NavigationManager {
eventRenderer: EventRenderingService, eventRenderer: EventRenderingService,
gridRenderer: GridRenderer, gridRenderer: GridRenderer,
dateService: DateService, dateService: DateService,
weekInfoRenderer: WeekInfoRenderer weekInfoRenderer: WeekInfoRenderer,
config: Configuration
) { ) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.dateService = dateService; this.dateService = dateService;
this.weekInfoRenderer = weekInfoRenderer; this.weekInfoRenderer = weekInfoRenderer;
this.gridRenderer = gridRenderer; this.gridRenderer = gridRenderer;
this.config = config;
this.currentWeek = this.getISOWeekStart(new Date()); this.currentWeek = this.getISOWeekStart(new Date());
this.targetWeek = new Date(this.currentWeek); this.targetWeek = new Date(this.currentWeek);
this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week' as CalendarView);
this.init(); this.init();
} }
@ -184,8 +191,12 @@ export class NavigationManager {
console.log('Calling GridRenderer instead of NavigationRenderer'); console.log('Calling GridRenderer instead of NavigationRenderer');
console.log('Target week:', targetWeek); console.log('Target week:', targetWeek);
// Update DataSource with target week and get dates
this.dataSource.setCurrentDate(targetWeek);
const dates = this.dataSource.getColumns();
// Always create a fresh container for consistent behavior // Always create a fresh container for consistent behavior
newGrid = this.gridRenderer.createNavigationGrid(container, targetWeek); newGrid = this.gridRenderer.createNavigationGrid(container, dates);
console.groupEnd(); console.groupEnd();

View file

@ -15,7 +15,7 @@ export interface IColumnRenderer {
* Context for column rendering * Context for column rendering
*/ */
export interface IColumnRenderContext { export interface IColumnRenderContext {
currentWeek: Date; dates: Date[];
config: Configuration; config: Configuration;
} }
@ -35,15 +35,9 @@ export class DateColumnRenderer implements IColumnRenderer {
} }
render(columnContainer: HTMLElement, context: IColumnRenderContext): void { render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
const { currentWeek, config } = context; const { dates } = context;
const workWeekSettings = config.getWorkWeekSettings(); dates.forEach((date) => {
const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays);
const dateSettings = config.dateViewSettings;
const daysToShow = dates.slice(0, dateSettings.weekDays);
daysToShow.forEach((date) => {
const column = document.createElement('swp-day-column'); const column = document.createElement('swp-day-column');
(column as any).dataset.date = this.dateService.formatISODate(date); (column as any).dataset.date = this.dateService.formatISODate(date);

View file

@ -1,5 +1,5 @@
import { Configuration } from '../configurations/CalendarConfig'; import { Configuration } from '../configurations/CalendarConfig';
import { CalendarView } from '../types/CalendarTypes'; import { CalendarView, ICalendarEvent } from '../types/CalendarTypes';
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer'; import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
@ -104,11 +104,15 @@ export class GridRenderer {
* @param grid - Container element where grid will be rendered * @param grid - Container element where grid will be rendered
* @param currentDate - Base date for the current view (e.g., any date in the week) * @param currentDate - Base date for the current view (e.g., any date in the week)
* @param view - Calendar view type (day/week/month) * @param view - Calendar view type (day/week/month)
* @param dates - Array of dates to render as columns
* @param events - All events for the period
*/ */
public renderGrid( public renderGrid(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView = 'week' view: CalendarView = 'week',
dates: Date[] = [],
events: ICalendarEvent[] = []
): void { ): void {
if (!grid || !currentDate) { if (!grid || !currentDate) {
@ -120,10 +124,10 @@ export class GridRenderer {
// Only clear and rebuild if grid is empty (first render) // Only clear and rebuild if grid is empty (first render)
if (grid.children.length === 0) { if (grid.children.length === 0) {
this.createCompleteGridStructure(grid, currentDate, view); this.createCompleteGridStructure(grid, currentDate, view, dates, events);
} else { } else {
// Optimized update - only refresh dynamic content // Optimized update - only refresh dynamic content
this.updateGridContent(grid, currentDate, view); this.updateGridContent(grid, currentDate, view, dates, events);
} }
} }
@ -141,11 +145,14 @@ export class GridRenderer {
* @param grid - Parent container * @param grid - Parent container
* @param currentDate - Current view date * @param currentDate - Current view date
* @param view - View type * @param view - View type
* @param dates - Array of dates to render
*/ */
private createCompleteGridStructure( private createCompleteGridStructure(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView view: CalendarView,
dates: Date[],
events: ICalendarEvent[]
): void { ): void {
// Create all elements in memory first for better performance // Create all elements in memory first for better performance
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
@ -160,7 +167,7 @@ export class GridRenderer {
fragment.appendChild(timeAxis); fragment.appendChild(timeAxis);
// Create grid container with caching // Create grid container with caching
const gridContainer = this.createOptimizedGridContainer(currentDate, view); const gridContainer = this.createOptimizedGridContainer(dates, events);
this.cachedGridContainer = gridContainer; this.cachedGridContainer = gridContainer;
fragment.appendChild(gridContainer); fragment.appendChild(gridContainer);
@ -207,11 +214,12 @@ export class GridRenderer {
* *
* @param currentDate - Current view date * @param currentDate - Current view date
* @param view - View type * @param view - View type
* @param dates - Array of dates to render
* @returns Complete grid container element * @returns Complete grid container element
*/ */
private createOptimizedGridContainer( private createOptimizedGridContainer(
currentDate: Date, dates: Date[],
view: CalendarView events: ICalendarEvent[]
): HTMLElement { ): HTMLElement {
const gridContainer = document.createElement('swp-grid-container'); const gridContainer = document.createElement('swp-grid-container');
@ -229,7 +237,7 @@ export class GridRenderer {
// Create column container // Create column container
const columnContainer = document.createElement('swp-day-columns'); const columnContainer = document.createElement('swp-day-columns');
this.renderColumnContainer(columnContainer, currentDate, view); this.renderColumnContainer(columnContainer, dates, events);
timeGrid.appendChild(columnContainer); timeGrid.appendChild(columnContainer);
scrollableContent.appendChild(timeGrid); scrollableContent.appendChild(timeGrid);
@ -240,27 +248,25 @@ export class GridRenderer {
/** /**
* Delegates column rendering to the injected ColumnRenderer strategy * Renders columns by delegating to ColumnRenderer
* *
* This is where the Strategy Pattern is applied: * GridRenderer delegates column creation to ColumnRenderer.
* - DateColumnRenderer iterates dates and creates day columns * Event rendering is handled by EventRenderingService listening to GRID_RENDERED.
* - Could be swapped with other implementations (e.g., ResourceColumnRenderer)
* *
* @param columnContainer - Empty container to populate * @param columnContainer - Empty container to populate
* @param currentDate - Current view date * @param dates - Array of dates to render
* @param view - View type * @param events - All events for the period (passed through, not used here)
*/ */
private renderColumnContainer( private renderColumnContainer(
columnContainer: HTMLElement, columnContainer: HTMLElement,
currentDate: Date, dates: Date[],
view: CalendarView events: ICalendarEvent[]
): void { ): void {
const context: IColumnRenderContext = { // Delegate to ColumnRenderer
currentWeek: currentDate, // ColumnRenderer expects currentWeek property this.columnRenderer.render(columnContainer, {
dates: dates,
config: this.config config: this.config
}; });
this.columnRenderer.render(columnContainer, context);
} }
/** /**
@ -272,17 +278,21 @@ export class GridRenderer {
* @param grid - Existing grid element * @param grid - Existing grid element
* @param currentDate - New view date * @param currentDate - New view date
* @param view - View type * @param view - View type
* @param dates - Array of dates to render
* @param events - All events for the period
*/ */
private updateGridContent( private updateGridContent(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView view: CalendarView,
dates: Date[],
events: ICalendarEvent[]
): void { ): void {
// Update column container if needed // Update column container if needed
const columnContainer = grid.querySelector('swp-day-columns'); const columnContainer = grid.querySelector('swp-day-columns');
if (columnContainer) { if (columnContainer) {
columnContainer.innerHTML = ''; columnContainer.innerHTML = '';
this.renderColumnContainer(columnContainer as HTMLElement, currentDate, view); this.renderColumnContainer(columnContainer as HTMLElement, dates, events);
} }
} }
/** /**
@ -292,14 +302,15 @@ export class GridRenderer {
* Creates a complete grid positioned absolutely for animation. * Creates a complete grid positioned absolutely for animation.
* *
* Note: Positioning is handled by Animation API, not here. * Note: Positioning is handled by Animation API, not here.
* Events will be rendered by EventRenderingService when GRID_RENDERED emits.
* *
* @param parentContainer - Container for the new grid * @param parentContainer - Container for the new grid
* @param weekStart - Start date of the new week * @param dates - Array of dates to render
* @returns New grid element ready for animation * @returns New grid element ready for animation
*/ */
public createNavigationGrid(parentContainer: HTMLElement, weekStart: Date): HTMLElement { public createNavigationGrid(parentContainer: HTMLElement, dates: Date[]): HTMLElement {
// Use SAME method as initial load - respects workweek settings // Create grid structure without events (events rendered by EventRenderingService)
const newGrid = this.createOptimizedGridContainer(weekStart, 'week'); const newGrid = this.createOptimizedGridContainer(dates, []);
// Position new grid for animation - NO transform here, let Animation API handle it // Position new grid for animation - NO transform here, let Animation API handle it
newGrid.style.position = 'absolute'; newGrid.style.position = 'absolute';

View file

@ -0,0 +1,18 @@
/**
* IColumnDataSource - Defines the contract for providing column data
*
* This interface abstracts away whether columns represent dates or resources,
* allowing the calendar to switch between date-based and resource-based views.
*/
export interface IColumnDataSource {
/**
* Get the list of column identifiers to render
* @returns Array of identifiers (dates or resource IDs)
*/
getColumns(): Date[];
/**
* Get the type of columns this datasource provides
*/
getType(): 'date' | 'resource';
}