Improves grid layout and navigation

Refactors the calendar grid to support smoother navigation transitions by using separate week containers.

This change introduces a GridManager to handle grid rendering and interactions within these containers, enabling scroll synchronization and click event handling for each week view.

Also, configures the calendar to start at midnight and span a full 24-hour day, providing a more comprehensive view.
This commit is contained in:
Janus Knudsen 2025-07-25 00:24:15 +02:00
parent f06c02121c
commit b443649ced
12 changed files with 719 additions and 302 deletions

View file

@ -33,6 +33,7 @@ export class GridManager {
private init(): void {
this.findElements();
this.subscribeToEvents();
this.setupScrollSync();
}
private findElements(): void {
@ -64,6 +65,19 @@ export class GridManager {
this.renderHeaders();
});
// 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 new week container creation
eventBus.on(EventTypes.WEEK_CONTAINER_CREATED, (e: Event) => {
const detail = (e as CustomEvent).detail;
this.renderGridForContainer(detail.container, detail.weekStart);
});
// Handle grid clicks
this.setupGridInteractions();
}
@ -345,4 +359,228 @@ export class GridManager {
this.scrollableContent.scrollTop = scrollTop;
}
/**
* Render grid for a specific container (used during navigation transitions)
*/
private renderGridForContainer(container: HTMLElement, weekStart: Date): void {
// Find the week header and scrollable content within this container
const weekHeader = container.querySelector('swp-week-header');
const scrollableContent = container.querySelector('swp-scrollable-content');
const timeGrid = container.querySelector('swp-time-grid');
if (!weekHeader || !scrollableContent || !timeGrid) {
console.warn('GridManager: Required elements not found in container');
return;
}
// Render week header for this container
this.renderWeekHeaderForContainer(weekHeader as HTMLElement, weekStart);
// Render grid content for this container - pass weekStart
this.renderGridForSpecificContainer(container, weekStart);
this.renderGridLinesForContainer(timeGrid as HTMLElement);
this.setupGridInteractionsForContainer(container);
// Setup scroll sync for this new container
this.setupScrollSyncForContainer(scrollableContent as HTMLElement);
}
/**
* Render week header for a specific container
*/
private renderWeekHeaderForContainer(weekHeader: HTMLElement, weekStart: Date): void {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
weekHeader.innerHTML = '';
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(date.getDate() + i);
const header = document.createElement('swp-day-header');
if (this.isToday(date)) {
(header as any).dataset.today = 'true';
}
header.innerHTML = `
<swp-day-name>${days[date.getDay()]}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
(header as any).dataset.date = this.formatDate(date);
weekHeader.appendChild(header);
}
}
/**
* Render grid structure for a specific container
*/
private renderGridForSpecificContainer(container: HTMLElement, weekStart?: Date): void {
const timeGrid = container.querySelector('swp-time-grid');
if (!timeGrid) {
console.warn('GridManager: No time-grid found in container');
return;
}
// Use the weekStart parameter or fall back to currentWeek
const targetWeek = weekStart || this.currentWeek;
if (!targetWeek) {
console.warn('GridManager: No target week available');
return;
}
// Clear existing columns
let dayColumns = timeGrid.querySelector('swp-day-columns');
if (!dayColumns) {
dayColumns = document.createElement('swp-day-columns');
timeGrid.appendChild(dayColumns);
}
dayColumns.innerHTML = '';
const view = calendarConfig.get('view');
const columnsCount = view === 'week' ? calendarConfig.get('weekDays') : 1;
// Create columns using the target week
for (let i = 0; i < columnsCount; i++) {
const column = document.createElement('swp-day-column');
(column as any).dataset.columnIndex = i;
const dates = this.getWeekDates(targetWeek);
if (dates[i]) {
(column as any).dataset.date = this.formatDate(dates[i]);
}
// Add events container
const eventsLayer = document.createElement('swp-events-layer');
column.appendChild(eventsLayer);
dayColumns.appendChild(column);
}
// Update grid styles for this container
const totalHeight = calendarConfig.totalHours * calendarConfig.get('hourHeight');
(timeGrid as HTMLElement).style.height = `${totalHeight}px`;
}
/**
* Render grid lines for a specific time grid
*/
private renderGridLinesForContainer(timeGrid: HTMLElement): void {
let gridLines = timeGrid.querySelector('swp-grid-lines');
if (!gridLines) {
gridLines = document.createElement('swp-grid-lines');
timeGrid.insertBefore(gridLines, timeGrid.firstChild);
}
const totalHours = calendarConfig.totalHours;
const hourHeight = calendarConfig.get('hourHeight');
// Set CSS variables
timeGrid.style.setProperty('--total-hours', totalHours.toString());
timeGrid.style.setProperty('--hour-height', `${hourHeight}px`);
}
/**
* Setup grid interactions for a specific container
*/
private setupGridInteractionsForContainer(container: HTMLElement): void {
const timeGrid = container.querySelector('swp-time-grid');
if (!timeGrid) return;
// Click handler
timeGrid.addEventListener('click', (e: Event) => {
const mouseEvent = e as MouseEvent;
// Ignore if clicking on an event
if ((mouseEvent.target as Element).closest('swp-event')) return;
const column = (mouseEvent.target as Element).closest('swp-day-column') as HTMLElement;
if (!column) return;
const position = this.getClickPositionForContainer(mouseEvent, column, container);
eventBus.emit(EventTypes.GRID_CLICK, {
date: (column as any).dataset.date,
time: position.time,
minutes: position.minutes,
columnIndex: parseInt((column as any).dataset.columnIndex)
});
});
// Double click handler
timeGrid.addEventListener('dblclick', (e: Event) => {
const mouseEvent = e as MouseEvent;
// Ignore if clicking on an event
if ((mouseEvent.target as Element).closest('swp-event')) return;
const column = (mouseEvent.target as Element).closest('swp-day-column') as HTMLElement;
if (!column) return;
const position = this.getClickPositionForContainer(mouseEvent, column, container);
eventBus.emit(EventTypes.GRID_DBLCLICK, {
date: (column as any).dataset.date,
time: position.time,
minutes: position.minutes,
columnIndex: parseInt((column as any).dataset.columnIndex)
});
});
}
/**
* Get click position for a specific container
*/
private getClickPositionForContainer(event: MouseEvent, column: HTMLElement, container: HTMLElement): GridPosition {
const rect = column.getBoundingClientRect();
const scrollableContent = container.querySelector('swp-scrollable-content') as HTMLElement;
const y = event.clientY - rect.top + (scrollableContent?.scrollTop || 0);
const minuteHeight = calendarConfig.minuteHeight;
const snapInterval = calendarConfig.get('snapInterval');
const dayStartHour = calendarConfig.get('dayStartHour');
// Calculate minutes from start of day
let minutes = Math.floor(y / minuteHeight);
// Snap to interval
minutes = Math.round(minutes / snapInterval) * snapInterval;
// Add day start offset
const totalMinutes = (dayStartHour * 60) + minutes;
return {
minutes: totalMinutes,
time: this.minutesToTime(totalMinutes),
y: minutes * minuteHeight
};
}
/**
* Setup scroll synchronization between time-axis and scrollable content
*/
private setupScrollSync(): void {
if (!this.scrollableContent || !this.timeAxis) return;
// Sync time-axis scroll with scrollable content
this.scrollableContent.addEventListener('scroll', () => {
if (this.timeAxis) {
this.timeAxis.scrollTop = this.scrollableContent!.scrollTop;
}
});
}
/**
* Setup scroll synchronization for a specific container's scrollable content
*/
private setupScrollSyncForContainer(scrollableContent: HTMLElement): void {
if (!this.timeAxis) return;
// Sync time-axis scroll with this container's scrollable content
scrollableContent.addEventListener('scroll', () => {
if (this.timeAxis) {
this.timeAxis.scrollTop = scrollableContent.scrollTop;
}
});
}
}

View file

@ -105,15 +105,15 @@ export class NavigationManager {
}
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
const container = document.querySelector('swp-calendar-container');
const calendarContainer = document.querySelector('swp-calendar-container');
const currentWeekContainer = document.querySelector('swp-week-container');
if (!container || !currentWeekContainer) {
if (!calendarContainer || !currentWeekContainer) {
console.warn('NavigationManager: Required DOM elements not found');
return;
}
// Create new week container
// Create new week container (following POC structure)
const newWeekContainer = document.createElement('swp-week-container');
newWeekContainer.innerHTML = `
<swp-week-header></swp-week-header>
@ -133,8 +133,8 @@ export class NavigationManager {
newWeekContainer.style.height = '100%';
newWeekContainer.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)';
// Add to container
container.appendChild(newWeekContainer);
// Add to calendar container
calendarContainer.appendChild(newWeekContainer);
// Notify other managers to render content for the new week
this.eventBus.emit(EventTypes.WEEK_CONTAINER_CREATED, {