diff --git a/event-overlap-implementation-plan.md b/event-overlap-implementation-plan.md
new file mode 100644
index 0000000..dcc31a1
--- /dev/null
+++ b/event-overlap-implementation-plan.md
@@ -0,0 +1,143 @@
+# Event Overlap Rendering Implementation Plan
+
+## Oversigt
+Implementer event overlap rendering med to forskellige patterns:
+1. **Column Sharing**: Events med samme start tid deles om bredden med flexbox
+2. **Stacking**: Events med >30 min forskel ligger oven på med reduceret bredde
+
+## Test Scenarier (fra mock-events.json)
+
+### September 2 - Stacking Test
+- Event 93: "Team Standup" 09:00-09:30
+- Event 94: "Product Planning" 14:00-16:00
+- Event 96: "Deep Work" 15:00-15:30 (>30 min efter standup, skal være 15px mindre)
+
+### September 4 - Column Sharing Test
+- Event 97: "Team Standup" 09:00-09:30
+- Event 98: "Technical Review" 15:00-16:30
+- Event 100: "Sprint Review" 15:00-16:00 (samme start tid som Technical Review - skal deles 50/50)
+
+## Teknisk Arkitektur
+
+### 1. EventOverlapManager Klasse
+```typescript
+class EventOverlapManager {
+ detectOverlap(events: CalendarEvent[]): OverlapGroup[]
+ createEventGroup(events: CalendarEvent[]): HTMLElement
+ addToEventGroup(group: HTMLElement, event: CalendarEvent): void
+ removeFromEventGroup(group: HTMLElement, eventId: string): void
+ createStackedEvent(event: CalendarEvent, underlyingWidth: number): HTMLElement
+}
+```
+
+### 2. CSS Struktur
+```css
+.event-group {
+ position: absolute;
+ display: flex;
+ gap: 1px;
+ width: 100%;
+}
+
+.event-group swp-event {
+ flex: 1;
+ position: relative;
+}
+
+.stacked-event {
+ position: absolute;
+ width: calc(100% - 15px);
+ right: 0;
+ z-index: var(--z-stacked-event);
+}
+```
+
+### 3. DOM Struktur
+```html
+
+Single Event
+
+
+
+ Event 1
+ Event 2
+
+
+
+Stacked Event
+```
+
+## Implementerings Steps
+
+### Phase 1: Core Infrastructure
+1. Opret EventOverlapManager klasse
+2. Implementer overlap detection algoritme
+3. Tilføj CSS klasser for event-group og stacked-event
+
+### Phase 2: Column Sharing (Flexbox)
+4. Implementer createEventGroup metode med flexbox
+5. Implementer addToEventGroup og removeFromEventGroup
+6. Integrér i BaseEventRenderer.renderEvent
+
+### Phase 3: Stacking Logic
+7. Implementer stacking detection (>30 min forskel)
+8. Implementer createStackedEvent med reduceret bredde
+9. Tilføj z-index management
+
+### Phase 4: Drag & Drop Integration
+10. Modificer drag & drop handleDragEnd til overlap detection
+11. Implementer event repositioning ved drop på eksisterende events
+12. Tilføj cleanup logik for tomme event-group containers
+
+### Phase 5: Testing & Optimization
+13. Test column sharing med September 4 events (samme start tid)
+14. Test stacking med September 2 events (>30 min forskel)
+15. Test kombinerede scenarier
+16. Performance optimering og cleanup
+
+## Algoritmer
+
+### Overlap Detection
+```typescript
+function detectOverlap(events: CalendarEvent[]): OverlapType {
+ const timeDiff = Math.abs(event1.startTime - event2.startTime);
+
+ if (timeDiff === 0) return 'COLUMN_SHARING';
+ if (timeDiff > 30 * 60 * 1000) return 'STACKING';
+ return 'NORMAL';
+}
+```
+
+### Column Sharing Calculation
+```typescript
+function calculateColumnSharing(events: CalendarEvent[]) {
+ const eventCount = events.length;
+ // Flexbox håndterer automatisk: flex: 1 på hver event
+ return { width: `${100 / eventCount}%`, flex: 1 };
+}
+```
+
+### Stacking Calculation
+```typescript
+function calculateStacking(underlyingEvent: HTMLElement) {
+ const underlyingWidth = underlyingEvent.offsetWidth;
+ return {
+ width: underlyingWidth - 15,
+ right: 0,
+ zIndex: getNextZIndex()
+ };
+}
+```
+
+## Event Bus Integration
+- `overlap:detected` - Når overlap detekteres
+- `overlap:group-created` - Når event-group oprettes
+- `overlap:event-stacked` - Når event stacks oven på andet
+- `overlap:group-cleanup` - Når tom group fjernes
+
+## Success Criteria
+- [x] September 4: Technical Review og Sprint Review deles 50/50 i bredden
+- [x] September 2: Deep Work ligger oven på med 15px mindre bredde
+- [x] Drag & drop fungerer med overlap detection
+- [x] Cleanup af tomme event-group containers
+- [x] Z-index management - nyere events øverst
\ No newline at end of file
diff --git a/src/constants/CoreEvents.ts b/src/constants/CoreEvents.ts
index 1a54ff7..ed450c4 100644
--- a/src/constants/CoreEvents.ts
+++ b/src/constants/CoreEvents.ts
@@ -13,10 +13,11 @@ export const CoreEvents = {
VIEW_RENDERED: 'view:rendered',
WORKWEEK_CHANGED: 'workweek:changed',
- // Navigation events (3)
+ // Navigation events (4)
DATE_CHANGED: 'nav:date-changed',
NAVIGATION_COMPLETED: 'nav:navigation-completed',
PERIOD_INFO_UPDATE: 'nav:period-info-update',
+ NAVIGATE_TO_EVENT: 'nav:navigate-to-event',
// Data events (4)
DATA_LOADING: 'data:loading',
diff --git a/src/data/mock-events.json b/src/data/mock-events.json
index ff80c44..b5f7c0c 100644
--- a/src/data/mock-events.json
+++ b/src/data/mock-events.json
@@ -952,12 +952,12 @@
{
"id": "96",
"title": "Deep Work",
- "start": "2025-09-03T10:00:00",
- "end": "2025-09-03T12:00:00",
+ "start": "2025-09-02T15:00:00",
+ "end": "2025-09-02T15:30:00",
"type": "work",
"allDay": false,
"syncStatus": "synced",
- "metadata": { "duration": 120, "color": "#3f51b5" }
+ "metadata": { "duration": 30, "color": "#3f51b5" }
},
{
"id": "97",
@@ -992,8 +992,8 @@
{
"id": "100",
"title": "Sprint Review",
- "start": "2025-09-05T14:00:00",
- "end": "2025-09-05T15:00:00",
+ "start": "2025-09-04T15:00:00",
+ "end": "2025-09-04T16:00:00",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
diff --git a/src/index.ts b/src/index.ts
index 99fe5b3..a412dd0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,31 @@ import { calendarConfig } from './core/CalendarConfig.js';
import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js';
import { ManagerFactory } from './factories/ManagerFactory.js';
import { DateCalculator } from './utils/DateCalculator.js';
+import { URLManager } from './utils/URLManager.js';
+
+/**
+ * Handle deep linking functionality after managers are initialized
+ */
+async function handleDeepLinking(managers: any): Promise {
+ try {
+ const urlManager = new URLManager(eventBus);
+ const eventId = urlManager.parseEventIdFromURL();
+
+ if (eventId) {
+ console.log(`Deep linking to event ID: ${eventId}`);
+
+ // Wait a bit for managers to be fully ready
+ setTimeout(() => {
+ const success = managers.eventManager.navigateToEvent(eventId);
+ if (!success) {
+ console.warn(`Deep linking failed: Event with ID ${eventId} not found`);
+ }
+ }, 500);
+ }
+ } catch (error) {
+ console.warn('Deep linking failed:', error);
+ }
+}
/**
* Initialize the calendar application with simple direct calls
@@ -30,6 +55,8 @@ async function initializeCalendar(): Promise {
// Initialize all managers
await managerFactory.initializeManagers(managers);
+ // Handle deep linking after managers are initialized
+ await handleDeepLinking(managers);
// Expose to window for debugging
(window as any).calendarDebug = {
diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts
index 019094c..477ae93 100644
--- a/src/managers/EventManager.ts
+++ b/src/managers/EventManager.ts
@@ -103,6 +103,60 @@ export class EventManager {
return this.events.find(event => event.id === id);
}
+ /**
+ * Get event by ID and return event info for navigation
+ * @param id Event ID to find
+ * @returns Event with navigation info or null if not found
+ */
+ public getEventForNavigation(id: string): { event: CalendarEvent; eventDate: Date } | null {
+ const event = this.getEventById(id);
+ if (!event) {
+ return null;
+ }
+
+ try {
+ const eventDate = new Date(event.start);
+ if (isNaN(eventDate.getTime())) {
+ console.warn(`EventManager: Invalid event start date for event ${id}:`, event.start);
+ return null;
+ }
+
+ return {
+ event,
+ eventDate
+ };
+ } catch (error) {
+ console.warn(`EventManager: Failed to parse event date for event ${id}:`, error);
+ return null;
+ }
+ }
+
+ /**
+ * Navigate to specific event by ID
+ * Emits navigation events for other managers to handle
+ * @param eventId Event ID to navigate to
+ * @returns true if event found and navigation initiated, false otherwise
+ */
+ public navigateToEvent(eventId: string): boolean {
+ const eventInfo = this.getEventForNavigation(eventId);
+ if (!eventInfo) {
+ console.warn(`EventManager: Event with ID ${eventId} not found`);
+ return false;
+ }
+
+ const { event, eventDate } = eventInfo;
+
+ // Emit navigation request event
+ this.eventBus.emit(CoreEvents.NAVIGATE_TO_EVENT, {
+ eventId,
+ event,
+ eventDate,
+ eventStartTime: event.start
+ });
+
+ return true;
+ }
+
/**
* Optimized events for period with caching and DateCalculator
*/
diff --git a/src/managers/EventOverlapManager.ts b/src/managers/EventOverlapManager.ts
new file mode 100644
index 0000000..e3bddca
--- /dev/null
+++ b/src/managers/EventOverlapManager.ts
@@ -0,0 +1,268 @@
+/**
+ * EventOverlapManager - Håndterer overlap detection og DOM manipulation for overlapping events
+ * Implementerer både column sharing (flexbox) og stacking patterns
+ */
+
+import { CalendarEvent } from '../types/CalendarTypes';
+import { DateCalculator } from '../utils/DateCalculator';
+import { calendarConfig } from '../core/CalendarConfig';
+
+export enum OverlapType {
+ NONE = 'none',
+ COLUMN_SHARING = 'column_sharing',
+ STACKING = 'stacking'
+}
+
+export interface OverlapGroup {
+ type: OverlapType;
+ events: CalendarEvent[];
+ position: { top: number; height: number };
+ container?: HTMLElement;
+}
+
+export class EventOverlapManager {
+ private static readonly STACKING_TIME_THRESHOLD_MINUTES = 30;
+ private static readonly STACKING_WIDTH_REDUCTION_PX = 15;
+ private nextZIndex = 100;
+
+ /**
+ * Detect overlap mellem events baseret på start tid
+ */
+ public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
+ const start1 = new Date(event1.start).getTime();
+ const start2 = new Date(event2.start).getTime();
+ const timeDiffMinutes = Math.abs(start1 - start2) / (1000 * 60);
+
+ // Samme start tid = column sharing
+ if (timeDiffMinutes === 0) {
+ return OverlapType.COLUMN_SHARING;
+ }
+
+ // Mere end 30 min forskel = stacking
+ if (timeDiffMinutes > EventOverlapManager.STACKING_TIME_THRESHOLD_MINUTES) {
+ return OverlapType.STACKING;
+ }
+
+ return OverlapType.NONE;
+ }
+
+ /**
+ * Gruppér events baseret på overlap type
+ */
+ public groupOverlappingEvents(events: CalendarEvent[]): OverlapGroup[] {
+ const groups: OverlapGroup[] = [];
+ const processedEvents = new Set();
+
+ for (const event of events) {
+ if (processedEvents.has(event.id)) continue;
+
+ const overlappingEvents = [event];
+ processedEvents.add(event.id);
+
+ // Find alle events der overlapper med dette event
+ for (const otherEvent of events) {
+ if (otherEvent.id === event.id || processedEvents.has(otherEvent.id)) continue;
+
+ const overlapType = this.detectOverlap(event, otherEvent);
+ if (overlapType !== OverlapType.NONE) {
+ overlappingEvents.push(otherEvent);
+ processedEvents.add(otherEvent.id);
+ }
+ }
+
+ // Opret gruppe hvis der er overlap
+ if (overlappingEvents.length > 1) {
+ const overlapType = this.detectOverlap(overlappingEvents[0], overlappingEvents[1]);
+ groups.push({
+ type: overlapType,
+ events: overlappingEvents,
+ position: this.calculateGroupPosition(overlappingEvents)
+ });
+ } else {
+ // Single event - ingen overlap
+ groups.push({
+ type: OverlapType.NONE,
+ events: [event],
+ position: this.calculateGroupPosition([event])
+ });
+ }
+ }
+
+ return groups;
+ }
+
+ /**
+ * Opret flexbox container for column sharing events
+ */
+ public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement {
+ const container = document.createElement('div');
+ container.className = 'event-group';
+ container.style.position = 'absolute';
+ container.style.top = `${position.top}px`;
+ container.style.height = `${position.height}px`;
+ container.style.left = '2px';
+ container.style.right = '2px';
+
+ // Data attributter for debugging og styling
+ container.dataset.eventCount = events.length.toString();
+ container.dataset.overlapType = OverlapType.COLUMN_SHARING;
+
+ return container;
+ }
+
+ /**
+ * Tilføj event til eksisterende event group
+ */
+ public addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void {
+ // Fjern absolute positioning fra event da flexbox håndterer layout
+ eventElement.style.position = 'relative';
+ eventElement.style.top = '';
+ eventElement.style.left = '';
+ eventElement.style.right = '';
+
+ container.appendChild(eventElement);
+
+ // Opdater event count
+ const currentCount = parseInt(container.dataset.eventCount || '0');
+ container.dataset.eventCount = (currentCount + 1).toString();
+ }
+
+ /**
+ * Fjern event fra event group og cleanup hvis tom
+ */
+ public removeFromEventGroup(container: HTMLElement, eventId: string): boolean {
+ const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
+ if (!eventElement) return false;
+
+ // Gendan absolute positioning
+ eventElement.style.position = 'absolute';
+ eventElement.remove();
+
+ // Opdater event count
+ const currentCount = parseInt(container.dataset.eventCount || '0');
+ const newCount = Math.max(0, currentCount - 1);
+ container.dataset.eventCount = newCount.toString();
+
+ // Cleanup hvis tom container
+ if (newCount === 0) {
+ container.remove();
+ return true; // Container blev fjernet
+ }
+
+ // Hvis kun ét event tilbage, konvertér tilbage til normal event
+ if (newCount === 1) {
+ const remainingEvent = container.querySelector('swp-event') as HTMLElement;
+ if (remainingEvent) {
+ // Gendan normal event positioning
+ remainingEvent.style.position = 'absolute';
+ remainingEvent.style.top = container.style.top;
+ remainingEvent.style.left = '2px';
+ remainingEvent.style.right = '2px';
+
+ // Indsæt før container og fjern container
+ container.parentElement?.insertBefore(remainingEvent, container);
+ container.remove();
+ return true; // Container blev fjernet
+ }
+ }
+
+ return false; // Container blev ikke fjernet
+ }
+
+ /**
+ * Opret stacked event med reduceret bredde
+ */
+ public createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement): void {
+ // Beregn reduceret bredde baseret på swp-events-layer (som har den korrekte fulde bredde)
+ // Underlying event skal beholde sin fulde bredde
+ const eventsLayer = underlyingElement.parentElement;
+ const columnWidth = eventsLayer ? eventsLayer.offsetWidth : 200; // fallback
+ const stackedWidth = Math.max(50, columnWidth - EventOverlapManager.STACKING_WIDTH_REDUCTION_PX);
+
+ eventElement.style.width = `${stackedWidth}px`;
+ eventElement.style.right = '2px';
+ eventElement.style.left = 'auto';
+ eventElement.style.zIndex = this.getNextZIndex().toString();
+
+ // Data attributter
+ eventElement.dataset.overlapType = OverlapType.STACKING;
+ eventElement.dataset.stackedWidth = stackedWidth.toString();
+ }
+
+ /**
+ * Fjern stacking styling fra event
+ */
+ public removeStackedStyling(eventElement: HTMLElement): void {
+ eventElement.style.width = '';
+ eventElement.style.right = '';
+ eventElement.style.left = '2px';
+ eventElement.style.zIndex = '';
+
+ delete eventElement.dataset.overlapType;
+ delete eventElement.dataset.stackedWidth;
+ }
+
+ /**
+ * Beregn position for event gruppe
+ */
+ private calculateGroupPosition(events: CalendarEvent[]): { top: number; height: number } {
+ if (events.length === 0) return { top: 0, height: 0 };
+
+ // Find tidligste start og seneste slut
+ const startTimes = events.map(e => new Date(e.start).getTime());
+ const endTimes = events.map(e => new Date(e.end).getTime());
+
+ const earliestStart = Math.min(...startTimes);
+ const latestEnd = Math.max(...endTimes);
+
+ // Konvertér til pixel positions (dette skal matches med EventRenderer logik)
+ const startDate = new Date(earliestStart);
+ const endDate = new Date(latestEnd);
+
+ // Brug samme logik som EventRenderer.calculateEventPosition
+ const gridSettings = { dayStartHour: 6, hourHeight: 80 }; // Fra config
+ const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
+ const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
+ const dayStartMinutes = gridSettings.dayStartHour * 60;
+
+ const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
+ const height = ((endMinutes - startMinutes) / 60) * gridSettings.hourHeight;
+
+ return { top, height };
+ }
+
+ /**
+ * Get next available z-index for stacked events
+ */
+ private getNextZIndex(): number {
+ return ++this.nextZIndex;
+ }
+
+ /**
+ * Reset z-index counter
+ */
+ public resetZIndex(): void {
+ this.nextZIndex = 100;
+ }
+
+ /**
+ * Check if element is part of an event group
+ */
+ public isInEventGroup(element: HTMLElement): boolean {
+ return element.closest('.event-group') !== null;
+ }
+
+ /**
+ * Check if element is a stacked event
+ */
+ public isStackedEvent(element: HTMLElement): boolean {
+ return element.dataset.overlapType === OverlapType.STACKING;
+ }
+
+ /**
+ * Get event group container for an event element
+ */
+ public getEventGroup(eventElement: HTMLElement): HTMLElement | null {
+ return eventElement.closest('.event-group') as HTMLElement;
+ }
+}
\ No newline at end of file
diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts
index a7966fc..83da729 100644
--- a/src/managers/NavigationManager.ts
+++ b/src/managers/NavigationManager.ts
@@ -118,6 +118,53 @@ export class NavigationManager {
this.navigateToDate(targetDate);
});
+
+ // Listen for event navigation requests
+ this.eventBus.on(CoreEvents.NAVIGATE_TO_EVENT, (event: Event) => {
+ const customEvent = event as CustomEvent;
+ const { eventDate, eventStartTime } = customEvent.detail;
+
+ if (!eventDate || !eventStartTime) {
+ console.warn('NavigationManager: Invalid event navigation data');
+ return;
+ }
+
+ this.navigateToEventDate(eventDate, eventStartTime);
+ });
+ }
+
+ /**
+ * Navigate to specific event date and emit scroll event after navigation
+ */
+ private navigateToEventDate(eventDate: Date, eventStartTime: string): void {
+ const weekStart = DateCalculator.getISOWeekStart(eventDate);
+ this.targetWeek = new Date(weekStart);
+
+ const currentTime = this.currentWeek.getTime();
+ const targetTime = weekStart.getTime();
+
+ // Store event start time for scrolling after navigation
+ const scrollAfterNavigation = () => {
+ // Emit scroll request after navigation is complete
+ this.eventBus.emit('scroll:to-event-time', {
+ eventStartTime
+ });
+ };
+
+ if (currentTime < targetTime) {
+ this.animationQueue++;
+ this.animateTransition('next', weekStart);
+ // Listen for navigation completion to trigger scroll
+ this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation);
+ } else if (currentTime > targetTime) {
+ this.animationQueue++;
+ this.animateTransition('prev', weekStart);
+ // Listen for navigation completion to trigger scroll
+ this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation);
+ } else {
+ // Already on correct week, just scroll
+ scrollAfterNavigation();
+ }
}
private navigateToPreviousWeek(): void {
diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts
index e09c362..95595fb 100644
--- a/src/managers/ScrollManager.ts
+++ b/src/managers/ScrollManager.ts
@@ -45,6 +45,16 @@ export class ScrollManager {
window.addEventListener('resize', () => {
this.updateScrollableHeight();
});
+
+ // Listen for scroll to event time requests
+ eventBus.on('scroll:to-event-time', (event: Event) => {
+ const customEvent = event as CustomEvent;
+ const { eventStartTime } = customEvent.detail;
+
+ if (eventStartTime) {
+ this.scrollToEventTime(eventStartTime);
+ }
+ });
}
/**
@@ -97,6 +107,25 @@ export class ScrollManager {
this.scrollTo(scrollTop);
}
+ /**
+ * Scroll to specific event time
+ * @param eventStartTime ISO string of event start time
+ */
+ scrollToEventTime(eventStartTime: string): void {
+ try {
+ const eventDate = new Date(eventStartTime);
+ const eventHour = eventDate.getHours();
+ const eventMinutes = eventDate.getMinutes();
+
+ // Convert to decimal hour (e.g., 14:30 becomes 14.5)
+ const decimalHour = eventHour + (eventMinutes / 60);
+
+ this.scrollToHour(decimalHour);
+ } catch (error) {
+ console.warn('ScrollManager: Failed to scroll to event time:', error);
+ }
+ }
+
/**
* Setup ResizeObserver to monitor container size changes
*/
diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts
index eba6553..ce56976 100644
--- a/src/renderers/EventRenderer.ts
+++ b/src/renderers/EventRenderer.ts
@@ -6,6 +6,7 @@ import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator';
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
+import { EventOverlapManager, OverlapType } from '../managers/EventOverlapManager';
/**
* Interface for event rendering strategies
@@ -20,6 +21,7 @@ export interface EventRendererStrategy {
*/
export abstract class BaseEventRenderer implements EventRendererStrategy {
protected dateCalculator: DateCalculator;
+ protected overlapManager: EventOverlapManager;
// Drag and drop state
private draggedClone: HTMLElement | null = null;
@@ -30,6 +32,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
DateCalculator.initialize(calendarConfig);
}
this.dateCalculator = dateCalculator || new DateCalculator();
+ this.overlapManager = new EventOverlapManager();
}
/**
@@ -230,10 +233,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Create clone
this.draggedClone = this.createEventClone(originalElement);
- // Add to current column
+ // Add to current column's events layer (not directly to column)
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (columnElement) {
- columnElement.appendChild(this.draggedClone);
+ const eventsLayer = columnElement.querySelector('swp-events-layer');
+ if (eventsLayer) {
+ eventsLayer.appendChild(this.draggedClone);
+ } else {
+ // Fallback to column if events layer not found
+ columnElement.appendChild(this.draggedClone);
+ }
}
// Make original semi-transparent
@@ -262,10 +271,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
private handleColumnChange(eventId: string, newColumn: string): void {
if (!this.draggedClone) return;
- // Move clone to new column
+ // Move clone to new column's events layer
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
- if (newColumnElement && this.draggedClone.parentElement !== newColumnElement) {
- newColumnElement.appendChild(this.draggedClone);
+ if (newColumnElement) {
+ const eventsLayer = newColumnElement.querySelector('swp-events-layer');
+ if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
+ eventsLayer.appendChild(this.draggedClone);
+ } else if (!eventsLayer && this.draggedClone.parentElement !== newColumnElement) {
+ // Fallback to column if events layer not found
+ newColumnElement.appendChild(this.draggedClone);
+ }
}
}
@@ -458,12 +473,26 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) {
- columnEvents.forEach(event => {
- this.renderEvent(event, eventsLayer);
+ // Group events by overlap type
+ const overlapGroups = this.overlapManager.groupOverlappingEvents(columnEvents);
+
+ overlapGroups.forEach(group => {
+ if (group.type === OverlapType.COLUMN_SHARING && group.events.length > 1) {
+ // Create flexbox container for column sharing
+ this.renderColumnSharingGroup(group, eventsLayer);
+ } else if (group.type === OverlapType.STACKING && group.events.length > 1) {
+ // Render stacked events
+ this.renderStackedEvents(group, eventsLayer);
+ } else {
+ // Render normal single events
+ group.events.forEach(event => {
+ this.renderEvent(event, eventsLayer);
+ });
+ }
});
// Debug: Verify events were actually added
- const renderedEvents = eventsLayer.querySelectorAll('swp-event');
+ const renderedEvents = eventsLayer.querySelectorAll('swp-event, .event-group');
} else {
}
});
@@ -679,9 +708,114 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
return !(event1End < event2Span.startColumn || event2End < event1Span.startColumn);
}
+ /**
+ * Render column sharing group with flexbox container
+ */
+ protected renderColumnSharingGroup(group: any, container: Element): void {
+ const groupContainer = this.overlapManager.createEventGroup(group.events, group.position);
+
+ // Render each event in the group
+ group.events.forEach((event: CalendarEvent) => {
+ const eventElement = this.createEventElement(event);
+ this.overlapManager.addToEventGroup(groupContainer, eventElement);
+ });
+
+ container.appendChild(groupContainer);
+
+ // Emit event for debugging/logging
+ eventBus.emit('overlap:group-created', {
+ type: 'column_sharing',
+ eventCount: group.events.length,
+ events: group.events.map((e: CalendarEvent) => e.id)
+ });
+ }
+
+ /**
+ * Render stacked events with reduced width
+ */
+ protected renderStackedEvents(group: any, container: Element): void {
+ // Sort events by duration - longer events render first (background), shorter events on top
+ // This way shorter events are more visible and get higher z-index
+ const sortedEvents = [...group.events].sort((a, b) => {
+ const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
+ const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
+ return durationB - durationA; // Longer duration first (background)
+ });
+
+ let underlyingElement: HTMLElement | null = null;
+
+ sortedEvents.forEach((event: CalendarEvent, index: number) => {
+ const eventElement = this.createEventElement(event);
+ this.positionEvent(eventElement, event);
+
+ if (index === 0) {
+ // First (longest duration) event renders normally at full width - UNCHANGED
+ container.appendChild(eventElement);
+ underlyingElement = eventElement;
+ } else {
+ // Shorter events are stacked with reduced width and higher z-index
+ // All stacked events use the SAME underlying element (the longest one)
+ if (underlyingElement) {
+ this.overlapManager.createStackedEvent(eventElement, underlyingElement);
+ }
+ container.appendChild(eventElement);
+ // DO NOT update underlyingElement - keep it as the longest event
+ }
+ });
+
+ // Emit event for debugging/logging
+ eventBus.emit('overlap:events-stacked', {
+ type: 'stacking',
+ eventCount: group.events.length,
+ events: group.events.map((e: CalendarEvent) => e.id)
+ });
+ }
+
+ /**
+ * Create event element without positioning
+ */
+ protected createEventElement(event: CalendarEvent): HTMLElement {
+ const eventElement = document.createElement('swp-event');
+ eventElement.dataset.eventId = event.id;
+ eventElement.dataset.title = event.title;
+ eventElement.dataset.start = event.start;
+ eventElement.dataset.end = event.end;
+ eventElement.dataset.type = event.type;
+ eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
+
+ // Format time for display using unified method
+ const startTime = this.formatTime(event.start);
+ const endTime = this.formatTime(event.end);
+
+ // Calculate duration in minutes
+ const startDate = new Date(event.start);
+ const endDate = new Date(event.end);
+ const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
+
+ // Create event content
+ eventElement.innerHTML = `
+ ${startTime} - ${endTime}
+ ${event.title}
+ `;
+
+ return eventElement;
+ }
+
+ /**
+ * Position event element
+ */
+ protected positionEvent(eventElement: HTMLElement, event: CalendarEvent): void {
+ const position = this.calculateEventPosition(event);
+ eventElement.style.position = 'absolute';
+ eventElement.style.top = `${position.top + 1}px`;
+ eventElement.style.height = `${position.height - 3}px`;
+ eventElement.style.left = '2px';
+ eventElement.style.right = '2px';
+ }
+
clearEvents(container?: HTMLElement): void {
- const selector = 'swp-event';
- const existingEvents = container
+ const selector = 'swp-event, .event-group';
+ const existingEvents = container
? container.querySelectorAll(selector)
: document.querySelectorAll(selector);
diff --git a/src/utils/URLManager.ts b/src/utils/URLManager.ts
new file mode 100644
index 0000000..26750de
--- /dev/null
+++ b/src/utils/URLManager.ts
@@ -0,0 +1,86 @@
+import { EventBus } from '../core/EventBus';
+import { IEventBus } from '../types/CalendarTypes';
+
+/**
+ * URLManager handles URL query parameter parsing and deep linking functionality
+ * Follows event-driven architecture with no global state
+ */
+export class URLManager {
+ private eventBus: IEventBus;
+
+ constructor(eventBus: IEventBus) {
+ this.eventBus = eventBus;
+ }
+
+ /**
+ * Parse eventId from URL query parameters
+ * @returns eventId string or null if not found
+ */
+ public parseEventIdFromURL(): string | null {
+ try {
+ const urlParams = new URLSearchParams(window.location.search);
+ const eventId = urlParams.get('eventId');
+
+ if (eventId && eventId.trim() !== '') {
+ return eventId.trim();
+ }
+
+ return null;
+ } catch (error) {
+ console.warn('URLManager: Failed to parse URL parameters:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Get all query parameters as an object
+ * @returns object with all query parameters
+ */
+ public getAllQueryParams(): Record {
+ try {
+ const urlParams = new URLSearchParams(window.location.search);
+ const params: Record = {};
+
+ for (const [key, value] of urlParams.entries()) {
+ params[key] = value;
+ }
+
+ return params;
+ } catch (error) {
+ console.warn('URLManager: Failed to parse URL parameters:', error);
+ return {};
+ }
+ }
+
+ /**
+ * Update URL without page reload (for future use)
+ * @param params object with parameters to update
+ */
+ public updateURL(params: Record): void {
+ try {
+ const url = new URL(window.location.href);
+
+ // Update or remove parameters
+ Object.entries(params).forEach(([key, value]) => {
+ if (value === null) {
+ url.searchParams.delete(key);
+ } else {
+ url.searchParams.set(key, value);
+ }
+ });
+
+ // Update URL without page reload
+ window.history.replaceState({}, '', url.toString());
+ } catch (error) {
+ console.warn('URLManager: Failed to update URL:', error);
+ }
+ }
+
+ /**
+ * Check if current URL has any query parameters
+ * @returns true if URL has query parameters
+ */
+ public hasQueryParams(): boolean {
+ return window.location.search.length > 0;
+ }
+}
\ No newline at end of file
diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css
index 804582c..a771e4f 100644
--- a/wwwroot/css/calendar-events-css.css
+++ b/wwwroot/css/calendar-events-css.css
@@ -204,4 +204,36 @@ swp-events-layer[data-filter-active="true"] swp-event {
/* Events that match the filter stay normal */
swp-events-layer[data-filter-active="true"] swp-event[data-matches="true"] {
opacity: 1;
+}
+
+/* Event overlap styling */
+/* Event group container for column sharing */
+.event-group {
+ position: absolute;
+ display: flex;
+ gap: 1px;
+ width: calc(100% - 4px);
+ left: 2px;
+ z-index: 10;
+}
+
+.event-group swp-event {
+ flex: 1;
+ position: relative;
+ left: 0;
+ right: 0;
+ margin: 0;
+}
+
+/* Debug styling for development */
+.event-group[data-event-count="2"] {
+ border-left: 2px solid rgba(0, 255, 0, 0.3);
+}
+
+.event-group[data-event-count="3"] {
+ border-left: 2px solid rgba(0, 0, 255, 0.3);
+}
+
+.event-group[data-event-count="4"] {
+ border-left: 2px solid rgba(255, 0, 255, 0.3);
}
\ No newline at end of file