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
24
Calendar Plantempus.sln
Normal file
24
Calendar Plantempus.sln
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalendarServer", "CalendarServer.csproj", "{012B9532-C22E-001C-4A02-5B97A6446613}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{012B9532-C22E-001C-4A02-5B97A6446613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{012B9532-C22E-001C-4A02-5B97A6446613}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{012B9532-C22E-001C-4A02-5B97A6446613}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{012B9532-C22E-001C-4A02-5B97A6446613}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {84660DE0-41AA-4852-B51E-EBA1072CC7A4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
--snap-interval: 15;
|
||||
|
||||
/* Time boundaries */
|
||||
--day-start-hour: 7;
|
||||
--day-end-hour: 19;
|
||||
--day-start-hour: 0;
|
||||
--day-end-hour: 24;
|
||||
--work-start-hour: 8;
|
||||
--work-end-hour: 17;
|
||||
|
||||
|
|
@ -94,9 +94,26 @@ swp-day-columns,
|
|||
swp-day-column,
|
||||
swp-events-layer,
|
||||
swp-event,
|
||||
swp-allday-container,
|
||||
swp-loading-overlay,
|
||||
swp-event-popup {
|
||||
swp-week-container,
|
||||
swp-grid-lines,
|
||||
swp-nav-group,
|
||||
swp-nav-button,
|
||||
swp-search-container,
|
||||
swp-search-icon,
|
||||
swp-search-clear,
|
||||
swp-view-selector,
|
||||
swp-view-button,
|
||||
swp-week-info,
|
||||
swp-week-number,
|
||||
swp-date-range,
|
||||
swp-day-header,
|
||||
swp-day-name,
|
||||
swp-day-date,
|
||||
swp-hour-marker,
|
||||
swp-event-time,
|
||||
swp-event-title,
|
||||
swp-spinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,5 @@
|
|||
/* styles/components/navigation.css */
|
||||
|
||||
/* Navigation bar */
|
||||
swp-calendar-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 12px 16px;
|
||||
background: var(--color-background);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Navigation groups */
|
||||
swp-nav-group {
|
||||
display: flex;
|
||||
|
|
@ -181,4 +170,23 @@ swp-calendar[data-searching="true"] {
|
|||
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Week info display */
|
||||
swp-week-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
swp-week-number {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
swp-date-range {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
|
@ -6,12 +6,11 @@ swp-event {
|
|||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
z-index: var(--z-event);
|
||||
|
||||
/* CSS-based positioning */
|
||||
top: calc(var(--start-minutes) * var(--minute-height));
|
||||
height: calc(var(--duration-minutes) * var(--minute-height));
|
||||
transition: box-shadow 150ms ease, transform 150ms ease;
|
||||
z-index: 10;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
padding: 8px;
|
||||
|
||||
/* Event types */
|
||||
&[data-type="meeting"] {
|
||||
|
|
@ -34,110 +33,26 @@ swp-event {
|
|||
border-left: 4px solid var(--color-event-milestone-border);
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: scale(1.02);
|
||||
z-index: var(--z-event-hover);
|
||||
|
||||
swp-resize-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Active/selected state */
|
||||
&[data-selected="true"] {
|
||||
box-shadow: 0 0 0 2px var(--color-primary);
|
||||
z-index: var(--z-event-hover);
|
||||
}
|
||||
|
||||
/* Dragging state */
|
||||
&[data-dragging="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing;
|
||||
z-index: var(--z-drag-ghost);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resizing state */
|
||||
&[data-resizing="true"] {
|
||||
opacity: 0.8;
|
||||
|
||||
swp-resize-handle {
|
||||
opacity: 1;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Sync status indicators */
|
||||
&[data-sync-status="pending"] {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-warning);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-sync-status="error"] {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-danger);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Event header */
|
||||
swp-event-header {
|
||||
padding: 8px 12px 4px;
|
||||
|
||||
swp-event-time {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
}
|
||||
swp-event:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: scale(1.02);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* Event body */
|
||||
swp-event-body {
|
||||
padding: 0 12px 8px;
|
||||
|
||||
swp-event-title {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
/* Multi-line ellipsis */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
swp-event-time {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
swp-event-title {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Resize handles */
|
||||
|
|
|
|||
|
|
@ -9,24 +9,89 @@ swp-calendar {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
/* Calendar container grid */
|
||||
/* Navigation bar layout */
|
||||
swp-calendar-nav {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--color-background);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Calendar container grid (following POC structure) */
|
||||
swp-calendar-container {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Time axis (left side) */
|
||||
/* Time axis (fixed, left side) */
|
||||
swp-time-axis {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
grid-row: 1;
|
||||
background: var(--color-surface);
|
||||
border-right: 1px solid var(--color-border);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
padding-top: 80px; /* Match header height */
|
||||
overflow-y: hidden; /* Hide scrollbar but allow programmatic scrolling */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Week container for sliding */
|
||||
swp-week-container {
|
||||
grid-column: 2;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Week header (inside week container) */
|
||||
swp-week-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
height: 80px; /* Fixed height */
|
||||
}
|
||||
|
||||
/* Scrollable content */
|
||||
swp-scrollable-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0; /* Important for flex children to shrink */
|
||||
max-height: calc(100vh - 80px - 80px); /* Subtract nav height and week-header height */
|
||||
}
|
||||
|
||||
swp-week-container.slide-out-left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
swp-week-container.slide-out-right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
swp-week-container.slide-in-left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
swp-week-container.slide-in-right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
swp-hour-marker {
|
||||
|
|
@ -51,66 +116,46 @@ swp-hour-marker {
|
|||
}
|
||||
}
|
||||
|
||||
/* Week header */
|
||||
swp-week-header {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--week-days, 7), 1fr);
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
/* Day header styling (inside week-header) */
|
||||
|
||||
swp-day-header {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--color-grid-line);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
swp-day-name {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
swp-day-date {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Today indicator */
|
||||
&[data-today="true"] {
|
||||
swp-day-date {
|
||||
color: var(--color-primary);
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px auto 0;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Scrollable content */
|
||||
swp-scrollable-content {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
swp-day-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
swp-day-name {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
swp-day-date {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
swp-day-header[data-today="true"] swp-day-date {
|
||||
color: var(--color-primary);
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px auto 0;
|
||||
}
|
||||
|
||||
/* All-day events container */
|
||||
|
|
@ -130,19 +175,18 @@ swp-allday-container {
|
|||
/* Time grid */
|
||||
swp-time-grid {
|
||||
position: relative;
|
||||
height: calc(var(--total-hours, 12) * var(--hour-height));
|
||||
|
||||
/* Work hours background */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height));
|
||||
height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-work-hours);
|
||||
pointer-events: none;
|
||||
}
|
||||
height: calc(24 * var(--hour-height));
|
||||
}
|
||||
|
||||
swp-time-grid::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc((var(--work-start-hour) - var(--day-start-hour)) * var(--hour-height));
|
||||
height: calc((var(--work-end-hour) - var(--work-start-hour)) * var(--hour-height));
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-work-hours);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Grid lines */
|
||||
|
|
@ -150,26 +194,39 @@ swp-grid-lines {
|
|||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
/* 15-minute intervals */
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
transparent calc(var(--hour-height) / 4 - 1px),
|
||||
var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px),
|
||||
var(--color-grid-line-light) calc(var(--hour-height) / 4)
|
||||
);
|
||||
|
||||
/* Show stronger lines when dragging */
|
||||
&[data-dragging="true"] {
|
||||
background-image: repeating-linear-gradient(
|
||||
z-index: var(--z-grid);
|
||||
background-image:
|
||||
/* Hour lines (stronger) */
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
transparent calc(var(--hour-height) - 1px),
|
||||
var(--color-grid-line) calc(var(--hour-height) - 1px),
|
||||
var(--color-grid-line) var(--hour-height)
|
||||
),
|
||||
/* Quarter hour lines (lighter) */
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
transparent calc(var(--hour-height) / 4 - 1px),
|
||||
rgba(33, 150, 243, 0.2) calc(var(--hour-height) / 4 - 1px),
|
||||
rgba(33, 150, 243, 0.2) calc(var(--hour-height) / 4)
|
||||
var(--color-grid-line-light) calc(var(--hour-height) / 4 - 1px),
|
||||
var(--color-grid-line-light) calc(var(--hour-height) / 4)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure grid lines are visible during transitions */
|
||||
swp-week-container swp-grid-lines {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Grid lines should remain visible even during animations */
|
||||
swp-week-container.slide-out-left swp-grid-lines,
|
||||
swp-week-container.slide-out-right swp-grid-lines,
|
||||
swp-week-container.slide-in-left swp-grid-lines,
|
||||
swp-week-container.slide-in-right swp-grid-lines {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Day columns */
|
||||
|
|
@ -177,42 +234,21 @@ swp-day-columns {
|
|||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--week-days, 7), 1fr);
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
swp-day-column {
|
||||
position: relative;
|
||||
border-right: 1px solid var(--color-grid-line);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Hover effect for empty slots */
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
/* Events layer */
|
||||
swp-day-column:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
swp-events-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
/* Layout modes */
|
||||
&[data-layout="overlap"] {
|
||||
swp-event {
|
||||
width: calc(100% - 16px);
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-layout="side-by-side"] {
|
||||
swp-event {
|
||||
width: calc(var(--event-width, 100%) - 16px);
|
||||
left: calc(8px + var(--event-offset, 0px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Current time indicator */
|
||||
|
|
|
|||
|
|
@ -130,23 +130,27 @@ swp-loading-overlay {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-loading);
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
swp-loading-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
swp-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-surface);
|
||||
border-top-color: var(--color-primary);
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Snap indicator */
|
||||
swp-snap-indicator {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -5,58 +5,12 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calendar Plantempus - Week View</title>
|
||||
|
||||
<!-- CSS Files -->
|
||||
<link rel="stylesheet" href="css/calendar.css">
|
||||
|
||||
<!-- Additional styles for view selector -->
|
||||
<style>
|
||||
swp-view-selector {
|
||||
display: flex;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
swp-view-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
swp-view-button:not(:last-child) {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
swp-view-button[data-active="true"] {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Week info display */
|
||||
swp-week-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
justify-self: start;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
swp-week-number {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
swp-date-range {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
<!-- Modular CSS Files -->
|
||||
<link rel="stylesheet" href="css/calendar-base-css.css">
|
||||
<link rel="stylesheet" href="css/calendar-layout-css.css">
|
||||
<link rel="stylesheet" href="css/calendar-components-css.css">
|
||||
<link rel="stylesheet" href="css/calendar-events-css.css">
|
||||
<link rel="stylesheet" href="css/calendar-popup-css.css">
|
||||
</head>
|
||||
<body>
|
||||
<swp-calendar data-view="week" data-week-days="7" data-snap-interval="15">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue