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:
parent
f06c02121c
commit
b443649ced
12 changed files with 719 additions and 302 deletions
|
|
@ -27,8 +27,8 @@ export class CalendarConfig {
|
|||
firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday
|
||||
|
||||
// Time settings
|
||||
dayStartHour: 7, // Calendar starts at 7 AM
|
||||
dayEndHour: 19, // Calendar ends at 7 PM
|
||||
dayStartHour: 0, // Calendar starts at midnight
|
||||
dayEndHour: 24, // Calendar ends at midnight (24 hours)
|
||||
workStartHour: 8, // Work hours start
|
||||
workEndHour: 17, // Work hours end
|
||||
snapInterval: 15, // Minutes: 5, 10, 15, 30, 60
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { NavigationManager } from './managers/NavigationManager.js';
|
|||
import { ViewManager } from './managers/ViewManager.js';
|
||||
import { EventManager } from './managers/EventManager.js';
|
||||
import { EventRenderer } from './managers/EventRenderer.js';
|
||||
import { GridManager } from './managers/GridManager.js';
|
||||
import { CalendarConfig } from './core/CalendarConfig.js';
|
||||
|
||||
/**
|
||||
|
|
@ -22,6 +23,7 @@ function initializeCalendar(): void {
|
|||
const viewManager = new ViewManager(eventBus);
|
||||
const eventManager = new EventManager(eventBus);
|
||||
const eventRenderer = new EventRenderer(eventBus);
|
||||
const gridManager = new GridManager();
|
||||
|
||||
// Enable debug mode for development
|
||||
eventBus.setDebug(true);
|
||||
|
|
@ -38,7 +40,8 @@ function initializeCalendar(): void {
|
|||
navigationManager,
|
||||
viewManager,
|
||||
eventManager,
|
||||
eventRenderer
|
||||
eventRenderer,
|
||||
gridManager
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
218
src/utils/PositionUtils.js
Normal file
218
src/utils/PositionUtils.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* PositionUtils - Utility functions for converting between pixels and minutes/hours in the calendar
|
||||
* This module provides essential conversion functions for positioning events and calculating dimensions
|
||||
*/
|
||||
|
||||
import { calendarConfig } from '../core/CalendarConfig.js';
|
||||
|
||||
export class PositionUtils {
|
||||
/**
|
||||
* Convert minutes to pixels based on the current time scale
|
||||
* @param {number} minutes - Number of minutes to convert
|
||||
* @returns {number} Pixel value
|
||||
*/
|
||||
static minutesToPixels(minutes) {
|
||||
return minutes * calendarConfig.minuteHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pixels to minutes based on the current time scale
|
||||
* @param {number} pixels - Number of pixels to convert
|
||||
* @returns {number} Minutes value
|
||||
*/
|
||||
static pixelsToMinutes(pixels) {
|
||||
return pixels / calendarConfig.minuteHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a time string (HH:MM) to minutes from start of day
|
||||
* @param {string} timeString - Time in format "HH:MM"
|
||||
* @returns {number} Minutes from start of day
|
||||
*/
|
||||
static timeStringToMinutes(timeString) {
|
||||
const [hours, minutes] = timeString.split(':').map(Number);
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert minutes from start of day to time string (HH:MM)
|
||||
* @param {number} minutes - Minutes from start of day
|
||||
* @returns {string} Time in format "HH:MM"
|
||||
*/
|
||||
static minutesToTimeString(minutes) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the pixel position for a given time
|
||||
* @param {string|number} time - Time as string "HH:MM" or minutes from start of day
|
||||
* @returns {number} Pixel position from top of calendar
|
||||
*/
|
||||
static getPixelPositionForTime(time) {
|
||||
const startHour = calendarConfig.get('dayStartHour');
|
||||
|
||||
let minutes;
|
||||
if (typeof time === 'string') {
|
||||
minutes = this.timeStringToMinutes(time);
|
||||
} else {
|
||||
minutes = time;
|
||||
}
|
||||
|
||||
// Subtract start hour offset
|
||||
const adjustedMinutes = minutes - (startHour * 60);
|
||||
|
||||
return this.minutesToPixels(adjustedMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the time for a given pixel position
|
||||
* @param {number} pixelPosition - Pixel position from top of calendar
|
||||
* @returns {number} Minutes from start of day
|
||||
*/
|
||||
static getTimeForPixelPosition(pixelPosition) {
|
||||
const startHour = calendarConfig.get('dayStartHour');
|
||||
|
||||
const minutes = this.pixelsToMinutes(pixelPosition);
|
||||
|
||||
// Add start hour offset
|
||||
return minutes + (startHour * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate event height based on duration
|
||||
* @param {number} durationMinutes - Duration in minutes
|
||||
* @returns {number} Height in pixels
|
||||
*/
|
||||
static getEventHeight(durationMinutes) {
|
||||
return this.minutesToPixels(durationMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate event duration based on height
|
||||
* @param {number} heightPixels - Height in pixels
|
||||
* @returns {number} Duration in minutes
|
||||
*/
|
||||
static getEventDuration(heightPixels) {
|
||||
return this.pixelsToMinutes(heightPixels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pixel position for a specific day column
|
||||
* @param {number} dayIndex - Day index (0 = Monday, 6 = Sunday)
|
||||
* @returns {number} Pixel position from left
|
||||
*/
|
||||
static getDayColumnPosition(dayIndex) {
|
||||
// These values should be calculated based on actual calendar layout
|
||||
const timeAxisWidth = 60; // Default time axis width
|
||||
const calendarElement = document.querySelector('swp-calendar-content');
|
||||
const dayColumnWidth = calendarElement ?
|
||||
(calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') :
|
||||
120; // Default day column width
|
||||
|
||||
return timeAxisWidth + (dayIndex * dayColumnWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the day index for a given pixel position
|
||||
* @param {number} pixelPosition - Pixel position from left
|
||||
* @returns {number} Day index (0-6) or -1 if outside day columns
|
||||
*/
|
||||
static getDayIndexForPosition(pixelPosition) {
|
||||
const timeAxisWidth = 60; // Default time axis width
|
||||
const calendarElement = document.querySelector('swp-calendar-content');
|
||||
const dayColumnWidth = calendarElement ?
|
||||
(calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') :
|
||||
120; // Default day column width
|
||||
|
||||
if (pixelPosition < timeAxisWidth) {
|
||||
return -1; // In time axis area
|
||||
}
|
||||
|
||||
const dayPosition = pixelPosition - timeAxisWidth;
|
||||
const dayIndex = Math.floor(dayPosition / dayColumnWidth);
|
||||
|
||||
return dayIndex >= 0 && dayIndex < calendarConfig.get('weekDays') ? dayIndex : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the bounds for an event element
|
||||
* @param {Object} eventData - Event data with startTime, endTime, and day
|
||||
* @returns {Object} Bounds object with top, left, width, height
|
||||
*/
|
||||
static getEventBounds(eventData) {
|
||||
const startMinutes = typeof eventData.startTime === 'string'
|
||||
? this.timeStringToMinutes(eventData.startTime)
|
||||
: eventData.startTime;
|
||||
|
||||
const endMinutes = typeof eventData.endTime === 'string'
|
||||
? this.timeStringToMinutes(eventData.endTime)
|
||||
: eventData.endTime;
|
||||
|
||||
const duration = endMinutes - startMinutes;
|
||||
|
||||
const calendarElement = document.querySelector('swp-calendar-content');
|
||||
const timeAxisWidth = 60; // Default time axis width
|
||||
const dayColumnWidth = calendarElement ?
|
||||
(calendarElement.clientWidth - timeAxisWidth) / calendarConfig.get('weekDays') :
|
||||
120; // Default day column width
|
||||
|
||||
return {
|
||||
top: this.getPixelPositionForTime(startMinutes),
|
||||
left: this.getDayColumnPosition(eventData.day),
|
||||
width: dayColumnWidth,
|
||||
height: this.getEventHeight(duration)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a pixel position is within the visible time range
|
||||
* @param {number} pixelPosition - Pixel position from top
|
||||
* @returns {boolean} True if within visible range
|
||||
*/
|
||||
static isWithinVisibleTimeRange(pixelPosition) {
|
||||
const startHour = calendarConfig.get('dayStartHour');
|
||||
const endHour = calendarConfig.get('dayEndHour');
|
||||
|
||||
const minutes = this.getTimeForPixelPosition(pixelPosition);
|
||||
const hours = minutes / 60;
|
||||
|
||||
return hours >= startHour && hours <= endHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a pixel position to the visible time range
|
||||
* @param {number} pixelPosition - Pixel position from top
|
||||
* @returns {number} Clamped pixel position
|
||||
*/
|
||||
static clampToVisibleTimeRange(pixelPosition) {
|
||||
const startHour = calendarConfig.get('dayStartHour');
|
||||
const endHour = calendarConfig.get('dayEndHour');
|
||||
|
||||
const minPosition = this.getPixelPositionForTime(startHour * 60);
|
||||
const maxPosition = this.getPixelPositionForTime(endHour * 60);
|
||||
|
||||
return Math.max(minPosition, Math.min(maxPosition, pixelPosition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total height of the calendar content area
|
||||
* @returns {number} Total height in pixels
|
||||
*/
|
||||
static getTotalCalendarHeight() {
|
||||
return calendarConfig.get('hourHeight') * calendarConfig.totalHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a pixel position to the nearest time interval
|
||||
* @param {number} pixelPosition - Pixel position to round
|
||||
* @param {number} intervalMinutes - Interval in minutes (default: 15)
|
||||
* @returns {number} Rounded pixel position
|
||||
*/
|
||||
static roundToTimeInterval(pixelPosition, intervalMinutes = 15) {
|
||||
const minutes = this.getTimeForPixelPosition(pixelPosition);
|
||||
const roundedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
|
||||
return this.getPixelPositionForTime(roundedMinutes);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue