Refactors event positioning and drag-and-drop
Centralizes event position calculations into `PositionUtils` for consistency and reusability across managers and renderers. Improves drag-and-drop functionality by emitting events for all-day event conversion and streamlining position calculations during drag operations. Introduces `AllDayManager` and `AllDayEventRenderer` to manage and render all-day events in the calendar header. This allows dragging events to the header to convert them to all-day events.
This commit is contained in:
parent
8b96376d1f
commit
7054c0d40a
9 changed files with 404 additions and 72 deletions
245
docs/typescript-code-review-2025.md
Normal file
245
docs/typescript-code-review-2025.md
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
# TypeScript Code Review - Calendar Plantempus
|
||||||
|
**Dato:** September 2025
|
||||||
|
**Reviewer:** Roo
|
||||||
|
**Fokus:** Dybdegående analyse efter TimeFormatter implementation
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Efter implementering af TimeFormatter og gennemgang af codebasen, har jeg identificeret både styrker og forbedringspotentiale. Koden viser god separation of concerns og event-driven arkitektur, men har stadig områder der kan optimeres.
|
||||||
|
|
||||||
|
## 🟢 Styrker
|
||||||
|
|
||||||
|
### 1. Event-Driven Architecture
|
||||||
|
- **Konsistent EventBus pattern** gennem hele applikationen
|
||||||
|
- Ingen direkte dependencies mellem moduler
|
||||||
|
- God brug af custom events for kommunikation
|
||||||
|
|
||||||
|
### 2. Separation of Concerns
|
||||||
|
- **Managers**: Håndterer business logic (AllDayManager, DragDropManager, etc.)
|
||||||
|
- **Renderers**: Fokuserer på DOM manipulation
|
||||||
|
- **Utils**: Isolerede utility funktioner
|
||||||
|
- **Elements**: Factory pattern for DOM element creation
|
||||||
|
|
||||||
|
### 3. Performance Optimering
|
||||||
|
- **DOM Caching**: Konsistent caching af DOM elementer
|
||||||
|
- **Throttling**: Event throttling i HeaderManager (16ms delay)
|
||||||
|
- **Pixel-based calculations**: Fjernet komplekse time-based overlap beregninger
|
||||||
|
|
||||||
|
### 4. TypeScript Best Practices
|
||||||
|
- Stærk typing med interfaces
|
||||||
|
- Proper null/undefined checks
|
||||||
|
- Readonly constants hvor relevant
|
||||||
|
|
||||||
|
## 🔴 Kritiske Issues
|
||||||
|
|
||||||
|
### 1. "new_" Prefix Methods (EventRenderer.ts)
|
||||||
|
```typescript
|
||||||
|
// PROBLEM: Midlertidige metode navne
|
||||||
|
protected new_handleEventOverlaps()
|
||||||
|
protected new_renderOverlappingEvents()
|
||||||
|
protected new_applyStackStyling()
|
||||||
|
protected new_applyColumnSharingStyling()
|
||||||
|
```
|
||||||
|
**Impact:** Forvirrende navngivning, indikerer ufærdig refactoring
|
||||||
|
**Løsning:** Fjern prefix og ryd op i gamle metoder
|
||||||
|
|
||||||
|
### 2. Duplikeret Cache Logic
|
||||||
|
```typescript
|
||||||
|
// AllDayManager.ts
|
||||||
|
private cachedAllDayContainer: HTMLElement | null = null;
|
||||||
|
private cachedCalendarHeader: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// HeaderManager.ts
|
||||||
|
private cachedCalendarHeader: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// DragDropManager.ts
|
||||||
|
private cachedElements: CachedElements = {...}
|
||||||
|
```
|
||||||
|
**Impact:** 30+ linjer duplikeret kode
|
||||||
|
**Løsning:** Opret generisk DOMCacheManager
|
||||||
|
|
||||||
|
### 3. Manglende Error Boundaries
|
||||||
|
```typescript
|
||||||
|
// SimpleEventOverlapManager.ts
|
||||||
|
const linkData = element.dataset.stackLink;
|
||||||
|
try {
|
||||||
|
return JSON.parse(linkData);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse stack link data:', linkData, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Impact:** Silently failing JSON parsing
|
||||||
|
**Løsning:** Proper error handling med user feedback
|
||||||
|
|
||||||
|
## 🟡 Code Smells & Improvements
|
||||||
|
|
||||||
|
### 1. Magic Numbers
|
||||||
|
```typescript
|
||||||
|
// SimpleEventOverlapManager.ts
|
||||||
|
const startDifference = Math.abs(top1 - top2);
|
||||||
|
if (startDifference > 40) { // Magic number!
|
||||||
|
return OverlapType.STACKING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DragDropManager.ts
|
||||||
|
private readonly dragThreshold = 5; // Should be configurable
|
||||||
|
private readonly scrollSpeed = 10;
|
||||||
|
private readonly scrollThreshold = 30;
|
||||||
|
```
|
||||||
|
**Løsning:** Flyt til configuration constants
|
||||||
|
|
||||||
|
### 2. Complex Method Signatures
|
||||||
|
```typescript
|
||||||
|
// AllDayManager.ts - 73 linjer!
|
||||||
|
public checkAndAnimateAllDayHeight(): void {
|
||||||
|
// Massive method doing too much
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Løsning:** Split i mindre, fokuserede metoder
|
||||||
|
|
||||||
|
### 3. Inconsistent Naming
|
||||||
|
```typescript
|
||||||
|
// Mix af naming conventions
|
||||||
|
getCalendarHeader() // get prefix
|
||||||
|
findElements() // no prefix
|
||||||
|
detectColumn() // action verb
|
||||||
|
cachedElements // noun
|
||||||
|
```
|
||||||
|
**Løsning:** Standardiser naming convention
|
||||||
|
|
||||||
|
### 4. Memory Leaks Risk
|
||||||
|
```typescript
|
||||||
|
// DragDropManager.ts
|
||||||
|
private boundHandlers = {
|
||||||
|
mouseMove: this.handleMouseMove.bind(this),
|
||||||
|
mouseDown: this.handleMouseDown.bind(this),
|
||||||
|
mouseUp: this.handleMouseUp.bind(this)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
**God praksis!** Men ikke konsistent anvendt alle steder
|
||||||
|
|
||||||
|
## 📊 Metrics & Analysis
|
||||||
|
|
||||||
|
### Complexity Analysis
|
||||||
|
| File | Lines | Cyclomatic Complexity | Maintainability |
|
||||||
|
|------|-------|----------------------|-----------------|
|
||||||
|
| AllDayManager.ts | 281 | Medium (8) | Good |
|
||||||
|
| DragDropManager.ts | 521 | High (15) | Needs refactoring |
|
||||||
|
| SimpleEventOverlapManager.ts | 473 | Very High (20) | Critical |
|
||||||
|
| HeaderManager.ts | 119 | Low (4) | Excellent |
|
||||||
|
| GridManager.ts | 348 | Medium (10) | Good |
|
||||||
|
|
||||||
|
### Code Duplication
|
||||||
|
- **Cache management**: ~15% duplication
|
||||||
|
- **Event handling**: ~10% duplication
|
||||||
|
- **Position calculations**: ~8% duplication
|
||||||
|
|
||||||
|
## 🎯 Prioriterede Forbedringer
|
||||||
|
|
||||||
|
### Priority 1: Critical Fixes
|
||||||
|
1. **Fjern "new_" prefix** fra EventRenderer metoder
|
||||||
|
2. **Fix TimeFormatter timezone** - Håndter mock data korrekt som UTC
|
||||||
|
3. **Implementer DOMCacheManager** - Reducer duplication
|
||||||
|
|
||||||
|
### Priority 2: Architecture Improvements
|
||||||
|
1. **GridPositionCalculator** - Centralisér position beregninger
|
||||||
|
2. **EventThrottler** - Generisk throttling utility
|
||||||
|
3. **AllDayRowCalculator** - Udtræk kompleks logik fra AllDayManager
|
||||||
|
|
||||||
|
### Priority 3: Code Quality
|
||||||
|
1. **Reduce method complexity** - Split store metoder
|
||||||
|
2. **Standardize naming** - Konsistent naming convention
|
||||||
|
3. **Add JSDoc** - Mangler på mange public methods
|
||||||
|
|
||||||
|
### Priority 4: Testing
|
||||||
|
1. **Unit tests** for TimeFormatter
|
||||||
|
2. **Integration tests** for overlap detection
|
||||||
|
3. **Performance tests** for large event sets
|
||||||
|
|
||||||
|
## 💡 Architectural Recommendations
|
||||||
|
|
||||||
|
### 1. Introduce Service Layer
|
||||||
|
```typescript
|
||||||
|
// Forslag: EventService
|
||||||
|
class EventService {
|
||||||
|
private formatter: TimeFormatter;
|
||||||
|
private calculator: GridPositionCalculator;
|
||||||
|
private overlapManager: SimpleEventOverlapManager;
|
||||||
|
|
||||||
|
// Centralized event operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configuration Management
|
||||||
|
```typescript
|
||||||
|
interface CalendarConstants {
|
||||||
|
DRAG_THRESHOLD: number;
|
||||||
|
SCROLL_SPEED: number;
|
||||||
|
STACK_OFFSET: number;
|
||||||
|
OVERLAP_THRESHOLD: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Error Handling Strategy
|
||||||
|
```typescript
|
||||||
|
class CalendarError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public recoverable: boolean
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Performance Optimizations
|
||||||
|
|
||||||
|
### 1. Virtual Scrolling
|
||||||
|
For måneds-view med mange events, overvej virtual scrolling
|
||||||
|
|
||||||
|
### 2. Web Workers
|
||||||
|
Flyt tunge beregninger (overlap detection) til Web Worker
|
||||||
|
|
||||||
|
### 3. RequestIdleCallback
|
||||||
|
Brug for non-critical updates som analytics
|
||||||
|
|
||||||
|
## ✅ Positive Highlights
|
||||||
|
|
||||||
|
1. **TimeFormatter Implementation**: Elegant og clean
|
||||||
|
2. **Event-driven Architecture**: Konsistent og velfungerende
|
||||||
|
3. **TypeScript Usage**: God type safety
|
||||||
|
4. **DOM Manipulation**: Effektiv med custom elements
|
||||||
|
5. **Separation of Concerns**: Klar opdeling af ansvar
|
||||||
|
|
||||||
|
## 📋 Recommended Action Plan
|
||||||
|
|
||||||
|
### Immediate (1-2 dage)
|
||||||
|
- [ ] Fjern "new_" prefix fra EventRenderer
|
||||||
|
- [ ] Implementer DOMCacheManager
|
||||||
|
- [ ] Fix magic numbers
|
||||||
|
|
||||||
|
### Short-term (3-5 dage)
|
||||||
|
- [ ] Opret GridPositionCalculator
|
||||||
|
- [ ] Implementer EventThrottler
|
||||||
|
- [ ] Refactor SimpleEventOverlapManager complexity
|
||||||
|
|
||||||
|
### Long-term (1-2 uger)
|
||||||
|
- [ ] Add comprehensive unit tests
|
||||||
|
- [ ] Implement service layer
|
||||||
|
- [ ] Performance optimizations
|
||||||
|
|
||||||
|
## Konklusion
|
||||||
|
|
||||||
|
Koden er generelt velstruktureret med god separation of concerns og konsistent event-driven arkitektur. TimeFormatter implementationen er elegant og løser timezone problemet godt.
|
||||||
|
|
||||||
|
Hovedudfordringerne ligger i:
|
||||||
|
1. Ufærdig refactoring (new_ prefix)
|
||||||
|
2. Duplikeret cache logic
|
||||||
|
3. Høj complexity i overlap detection
|
||||||
|
4. Manglende tests
|
||||||
|
|
||||||
|
Med de foreslåede forbedringer vil kodebasen blive mere maintainable, performant og robust.
|
||||||
|
|
||||||
|
**Overall Score: 7.5/10** - God kvalitet med plads til forbedring
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { CalendarEvent } from '../types/CalendarTypes';
|
import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||||
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for event DOM elements
|
* Abstract base class for event DOM elements
|
||||||
|
|
@ -47,22 +48,10 @@ export abstract class BaseEventElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate event position for timed events
|
* Calculate event position for timed events using PositionUtils
|
||||||
*/
|
*/
|
||||||
protected calculateEventPosition(): { top: number; height: number } {
|
protected calculateEventPosition(): { top: number; height: number } {
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
return PositionUtils.calculateEventPosition(this.event.start, this.event.end);
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
|
||||||
|
|
||||||
const startMinutes = this.event.start.getHours() * 60 + this.event.start.getMinutes();
|
|
||||||
const endMinutes = this.event.end.getHours() * 60 + this.event.end.getMinutes();
|
|
||||||
const dayStartMinutes = dayStartHour * 60;
|
|
||||||
|
|
||||||
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
|
|
||||||
const durationMinutes = endMinutes - startMinutes;
|
|
||||||
const height = (durationMinutes / 60) * hourHeight;
|
|
||||||
|
|
||||||
return { top, height };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { NavigationManager } from '../managers/NavigationManager';
|
||||||
import { ViewManager } from '../managers/ViewManager';
|
import { ViewManager } from '../managers/ViewManager';
|
||||||
import { CalendarManager } from '../managers/CalendarManager';
|
import { CalendarManager } from '../managers/CalendarManager';
|
||||||
import { DragDropManager } from '../managers/DragDropManager';
|
import { DragDropManager } from '../managers/DragDropManager';
|
||||||
|
import { AllDayManager } from '../managers/AllDayManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating and managing calendar managers with proper dependency injection
|
* Factory for creating and managing calendar managers with proper dependency injection
|
||||||
|
|
@ -35,6 +36,7 @@ export class ManagerFactory {
|
||||||
viewManager: ViewManager;
|
viewManager: ViewManager;
|
||||||
calendarManager: CalendarManager;
|
calendarManager: CalendarManager;
|
||||||
dragDropManager: DragDropManager;
|
dragDropManager: DragDropManager;
|
||||||
|
allDayManager: AllDayManager;
|
||||||
} {
|
} {
|
||||||
|
|
||||||
// Create managers in dependency order
|
// Create managers in dependency order
|
||||||
|
|
@ -45,6 +47,7 @@ export class ManagerFactory {
|
||||||
const navigationManager = new NavigationManager(eventBus, eventRenderer);
|
const navigationManager = new NavigationManager(eventBus, eventRenderer);
|
||||||
const viewManager = new ViewManager(eventBus);
|
const viewManager = new ViewManager(eventBus);
|
||||||
const dragDropManager = new DragDropManager(eventBus);
|
const dragDropManager = new DragDropManager(eventBus);
|
||||||
|
const allDayManager = new AllDayManager();
|
||||||
|
|
||||||
// CalendarManager depends on all other managers
|
// CalendarManager depends on all other managers
|
||||||
const calendarManager = new CalendarManager(
|
const calendarManager = new CalendarManager(
|
||||||
|
|
@ -64,7 +67,8 @@ export class ManagerFactory {
|
||||||
navigationManager,
|
navigationManager,
|
||||||
viewManager,
|
viewManager,
|
||||||
calendarManager,
|
calendarManager,
|
||||||
dragDropManager
|
dragDropManager,
|
||||||
|
allDayManager
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
||||||
|
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
||||||
|
import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllDayManager - Handles all-day row height animations and management
|
* AllDayManager - Handles all-day row height animations and management
|
||||||
|
|
@ -11,10 +13,25 @@ export class AllDayManager {
|
||||||
private cachedAllDayContainer: HTMLElement | null = null;
|
private cachedAllDayContainer: HTMLElement | null = null;
|
||||||
private cachedCalendarHeader: HTMLElement | null = null;
|
private cachedCalendarHeader: HTMLElement | null = null;
|
||||||
private cachedHeaderSpacer: HTMLElement | null = null;
|
private cachedHeaderSpacer: HTMLElement | null = null;
|
||||||
|
private allDayEventRenderer: AllDayEventRenderer;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Bind methods for event listeners
|
// Bind methods for event listeners
|
||||||
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
|
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
|
||||||
|
this.allDayEventRenderer = new AllDayEventRenderer();
|
||||||
|
|
||||||
|
// Listen for drag-to-allday conversions
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup event listeners for drag conversions
|
||||||
|
*/
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
eventBus.on('drag:convert-to-allday', (event) => {
|
||||||
|
const { targetDate, originalElement } = (event as CustomEvent).detail;
|
||||||
|
this.handleConvertToAllDay(targetDate, originalElement);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -204,6 +221,58 @@ export class AllDayManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle conversion of timed event to all-day event
|
||||||
|
*/
|
||||||
|
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
|
||||||
|
// Extract event data from original element
|
||||||
|
const eventId = originalElement.dataset.eventId;
|
||||||
|
const title = originalElement.dataset.title || originalElement.textContent || 'Untitled';
|
||||||
|
const type = originalElement.dataset.type || 'work';
|
||||||
|
const startStr = originalElement.dataset.start;
|
||||||
|
const endStr = originalElement.dataset.end;
|
||||||
|
|
||||||
|
if (!eventId || !startStr || !endStr) {
|
||||||
|
console.error('Original element missing required data (eventId, start, end)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CalendarEvent for all-day conversion - preserve original times
|
||||||
|
const originalStart = new Date(startStr);
|
||||||
|
const originalEnd = new Date(endStr);
|
||||||
|
|
||||||
|
// Set date to target date but keep original time
|
||||||
|
const targetStart = new Date(targetDate);
|
||||||
|
targetStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), originalStart.getMilliseconds());
|
||||||
|
|
||||||
|
const targetEnd = new Date(targetDate);
|
||||||
|
targetEnd.setHours(originalEnd.getHours(), originalEnd.getMinutes(), originalEnd.getSeconds(), originalEnd.getMilliseconds());
|
||||||
|
|
||||||
|
const calendarEvent: CalendarEvent = {
|
||||||
|
id: eventId,
|
||||||
|
title: title,
|
||||||
|
start: targetStart,
|
||||||
|
end: targetEnd,
|
||||||
|
type: type,
|
||||||
|
allDay: true,
|
||||||
|
syncStatus: 'synced',
|
||||||
|
metadata: {
|
||||||
|
duration: originalElement.dataset.duration || '60'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use renderer to create and add all-day event
|
||||||
|
const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate);
|
||||||
|
|
||||||
|
if (allDayElement) {
|
||||||
|
// Remove original timed event
|
||||||
|
originalElement.remove();
|
||||||
|
|
||||||
|
// Animate height change
|
||||||
|
this.checkAndAnimateAllDayHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update row height when all-day events change
|
* Update row height when all-day events change
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { IEventBus } from '../types/CalendarTypes';
|
import { IEventBus } from '../types/CalendarTypes';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
|
||||||
interface CachedElements {
|
interface CachedElements {
|
||||||
scrollContainer: HTMLElement | null;
|
scrollContainer: HTMLElement | null;
|
||||||
|
|
@ -93,14 +94,13 @@ export class DragDropManager {
|
||||||
|
|
||||||
// Listen for header mouseover events
|
// Listen for header mouseover events
|
||||||
this.eventBus.on('header:mouseover', (event) => {
|
this.eventBus.on('header:mouseover', (event) => {
|
||||||
const { element, targetDate, headerRenderer } = (event as CustomEvent).detail;
|
const { targetDate, headerRenderer } = (event as CustomEvent).detail;
|
||||||
|
|
||||||
if (this.draggedEventId && targetDate) {
|
if (this.draggedEventId && targetDate) {
|
||||||
// Emit event to convert to all-day
|
// Emit event to convert to all-day
|
||||||
this.eventBus.emit('drag:convert-to-allday', {
|
this.eventBus.emit('drag:convert-to-allday', {
|
||||||
eventId: this.draggedEventId,
|
|
||||||
targetDate,
|
targetDate,
|
||||||
element,
|
originalElement: this.originalElement,
|
||||||
headerRenderer
|
headerRenderer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +110,7 @@ export class DragDropManager {
|
||||||
this.eventBus.on('column:mouseover', (event) => {
|
this.eventBus.on('column:mouseover', (event) => {
|
||||||
const { targetColumn, targetY } = (event as CustomEvent).detail;
|
const { targetColumn, targetY } = (event as CustomEvent).detail;
|
||||||
|
|
||||||
if ((event as any).buttons === 1 && this.draggedEventId && this.isAllDayEventBeingDragged()) {
|
if (this.draggedEventId && this.isAllDayEventBeingDragged()) {
|
||||||
// Emit event to convert to timed
|
// Emit event to convert to timed
|
||||||
this.eventBus.emit('drag:convert-to-timed', {
|
this.eventBus.emit('drag:convert-to-timed', {
|
||||||
eventId: this.draggedEventId,
|
eventId: this.draggedEventId,
|
||||||
|
|
@ -291,7 +291,7 @@ export class DragDropManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consolidated position calculation method
|
* Consolidated position calculation method using PositionUtils
|
||||||
*/
|
*/
|
||||||
private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } {
|
private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } {
|
||||||
const column = this.detectColumn(mousePosition.x, mousePosition.y);
|
const column = this.detectColumn(mousePosition.x, mousePosition.y);
|
||||||
|
|
@ -310,15 +310,14 @@ export class DragDropManager {
|
||||||
const columnElement = this.getCachedColumnElement(targetColumn);
|
const columnElement = this.getCachedColumnElement(targetColumn);
|
||||||
if (!columnElement) return mouseY;
|
if (!columnElement) return mouseY;
|
||||||
|
|
||||||
const columnRect = columnElement.getBoundingClientRect();
|
const relativeY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
|
||||||
const relativeY = mouseY - columnRect.top - this.mouseOffset.y;
|
|
||||||
|
|
||||||
// Return free position (no snapping)
|
// Return free position (no snapping)
|
||||||
return Math.max(0, relativeY);
|
return Math.max(0, relativeY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optimized snap position calculation with caching (used only on drop)
|
* Optimized snap position calculation using PositionUtils
|
||||||
*/
|
*/
|
||||||
private calculateSnapPosition(mouseY: number, column: string | null = null): number {
|
private calculateSnapPosition(mouseY: number, column: string | null = null): number {
|
||||||
const targetColumn = column || this.currentColumn;
|
const targetColumn = column || this.currentColumn;
|
||||||
|
|
@ -327,11 +326,8 @@ export class DragDropManager {
|
||||||
const columnElement = this.getCachedColumnElement(targetColumn);
|
const columnElement = this.getCachedColumnElement(targetColumn);
|
||||||
if (!columnElement) return mouseY;
|
if (!columnElement) return mouseY;
|
||||||
|
|
||||||
const columnRect = columnElement.getBoundingClientRect();
|
// Use PositionUtils for consistent snapping behavior
|
||||||
const relativeY = mouseY - columnRect.top - this.mouseOffset.y;
|
const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
|
||||||
|
|
||||||
// Snap to nearest interval using DateCalculator precision
|
|
||||||
const snappedY = Math.round(relativeY / this.snapDistancePx) * this.snapDistancePx;
|
|
||||||
|
|
||||||
return Math.max(0, snappedY);
|
return Math.max(0, snappedY);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages scrolling functionality for the calendar using native scrollbars
|
* Manages scrolling functionality for the calendar using native scrollbars
|
||||||
|
|
@ -96,13 +97,12 @@ export class ScrollManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll to specific hour
|
* Scroll to specific hour using PositionUtils
|
||||||
*/
|
*/
|
||||||
scrollToHour(hour: number): void {
|
scrollToHour(hour: number): void {
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
// Create time string for the hour
|
||||||
const hourHeight = gridSettings.hourHeight;
|
const timeString = `${hour.toString().padStart(2, '0')}:00`;
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
const scrollTop = PositionUtils.timeToPixels(timeString);
|
||||||
const scrollTop = (hour - dayStartHour) * hourHeight;
|
|
||||||
|
|
||||||
this.scrollTo(scrollTop);
|
this.scrollTo(scrollTop);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Work hours for a specific day
|
* Work hours for a specific day
|
||||||
|
|
@ -91,7 +92,7 @@ export class WorkHoursManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate CSS custom properties for non-work hour overlays (before and after work)
|
* Calculate CSS custom properties for non-work hour overlays using PositionUtils
|
||||||
*/
|
*/
|
||||||
calculateNonWorkHoursStyle(workHours: DayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null {
|
calculateNonWorkHoursStyle(workHours: DayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null {
|
||||||
if (workHours === 'off') {
|
if (workHours === 'off') {
|
||||||
|
|
@ -100,7 +101,6 @@ export class WorkHoursManager {
|
||||||
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
const dayStartHour = gridSettings.dayStartHour;
|
||||||
const dayEndHour = gridSettings.dayEndHour;
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
const hourHeight = gridSettings.hourHeight;
|
||||||
|
|
||||||
// Before work: from day start to work start
|
// Before work: from day start to work start
|
||||||
|
|
@ -109,28 +109,28 @@ export class WorkHoursManager {
|
||||||
// After work: from work end to day end
|
// After work: from work end to day end
|
||||||
const afterWorkTop = (workHours.end - dayStartHour) * hourHeight;
|
const afterWorkTop = (workHours.end - dayStartHour) * hourHeight;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beforeWorkHeight: Math.max(0, beforeWorkHeight),
|
beforeWorkHeight: Math.max(0, beforeWorkHeight),
|
||||||
afterWorkTop: Math.max(0, afterWorkTop)
|
afterWorkTop: Math.max(0, afterWorkTop)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate CSS custom properties for work hours overlay (legacy - for backward compatibility)
|
* Calculate CSS custom properties for work hours overlay using PositionUtils
|
||||||
*/
|
*/
|
||||||
calculateWorkHoursStyle(workHours: DayWorkHours | 'off'): { top: number; height: number } | null {
|
calculateWorkHoursStyle(workHours: DayWorkHours | 'off'): { top: number; height: number } | null {
|
||||||
if (workHours === 'off') {
|
if (workHours === 'off') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
// Create dummy time strings for start and end of work hours
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
const startTime = `${workHours.start.toString().padStart(2, '0')}:00`;
|
||||||
const hourHeight = gridSettings.hourHeight;
|
const endTime = `${workHours.end.toString().padStart(2, '0')}:00`;
|
||||||
|
|
||||||
const top = (workHours.start - dayStartHour) * hourHeight;
|
// Use PositionUtils for consistent position calculation
|
||||||
const height = (workHours.end - workHours.start) * hourHeight;
|
const position = PositionUtils.calculateEventPosition(startTime, endTime);
|
||||||
|
|
||||||
return { top, height };
|
return { top: position.top, height: position.height };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,61 @@
|
||||||
// All-day event rendering using factory pattern
|
|
||||||
|
|
||||||
import { CalendarEvent } from '../types/CalendarTypes';
|
import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllDayEventRenderer - Handles rendering of all-day events in header row
|
* AllDayEventRenderer - Simple rendering of all-day events
|
||||||
* Uses factory pattern with SwpAllDayEventElement for clean DOM creation
|
* Handles adding and removing all-day events from the header container
|
||||||
*/
|
*/
|
||||||
export class AllDayEventRenderer {
|
export class AllDayEventRenderer {
|
||||||
|
private container: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.getContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or cache all-day container
|
||||||
|
*/
|
||||||
|
private getContainer(): HTMLElement | null {
|
||||||
|
if (!this.container) {
|
||||||
|
const header = document.querySelector('swp-calendar-header');
|
||||||
|
if (header) {
|
||||||
|
this.container = header.querySelector('swp-allday-container');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an all-day event using factory pattern
|
||||||
|
*/
|
||||||
|
public renderAllDayEvent(event: CalendarEvent, targetDate: string): HTMLElement | null {
|
||||||
|
const container = this.getContainer();
|
||||||
|
if (!container) return null;
|
||||||
|
|
||||||
|
const allDayElement = SwpAllDayEventElement.fromCalendarEvent(event, targetDate);
|
||||||
|
const element = allDayElement.getElement();
|
||||||
|
|
||||||
|
container.appendChild(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an all-day event by ID
|
||||||
|
*/
|
||||||
|
public removeAllDayEvent(eventId: string): void {
|
||||||
|
const container = this.getContainer();
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const eventElement = container.querySelector(`swp-allday-event[data-event-id="${eventId}"]`);
|
||||||
|
if (eventElement) {
|
||||||
|
eventElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache when DOM changes
|
||||||
|
*/
|
||||||
|
public clearCache(): void {
|
||||||
|
this.container = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
||||||
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
|
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
|
||||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||||
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -695,26 +696,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
|
protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
|
||||||
|
// Delegate to PositionUtils for centralized position calculation
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
return PositionUtils.calculateEventPosition(event.start, event.end);
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
|
||||||
|
|
||||||
// Calculate minutes from midnight
|
|
||||||
const startMinutes = event.start.getHours() * 60 + event.start.getMinutes();
|
|
||||||
const endMinutes = event.end.getHours() * 60 + event.end.getMinutes();
|
|
||||||
const dayStartMinutes = dayStartHour * 60;
|
|
||||||
|
|
||||||
// Calculate top position relative to visible grid start
|
|
||||||
// If dayStartHour=6 and event starts at 09:00 (540 min), then:
|
|
||||||
// top = ((540 - 360) / 60) * hourHeight = 3 * hourHeight (3 hours from grid start)
|
|
||||||
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
|
|
||||||
|
|
||||||
// Calculate height based on event duration
|
|
||||||
const durationMinutes = endMinutes - startMinutes;
|
|
||||||
const height = (durationMinutes / 60) * hourHeight;
|
|
||||||
|
|
||||||
return { top, height };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEvents(container?: HTMLElement): void {
|
clearEvents(container?: HTMLElement): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue