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 { CalendarManager } from './managers/CalendarManager.js';
|
||||||
import { NavigationManager } from './managers/NavigationManager.js';
|
import { NavigationManager } from './managers/NavigationManager.js';
|
||||||
import { ViewManager } from './managers/ViewManager.js';
|
import { ViewManager } from './managers/ViewManager.js';
|
||||||
import { EventManager } from './managers/EventManager.js';
|
import { EventManager } from './managers/EventManager.js';
|
||||||
import { EventRenderer } from './managers/EventRenderer.js';
|
import { EventRenderer } from './renderers/EventRendererManager.js';
|
||||||
import { GridManager } from './managers/GridManager.js';
|
import { GridManager } from './managers/GridManager.js';
|
||||||
import { ScrollManager } from './managers/ScrollManager.js';
|
import { ScrollManager } from './managers/ScrollManager.js';
|
||||||
import { calendarConfig } from './core/CalendarConfig.js';
|
import { calendarConfig } from './core/CalendarConfig.js';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { CalendarConfig } from '../core/CalendarConfig.js';
|
||||||
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
|
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
|
||||||
import { EventManager } from './EventManager.js';
|
import { EventManager } from './EventManager.js';
|
||||||
import { GridManager } from './GridManager.js';
|
import { GridManager } from './GridManager.js';
|
||||||
import { EventRenderer } from './EventRenderer.js';
|
import { EventRenderer } from '../renderers/EventRendererManager.js';
|
||||||
import { ScrollManager } from './ScrollManager.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,9 +6,8 @@ import { EventTypes } from '../constants/EventTypes';
|
||||||
import { StateEvents } from '../types/CalendarState';
|
import { StateEvents } from '../types/CalendarState';
|
||||||
import { DateUtils } from '../utils/DateUtils';
|
import { DateUtils } from '../utils/DateUtils';
|
||||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { GridRenderer } from '../renderers/GridRenderer';
|
||||||
import { HeaderRenderContext } from '../renderers/HeaderRenderer';
|
import { GridStyleManager } from '../renderers/GridStyleManager';
|
||||||
import { ColumnRenderContext } from '../renderers/ColumnRenderer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid position interface
|
* Grid position interface
|
||||||
|
|
@ -28,9 +27,13 @@ export class GridManager {
|
||||||
private currentWeek: Date | null = null;
|
private currentWeek: Date | null = null;
|
||||||
private allDayEvents: any[] = []; // Store all-day events for current week
|
private allDayEvents: any[] = []; // Store all-day events for current week
|
||||||
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
|
||||||
|
private gridRenderer: GridRenderer;
|
||||||
|
private styleManager: GridStyleManager;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('🏗️ GridManager: Constructor called');
|
console.log('🏗️ GridManager: Constructor called');
|
||||||
|
this.gridRenderer = new GridRenderer(calendarConfig);
|
||||||
|
this.styleManager = new GridStyleManager(calendarConfig);
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +109,7 @@ export class GridManager {
|
||||||
if (detail.data && detail.data.calendarMode === 'resource') {
|
if (detail.data && detail.data.calendarMode === 'resource') {
|
||||||
// Resource data will be passed in the state event
|
// Resource data will be passed in the state event
|
||||||
// For now just update grid styles
|
// 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);
|
console.log('GridManager: Starting render with grid element:', this.grid);
|
||||||
this.updateGridStyles();
|
this.styleManager.updateGridStyles(this.resourceData);
|
||||||
this.renderGrid();
|
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`);
|
console.log(`GridManager: Render complete - created ${columnCount} columns`);
|
||||||
|
|
||||||
// Emit GRID_RENDERED event to trigger event rendering
|
// Emit GRID_RENDERED event to trigger event rendering
|
||||||
|
|
@ -152,181 +155,9 @@ export class GridManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Column count calculation moved to GridStyleManager
|
||||||
* Get current column count based on calendar mode
|
|
||||||
*/
|
|
||||||
private getColumnCount(): number {
|
|
||||||
const calendarType = calendarConfig.getCalendarMode();
|
|
||||||
|
|
||||||
if (calendarType === 'resource' && this.resourceData) {
|
// Grid rendering methods moved to GridRenderer
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all-day events data and re-render if needed
|
* 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
|
// Update only the calendar header if grid is already rendered
|
||||||
if (this.grid && this.grid.children.length > 0) {
|
if (this.grid && this.grid.children.length > 0) {
|
||||||
this.updateCalendarHeader();
|
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// CSS management methods moved to GridStyleManager
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup grid interaction handlers for POC structure
|
* Setup grid interaction handlers for POC structure
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { IEventBus } from '../types/CalendarTypes.js';
|
import { IEventBus } from '../types/CalendarTypes.js';
|
||||||
import { DateUtils } from '../utils/DateUtils.js';
|
import { DateUtils } from '../utils/DateUtils.js';
|
||||||
import { EventTypes } from '../constants/EventTypes.js';
|
import { EventTypes } from '../constants/EventTypes.js';
|
||||||
|
import { NavigationRenderer } from '../renderers/NavigationRenderer.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
||||||
|
|
@ -8,6 +9,7 @@ import { EventTypes } from '../constants/EventTypes.js';
|
||||||
*/
|
*/
|
||||||
export class NavigationManager {
|
export class NavigationManager {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
|
private navigationRenderer: NavigationRenderer;
|
||||||
private currentWeek: Date;
|
private currentWeek: Date;
|
||||||
private targetWeek: Date;
|
private targetWeek: Date;
|
||||||
private animationQueue: number = 0;
|
private animationQueue: number = 0;
|
||||||
|
|
@ -15,6 +17,7 @@ export class NavigationManager {
|
||||||
constructor(eventBus: IEventBus) {
|
constructor(eventBus: IEventBus) {
|
||||||
console.log('🧭 NavigationManager: Constructor called');
|
console.log('🧭 NavigationManager: Constructor called');
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.navigationRenderer = new NavigationRenderer(eventBus);
|
||||||
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
||||||
this.targetWeek = new Date(this.currentWeek);
|
this.targetWeek = new Date(this.currentWeek);
|
||||||
this.init();
|
this.init();
|
||||||
|
|
@ -130,7 +133,7 @@ export class NavigationManager {
|
||||||
|
|
||||||
// Always create a fresh container for consistent behavior
|
// Always create a fresh container for consistent behavior
|
||||||
console.log('NavigationManager: Creating new container');
|
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
|
// Clear any existing transforms before animation
|
||||||
newGrid.style.transform = '';
|
newGrid.style.transform = '';
|
||||||
|
|
@ -194,15 +197,7 @@ export class NavigationManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility functions (from POC)
|
// Utility functions (from POC) - moved formatting to NavigationRenderer
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateWeekInfo(): void {
|
private updateWeekInfo(): void {
|
||||||
const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
|
const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
|
||||||
|
|
@ -264,95 +259,5 @@ export class NavigationManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Rendering methods moved to NavigationRenderer for better separation of concerns
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
// clearEvents() would remove events from all containers, breaking the animation
|
// clearEvents() would remove events from all containers, breaking the animation
|
||||||
// Events are now rendered directly into the new container without clearing
|
// Events are now rendered directly into the new container without clearing
|
||||||
|
|
||||||
// Events should already be filtered by DataManager - no need to filter here
|
// Events should already be filtered by EventManager - no need to filter here
|
||||||
console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events');
|
console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events');
|
||||||
|
|
||||||
// Find columns in the specific container
|
// Find columns in the specific container
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { EventTypes } from '../constants/EventTypes';
|
||||||
import { StateEvents } from '../types/CalendarState';
|
import { StateEvents } from '../types/CalendarState';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
import { EventManager } from './EventManager';
|
import { EventManager } from '../managers/EventManager';
|
||||||
import { EventRendererStrategy } from '../renderers/EventRenderer';
|
import { EventRendererStrategy } from './EventRenderer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventRenderer - Render events i DOM med positionering using Strategy Pattern
|
* EventRenderer - Render events i DOM med positionering using Strategy Pattern
|
||||||
182
src/renderers/GridRenderer.ts
Normal file
182
src/renderers/GridRenderer.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { CalendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
|
import { HeaderRenderContext } from './HeaderRenderer';
|
||||||
|
import { ColumnRenderContext } from './ColumnRenderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridRenderer - Handles DOM rendering for the calendar grid
|
||||||
|
* Separated from GridManager to follow Single Responsibility Principle
|
||||||
|
*/
|
||||||
|
export class GridRenderer {
|
||||||
|
private config: CalendarConfig;
|
||||||
|
|
||||||
|
constructor(config: CalendarConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the complete grid structure
|
||||||
|
*/
|
||||||
|
public renderGrid(
|
||||||
|
grid: HTMLElement,
|
||||||
|
currentWeek: Date,
|
||||||
|
resourceData: ResourceCalendarData | null,
|
||||||
|
allDayEvents: any[]
|
||||||
|
): void {
|
||||||
|
console.log('GridRenderer: renderGrid called', {
|
||||||
|
hasGrid: !!grid,
|
||||||
|
hasCurrentWeek: !!currentWeek,
|
||||||
|
currentWeek: currentWeek
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!grid || !currentWeek) {
|
||||||
|
console.warn('GridRenderer: Cannot render - missing grid or currentWeek');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only clear and rebuild if grid is empty (first render)
|
||||||
|
if (grid.children.length === 0) {
|
||||||
|
console.log('GridRenderer: First render - creating grid structure');
|
||||||
|
// Create POC structure: header-spacer + time-axis + grid-container
|
||||||
|
this.createHeaderSpacer(grid);
|
||||||
|
this.createTimeAxis(grid);
|
||||||
|
this.createGridContainer(grid, currentWeek, resourceData, allDayEvents);
|
||||||
|
} else {
|
||||||
|
console.log('GridRenderer: Re-render - updating existing structure');
|
||||||
|
// Just update the calendar header for all-day events
|
||||||
|
this.updateCalendarHeader(grid, currentWeek, resourceData, allDayEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GridRenderer: Grid rendered successfully with POC structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create header spacer to align time axis with week content
|
||||||
|
*/
|
||||||
|
private createHeaderSpacer(grid: HTMLElement): void {
|
||||||
|
const headerSpacer = document.createElement('swp-header-spacer');
|
||||||
|
grid.appendChild(headerSpacer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create time axis (positioned beside grid container)
|
||||||
|
*/
|
||||||
|
private createTimeAxis(grid: HTMLElement): void {
|
||||||
|
const timeAxis = document.createElement('swp-time-axis');
|
||||||
|
const timeAxisContent = document.createElement('swp-time-axis-content');
|
||||||
|
const gridSettings = this.config.getGridSettings();
|
||||||
|
const startHour = gridSettings.dayStartHour;
|
||||||
|
const endHour = gridSettings.dayEndHour;
|
||||||
|
|
||||||
|
console.log('GridRenderer: 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);
|
||||||
|
grid.appendChild(timeAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create grid container with header and scrollable content
|
||||||
|
*/
|
||||||
|
private createGridContainer(
|
||||||
|
grid: HTMLElement,
|
||||||
|
currentWeek: Date,
|
||||||
|
resourceData: ResourceCalendarData | null,
|
||||||
|
allDayEvents: any[]
|
||||||
|
): void {
|
||||||
|
const gridContainer = document.createElement('swp-grid-container');
|
||||||
|
|
||||||
|
// Create calendar header using Strategy Pattern
|
||||||
|
const calendarHeader = document.createElement('swp-calendar-header');
|
||||||
|
this.renderCalendarHeader(calendarHeader, currentWeek, resourceData, allDayEvents);
|
||||||
|
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, currentWeek, resourceData);
|
||||||
|
timeGrid.appendChild(columnContainer);
|
||||||
|
|
||||||
|
scrollableContent.appendChild(timeGrid);
|
||||||
|
gridContainer.appendChild(scrollableContent);
|
||||||
|
|
||||||
|
grid.appendChild(gridContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render calendar header using Strategy Pattern
|
||||||
|
*/
|
||||||
|
private renderCalendarHeader(
|
||||||
|
calendarHeader: HTMLElement,
|
||||||
|
currentWeek: Date,
|
||||||
|
resourceData: ResourceCalendarData | null,
|
||||||
|
allDayEvents: any[]
|
||||||
|
): void {
|
||||||
|
const calendarType = this.config.getCalendarMode();
|
||||||
|
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||||
|
|
||||||
|
const context: HeaderRenderContext = {
|
||||||
|
currentWeek: currentWeek,
|
||||||
|
config: this.config,
|
||||||
|
allDayEvents: allDayEvents,
|
||||||
|
resourceData: resourceData
|
||||||
|
};
|
||||||
|
|
||||||
|
headerRenderer.render(calendarHeader, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render column container using Strategy Pattern
|
||||||
|
*/
|
||||||
|
private renderColumnContainer(
|
||||||
|
columnContainer: HTMLElement,
|
||||||
|
currentWeek: Date,
|
||||||
|
resourceData: ResourceCalendarData | null
|
||||||
|
): void {
|
||||||
|
console.log('GridRenderer: renderColumnContainer called');
|
||||||
|
const calendarType = this.config.getCalendarMode();
|
||||||
|
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
||||||
|
|
||||||
|
const context: ColumnRenderContext = {
|
||||||
|
currentWeek: currentWeek,
|
||||||
|
config: this.config,
|
||||||
|
resourceData: resourceData
|
||||||
|
};
|
||||||
|
|
||||||
|
columnRenderer.render(columnContainer, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update only the calendar header without rebuilding entire grid
|
||||||
|
*/
|
||||||
|
private updateCalendarHeader(
|
||||||
|
grid: HTMLElement,
|
||||||
|
currentWeek: Date,
|
||||||
|
resourceData: ResourceCalendarData | null,
|
||||||
|
allDayEvents: any[]
|
||||||
|
): void {
|
||||||
|
const calendarHeader = grid.querySelector('swp-calendar-header');
|
||||||
|
if (!calendarHeader) return;
|
||||||
|
|
||||||
|
// Clear existing content
|
||||||
|
calendarHeader.innerHTML = '';
|
||||||
|
|
||||||
|
// Re-render headers using Strategy Pattern
|
||||||
|
this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData, allDayEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/renderers/GridStyleManager.ts
Normal file
110
src/renderers/GridStyleManager.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { CalendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GridStyleManager - Manages CSS variables and styling for the grid
|
||||||
|
* Separated from GridManager to follow Single Responsibility Principle
|
||||||
|
*/
|
||||||
|
export class GridStyleManager {
|
||||||
|
private config: CalendarConfig;
|
||||||
|
|
||||||
|
constructor(config: CalendarConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all grid CSS variables
|
||||||
|
*/
|
||||||
|
public updateGridStyles(resourceData: ResourceCalendarData | null = null): void {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const gridSettings = this.config.getGridSettings();
|
||||||
|
const calendar = document.querySelector('swp-calendar') as HTMLElement;
|
||||||
|
const calendarType = this.config.getCalendarMode();
|
||||||
|
|
||||||
|
// Set CSS variables for time and grid measurements
|
||||||
|
this.setTimeVariables(root, gridSettings);
|
||||||
|
|
||||||
|
// Set column count based on calendar type
|
||||||
|
const columnCount = this.calculateColumnCount(calendarType, resourceData);
|
||||||
|
root.style.setProperty('--grid-columns', columnCount.toString());
|
||||||
|
|
||||||
|
// Set column width based on fitToWidth setting
|
||||||
|
this.setColumnWidth(root, gridSettings);
|
||||||
|
|
||||||
|
// Set fitToWidth data attribute for CSS targeting
|
||||||
|
if (calendar) {
|
||||||
|
calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GridStyleManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set time-related CSS variables
|
||||||
|
*/
|
||||||
|
private setTimeVariables(root: HTMLElement, gridSettings: any): void {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate number of columns based on calendar type and view
|
||||||
|
*/
|
||||||
|
private calculateColumnCount(calendarType: string, resourceData: ResourceCalendarData | null): number {
|
||||||
|
if (calendarType === 'resource' && resourceData) {
|
||||||
|
return resourceData.resources.length;
|
||||||
|
} else if (calendarType === 'date') {
|
||||||
|
const dateSettings = this.config.getDateViewSettings();
|
||||||
|
switch (dateSettings.period) {
|
||||||
|
case 'day':
|
||||||
|
return 1;
|
||||||
|
case 'week':
|
||||||
|
return dateSettings.weekDays;
|
||||||
|
case 'month':
|
||||||
|
return 7;
|
||||||
|
default:
|
||||||
|
return dateSettings.weekDays;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 7; // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set column width based on fitToWidth setting
|
||||||
|
*/
|
||||||
|
private setColumnWidth(root: HTMLElement, gridSettings: any): void {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update spacer heights based on all-day events
|
||||||
|
*/
|
||||||
|
public updateSpacerHeights(allDayEventCount: number = 1): void {
|
||||||
|
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('GridStyleManager: Updated --all-day-row-height to', `${allDayHeight}px`, 'for', allDayEventCount, 'events');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current column count
|
||||||
|
*/
|
||||||
|
public getColumnCount(resourceData: ResourceCalendarData | null = null): number {
|
||||||
|
const calendarType = this.config.getCalendarMode();
|
||||||
|
return this.calculateColumnCount(calendarType, resourceData);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/renderers/NavigationRenderer.ts
Normal file
119
src/renderers/NavigationRenderer.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { IEventBus } from '../types/CalendarTypes';
|
||||||
|
import { EventTypes } from '../constants/EventTypes';
|
||||||
|
import { DateUtils } from '../utils/DateUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NavigationRenderer - Handles DOM rendering for navigation containers
|
||||||
|
* Separated from NavigationManager to follow Single Responsibility Principle
|
||||||
|
*/
|
||||||
|
export class NavigationRenderer {
|
||||||
|
private eventBus: IEventBus;
|
||||||
|
|
||||||
|
constructor(eventBus: IEventBus) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a complete container with content and events
|
||||||
|
*/
|
||||||
|
public renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement {
|
||||||
|
console.log('NavigationRenderer: 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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to format date
|
||||||
|
*/
|
||||||
|
private formatDate(date: Date): string {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if date is today
|
||||||
|
*/
|
||||||
|
private isToday(date: Date): boolean {
|
||||||
|
const today = new Date();
|
||||||
|
return date.toDateString() === today.toDateString();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue