Calendar/src/managers/GridManager.ts
Janus Knudsen 1d25ab7b53 Adds fixed scrollbars for improved navigation
Implements fixed scrollbars at the browser edges to enhance navigation within the calendar view. This ensures that the scrollbars remain visible regardless of the user's scroll position, providing consistent access to horizontal and vertical scrolling.

Removes the right header spacer and right column, integrating their functionality into the new fixed scrollbar components.

Additionally, synchronizes the week header position with the horizontal scroll, improving the user experience.

Scrollbar hiding is now handled in the CSS file.
2025-07-29 21:22:13 +02:00

430 lines
No EOL
13 KiB
TypeScript

// Grid structure management - Simple CSS Grid Implementation
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
import { DateUtils } from '../utils/DateUtils';
/**
* Grid position interface
*/
interface GridPosition {
minutes: number;
time: string;
y: number;
}
/**
* Manages the calendar grid structure using simple CSS Grid
*/
export class GridManager {
private container: HTMLElement | null = null;
private grid: HTMLElement | null = null;
private currentWeek: Date | null = null;
constructor() {
this.init();
}
private init(): void {
this.findElements();
this.subscribeToEvents();
// Set initial current week to today if not set
if (!this.currentWeek) {
this.currentWeek = this.getWeekStart(new Date());
console.log('GridManager: Set initial currentWeek to', this.currentWeek);
// Render initial grid
this.render();
}
}
private getWeekStart(date: Date): Date {
const weekStart = new Date(date);
const day = weekStart.getDay();
const diff = weekStart.getDate() - day; // Sunday is 0
weekStart.setDate(diff);
weekStart.setHours(0, 0, 0, 0);
return weekStart;
}
private findElements(): void {
this.grid = document.querySelector('swp-calendar-container');
}
private subscribeToEvents(): void {
// Re-render grid on config changes
eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => {
const detail = (e as CustomEvent).detail;
if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays'].includes(detail.key)) {
this.render();
}
});
// Re-render on view change
eventBus.on(EventTypes.VIEW_CHANGE, () => {
this.render();
});
// Re-render on period change
eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => {
const detail = (e as CustomEvent).detail;
this.currentWeek = detail.week;
this.render();
});
// Handle week changes from NavigationManager
eventBus.on(EventTypes.WEEK_CHANGED, (e: Event) => {
const detail = (e as CustomEvent).detail;
this.currentWeek = detail.weekStart;
this.render();
});
// Handle grid clicks
this.setupGridInteractions();
}
/**
* Render the complete grid structure
*/
render(): void {
if (!this.grid) return;
this.updateGridStyles();
this.renderGrid();
// Emit grid rendered event
console.log('GridManager: Emitting GRID_RENDERED event');
eventBus.emit(EventTypes.GRID_RENDERED);
console.log('GridManager: GRID_RENDERED event emitted');
}
/**
* 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;
}
// Clear existing grid and rebuild POC structure
this.grid.innerHTML = '';
// Create POC structure: header-spacer + time-axis + week-container + fixed scrollbars
this.createHeaderSpacer();
this.createTimeAxis();
this.createWeekContainer();
this.createBottomRow();
this.createFixedScrollbars();
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 fixed scrollbars at browser edges
*/
private createFixedScrollbars(): void {
if (!document.body) return;
// Create right scrollbar at browser edge
const rightScrollbar = document.createElement('swp-right-scrollbar');
const rightColumn = document.createElement('swp-right-column');
rightScrollbar.appendChild(rightColumn);
document.body.appendChild(rightScrollbar);
// Create bottom scrollbar at browser edge
const bottomScrollbar = document.createElement('swp-bottom-scrollbar');
const bottomColumn = document.createElement('swp-bottom-column');
bottomScrollbar.appendChild(bottomColumn);
document.body.appendChild(bottomScrollbar);
}
/**
* Create time axis (positioned beside week container) like in POC
*/
private createTimeAxis(): void {
if (!this.grid) return;
const timeAxis = document.createElement('swp-time-axis');
const startHour = calendarConfig.get('dayStartHour');
const endHour = calendarConfig.get('dayEndHour');
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}`;
timeAxis.appendChild(marker);
}
this.grid.appendChild(timeAxis);
}
/**
* Create week container with header and scrollable content like in POC
*/
private createWeekContainer(): void {
if (!this.grid || !this.currentWeek) return;
const weekContainer = document.createElement('swp-week-container');
// Create week header
const weekHeader = document.createElement('swp-week-header');
this.renderWeekHeaders(weekHeader);
weekContainer.appendChild(weekHeader);
// 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 day columns
const dayColumns = document.createElement('swp-day-columns');
this.renderDayColumns(dayColumns);
timeGrid.appendChild(dayColumns);
scrollableContent.appendChild(timeGrid);
weekContainer.appendChild(scrollableContent);
this.grid.appendChild(weekContainer);
}
/**
* Create bottom row with spacer
*/
private createBottomRow(): void {
if (!this.grid) return;
// Bottom spacer (left)
const bottomSpacer = document.createElement('swp-bottom-spacer');
this.grid.appendChild(bottomSpacer);
}
/**
* Render week headers like in POC
*/
private renderWeekHeaders(weekHeader: HTMLElement): void {
if (!this.currentWeek) return;
const dates = this.getWeekDates(this.currentWeek);
const weekDays = calendarConfig.get('weekDays');
const daysToShow = dates.slice(0, weekDays);
daysToShow.forEach((date) => {
const header = document.createElement('swp-day-header');
if (this.isToday(date)) {
(header as any).dataset.today = 'true';
}
header.innerHTML = `
<swp-day-name>${this.getDayName(date)}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
(header as any).dataset.date = this.formatDate(date);
weekHeader.appendChild(header);
});
}
/**
* Render day columns like in POC
*/
private renderDayColumns(dayColumns: HTMLElement): void {
console.log('GridManager: renderDayColumns called');
if (!this.currentWeek) {
console.log('GridManager: No currentWeek, returning');
return;
}
const dates = this.getWeekDates(this.currentWeek);
const weekDays = calendarConfig.get('weekDays');
const daysToShow = dates.slice(0, weekDays);
console.log('GridManager: About to render', daysToShow.length, 'day columns');
daysToShow.forEach((date, dayIndex) => {
const column = document.createElement('swp-day-column');
(column as any).dataset.date = this.formatDate(date);
// Add dummy content to force column width (temporary test)
const dummyContent = document.createElement('div');
dummyContent.style.height = '20px';
dummyContent.style.width = '100%';
dummyContent.style.backgroundColor = 'red';
dummyContent.style.color = 'white';
dummyContent.style.fontSize = '12px';
dummyContent.style.textAlign = 'center';
dummyContent.textContent = `Day ${dayIndex + 1}`;
column.appendChild(dummyContent);
const eventsLayer = document.createElement('swp-events-layer');
column.appendChild(eventsLayer);
dayColumns.appendChild(column);
});
}
/**
* Update grid CSS variables
*/
private updateGridStyles(): void {
const root = document.documentElement;
const config = calendarConfig.getAll();
// Set CSS variables
root.style.setProperty('--hour-height', `${config.hourHeight}px`);
root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`);
root.style.setProperty('--snap-interval', config.snapInterval.toString());
root.style.setProperty('--day-start-hour', config.dayStartHour.toString());
root.style.setProperty('--day-end-hour', config.dayEndHour.toString());
root.style.setProperty('--work-start-hour', config.workStartHour.toString());
root.style.setProperty('--work-end-hour', config.workEndHour.toString());
}
/**
* Setup grid interaction handlers for POC structure
*/
private setupGridInteractions(): void {
if (!this.grid) return;
// Click handler for day columns
this.grid.addEventListener('click', (e: MouseEvent) => {
// Ignore if clicking on an event
if ((e.target as Element).closest('swp-event')) return;
const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement;
if (!dayColumn) return;
const position = this.getClickPosition(e, dayColumn);
eventBus.emit(EventTypes.GRID_CLICK, {
date: (dayColumn as any).dataset.date,
time: position.time,
minutes: position.minutes
});
});
// Double click handler for day columns
this.grid.addEventListener('dblclick', (e: MouseEvent) => {
// Ignore if clicking on an event
if ((e.target as Element).closest('swp-event')) return;
const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement;
if (!dayColumn) return;
const position = this.getClickPosition(e, dayColumn);
eventBus.emit(EventTypes.GRID_DBLCLICK, {
date: (dayColumn as any).dataset.date,
time: position.time,
minutes: position.minutes
});
});
}
/**
* Get click position in day column (POC structure)
*/
private getClickPosition(event: MouseEvent, dayColumn: HTMLElement): GridPosition {
const rect = dayColumn.getBoundingClientRect();
const y = event.clientY - rect.top;
const hourHeight = calendarConfig.get('hourHeight');
const minuteHeight = hourHeight / 60;
const snapInterval = calendarConfig.get('snapInterval');
const dayStartHour = calendarConfig.get('dayStartHour');
// Calculate total minutes from day start
let totalMinutes = Math.floor(y / minuteHeight);
// Snap to interval
totalMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
// Add day start offset
totalMinutes += dayStartHour * 60;
return {
minutes: totalMinutes,
time: this.minutesToTime(totalMinutes),
y: y
};
}
/**
* Scroll to specific hour
*/
scrollToHour(hour: number): void {
if (!this.grid) return;
const hourHeight = calendarConfig.get('hourHeight');
const dayStartHour = calendarConfig.get('dayStartHour');
const headerHeight = 80; // Header row height
const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight);
this.grid.scrollTop = scrollTop;
}
/**
* Utility methods
*/
private formatHour(hour: number): string {
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
return `${displayHour} ${period}`;
}
private formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
private getDayName(date: Date): string {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[date.getDay()];
}
private getWeekDates(weekStart: Date): Date[] {
const dates: Date[] = [];
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(weekStart.getDate() + i);
dates.push(date);
}
return dates;
}
private isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
private minutesToTime(totalMinutes: number): string {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
}
}