Refactors grid and navigation rendering
Attempt 1
This commit is contained in:
parent
6026d28e6f
commit
32ee35eb02
10 changed files with 436 additions and 811 deletions
|
|
@ -4,7 +4,7 @@ import { CalendarConfig } from '../core/CalendarConfig.js';
|
|||
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
|
||||
import { EventManager } from './EventManager.js';
|
||||
import { GridManager } from './GridManager.js';
|
||||
import { EventRenderer } from './EventRenderer.js';
|
||||
import { EventRenderer } from '../renderers/EventRendererManager.js';
|
||||
import { ScrollManager } from './ScrollManager.js';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,453 +0,0 @@
|
|||
// Data management and API communication
|
||||
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { EventTypes } from '../constants/EventTypes';
|
||||
import { CalendarEvent, EventData, Period } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* Event creation data interface
|
||||
*/
|
||||
interface EventCreateData {
|
||||
title: string;
|
||||
type: string;
|
||||
start: string;
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event update data interface
|
||||
*/
|
||||
interface EventUpdateData {
|
||||
eventId: string;
|
||||
changes: Partial<CalendarEvent>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages data fetching and API communication
|
||||
* Currently uses mock data until backend is implemented
|
||||
*/
|
||||
export class DataManager {
|
||||
private baseUrl: string = '/api/events';
|
||||
private useMockData: boolean = true; // Toggle this when backend is ready
|
||||
private cache: Map<string, EventData> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
|
||||
private subscribeToEvents(): void {
|
||||
// Listen for period changes to fetch new data
|
||||
eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => {
|
||||
this.fetchEventsForPeriod((e as CustomEvent).detail);
|
||||
});
|
||||
|
||||
// Listen for event updates
|
||||
eventBus.on(EventTypes.EVENT_UPDATE, (e: Event) => {
|
||||
this.updateEvent((e as CustomEvent).detail);
|
||||
});
|
||||
|
||||
// Listen for event creation
|
||||
eventBus.on(EventTypes.EVENT_CREATE, (e: Event) => {
|
||||
this.createEvent((e as CustomEvent).detail);
|
||||
});
|
||||
|
||||
// Listen for event deletion
|
||||
eventBus.on(EventTypes.EVENT_DELETE, (e: Event) => {
|
||||
this.deleteEvent((e as CustomEvent).detail.eventId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch events for a specific period
|
||||
*/
|
||||
async fetchEventsForPeriod(period: Period): Promise<EventData> {
|
||||
const cacheKey = `${period.start}-${period.end}`;
|
||||
|
||||
// Check cache first
|
||||
if (this.cache.has(cacheKey)) {
|
||||
const cachedData = this.cache.get(cacheKey)!;
|
||||
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, cachedData);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// Emit loading start
|
||||
eventBus.emit(EventTypes.DATA_FETCH_START, { period });
|
||||
|
||||
try {
|
||||
let data: EventData;
|
||||
|
||||
if (this.useMockData) {
|
||||
// Simulate network delay
|
||||
await this.delay(300);
|
||||
data = this.getMockData(period);
|
||||
} else {
|
||||
// Real API call
|
||||
const params = new URLSearchParams({
|
||||
start: period.start,
|
||||
end: period.end
|
||||
});
|
||||
|
||||
const response = await fetch(`${this.baseUrl}?${params}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch events');
|
||||
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
// Cache the data
|
||||
this.cache.set(cacheKey, data);
|
||||
|
||||
// Emit success
|
||||
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events to only include those within the specified period
|
||||
*/
|
||||
public filterEventsForPeriod(events: CalendarEvent[], period: Period): CalendarEvent[] {
|
||||
const startDate = new Date(period.start);
|
||||
const endDate = new Date(period.end);
|
||||
|
||||
return events.filter(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
// Include event if it overlaps with the period
|
||||
return eventStart <= endDate && eventEnd >= startDate;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events filtered by period and optionally by all-day status
|
||||
*/
|
||||
public getFilteredEvents(period: Period, excludeAllDay: boolean = false): CalendarEvent[] {
|
||||
const cacheKey = `${period.start}-${period.end}`;
|
||||
const cachedData = this.cache.get(cacheKey);
|
||||
|
||||
if (!cachedData) {
|
||||
console.warn('DataManager: No cached data found for period', period);
|
||||
return [];
|
||||
}
|
||||
|
||||
let filteredEvents = this.filterEventsForPeriod(cachedData.events, period);
|
||||
|
||||
if (excludeAllDay) {
|
||||
filteredEvents = filteredEvents.filter(event => !event.allDay);
|
||||
console.log(`DataManager: Filtered out all-day events, ${filteredEvents.length} non-all-day events remaining`);
|
||||
}
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event
|
||||
*/
|
||||
async createEvent(eventData: EventCreateData): Promise<CalendarEvent> {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
const newEvent: CalendarEvent = {
|
||||
id: `evt-${Date.now()}`,
|
||||
title: eventData.title,
|
||||
start: eventData.start,
|
||||
end: eventData.end,
|
||||
type: eventData.type,
|
||||
allDay: eventData.allDay,
|
||||
syncStatus: 'synced',
|
||||
metadata: eventData.description ? { description: eventData.description } : undefined
|
||||
};
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'create',
|
||||
event: newEvent
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to create event');
|
||||
|
||||
const newEvent = await response.json();
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'create',
|
||||
event: newEvent
|
||||
});
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'create',
|
||||
error: errorMessage
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing event
|
||||
*/
|
||||
async updateEvent(updateData: EventUpdateData): Promise<boolean> {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'update' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'update',
|
||||
eventId: updateData.eventId,
|
||||
changes: updateData.changes
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(`${this.baseUrl}/${updateData.eventId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData.changes)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update event');
|
||||
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'update',
|
||||
eventId: updateData.eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'update',
|
||||
error: errorMessage,
|
||||
eventId: updateData.eventId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event
|
||||
*/
|
||||
async deleteEvent(eventId: string): Promise<boolean> {
|
||||
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'delete' });
|
||||
|
||||
try {
|
||||
if (this.useMockData) {
|
||||
await this.delay(200);
|
||||
|
||||
// Clear cache to force refresh
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'delete',
|
||||
eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Real API call
|
||||
const response = await fetch(`${this.baseUrl}/${eventId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to delete event');
|
||||
|
||||
this.cache.clear();
|
||||
|
||||
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
|
||||
action: 'delete',
|
||||
eventId
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
|
||||
action: 'delete',
|
||||
error: errorMessage,
|
||||
eventId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate mock data for testing - only generates events within the specified period
|
||||
*/
|
||||
private getMockData(period: Period): EventData {
|
||||
const events: CalendarEvent[] = [];
|
||||
const types: string[] = ['meeting', 'meal', 'work', 'milestone'];
|
||||
const titles: Record<string, string[]> = {
|
||||
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
|
||||
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
|
||||
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
|
||||
milestone: ['Project Deadline', 'Release Day', 'Demo Day']
|
||||
};
|
||||
|
||||
// Parse dates - only generate events within this exact period
|
||||
const startDate = new Date(period.start);
|
||||
const endDate = new Date(period.end);
|
||||
|
||||
console.log(`DataManager: Generating mock events for period ${period.start} to ${period.end}`);
|
||||
|
||||
// Generate some events for each day within the period
|
||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||
// Skip weekends for most events
|
||||
const dayOfWeek = d.getDay();
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
|
||||
if (isWeekend) {
|
||||
// Maybe one or two events on weekends
|
||||
if (Math.random() > 0.7) {
|
||||
const type: string = 'meal';
|
||||
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
|
||||
const hour = 12 + Math.floor(Math.random() * 4);
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title,
|
||||
type,
|
||||
start: `${this.formatDate(d)}T${hour}:00:00`,
|
||||
end: `${this.formatDate(d)}T${hour + 1}:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Regular workday events
|
||||
|
||||
// Morning standup
|
||||
if (Math.random() > 0.3) {
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Team Standup',
|
||||
type: 'meeting',
|
||||
start: `${this.formatDate(d)}T09:00:00`,
|
||||
end: `${this.formatDate(d)}T09:30:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
|
||||
// Lunch
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Lunch',
|
||||
type: 'meal',
|
||||
start: `${this.formatDate(d)}T12:00:00`,
|
||||
end: `${this.formatDate(d)}T13:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
|
||||
// Random afternoon events
|
||||
const numAfternoonEvents = Math.floor(Math.random() * 3) + 1;
|
||||
for (let i = 0; i < numAfternoonEvents; i++) {
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
|
||||
const startHour = 13 + Math.floor(Math.random() * 4);
|
||||
const duration = 1 + Math.floor(Math.random() * 2);
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title,
|
||||
type,
|
||||
start: `${this.formatDate(d)}T${startHour}:${Math.random() > 0.5 ? '00' : '30'}:00`,
|
||||
end: `${this.formatDate(d)}T${startHour + duration}:00:00`,
|
||||
allDay: false,
|
||||
syncStatus: Math.random() > 0.9 ? 'pending' : 'synced'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a multi-day event if period spans multiple days
|
||||
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
if (daysDiff > 1) {
|
||||
const midWeek = new Date(startDate);
|
||||
midWeek.setDate(midWeek.getDate() + Math.min(2, daysDiff - 1));
|
||||
|
||||
events.push({
|
||||
id: `evt-${events.length + 1}`,
|
||||
title: 'Project Sprint',
|
||||
type: 'milestone',
|
||||
start: `${this.formatDate(startDate)}T00:00:00`,
|
||||
end: `${this.formatDate(midWeek)}T23:59:59`,
|
||||
allDay: true,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
events,
|
||||
meta: {
|
||||
start: period.start,
|
||||
end: period.end,
|
||||
total: events.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility methods
|
||||
*/
|
||||
|
||||
private formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between mock and real data
|
||||
*/
|
||||
setUseMockData(useMock: boolean): void {
|
||||
this.useMockData = useMock;
|
||||
this.clearCache();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
import { EventBus } from '../core/EventBus';
|
||||
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
|
||||
import { EventTypes } from '../constants/EventTypes';
|
||||
import { StateEvents } from '../types/CalendarState';
|
||||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||
import { EventManager } from './EventManager';
|
||||
import { EventRendererStrategy } from '../renderers/EventRenderer';
|
||||
|
||||
/**
|
||||
* EventRenderer - Render events i DOM med positionering using Strategy Pattern
|
||||
* Håndterer event positioning og overlap detection
|
||||
*/
|
||||
export class EventRenderer {
|
||||
private eventBus: IEventBus;
|
||||
private eventManager: EventManager;
|
||||
private strategy: EventRendererStrategy;
|
||||
|
||||
constructor(eventBus: IEventBus, eventManager: EventManager) {
|
||||
this.eventBus = eventBus;
|
||||
this.eventManager = eventManager;
|
||||
|
||||
// Cache strategy at initialization
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render events in a specific container for a given period
|
||||
*/
|
||||
public renderEvents(context: RenderContext): void {
|
||||
console.log('EventRenderer: Rendering events for period', {
|
||||
startDate: context.startDate,
|
||||
endDate: context.endDate,
|
||||
container: context.container
|
||||
});
|
||||
|
||||
// Get events from EventManager for the period
|
||||
const events = this.eventManager.getEventsForPeriod(
|
||||
context.startDate,
|
||||
context.endDate
|
||||
);
|
||||
|
||||
console.log(`EventRenderer: Found ${events.length} events for period`);
|
||||
|
||||
if (events.length === 0) {
|
||||
console.log('EventRenderer: No events to render for this period');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use cached strategy to render events in the specific container
|
||||
this.strategy.renderEvents(events, context.container, calendarConfig);
|
||||
|
||||
console.log(`EventRenderer: Successfully rendered ${events.length} events`);
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Event-driven rendering: React to grid and container events
|
||||
this.eventBus.on(EventTypes.GRID_RENDERED, (event: Event) => {
|
||||
console.log('EventRenderer: Received GRID_RENDERED event');
|
||||
this.handleGridRendered(event as CustomEvent);
|
||||
});
|
||||
|
||||
this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => {
|
||||
console.log('EventRenderer: Received CONTAINER_READY_FOR_EVENTS event');
|
||||
this.handleContainerReady(event as CustomEvent);
|
||||
});
|
||||
|
||||
this.eventBus.on(EventTypes.VIEW_CHANGED, (event: Event) => {
|
||||
console.log('EventRenderer: Received VIEW_CHANGED event');
|
||||
this.handleViewChanged(event as CustomEvent);
|
||||
});
|
||||
|
||||
// Handle calendar type changes - update cached strategy
|
||||
this.eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => {
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
|
||||
console.log(`EventRenderer: Updated strategy to ${calendarType}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle GRID_RENDERED event - render events in the current grid
|
||||
*/
|
||||
private handleGridRendered(event: CustomEvent): void {
|
||||
const { container, startDate, endDate } = event.detail;
|
||||
|
||||
if (!container) {
|
||||
console.error('EventRenderer: No container in GRID_RENDERED event', event.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use period from event or fallback to calculated period
|
||||
const periodStart = startDate;
|
||||
const periodEnd = endDate;
|
||||
|
||||
this.renderEvents({
|
||||
container: container,
|
||||
startDate: periodStart,
|
||||
endDate: periodEnd
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CONTAINER_READY_FOR_EVENTS event - render events in pre-rendered container
|
||||
*/
|
||||
private handleContainerReady(event: CustomEvent): void {
|
||||
const { container, startDate, endDate } = event.detail;
|
||||
|
||||
if (!container || !startDate || !endDate) {
|
||||
console.error('EventRenderer: Invalid CONTAINER_READY_FOR_EVENTS event data', event.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderEvents({
|
||||
container: container,
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle VIEW_CHANGED event - clear and re-render for new view
|
||||
*/
|
||||
private handleViewChanged(event: CustomEvent): void {
|
||||
// Clear all existing events since view structure may have changed
|
||||
this.clearEvents();
|
||||
|
||||
// New rendering will be triggered by subsequent GRID_RENDERED event
|
||||
console.log('EventRenderer: Cleared events for view change, waiting for GRID_RENDERED');
|
||||
}
|
||||
private clearEvents(container?: HTMLElement): void {
|
||||
console.log(`EventRenderer: Clearing events`, container ? 'in container' : 'globally');
|
||||
this.strategy.clearEvents(container);
|
||||
}
|
||||
|
||||
public refresh(container?: HTMLElement): void {
|
||||
// Clear events in specific container or globally
|
||||
this.clearEvents(container);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.clearEvents();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,8 @@ import { EventTypes } from '../constants/EventTypes';
|
|||
import { StateEvents } from '../types/CalendarState';
|
||||
import { DateUtils } from '../utils/DateUtils';
|
||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||
import { HeaderRenderContext } from '../renderers/HeaderRenderer';
|
||||
import { ColumnRenderContext } from '../renderers/ColumnRenderer';
|
||||
import { GridRenderer } from '../renderers/GridRenderer';
|
||||
import { GridStyleManager } from '../renderers/GridStyleManager';
|
||||
|
||||
/**
|
||||
* Grid position interface
|
||||
|
|
@ -28,9 +27,13 @@ export class GridManager {
|
|||
private currentWeek: Date | null = null;
|
||||
private allDayEvents: any[] = []; // Store all-day events for current week
|
||||
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
||||
private gridRenderer: GridRenderer;
|
||||
private styleManager: GridStyleManager;
|
||||
|
||||
constructor() {
|
||||
console.log('🏗️ GridManager: Constructor called');
|
||||
this.gridRenderer = new GridRenderer(calendarConfig);
|
||||
this.styleManager = new GridStyleManager(calendarConfig);
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ export class GridManager {
|
|||
if (detail.data && detail.data.calendarMode === 'resource') {
|
||||
// Resource data will be passed in the state event
|
||||
// For now just update grid styles
|
||||
this.updateGridStyles();
|
||||
this.styleManager.updateGridStyles(this.resourceData);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -135,10 +138,10 @@ export class GridManager {
|
|||
}
|
||||
|
||||
console.log('GridManager: Starting render with grid element:', this.grid);
|
||||
this.updateGridStyles();
|
||||
this.renderGrid();
|
||||
this.styleManager.updateGridStyles(this.resourceData);
|
||||
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
|
||||
|
||||
const columnCount = this.getColumnCount();
|
||||
const columnCount = this.styleManager.getColumnCount(this.resourceData);
|
||||
console.log(`GridManager: Render complete - created ${columnCount} columns`);
|
||||
|
||||
// Emit GRID_RENDERED event to trigger event rendering
|
||||
|
|
@ -152,181 +155,9 @@ export class GridManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current column count based on calendar mode
|
||||
*/
|
||||
private getColumnCount(): number {
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
|
||||
if (calendarType === 'resource' && this.resourceData) {
|
||||
return this.resourceData.resources.length;
|
||||
} else if (calendarType === 'date') {
|
||||
const dateSettings = calendarConfig.getDateViewSettings();
|
||||
switch (dateSettings.period) {
|
||||
case 'day': return 1;
|
||||
case 'week': return dateSettings.weekDays;
|
||||
case 'month': return 7;
|
||||
default: return dateSettings.weekDays;
|
||||
}
|
||||
}
|
||||
|
||||
return 7; // Default
|
||||
}
|
||||
// Column count calculation moved to GridStyleManager
|
||||
|
||||
/**
|
||||
* Render the complete grid using POC structure
|
||||
*/
|
||||
private renderGrid(): void {
|
||||
console.log('GridManager: renderGrid called', {
|
||||
hasGrid: !!this.grid,
|
||||
hasCurrentWeek: !!this.currentWeek,
|
||||
currentWeek: this.currentWeek
|
||||
});
|
||||
|
||||
if (!this.grid || !this.currentWeek) {
|
||||
console.warn('GridManager: Cannot render - missing grid or currentWeek');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only clear and rebuild if grid is empty (first render)
|
||||
if (this.grid.children.length === 0) {
|
||||
console.log('GridManager: First render - creating grid structure');
|
||||
// Create POC structure: header-spacer + time-axis + grid-container
|
||||
this.createHeaderSpacer();
|
||||
this.createTimeAxis();
|
||||
this.createGridContainer();
|
||||
} else {
|
||||
console.log('GridManager: Re-render - updating existing structure');
|
||||
// Just update the calendar header for all-day events
|
||||
this.updateCalendarHeader();
|
||||
}
|
||||
|
||||
console.log('GridManager: Grid rendered successfully with POC structure');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create header spacer to align time axis with week content
|
||||
*/
|
||||
private createHeaderSpacer(): void {
|
||||
if (!this.grid) return;
|
||||
|
||||
const headerSpacer = document.createElement('swp-header-spacer');
|
||||
this.grid.appendChild(headerSpacer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create time axis (positioned beside grid container) like in POC
|
||||
*/
|
||||
private createTimeAxis(): void {
|
||||
if (!this.grid) return;
|
||||
|
||||
const timeAxis = document.createElement('swp-time-axis');
|
||||
const timeAxisContent = document.createElement('swp-time-axis-content');
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const startHour = gridSettings.dayStartHour;
|
||||
const endHour = gridSettings.dayEndHour;
|
||||
console.log('GridManager: Creating time axis - startHour:', startHour, 'endHour:', endHour);
|
||||
|
||||
for (let hour = startHour; hour < endHour; hour++) {
|
||||
const marker = document.createElement('swp-hour-marker');
|
||||
const period = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
|
||||
marker.textContent = `${displayHour} ${period}`;
|
||||
timeAxisContent.appendChild(marker);
|
||||
}
|
||||
|
||||
timeAxis.appendChild(timeAxisContent);
|
||||
this.grid.appendChild(timeAxis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create grid container with header and scrollable content using Strategy Pattern
|
||||
*/
|
||||
private createGridContainer(): void {
|
||||
if (!this.grid || !this.currentWeek) return;
|
||||
|
||||
const gridContainer = document.createElement('swp-grid-container');
|
||||
|
||||
// Create calendar header using Strategy Pattern
|
||||
const calendarHeader = document.createElement('swp-calendar-header');
|
||||
this.renderCalendarHeader(calendarHeader);
|
||||
gridContainer.appendChild(calendarHeader);
|
||||
|
||||
// Create scrollable content
|
||||
const scrollableContent = document.createElement('swp-scrollable-content');
|
||||
const timeGrid = document.createElement('swp-time-grid');
|
||||
|
||||
// Add grid lines
|
||||
const gridLines = document.createElement('swp-grid-lines');
|
||||
timeGrid.appendChild(gridLines);
|
||||
|
||||
// Create column container using Strategy Pattern
|
||||
const columnContainer = document.createElement('swp-day-columns');
|
||||
this.renderColumnContainer(columnContainer);
|
||||
timeGrid.appendChild(columnContainer);
|
||||
|
||||
scrollableContent.appendChild(timeGrid);
|
||||
gridContainer.appendChild(scrollableContent);
|
||||
|
||||
this.grid.appendChild(gridContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render calendar header using Strategy Pattern
|
||||
*/
|
||||
private renderCalendarHeader(calendarHeader: HTMLElement): void {
|
||||
if (!this.currentWeek) return;
|
||||
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||
|
||||
const context: HeaderRenderContext = {
|
||||
currentWeek: this.currentWeek,
|
||||
config: calendarConfig,
|
||||
allDayEvents: this.allDayEvents,
|
||||
resourceData: this.resourceData
|
||||
};
|
||||
|
||||
headerRenderer.render(calendarHeader, context);
|
||||
|
||||
// Update spacer heights based on all-day events
|
||||
this.updateSpacerHeights();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render column container using Strategy Pattern
|
||||
*/
|
||||
private renderColumnContainer(columnContainer: HTMLElement): void {
|
||||
if (!this.currentWeek) return;
|
||||
|
||||
console.log('GridManager: renderColumnContainer called');
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
||||
|
||||
const context: ColumnRenderContext = {
|
||||
currentWeek: this.currentWeek,
|
||||
config: calendarConfig,
|
||||
resourceData: this.resourceData
|
||||
};
|
||||
|
||||
columnRenderer.render(columnContainer, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update only the calendar header (for all-day events) without rebuilding entire grid
|
||||
*/
|
||||
private updateCalendarHeader(): void {
|
||||
if (!this.grid || !this.currentWeek) return;
|
||||
|
||||
const calendarHeader = this.grid.querySelector('swp-calendar-header');
|
||||
if (!calendarHeader) return;
|
||||
|
||||
// Clear existing content
|
||||
calendarHeader.innerHTML = '';
|
||||
|
||||
// Re-render headers using Strategy Pattern
|
||||
this.renderCalendarHeader(calendarHeader as HTMLElement);
|
||||
}
|
||||
// Grid rendering methods moved to GridRenderer
|
||||
|
||||
/**
|
||||
* Update all-day events data and re-render if needed
|
||||
|
|
@ -350,80 +181,11 @@ export class GridManager {
|
|||
|
||||
// Update only the calendar header if grid is already rendered
|
||||
if (this.grid && this.grid.children.length > 0) {
|
||||
this.updateCalendarHeader();
|
||||
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update spacer heights based on all-day events presence
|
||||
*/
|
||||
private updateSpacerHeights(): void {
|
||||
const allDayEventCount = 1;
|
||||
const eventHeight = 26; // Height per all-day event in pixels
|
||||
const padding = 0; // Top/bottom padding
|
||||
const allDayHeight = allDayEventCount > 0 ? (allDayEventCount * eventHeight) + padding : 0;
|
||||
|
||||
// Set CSS variable for dynamic spacer height
|
||||
document.documentElement.style.setProperty('--all-day-row-height', `${allDayHeight}px`);
|
||||
|
||||
console.log('GridManager: Updated --all-day-row-height to', `${allDayHeight}px`, 'for', allDayEventCount, 'events');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grid CSS variables
|
||||
*/
|
||||
private updateGridStyles(): void {
|
||||
const root = document.documentElement;
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const calendar = document.querySelector('swp-calendar') as HTMLElement;
|
||||
const calendarType = calendarConfig.getCalendarMode();
|
||||
|
||||
// Set CSS variables
|
||||
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 number of columns based on calendar type
|
||||
let columnCount = 7; // Default for date mode
|
||||
if (calendarType === 'resource' && this.resourceData) {
|
||||
columnCount = this.resourceData.resources.length;
|
||||
} else if (calendarType === 'date') {
|
||||
const dateSettings = calendarConfig.getDateViewSettings();
|
||||
// Calculate columns based on view type - business logic moved from config
|
||||
switch (dateSettings.period) {
|
||||
case 'day':
|
||||
columnCount = 1;
|
||||
break;
|
||||
case 'week':
|
||||
columnCount = dateSettings.weekDays;
|
||||
break;
|
||||
case 'month':
|
||||
columnCount = 7;
|
||||
break;
|
||||
default:
|
||||
columnCount = dateSettings.weekDays;
|
||||
}
|
||||
}
|
||||
root.style.setProperty('--grid-columns', columnCount.toString());
|
||||
|
||||
// Set day column min 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());
|
||||
}
|
||||
|
||||
console.log('GridManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
|
||||
}
|
||||
// CSS management methods moved to GridStyleManager
|
||||
|
||||
/**
|
||||
* Setup grid interaction handlers for POC structure
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { IEventBus } from '../types/CalendarTypes.js';
|
||||
import { DateUtils } from '../utils/DateUtils.js';
|
||||
import { EventTypes } from '../constants/EventTypes.js';
|
||||
import { NavigationRenderer } from '../renderers/NavigationRenderer.js';
|
||||
|
||||
/**
|
||||
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
||||
|
|
@ -8,6 +9,7 @@ import { EventTypes } from '../constants/EventTypes.js';
|
|||
*/
|
||||
export class NavigationManager {
|
||||
private eventBus: IEventBus;
|
||||
private navigationRenderer: NavigationRenderer;
|
||||
private currentWeek: Date;
|
||||
private targetWeek: Date;
|
||||
private animationQueue: number = 0;
|
||||
|
|
@ -15,6 +17,7 @@ export class NavigationManager {
|
|||
constructor(eventBus: IEventBus) {
|
||||
console.log('🧭 NavigationManager: Constructor called');
|
||||
this.eventBus = eventBus;
|
||||
this.navigationRenderer = new NavigationRenderer(eventBus);
|
||||
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
||||
this.targetWeek = new Date(this.currentWeek);
|
||||
this.init();
|
||||
|
|
@ -130,7 +133,7 @@ export class NavigationManager {
|
|||
|
||||
// Always create a fresh container for consistent behavior
|
||||
console.log('NavigationManager: Creating new container');
|
||||
newGrid = this.renderContainer(container as HTMLElement, targetWeek);
|
||||
newGrid = this.navigationRenderer.renderContainer(container as HTMLElement, targetWeek);
|
||||
|
||||
// Clear any existing transforms before animation
|
||||
newGrid.style.transform = '';
|
||||
|
|
@ -194,15 +197,7 @@ export class NavigationManager {
|
|||
});
|
||||
}
|
||||
|
||||
// Utility functions (from POC)
|
||||
private formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
private isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
// Utility functions (from POC) - moved formatting to NavigationRenderer
|
||||
|
||||
private updateWeekInfo(): void {
|
||||
const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
|
||||
|
|
@ -264,95 +259,5 @@ export class NavigationManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a complete container with content and events
|
||||
*/
|
||||
private renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement {
|
||||
console.log('NavigationManager: Rendering new container for week:', weekStart.toDateString());
|
||||
|
||||
// Create new grid container
|
||||
const newGrid = document.createElement('swp-grid-container');
|
||||
newGrid.innerHTML = `
|
||||
<swp-calendar-header></swp-calendar-header>
|
||||
<swp-scrollable-content>
|
||||
<swp-time-grid>
|
||||
<swp-grid-lines></swp-grid-lines>
|
||||
<swp-day-columns></swp-day-columns>
|
||||
</swp-time-grid>
|
||||
</swp-scrollable-content>
|
||||
`;
|
||||
|
||||
// Position new grid - NO transform here, let Animation API handle it
|
||||
newGrid.style.position = 'absolute';
|
||||
newGrid.style.top = '0';
|
||||
newGrid.style.left = '0';
|
||||
newGrid.style.width = '100%';
|
||||
newGrid.style.height = '100%';
|
||||
|
||||
// Add to parent container
|
||||
parentContainer.appendChild(newGrid);
|
||||
|
||||
// Render week content (headers and columns)
|
||||
this.renderWeekContentInContainer(newGrid, weekStart);
|
||||
|
||||
// Emit event to trigger event rendering
|
||||
const weekEnd = DateUtils.addDays(weekStart, 6);
|
||||
this.eventBus.emit(EventTypes.CONTAINER_READY_FOR_EVENTS, {
|
||||
container: newGrid,
|
||||
startDate: weekStart,
|
||||
endDate: weekEnd
|
||||
});
|
||||
|
||||
return newGrid;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Render week content in specific container
|
||||
*/
|
||||
private renderWeekContentInContainer(gridContainer: HTMLElement, weekStart: Date): void {
|
||||
const header = gridContainer.querySelector('swp-calendar-header');
|
||||
const dayColumns = gridContainer.querySelector('swp-day-columns');
|
||||
|
||||
if (!header || !dayColumns) return;
|
||||
|
||||
// Clear existing content
|
||||
header.innerHTML = '';
|
||||
dayColumns.innerHTML = '';
|
||||
|
||||
// Render headers for target week
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(weekStart);
|
||||
date.setDate(date.getDate() + i);
|
||||
|
||||
const headerElement = document.createElement('swp-day-header');
|
||||
if (this.isToday(date)) {
|
||||
headerElement.dataset.today = 'true';
|
||||
}
|
||||
|
||||
headerElement.innerHTML = `
|
||||
<swp-day-name>${days[date.getDay()]}</swp-day-name>
|
||||
<swp-day-date>${date.getDate()}</swp-day-date>
|
||||
`;
|
||||
headerElement.dataset.date = this.formatDate(date);
|
||||
|
||||
header.appendChild(headerElement);
|
||||
}
|
||||
|
||||
// Render day columns for target week (with hardcoded test event)
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const column = document.createElement('swp-day-column');
|
||||
const date = new Date(weekStart);
|
||||
date.setDate(date.getDate() + i);
|
||||
column.dataset.date = this.formatDate(date);
|
||||
|
||||
const eventsLayer = document.createElement('swp-events-layer');
|
||||
column.appendChild(eventsLayer);
|
||||
|
||||
|
||||
dayColumns.appendChild(column);
|
||||
}
|
||||
}
|
||||
// Rendering methods moved to NavigationRenderer for better separation of concerns
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue