Implements event overlap rendering
Adds logic to handle event overlaps in the calendar view. It introduces two patterns: column sharing for events with the same start time (rendered using flexbox) and stacking for events with a >30 min difference (rendered with reduced width and z-index). It also introduces deep linking to specific events via URL parameters.
This commit is contained in:
parent
7a1c776bc1
commit
ff067cfac3
11 changed files with 837 additions and 16 deletions
143
event-overlap-implementation-plan.md
Normal file
143
event-overlap-implementation-plan.md
Normal file
|
|
@ -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
|
||||||
|
<!-- Normal event -->
|
||||||
|
<swp-event>Single Event</swp-event>
|
||||||
|
|
||||||
|
<!-- Column sharing group -->
|
||||||
|
<div class="event-group" style="position: absolute; top: 200px;">
|
||||||
|
<swp-event>Event 1</swp-event>
|
||||||
|
<swp-event>Event 2</swp-event>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stacked event -->
|
||||||
|
<swp-event class="stacked-event" style="top: 210px;">Stacked Event</swp-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
|
||||||
|
|
@ -13,10 +13,11 @@ export const CoreEvents = {
|
||||||
VIEW_RENDERED: 'view:rendered',
|
VIEW_RENDERED: 'view:rendered',
|
||||||
WORKWEEK_CHANGED: 'workweek:changed',
|
WORKWEEK_CHANGED: 'workweek:changed',
|
||||||
|
|
||||||
// Navigation events (3)
|
// Navigation events (4)
|
||||||
DATE_CHANGED: 'nav:date-changed',
|
DATE_CHANGED: 'nav:date-changed',
|
||||||
NAVIGATION_COMPLETED: 'nav:navigation-completed',
|
NAVIGATION_COMPLETED: 'nav:navigation-completed',
|
||||||
PERIOD_INFO_UPDATE: 'nav:period-info-update',
|
PERIOD_INFO_UPDATE: 'nav:period-info-update',
|
||||||
|
NAVIGATE_TO_EVENT: 'nav:navigate-to-event',
|
||||||
|
|
||||||
// Data events (4)
|
// Data events (4)
|
||||||
DATA_LOADING: 'data:loading',
|
DATA_LOADING: 'data:loading',
|
||||||
|
|
|
||||||
|
|
@ -952,12 +952,12 @@
|
||||||
{
|
{
|
||||||
"id": "96",
|
"id": "96",
|
||||||
"title": "Deep Work",
|
"title": "Deep Work",
|
||||||
"start": "2025-09-03T10:00:00",
|
"start": "2025-09-02T15:00:00",
|
||||||
"end": "2025-09-03T12:00:00",
|
"end": "2025-09-02T15:30:00",
|
||||||
"type": "work",
|
"type": "work",
|
||||||
"allDay": false,
|
"allDay": false,
|
||||||
"syncStatus": "synced",
|
"syncStatus": "synced",
|
||||||
"metadata": { "duration": 120, "color": "#3f51b5" }
|
"metadata": { "duration": 30, "color": "#3f51b5" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "97",
|
"id": "97",
|
||||||
|
|
@ -992,8 +992,8 @@
|
||||||
{
|
{
|
||||||
"id": "100",
|
"id": "100",
|
||||||
"title": "Sprint Review",
|
"title": "Sprint Review",
|
||||||
"start": "2025-09-05T14:00:00",
|
"start": "2025-09-04T15:00:00",
|
||||||
"end": "2025-09-05T15:00:00",
|
"end": "2025-09-04T16:00:00",
|
||||||
"type": "meeting",
|
"type": "meeting",
|
||||||
"allDay": false,
|
"allDay": false,
|
||||||
"syncStatus": "synced",
|
"syncStatus": "synced",
|
||||||
|
|
|
||||||
27
src/index.ts
27
src/index.ts
|
|
@ -4,6 +4,31 @@ import { calendarConfig } from './core/CalendarConfig.js';
|
||||||
import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js';
|
import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js';
|
||||||
import { ManagerFactory } from './factories/ManagerFactory.js';
|
import { ManagerFactory } from './factories/ManagerFactory.js';
|
||||||
import { DateCalculator } from './utils/DateCalculator.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<void> {
|
||||||
|
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
|
* Initialize the calendar application with simple direct calls
|
||||||
|
|
@ -30,6 +55,8 @@ async function initializeCalendar(): Promise<void> {
|
||||||
// Initialize all managers
|
// Initialize all managers
|
||||||
await managerFactory.initializeManagers(managers);
|
await managerFactory.initializeManagers(managers);
|
||||||
|
|
||||||
|
// Handle deep linking after managers are initialized
|
||||||
|
await handleDeepLinking(managers);
|
||||||
|
|
||||||
// Expose to window for debugging
|
// Expose to window for debugging
|
||||||
(window as any).calendarDebug = {
|
(window as any).calendarDebug = {
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,60 @@ export class EventManager {
|
||||||
return this.events.find(event => event.id === id);
|
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
|
* Optimized events for period with caching and DateCalculator
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
268
src/managers/EventOverlapManager.ts
Normal file
268
src/managers/EventOverlapManager.ts
Normal file
|
|
@ -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<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -118,6 +118,53 @@ export class NavigationManager {
|
||||||
|
|
||||||
this.navigateToDate(targetDate);
|
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 {
|
private navigateToPreviousWeek(): void {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,16 @@ export class ScrollManager {
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.updateScrollableHeight();
|
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);
|
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
|
* Setup ResizeObserver to monitor container size changes
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
|
import { EventOverlapManager, OverlapType } from '../managers/EventOverlapManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -20,6 +21,7 @@ export interface EventRendererStrategy {
|
||||||
*/
|
*/
|
||||||
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
protected dateCalculator: DateCalculator;
|
protected dateCalculator: DateCalculator;
|
||||||
|
protected overlapManager: EventOverlapManager;
|
||||||
|
|
||||||
// Drag and drop state
|
// Drag and drop state
|
||||||
private draggedClone: HTMLElement | null = null;
|
private draggedClone: HTMLElement | null = null;
|
||||||
|
|
@ -30,6 +32,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
DateCalculator.initialize(calendarConfig);
|
DateCalculator.initialize(calendarConfig);
|
||||||
}
|
}
|
||||||
this.dateCalculator = dateCalculator || new DateCalculator();
|
this.dateCalculator = dateCalculator || new DateCalculator();
|
||||||
|
this.overlapManager = new EventOverlapManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -230,10 +233,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
// Create clone
|
// Create clone
|
||||||
this.draggedClone = this.createEventClone(originalElement);
|
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}"]`);
|
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
||||||
if (columnElement) {
|
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
|
// Make original semi-transparent
|
||||||
|
|
@ -262,10 +271,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
private handleColumnChange(eventId: string, newColumn: string): void {
|
private handleColumnChange(eventId: string, newColumn: string): void {
|
||||||
if (!this.draggedClone) return;
|
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}"]`);
|
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
||||||
if (newColumnElement && this.draggedClone.parentElement !== newColumnElement) {
|
if (newColumnElement) {
|
||||||
newColumnElement.appendChild(this.draggedClone);
|
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');
|
const eventsLayer = column.querySelector('swp-events-layer');
|
||||||
if (eventsLayer) {
|
if (eventsLayer) {
|
||||||
columnEvents.forEach(event => {
|
// Group events by overlap type
|
||||||
this.renderEvent(event, eventsLayer);
|
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
|
// Debug: Verify events were actually added
|
||||||
const renderedEvents = eventsLayer.querySelectorAll('swp-event');
|
const renderedEvents = eventsLayer.querySelectorAll('swp-event, .event-group');
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -679,9 +708,114 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
return !(event1End < event2Span.startColumn || event2End < event1Span.startColumn);
|
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 = `
|
||||||
|
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||||
|
<swp-event-title>${event.title}</swp-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 {
|
clearEvents(container?: HTMLElement): void {
|
||||||
const selector = 'swp-event';
|
const selector = 'swp-event, .event-group';
|
||||||
const existingEvents = container
|
const existingEvents = container
|
||||||
? container.querySelectorAll(selector)
|
? container.querySelectorAll(selector)
|
||||||
: document.querySelectorAll(selector);
|
: document.querySelectorAll(selector);
|
||||||
|
|
||||||
|
|
|
||||||
86
src/utils/URLManager.ts
Normal file
86
src/utils/URLManager.ts
Normal file
|
|
@ -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<string, string> {
|
||||||
|
try {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
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<string, string | null>): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -204,4 +204,36 @@ swp-events-layer[data-filter-active="true"] swp-event {
|
||||||
/* Events that match the filter stay normal */
|
/* Events that match the filter stay normal */
|
||||||
swp-events-layer[data-filter-active="true"] swp-event[data-matches="true"] {
|
swp-events-layer[data-filter-active="true"] swp-event[data-matches="true"] {
|
||||||
opacity: 1;
|
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);
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue