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.
430 lines
No EOL
13 KiB
TypeScript
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}`;
|
|
}
|
|
} |