Cleanup test files and move to another folder
This commit is contained in:
parent
8456d8aa28
commit
9c765b35ab
28 changed files with 0 additions and 1981 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -1,217 +0,0 @@
|
||||||
De 6 vigtigste fund (med fixes)
|
|
||||||
|
|
||||||
Gruppering kan “brygge” mellem to grupper uden at merge dem
|
|
||||||
groupEventsByStartTime finder første eksisterende gruppe med konflikt og lægger eventet deri. Hvis et nyt event konflikter med flere grupper, bliver grupperne ikke merged → inkonsistente “grid”-klumper. Løs: merge alle matchende grupper eller brug union-find/sweep-line konfliktsæt.
|
|
||||||
|
|
||||||
EventStackManager
|
|
||||||
|
|
||||||
ContainerType er “GRID” for alle grupper >1 — også ved dybe overlaps
|
|
||||||
decideContainerType returnerer altid 'GRID' når events.length > 1. Det kan være tilsigtet, men så skal du være tryg ved, at lange overlappende events, der kun næsten starter samtidigt, stadig pakkes i kolonner fremfor “stacking”. Overvej: GRID kun når samtidighed er vigtigere end varighed, ellers fald tilbage til STACKING.
|
|
||||||
|
|
||||||
EventStackManager
|
|
||||||
|
|
||||||
Stack level-algoritmen kan eskalere niveauer unødigt
|
|
||||||
createOptimizedStackLinks sætter stackLevel = max(overlappende tidligere) + 1. Det er mere “stak-tårn” end “før-ledig-kolonne” og giver højere niveauer end nødvendigt (ikke minimal farvelægning). Løs: interval partitioning med min-heap (giver laveste ledige level).
|
|
||||||
|
|
||||||
EventStackManager
|
|
||||||
|
|
||||||
Grid-top beregnes fra ét event, men børn positioneres relativt til containerStart
|
|
||||||
I koordinatoren bruges earliestEvent til top, og renderer bruger earliestEvent.start som containerStart. Det er ok — men sørg for, at earliestEvent garanteret er det tidligste i gruppen og sortér eksplicit inden brug (robusthed mod fremtidige ændringer).
|
|
||||||
|
|
||||||
EventLayoutCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
Drag bruger rå new Date(...) i stedet for DateService
|
|
||||||
Kan give TZ/DST-glitches. Brug samme parse/logik som resten.
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
Ingen reflow af kolonne efter drop
|
|
||||||
handleDragEnd normaliserer DOM men recalculerer ikke layout → forkert stacking/margin efter flyt. Kald din kolonne-pipeline igen for den berørte kolonne.
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
Bonus: getEventsForColumn matcher kun start-dato === kolonnedato; events der krydser midnat forsvinder. Overvej interval-overlap mod døgnets [00:00–23:59:59.999].
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
Målrettede patches (små og sikre)
|
|
||||||
A) Merge grupper når et event rammer flere (EventStackManager)
|
|
||||||
|
|
||||||
Erstat den nuværende “find første gruppe”-logik med merge af alle matchende:
|
|
||||||
|
|
||||||
// inde i groupEventsByStartTime
|
|
||||||
const matches: number[] = [];
|
|
||||||
for (let gi = 0; gi < groups.length; gi++) {
|
|
||||||
const group = groups[gi];
|
|
||||||
const conflict = group.events.some(ge => {
|
|
||||||
const s2s = Math.abs(event.start.getTime() - ge.start.getTime()) / 60000;
|
|
||||||
if (s2s <= thresholdMinutes) return true;
|
|
||||||
const e2s = (ge.end.getTime() - event.start.getTime()) / 60000;
|
|
||||||
if (e2s > 0 && e2s <= thresholdMinutes) return true;
|
|
||||||
const rev = (event.end.getTime() - ge.start.getTime()) / 60000;
|
|
||||||
if (rev > 0 && rev <= thresholdMinutes) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (conflict) matches.push(gi);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches.length === 0) {
|
|
||||||
groups.push({ events: [event], containerType: 'NONE', startTime: event.start });
|
|
||||||
} else {
|
|
||||||
// merge alle matchende grupper + dette event
|
|
||||||
const base = matches[0];
|
|
||||||
groups[base].events.push(event);
|
|
||||||
for (let i = matches.length - 1; i >= 1; i--) {
|
|
||||||
const idx = matches[i];
|
|
||||||
groups[base].events.push(...groups[idx].events);
|
|
||||||
groups.splice(idx, 1);
|
|
||||||
}
|
|
||||||
// opdatér startTime til min start
|
|
||||||
groups[base].startTime = new Date(
|
|
||||||
Math.min(...groups[base].events.map(e => e.start.getTime()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Nu undgår du “brobygning” der splitter reelt sammenhængende grupper.
|
|
||||||
|
|
||||||
EventStackManager
|
|
||||||
|
|
||||||
B) Minimal stack level med min-heap (EventStackManager)
|
|
||||||
|
|
||||||
Udskift level-tildeling med klassisk interval partitioning:
|
|
||||||
|
|
||||||
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
|
|
||||||
const res = new Map<string, StackLink>();
|
|
||||||
if (!events.length) return res;
|
|
||||||
|
|
||||||
const sorted = [...events].sort((a,b)=> a.start.getTime() - b.start.getTime());
|
|
||||||
type Col = { level: number; end: number };
|
|
||||||
const cols: Col[] = []; // min-heap på end
|
|
||||||
|
|
||||||
const push = (c: Col) => { cols.push(c); cols.sort((x,y)=> x.end - y.end); };
|
|
||||||
|
|
||||||
for (const ev of sorted) {
|
|
||||||
const t = ev.start.getTime();
|
|
||||||
// find første kolonne der er fri
|
|
||||||
let placed = false;
|
|
||||||
for (let i = 0; i < cols.length; i++) {
|
|
||||||
if (cols[i].end <= t) { cols[i].end = ev.end.getTime(); res.set(ev.id, { stackLevel: cols[i].level }); placed = true; break; }
|
|
||||||
}
|
|
||||||
if (!placed) { const level = cols.length; push({ level, end: ev.end.getTime() }); res.set(ev.id, { stackLevel: level }); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// evt. byg prev/next separat hvis nødvendigt
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Dette giver laveste ledige niveau og undgår “trappetårne”.
|
|
||||||
|
|
||||||
EventStackManager
|
|
||||||
|
|
||||||
C) Konsolidér margin/zIndex + brug DateService i drag (EventRenderer)
|
|
||||||
|
|
||||||
Lad StackManager styre marginLeft konsekvent (og undgå magic numbers):
|
|
||||||
|
|
||||||
// renderGridGroup
|
|
||||||
groupElement.style.top = `${gridGroup.position.top}px`;
|
|
||||||
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel); // i stedet for *15
|
|
||||||
this.stackManager.applyStackLinkToElement(groupElement, { stackLevel: gridGroup.stackLevel });
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
Brug DateService i drag:
|
|
||||||
|
|
||||||
public handleDragMove(payload: DragMoveEventPayload): void {
|
|
||||||
if (!this.draggedClone || !payload.columnBounds) return;
|
|
||||||
const swp = this.draggedClone as SwpEventElement;
|
|
||||||
const colDate = this.dateService.parseISODate?.(payload.columnBounds.date) ?? new Date(payload.columnBounds.date);
|
|
||||||
swp.updatePosition(colDate, payload.snappedY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleColumnChange(e: DragColumnChangeEventPayload): void {
|
|
||||||
if (!this.draggedClone) return;
|
|
||||||
const layer = e.newColumn.element.querySelector('swp-events-layer');
|
|
||||||
if (layer && this.draggedClone.parentElement !== layer) {
|
|
||||||
layer.appendChild(this.draggedClone);
|
|
||||||
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
|
||||||
const swp = this.draggedClone as SwpEventElement;
|
|
||||||
const colDate = this.dateService.parseISODate?.(e.newColumn.date) ?? new Date(e.newColumn.date);
|
|
||||||
swp.updatePosition(colDate, currentTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
D) Reflow efter drop (EventRenderer)
|
|
||||||
|
|
||||||
Genberegn layout for den berørte kolonne:
|
|
||||||
|
|
||||||
public handleDragEnd(id: string, original: HTMLElement, clone: HTMLElement, finalColumn: ColumnBounds): void {
|
|
||||||
if (!clone || !original) { console.warn('Missing clone/original'); return; }
|
|
||||||
this.fadeOutAndRemove(original);
|
|
||||||
const cid = clone.dataset.eventId;
|
|
||||||
if (cid && cid.startsWith('clone-')) clone.dataset.eventId = cid.replace('clone-','');
|
|
||||||
clone.classList.remove('dragging');
|
|
||||||
|
|
||||||
const layer = finalColumn.element.querySelector('swp-events-layer') as HTMLElement | null;
|
|
||||||
if (layer) {
|
|
||||||
// 1) Hent kolonnens events fra din model/state (inkl. opdateret event)
|
|
||||||
const columnEvents: CalendarEvent[] = /* ... */;
|
|
||||||
// 2) Ryd
|
|
||||||
layer.querySelectorAll('swp-event, swp-event-group').forEach(el => el.remove());
|
|
||||||
// 3) Render igen via layout
|
|
||||||
this.renderColumnEvents(columnEvents, layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draggedClone = null;
|
|
||||||
this.originalEvent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
E) Døgn-overlap i kolonnefilter (EventRenderer)
|
|
||||||
|
|
||||||
Hvis ønsket (ellers behold din nuværende):
|
|
||||||
|
|
||||||
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|
||||||
const d = column.dataset.date; if (!d) return [];
|
|
||||||
const start = this.dateService.parseISODate(`${d}T00:00:00`);
|
|
||||||
const end = this.dateService.parseISODate(`${d}T23:59:59.999`);
|
|
||||||
return events.filter(ev => ev.start < end && ev.end > start);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
|
|
||||||
F) Eksplicit “earliest” i GRID (Coordinator)
|
|
||||||
|
|
||||||
Gør det robust i tilfælde af usorteret input:
|
|
||||||
|
|
||||||
const earliestEvent = [...gridCandidates].sort((a,b)=> a.start.getTime()-b.start.getTime())[0];
|
|
||||||
const pos = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
|
||||||
|
|
||||||
|
|
||||||
EventLayoutCoordinator
|
|
||||||
|
|
||||||
Mini-noter
|
|
||||||
|
|
||||||
allocateColumns er O(n²); det er fint for typiske dagvisninger. Hvis I ser >100 events/kolonne, kan I optimere med sweep-line + min-heap.
|
|
||||||
|
|
||||||
EventLayoutCoordinator
|
|
||||||
|
|
||||||
Overvej at lade koordinatoren returnere rene layout-maps (id → {level, z, margin}) og holde DOM-påføring 100% i renderer — det gør DnD-”reflow” enklere at teste.
|
|
||||||
|
|
||||||
EventLayoutCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EventRenderer
|
|
||||||
237
CLAUDE.md
237
CLAUDE.md
|
|
@ -1,237 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Build and Development Commands
|
|
||||||
|
|
||||||
### TypeScript Build
|
|
||||||
- **Build:** `npm run build` - Uses esbuild with NovaDI plugin to bundle to `wwwroot/js/calendar.js`
|
|
||||||
- **Watch:** `npm run watch` - Auto-rebuild on file changes
|
|
||||||
- **Clean:** `npm run clean` - Remove compiled output
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- **Run tests:** `npm test` or `vitest` - Interactive watch mode
|
|
||||||
- **Run once:** `npm run test:run` or `vitest run`
|
|
||||||
- **Test UI:** `npm run test:ui` - Visual test interface
|
|
||||||
- Tests use Vitest with jsdom environment (see `vitest.config.ts`)
|
|
||||||
|
|
||||||
### CSS Build
|
|
||||||
- **Build CSS:** `npm run css:build` - PostCSS with nesting support
|
|
||||||
- **Watch CSS:** `npm run css:watch` - Auto-rebuild CSS on changes
|
|
||||||
- **Production CSS:** `npm run css:build:prod` - Minified with PurgeCSS
|
|
||||||
- **Analyze CSS:** `npm run css:analyze` - CSS statistics and analysis
|
|
||||||
|
|
||||||
### Server
|
|
||||||
- **Start:** `dotnet run` - ASP.NET Core Kestrel server on `http://localhost:8000`
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
### Core Architectural Pattern
|
|
||||||
This is a **manager-based, event-driven calendar application** using pure TypeScript with no UI frameworks. Communication happens exclusively through DOM CustomEvents via a central EventBus.
|
|
||||||
|
|
||||||
**Key Principles:**
|
|
||||||
- **No global state** - State lives in managers
|
|
||||||
- **Event-driven** - All inter-component communication via CustomEvents (see `CoreEvents` constants)
|
|
||||||
- **Dependency Injection** - Uses `@novadi/core` DI container
|
|
||||||
- **Pure DOM** - No React/Vue/Angular, just vanilla TypeScript + DOM manipulation
|
|
||||||
|
|
||||||
### Dependency Injection Flow
|
|
||||||
|
|
||||||
The application initializes in `src/index.ts` following this sequence:
|
|
||||||
|
|
||||||
1. **CalendarConfig.initialize()** - Static config from DOM attributes (`<swp-calendar>`)
|
|
||||||
2. **Container setup** - Register all services, managers, renderers, utilities
|
|
||||||
3. **Manager initialization** - CalendarManager coordinates all other managers
|
|
||||||
4. **Deep linking** - Handle URL-based event navigation
|
|
||||||
|
|
||||||
All dependencies are auto-wired using NovaDI's `@inject` decorators (configured in `build.js`).
|
|
||||||
|
|
||||||
### Event System
|
|
||||||
|
|
||||||
**EventBus** (`src/core/EventBus.ts`) wraps DOM CustomEvents with debugging/logging:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Emit
|
|
||||||
eventBus.emit('view:changed', { view: 'week', date: new Date() });
|
|
||||||
|
|
||||||
// Listen
|
|
||||||
eventBus.on('view:changed', (event: CustomEvent) => {
|
|
||||||
const { view, date } = event.detail;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Core events** are defined in `src/constants/CoreEvents.ts` (~20 essential events organized by category: lifecycle, view, navigation, data, grid, event management, system, filter, rendering).
|
|
||||||
|
|
||||||
### Manager Architecture
|
|
||||||
|
|
||||||
Managers are the core organizational units. Each has a specific responsibility:
|
|
||||||
|
|
||||||
**Primary Managers:**
|
|
||||||
- `CalendarManager` - Main coordinator, initializes all managers
|
|
||||||
- `ViewManager` - Handles view switching (day/week/month)
|
|
||||||
- `NavigationManager` - Prev/next/today navigation, date changes
|
|
||||||
- `EventManager` - Event CRUD operations, selection, lifecycle
|
|
||||||
- `GridManager` - Calendar grid structure and layout
|
|
||||||
- `HeaderManager` - Date headers and column rendering
|
|
||||||
- `AllDayManager` - All-day event section management
|
|
||||||
|
|
||||||
**Interaction Managers:**
|
|
||||||
- `DragDropManager` - Event drag-and-drop functionality
|
|
||||||
- `ResizeHandleManager` - Event resize handles
|
|
||||||
- `DragHoverManager` - Visual feedback during drag operations
|
|
||||||
- `EdgeScrollManager` - Auto-scroll when dragging near edges
|
|
||||||
- `ScrollManager` - Grid scroll behavior
|
|
||||||
|
|
||||||
**Support Managers:**
|
|
||||||
- `ConfigManager` - Event-driven config updates (wraps CalendarConfig) and manages CSS custom properties
|
|
||||||
- `EventLayoutCoordinator` - Coordinates event positioning
|
|
||||||
- `EventStackManager` - Handles overlapping events
|
|
||||||
- `EventFilterManager` - Filter events by criteria
|
|
||||||
- `WorkHoursManager` - Work hours highlighting
|
|
||||||
|
|
||||||
### Renderer Architecture
|
|
||||||
|
|
||||||
Renderers handle DOM creation and updates (separation of concerns from managers):
|
|
||||||
|
|
||||||
- `EventRenderingService` - Main event rendering coordinator
|
|
||||||
- `DateEventRenderer` / `AllDayEventRenderer` - Event DOM generation
|
|
||||||
- `DateHeaderRenderer` - Date header rendering
|
|
||||||
- `DateColumnRenderer` - Column structure
|
|
||||||
- `GridRenderer` - Grid structure and time slots
|
|
||||||
- `NavigationRenderer` - Navigation controls
|
|
||||||
|
|
||||||
### Core Services
|
|
||||||
|
|
||||||
**CalendarConfig** (`src/core/CalendarConfig.ts`):
|
|
||||||
- Static configuration class
|
|
||||||
- Loads settings from DOM data attributes on `<swp-calendar>` element
|
|
||||||
- Provides computed values (hourHeight, snapInterval, totalSlots, etc.)
|
|
||||||
- ConfigManager wraps it for event-driven updates and automatically syncs CSS custom properties to the DOM
|
|
||||||
|
|
||||||
**DateService** (`src/utils/DateService.ts`):
|
|
||||||
- Uses `date-fns` and `date-fns-tz` for date calculations
|
|
||||||
- Default timezone: `Europe/Copenhagen`, locale: `da-DK`
|
|
||||||
|
|
||||||
**TimeFormatter** (`src/utils/TimeFormatter.ts`):
|
|
||||||
- Consistent time/date formatting across the app
|
|
||||||
- Configured via CalendarConfig
|
|
||||||
|
|
||||||
**PositionUtils** (`src/utils/PositionUtils.ts`):
|
|
||||||
- Convert between pixels and times
|
|
||||||
- Snap-to-grid calculations
|
|
||||||
|
|
||||||
**URLManager** (`src/utils/URLManager.ts`):
|
|
||||||
- Deep linking to events
|
|
||||||
- Parses `eventId` from URL
|
|
||||||
|
|
||||||
### Repository Pattern
|
|
||||||
|
|
||||||
Event data is accessed through `IEventRepository` interface:
|
|
||||||
- `MockEventRepository` - Current implementation using mock data from `wwwroot/data/mock-events.json`
|
|
||||||
- Ready for API implementation swap
|
|
||||||
|
|
||||||
## Code Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── constants/ # CoreEvents and other constants
|
|
||||||
├── core/ # EventBus, CalendarConfig (core infrastructure)
|
|
||||||
├── data/ # Data models and utilities
|
|
||||||
├── elements/ # Custom HTML elements (if any)
|
|
||||||
├── managers/ # Manager classes (business logic)
|
|
||||||
├── renderers/ # DOM rendering logic
|
|
||||||
├── repositories/ # Data access layer (IEventRepository, MockEventRepository)
|
|
||||||
├── types/ # TypeScript interfaces and types
|
|
||||||
├── utils/ # Utility functions (DateService, PositionUtils, etc.)
|
|
||||||
└── index.ts # Application entry point and DI setup
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Patterns
|
|
||||||
|
|
||||||
### Adding a New Manager
|
|
||||||
|
|
||||||
1. Create in `src/managers/YourManager.ts`
|
|
||||||
2. Use `@inject` for dependencies
|
|
||||||
3. Implement optional `initialize()` method if needed
|
|
||||||
4. Register in `src/index.ts` DI container
|
|
||||||
5. Listen to events via `eventBus.on()` (injected as `IEventBus`)
|
|
||||||
6. Emit events via `eventBus.emit()`
|
|
||||||
|
|
||||||
### Event Naming Convention
|
|
||||||
|
|
||||||
Events follow `category:action` pattern:
|
|
||||||
- `view:changed`, `view:rendered`
|
|
||||||
- `nav:date-changed`, `nav:navigation-completed`
|
|
||||||
- `data:loaded`, `data:error`
|
|
||||||
- `event:created`, `event:updated`, `event:deleted`
|
|
||||||
- `grid:rendered`, `grid:clicked`
|
|
||||||
|
|
||||||
### Grid Positioning
|
|
||||||
|
|
||||||
Events are positioned using CSS Grid and absolute positioning:
|
|
||||||
- Time slots are calculated via `CalendarConfig.slotHeight` and `minuteHeight`
|
|
||||||
- `PositionUtils` handles pixel ↔ time conversions
|
|
||||||
- Snap-to-grid uses `CalendarConfig.getGridSettings().snapInterval`
|
|
||||||
|
|
||||||
### Work Week Configuration
|
|
||||||
|
|
||||||
CalendarConfig supports work week presets:
|
|
||||||
- `standard` - Mon-Fri (default)
|
|
||||||
- `compressed` - Mon-Thu
|
|
||||||
- `midweek` - Wed-Fri
|
|
||||||
- `weekend` - Sat-Sun
|
|
||||||
- `fullweek` - Mon-Sun
|
|
||||||
|
|
||||||
Change via `CalendarConfig.setWorkWeek('preset-id')`
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Tests are written using Vitest with jsdom. Setup file: `test/setup.ts`
|
|
||||||
|
|
||||||
Run individual test file:
|
|
||||||
```bash
|
|
||||||
vitest run path/to/test-file.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## CSS Architecture
|
|
||||||
|
|
||||||
CSS is modular and built with PostCSS:
|
|
||||||
- **Source:** `wwwroot/css/src/` (uses PostCSS nesting)
|
|
||||||
- **Output:** `wwwroot/css/`
|
|
||||||
- **Main file:** `calendar.css` (currently used)
|
|
||||||
|
|
||||||
Planned modular CSS files:
|
|
||||||
- `calendar-base-css.css` - Variables and base styles
|
|
||||||
- `calendar-components-css.css` - UI components
|
|
||||||
- `calendar-events-css.css` - Event styling
|
|
||||||
- `calendar-layout-css.css` - Grid layout
|
|
||||||
- `calendar-popup-css.css` - Modals and popups
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
Enable EventBus debug mode (already enabled in `src/index.ts`):
|
|
||||||
```typescript
|
|
||||||
eventBus.setDebug(true);
|
|
||||||
```
|
|
||||||
|
|
||||||
Access debug interface in browser console:
|
|
||||||
```javascript
|
|
||||||
window.calendarDebug.eventBus.getEventLog(); // All events
|
|
||||||
window.calendarDebug.eventManager; // Access EventManager
|
|
||||||
window.calendarDebug.calendarManager; // Access CalendarManager
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration via HTML
|
|
||||||
|
|
||||||
Set calendar options via data attributes on `<swp-calendar>`:
|
|
||||||
```html
|
|
||||||
<swp-calendar
|
|
||||||
data-view="week"
|
|
||||||
data-week-days="7"
|
|
||||||
data-snap-interval="15"
|
|
||||||
data-day-start-hour="6"
|
|
||||||
data-day-end-hour="22"
|
|
||||||
data-hour-height="60"
|
|
||||||
data-fit-to-width="false">
|
|
||||||
</swp-calendar>
|
|
||||||
```
|
|
||||||
|
|
@ -1,578 +0,0 @@
|
||||||
# Cyclomatic Complexity Analysis Report
|
|
||||||
**Calendar Plantempus Project**
|
|
||||||
Generated: 2025-10-04
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This report analyzes the cyclomatic complexity of the Calendar Plantempus TypeScript codebase, focusing on identifying methods that exceed recommended complexity thresholds and require refactoring.
|
|
||||||
|
|
||||||
### Key Metrics
|
|
||||||
|
|
||||||
| Metric | Value |
|
|
||||||
|--------|-------|
|
|
||||||
| **Total Files Analyzed** | 6 |
|
|
||||||
| **Total Methods Analyzed** | 74 |
|
|
||||||
| **Methods with Complexity >10** | 4 (5.4%) |
|
|
||||||
| **Methods with Complexity 6-10** | 5 (6.8%) |
|
|
||||||
| **Methods with Complexity 1-5** | 65 (87.8%) |
|
|
||||||
|
|
||||||
### Complexity Distribution
|
|
||||||
|
|
||||||
```
|
|
||||||
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ Low (1-5): 87.8%
|
|
||||||
■■■ Medium (6-10): 6.8%
|
|
||||||
■ High (>10): 5.4%
|
|
||||||
```
|
|
||||||
|
|
||||||
### Overall Assessment
|
|
||||||
|
|
||||||
✅ **Strengths:**
|
|
||||||
- 87.8% of methods have acceptable complexity
|
|
||||||
- Web Components (SwpEventElement) demonstrate excellent design
|
|
||||||
- Rendering services show clean separation of concerns
|
|
||||||
|
|
||||||
🔴 **Critical Issues:**
|
|
||||||
- 4 methods exceed complexity threshold of 10
|
|
||||||
- Stack management logic is overly complex (complexity 18!)
|
|
||||||
- Drag & drop handlers need refactoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detailed File Analysis
|
|
||||||
|
|
||||||
### 1. DragDropManager.ts
|
|
||||||
**File:** `src/managers/DragDropManager.ts`
|
|
||||||
**Overall Complexity:** HIGH ⚠️
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `init()` | 88-133 | 7 | 🟡 Medium | Event listener setup could be extracted |
|
|
||||||
| `handleMouseDown()` | 135-168 | 5 | ✅ OK | Acceptable complexity |
|
|
||||||
| `handleMouseMove()` | 173-260 | **15** | 🔴 **Critical** | **NEEDS IMMEDIATE REFACTORING** |
|
|
||||||
| `handleMouseUp()` | 265-310 | 4 | ✅ OK | Clean implementation |
|
|
||||||
| `cleanupAllClones()` | 312-320 | 2 | ✅ OK | Simple utility method |
|
|
||||||
| `cancelDrag()` | 325-350 | 3 | ✅ OK | Straightforward cleanup |
|
|
||||||
| `calculateDragPosition()` | 355-364 | 2 | ✅ OK | Simple calculation |
|
|
||||||
| `calculateSnapPosition()` | 369-377 | 1 | ✅ OK | Base complexity |
|
|
||||||
| `checkAutoScroll()` | 383-403 | 5 | ✅ OK | Could be simplified slightly |
|
|
||||||
| `startAutoScroll()` | 408-444 | 6 | 🟡 Medium | Autoscroll logic could be extracted |
|
|
||||||
| `stopAutoScroll()` | 449-454 | 2 | ✅ OK | Simple cleanup |
|
|
||||||
| `detectDropTarget()` | 468-483 | 4 | ✅ OK | Clear DOM traversal |
|
|
||||||
| `handleHeaderMouseEnter()` | 488-516 | 4 | ✅ OK | Clean event handling |
|
|
||||||
| `handleHeaderMouseLeave()` | 521-544 | 4 | ✅ OK | Clean event handling |
|
|
||||||
|
|
||||||
**Decision Points in handleMouseMove():**
|
|
||||||
1. `if (event.buttons === 1)` - Check if mouse button is pressed
|
|
||||||
2. `if (!this.isDragStarted && this.draggedElement)` - Check for drag initialization
|
|
||||||
3. `if (totalMovement >= this.dragThreshold)` - Movement threshold check
|
|
||||||
4. `if (this.isDragStarted && this.draggedElement && this.draggedClone)` - Drag state validation
|
|
||||||
5. `if (!this.draggedElement.hasAttribute("data-allday"))` - Event type check
|
|
||||||
6. `if (deltaY >= this.snapDistancePx)` - Snap interval check
|
|
||||||
7. Multiple autoscroll conditionals
|
|
||||||
8. `if (newColumn == null)` - Column validation
|
|
||||||
9. `if (newColumn?.index !== this.currentColumnBounds?.index)` - Column change detection
|
|
||||||
|
|
||||||
**Recommendation for handleMouseMove():**
|
|
||||||
```typescript
|
|
||||||
// Current: 88 lines, complexity 15
|
|
||||||
// Suggested refactoring:
|
|
||||||
|
|
||||||
private handleMouseMove(event: MouseEvent): void {
|
|
||||||
this.updateMousePosition(event);
|
|
||||||
|
|
||||||
if (!this.isMouseButtonPressed(event)) return;
|
|
||||||
|
|
||||||
if (this.shouldStartDrag()) {
|
|
||||||
this.initializeDrag();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isDragActive()) {
|
|
||||||
this.updateDragPosition();
|
|
||||||
this.handleColumnChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract methods with complexity 2-4 each:
|
|
||||||
// - initializeDrag()
|
|
||||||
// - updateDragPosition()
|
|
||||||
// - handleColumnChange()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. SwpEventElement.ts
|
|
||||||
**File:** `src/elements/SwpEventElement.ts`
|
|
||||||
**Overall Complexity:** LOW ✅
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `connectedCallback()` | 84-89 | 2 | ✅ OK | Simple initialization |
|
|
||||||
| `attributeChangedCallback()` | 94-98 | 2 | ✅ OK | Clean attribute handling |
|
|
||||||
| `updatePosition()` | 109-128 | 2 | ✅ OK | Straightforward update logic |
|
|
||||||
| `createClone()` | 133-152 | 2 | ✅ OK | Simple cloning |
|
|
||||||
| `render()` | 161-171 | 1 | ✅ OK | Base complexity |
|
|
||||||
| `updateDisplay()` | 176-194 | 3 | ✅ OK | Clean DOM updates |
|
|
||||||
| `applyPositioning()` | 199-205 | 1 | ✅ OK | Delegates to PositionUtils |
|
|
||||||
| `calculateTimesFromPosition()` | 210-230 | 1 | ✅ OK | Simple calculation |
|
|
||||||
| `fromCalendarEvent()` (static) | 239-252 | 1 | ✅ OK | Factory method |
|
|
||||||
| `extractCalendarEventFromElement()` (static) | 257-270 | 1 | ✅ OK | Clean extraction |
|
|
||||||
| `fromAllDayElement()` (static) | 275-311 | 4 | ✅ OK | Acceptable conversion logic |
|
|
||||||
| `SwpAllDayEventElement.connectedCallback()` | 319-323 | 2 | ✅ OK | Simple setup |
|
|
||||||
| `SwpAllDayEventElement.createClone()` | 328-335 | 1 | ✅ OK | Base complexity |
|
|
||||||
| `SwpAllDayEventElement.applyGridPositioning()` | 340-343 | 1 | ✅ OK | Simple positioning |
|
|
||||||
| `SwpAllDayEventElement.fromCalendarEvent()` (static) | 348-362 | 1 | ✅ OK | Factory method |
|
|
||||||
|
|
||||||
**Best Practices Demonstrated:**
|
|
||||||
- ✅ Clear separation of concerns
|
|
||||||
- ✅ Factory methods for object creation
|
|
||||||
- ✅ Delegation to utility classes (PositionUtils, DateService)
|
|
||||||
- ✅ BaseSwpEventElement abstraction reduces duplication
|
|
||||||
- ✅ All methods stay within complexity threshold
|
|
||||||
|
|
||||||
**This file serves as a model for good design in the codebase.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. SimpleEventOverlapManager.ts
|
|
||||||
**File:** `src/managers/SimpleEventOverlapManager.ts`
|
|
||||||
**Overall Complexity:** HIGH ⚠️
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `resolveOverlapType()` | 33-58 | 4 | ✅ OK | Clear overlap detection |
|
|
||||||
| `groupOverlappingElements()` | 64-84 | 4 | ✅ OK | Acceptable grouping logic |
|
|
||||||
| `createEventGroup()` | 89-92 | 1 | ✅ OK | Simple factory |
|
|
||||||
| `addToEventGroup()` | 97-113 | 2 | ✅ OK | Straightforward addition |
|
|
||||||
| `createStackedEvent()` | 118-165 | 7 | 🟡 Medium | Chain traversal could be extracted |
|
|
||||||
| `removeStackedStyling()` | 170-284 | **18** | 🔴 **Critical** | **MOST COMPLEX METHOD IN CODEBASE** |
|
|
||||||
| `updateSubsequentStackLevels()` | 289-313 | 5 | ✅ OK | Could be simplified |
|
|
||||||
| `isStackedEvent()` | 318-324 | 3 | ✅ OK | Simple boolean check |
|
|
||||||
| `removeFromEventGroup()` | 329-364 | 6 | 🟡 Medium | Remaining event handling complex |
|
|
||||||
| `restackEventsInContainer()` | 369-432 | **11** | 🔴 **High** | **NEEDS REFACTORING** |
|
|
||||||
| `getEventGroup()` | 438-440 | 1 | ✅ OK | Simple utility |
|
|
||||||
| `isInEventGroup()` | 442-444 | 1 | ✅ OK | Simple utility |
|
|
||||||
| `getStackLink()` | 449-459 | 3 | ✅ OK | JSON parsing with error handling |
|
|
||||||
| `setStackLink()` | 461-467 | 2 | ✅ OK | Simple setter |
|
|
||||||
| `findElementById()` | 469-471 | 1 | ✅ OK | Base complexity |
|
|
||||||
|
|
||||||
**Critical Issue: removeStackedStyling() - Complexity 18**
|
|
||||||
|
|
||||||
**Decision Points Breakdown:**
|
|
||||||
1. `if (link)` - Check if element has stack link
|
|
||||||
2. `if (link.prev && link.next)` - Middle element in chain
|
|
||||||
3. `if (prevElement && nextElement)` - Both neighbors exist
|
|
||||||
4. `if (!actuallyOverlap)` - Chain breaking decision (CRITICAL BRANCH)
|
|
||||||
5. `if (nextLink?.next)` - Subsequent elements exist
|
|
||||||
6. `while (subsequentId)` - Loop through chain
|
|
||||||
7. `if (!subsequentElement)` - Element validation
|
|
||||||
8. `else` - Normal stacking (chain maintenance)
|
|
||||||
9. `else if (link.prev)` - Last element case
|
|
||||||
10. `if (prevElement)` - Previous element exists
|
|
||||||
11. `else if (link.next)` - First element case
|
|
||||||
12. `if (nextElement)` - Next element exists
|
|
||||||
13. `if (link.prev && link.next)` - Middle element check (duplicate)
|
|
||||||
14. `if (nextLink && nextLink.next)` - Chain continuation
|
|
||||||
15. `else` - Chain was broken
|
|
||||||
16-18. Additional nested conditions
|
|
||||||
|
|
||||||
**Recommendation for removeStackedStyling():**
|
|
||||||
```typescript
|
|
||||||
// Current: 115 lines, complexity 18
|
|
||||||
// Suggested refactoring:
|
|
||||||
|
|
||||||
public removeStackedStyling(eventElement: HTMLElement): void {
|
|
||||||
this.clearVisualStyling(eventElement);
|
|
||||||
|
|
||||||
const link = this.getStackLink(eventElement);
|
|
||||||
if (!link) return;
|
|
||||||
|
|
||||||
// Delegate to specialized methods based on position in chain
|
|
||||||
if (link.prev && link.next) {
|
|
||||||
this.removeMiddleElementFromChain(eventElement, link);
|
|
||||||
} else if (link.prev) {
|
|
||||||
this.removeLastElementFromChain(eventElement, link);
|
|
||||||
} else if (link.next) {
|
|
||||||
this.removeFirstElementFromChain(eventElement, link);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setStackLink(eventElement, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract to separate methods:
|
|
||||||
// - clearVisualStyling() - complexity 1
|
|
||||||
// - removeMiddleElementFromChain() - complexity 5-6
|
|
||||||
// - removeLastElementFromChain() - complexity 3
|
|
||||||
// - removeFirstElementFromChain() - complexity 3
|
|
||||||
// - breakStackChain() - complexity 4
|
|
||||||
// - maintainStackChain() - complexity 4
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical Issue: restackEventsInContainer() - Complexity 11**
|
|
||||||
|
|
||||||
**Decision Points:**
|
|
||||||
1. `if (stackedEvents.length === 0)` - Early return
|
|
||||||
2. `for (const element of stackedEvents)` - Iterate events
|
|
||||||
3. `if (!eventId || processedEventIds.has(eventId))` - Validation
|
|
||||||
4. `while (rootLink?.prev)` - Find root of chain
|
|
||||||
5. `if (!prevElement)` - Break condition
|
|
||||||
6. `while (currentElement)` - Traverse chain
|
|
||||||
7. `if (!currentLink?.next)` - End of chain
|
|
||||||
8. `if (!nextElement)` - Break condition
|
|
||||||
9. `if (chain.length > 1)` - Only add multi-element chains
|
|
||||||
10. `forEach` - Restack each chain
|
|
||||||
11. `if (link)` - Update link data
|
|
||||||
|
|
||||||
**Recommendation for restackEventsInContainer():**
|
|
||||||
```typescript
|
|
||||||
// Current: 64 lines, complexity 11
|
|
||||||
// Suggested refactoring:
|
|
||||||
|
|
||||||
public restackEventsInContainer(container: HTMLElement): void {
|
|
||||||
const stackedEvents = this.getStackedEvents(container);
|
|
||||||
if (stackedEvents.length === 0) return;
|
|
||||||
|
|
||||||
const stackChains = this.collectStackChains(stackedEvents);
|
|
||||||
stackChains.forEach(chain => this.reapplyStackStyling(chain));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract to separate methods:
|
|
||||||
// - getStackedEvents() - complexity 2
|
|
||||||
// - collectStackChains() - complexity 6
|
|
||||||
// - findStackRoot() - complexity 3
|
|
||||||
// - traverseChain() - complexity 3
|
|
||||||
// - reapplyStackStyling() - complexity 2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. EventRendererManager.ts
|
|
||||||
**File:** `src/renderers/EventRendererManager.ts`
|
|
||||||
**Overall Complexity:** MEDIUM 🟡
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `renderEvents()` | 35-68 | 3 | ✅ OK | Clean rendering logic |
|
|
||||||
| `setupEventListeners()` | 70-95 | 1 | ✅ OK | Simple delegation |
|
|
||||||
| `handleGridRendered()` | 101-127 | 5 | ✅ OK | Could reduce conditionals |
|
|
||||||
| `handleViewChanged()` | 133-138 | 1 | ✅ OK | Simple cleanup |
|
|
||||||
| `setupDragEventListeners()` | 144-238 | **10** | 🔴 **High** | **NEEDS REFACTORING** |
|
|
||||||
| `handleConvertToTimeEvent()` | 243-292 | 4 | ✅ OK | Acceptable conversion logic |
|
|
||||||
| `clearEvents()` | 294-296 | 1 | ✅ OK | Delegates to strategy |
|
|
||||||
| `refresh()` | 298-300 | 1 | ✅ OK | Simple refresh |
|
|
||||||
|
|
||||||
**Issue: setupDragEventListeners() - Complexity 10**
|
|
||||||
|
|
||||||
**Decision Points:**
|
|
||||||
1. `if (hasAttribute('data-allday'))` - Filter all-day events
|
|
||||||
2. `if (draggedElement && strategy.handleDragStart && columnBounds)` - Validation
|
|
||||||
3. `if (hasAttribute('data-allday'))` - Filter check
|
|
||||||
4. `if (strategy.handleDragMove)` - Strategy check
|
|
||||||
5. `if (strategy.handleDragAutoScroll)` - Strategy check
|
|
||||||
6. `if (target === 'swp-day-column' && finalColumn)` - Drop target validation
|
|
||||||
7. `if (draggedElement && draggedClone && strategy.handleDragEnd)` - Validation
|
|
||||||
8. `if (dayEventClone)` - Cleanup check
|
|
||||||
9. `if (hasAttribute('data-allday'))` - Filter check
|
|
||||||
10. `if (strategy.handleColumnChange)` - Strategy check
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
```typescript
|
|
||||||
// Current: 95 lines, complexity 10
|
|
||||||
// Suggested refactoring:
|
|
||||||
|
|
||||||
private setupDragEventListeners(): void {
|
|
||||||
this.setupDragStartListener();
|
|
||||||
this.setupDragMoveListener();
|
|
||||||
this.setupDragEndListener();
|
|
||||||
this.setupDragAutoScrollListener();
|
|
||||||
this.setupColumnChangeListener();
|
|
||||||
this.setupConversionListener();
|
|
||||||
this.setupNavigationListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each listener method: complexity 2-3
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. EventRenderer.ts
|
|
||||||
**File:** `src/renderers/EventRenderer.ts`
|
|
||||||
**Overall Complexity:** LOW ✅
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `handleDragStart()` | 50-72 | 2 | ✅ OK | Clean drag initialization |
|
|
||||||
| `handleDragMove()` | 77-84 | 2 | ✅ OK | Simple position update |
|
|
||||||
| `handleDragAutoScroll()` | 89-97 | 2 | ✅ OK | Simple scroll handling |
|
|
||||||
| `handleColumnChange()` | 102-115 | 3 | ✅ OK | Clean column switching |
|
|
||||||
| `handleDragEnd()` | 120-141 | 3 | ✅ OK | Proper cleanup |
|
|
||||||
| `handleNavigationCompleted()` | 146-148 | 1 | ✅ OK | Placeholder method |
|
|
||||||
| `fadeOutAndRemove()` | 153-160 | 1 | ✅ OK | Simple animation |
|
|
||||||
| `renderEvents()` | 163-182 | 2 | ✅ OK | Straightforward rendering |
|
|
||||||
| `renderEvent()` | 184-186 | 1 | ✅ OK | Factory delegation |
|
|
||||||
| `calculateEventPosition()` | 188-191 | 1 | ✅ OK | Delegates to utility |
|
|
||||||
| `clearEvents()` | 193-200 | 2 | ✅ OK | Simple cleanup |
|
|
||||||
| `getColumns()` | 202-205 | 1 | ✅ OK | DOM query |
|
|
||||||
| `getEventsForColumn()` | 207-221 | 2 | ✅ OK | Filter logic |
|
|
||||||
|
|
||||||
**Best Practices:**
|
|
||||||
- ✅ All methods under complexity 4
|
|
||||||
- ✅ Clear method names
|
|
||||||
- ✅ Delegation to utilities
|
|
||||||
- ✅ Single responsibility per method
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. AllDayEventRenderer.ts
|
|
||||||
**File:** `src/renderers/AllDayEventRenderer.ts`
|
|
||||||
**Overall Complexity:** LOW ✅
|
|
||||||
|
|
||||||
| Method | Lines | Complexity | Status | Notes |
|
|
||||||
|--------|-------|------------|--------|-------|
|
|
||||||
| `getContainer()` | 20-32 | 3 | ✅ OK | Container initialization |
|
|
||||||
| `getAllDayContainer()` | 35-37 | 1 | ✅ OK | Simple query |
|
|
||||||
| `handleDragStart()` | 41-65 | 3 | ✅ OK | Clean drag setup |
|
|
||||||
| `renderAllDayEventWithLayout()` | 72-83 | 2 | ✅ OK | Simple rendering |
|
|
||||||
| `removeAllDayEvent()` | 89-97 | 3 | ✅ OK | Clean removal |
|
|
||||||
| `clearCache()` | 102-104 | 1 | ✅ OK | Simple reset |
|
|
||||||
| `renderAllDayEventsForPeriod()` | 109-116 | 1 | ✅ OK | Delegates to helper |
|
|
||||||
| `clearAllDayEvents()` | 118-123 | 2 | ✅ OK | Simple cleanup |
|
|
||||||
| `handleViewChanged()` | 125-127 | 1 | ✅ OK | Simple handler |
|
|
||||||
|
|
||||||
**Best Practices:**
|
|
||||||
- ✅ Consistent low complexity across all methods
|
|
||||||
- ✅ Clear separation of concerns
|
|
||||||
- ✅ Focused functionality
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Immediate Action Required (Complexity >10)
|
|
||||||
|
|
||||||
#### 1. SimpleEventOverlapManager.removeStackedStyling() - Priority: CRITICAL
|
|
||||||
**Current Complexity:** 18
|
|
||||||
**Target Complexity:** 4-6 per method
|
|
||||||
|
|
||||||
**Refactoring Steps:**
|
|
||||||
1. Extract `clearVisualStyling()` - Remove inline styles
|
|
||||||
2. Extract `removeMiddleElementFromChain()` - Handle middle element removal
|
|
||||||
3. Extract `removeLastElementFromChain()` - Handle last element removal
|
|
||||||
4. Extract `removeFirstElementFromChain()` - Handle first element removal
|
|
||||||
5. Extract `breakStackChain()` - Handle non-overlapping chain breaking
|
|
||||||
6. Extract `maintainStackChain()` - Handle overlapping chain maintenance
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Main method: complexity 4
|
|
||||||
- Helper methods: complexity 3-6 each
|
|
||||||
- Improved testability
|
|
||||||
- Easier maintenance
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 2. DragDropManager.handleMouseMove() - Priority: HIGH
|
|
||||||
**Current Complexity:** 15
|
|
||||||
**Target Complexity:** 4-5 per method
|
|
||||||
|
|
||||||
**Refactoring Steps:**
|
|
||||||
1. Extract `updateMousePosition()` - Update tracking variables
|
|
||||||
2. Extract `shouldStartDrag()` - Check movement threshold
|
|
||||||
3. Extract `initializeDrag()` - Create clone and emit start event
|
|
||||||
4. Extract `updateDragPosition()` - Handle position and autoscroll
|
|
||||||
5. Extract `handleColumnChange()` - Detect and handle column transitions
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Main method: complexity 4
|
|
||||||
- Helper methods: complexity 3-4 each
|
|
||||||
- Better separation of drag lifecycle stages
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 3. SimpleEventOverlapManager.restackEventsInContainer() - Priority: HIGH
|
|
||||||
**Current Complexity:** 11
|
|
||||||
**Target Complexity:** 3-4 per method
|
|
||||||
|
|
||||||
**Refactoring Steps:**
|
|
||||||
1. Extract `getStackedEvents()` - Filter stacked events
|
|
||||||
2. Extract `collectStackChains()` - Build stack chains
|
|
||||||
3. Extract `findStackRoot()` - Find root of chain
|
|
||||||
4. Extract `traverseChain()` - Collect chain elements
|
|
||||||
5. Extract `reapplyStackStyling()` - Apply visual styling
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Main method: complexity 3
|
|
||||||
- Helper methods: complexity 2-4 each
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4. EventRendererManager.setupDragEventListeners() - Priority: MEDIUM
|
|
||||||
**Current Complexity:** 10
|
|
||||||
**Target Complexity:** 2-3 per method
|
|
||||||
|
|
||||||
**Refactoring Steps:**
|
|
||||||
1. Extract `setupDragStartListener()`
|
|
||||||
2. Extract `setupDragMoveListener()`
|
|
||||||
3. Extract `setupDragEndListener()`
|
|
||||||
4. Extract `setupDragAutoScrollListener()`
|
|
||||||
5. Extract `setupColumnChangeListener()`
|
|
||||||
6. Extract `setupConversionListener()`
|
|
||||||
7. Extract `setupNavigationListener()`
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Main method: complexity 1 (just calls helpers)
|
|
||||||
- Helper methods: complexity 2-3 each
|
|
||||||
- Improved readability
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Medium Priority (Complexity 6-10)
|
|
||||||
|
|
||||||
#### 5. SimpleEventOverlapManager.createStackedEvent() - Complexity 7
|
|
||||||
Consider extracting chain traversal logic into `findEndOfChain()`
|
|
||||||
|
|
||||||
#### 6. DragDropManager.startAutoScroll() - Complexity 6
|
|
||||||
Extract scroll calculation into `calculateScrollAmount()`
|
|
||||||
|
|
||||||
#### 7. SimpleEventOverlapManager.removeFromEventGroup() - Complexity 6
|
|
||||||
Extract remaining event handling into `handleRemainingEvents()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality Metrics
|
|
||||||
|
|
||||||
### Complexity by File
|
|
||||||
|
|
||||||
```
|
|
||||||
DragDropManager.ts: ████████░░ 8/10 (1 critical, 2 medium)
|
|
||||||
SwpEventElement.ts: ██░░░░░░░░ 2/10 (excellent!)
|
|
||||||
SimpleEventOverlapManager.ts: ██████████ 10/10 (2 critical, 2 medium)
|
|
||||||
EventRendererManager.ts: ██████░░░░ 6/10 (1 critical)
|
|
||||||
EventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!)
|
|
||||||
AllDayEventRenderer.ts: ██░░░░░░░░ 2/10 (excellent!)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Methods Requiring Attention
|
|
||||||
|
|
||||||
| Priority | File | Method | Complexity | Effort |
|
|
||||||
|----------|------|--------|------------|--------|
|
|
||||||
| 🔴 Critical | SimpleEventOverlapManager | removeStackedStyling | 18 | High |
|
|
||||||
| 🔴 Critical | DragDropManager | handleMouseMove | 15 | High |
|
|
||||||
| 🔴 High | SimpleEventOverlapManager | restackEventsInContainer | 11 | Medium |
|
|
||||||
| 🔴 High | EventRendererManager | setupDragEventListeners | 10 | Low |
|
|
||||||
| 🟡 Medium | SimpleEventOverlapManager | createStackedEvent | 7 | Low |
|
|
||||||
| 🟡 Medium | DragDropManager | startAutoScroll | 6 | Low |
|
|
||||||
| 🟡 Medium | SimpleEventOverlapManager | removeFromEventGroup | 6 | Low |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Positive Examples
|
|
||||||
|
|
||||||
### SwpEventElement.ts - Excellent Design Pattern
|
|
||||||
|
|
||||||
This file demonstrates best practices:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Clear, focused methods with single responsibility
|
|
||||||
public updatePosition(columnDate: Date, snappedY: number): void {
|
|
||||||
this.style.top = `${snappedY + 1}px`;
|
|
||||||
const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY);
|
|
||||||
const startDate = this.dateService.createDateAtTime(columnDate, startMinutes);
|
|
||||||
let endDate = this.dateService.createDateAtTime(columnDate, endMinutes);
|
|
||||||
|
|
||||||
if (endMinutes >= 1440) {
|
|
||||||
const extraDays = Math.floor(endMinutes / 1440);
|
|
||||||
endDate = this.dateService.addDays(endDate, extraDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.start = startDate;
|
|
||||||
this.end = endDate;
|
|
||||||
}
|
|
||||||
// Complexity: 2 (one if statement)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this works:**
|
|
||||||
- Single responsibility (update position)
|
|
||||||
- Delegates complex calculations to helper methods
|
|
||||||
- Clear variable names
|
|
||||||
- Minimal branching
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Action Plan
|
|
||||||
|
|
||||||
### Phase 1: Critical Refactoring (Week 1-2)
|
|
||||||
1. ✅ Refactor `SimpleEventOverlapManager.removeStackedStyling()` (18 → 4-6)
|
|
||||||
2. ✅ Refactor `DragDropManager.handleMouseMove()` (15 → 4-5)
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Reduce highest complexity from 18 to 4-6
|
|
||||||
- Improve maintainability significantly
|
|
||||||
- Enable easier testing
|
|
||||||
|
|
||||||
### Phase 2: High Priority (Week 3)
|
|
||||||
3. ✅ Refactor `SimpleEventOverlapManager.restackEventsInContainer()` (11 → 3-4)
|
|
||||||
4. ✅ Refactor `EventRendererManager.setupDragEventListeners()` (10 → 2-3)
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- Eliminate all methods with complexity >10
|
|
||||||
- Improve overall code quality score
|
|
||||||
|
|
||||||
### Phase 3: Medium Priority (Week 4)
|
|
||||||
5. ✅ Review and simplify medium complexity methods (complexity 6-7)
|
|
||||||
6. ✅ Add unit tests for extracted methods
|
|
||||||
|
|
||||||
**Expected Impact:**
|
|
||||||
- All methods under complexity threshold of 10
|
|
||||||
- Comprehensive test coverage
|
|
||||||
|
|
||||||
### Phase 4: Continuous Improvement
|
|
||||||
7. ✅ Establish cyclomatic complexity checks in CI/CD
|
|
||||||
8. ✅ Set max complexity threshold to 10
|
|
||||||
9. ✅ Regular code reviews focusing on complexity
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tools & Resources
|
|
||||||
|
|
||||||
### Recommended Tools for Ongoing Monitoring:
|
|
||||||
- **TypeScript ESLint** with `complexity` rule
|
|
||||||
- **SonarQube** for continuous code quality monitoring
|
|
||||||
- **CodeClimate** for maintainability scoring
|
|
||||||
|
|
||||||
### Suggested ESLint Configuration:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"complexity": ["error", 10],
|
|
||||||
"max-lines-per-function": ["warn", 50],
|
|
||||||
"max-depth": ["error", 4]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The Calendar Plantempus codebase shows **mixed code quality**:
|
|
||||||
|
|
||||||
**Strengths:**
|
|
||||||
- 87.8% of methods have acceptable complexity
|
|
||||||
- Web Components demonstrate excellent design patterns
|
|
||||||
- Clear separation of concerns in rendering services
|
|
||||||
|
|
||||||
**Areas for Improvement:**
|
|
||||||
- Stack management logic is overly complex
|
|
||||||
- Some drag & drop handlers need refactoring
|
|
||||||
- File naming could better reflect complexity (e.g., "Simple"EventOverlapManager has complexity 18!)
|
|
||||||
|
|
||||||
**Overall Grade: B-**
|
|
||||||
|
|
||||||
With the recommended refactoring, the codebase can easily achieve an **A grade** by reducing the 4 critical methods to acceptable complexity levels.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Generated by:** Claude Code Cyclomatic Complexity Analyzer
|
|
||||||
**Date:** 2025-10-04
|
|
||||||
**Analyzer Version:** 1.0
|
|
||||||
177
README.md
177
README.md
|
|
@ -1,177 +0,0 @@
|
||||||
# Calendar Plantempus
|
|
||||||
|
|
||||||
En moderne, event-drevet kalenderapplikation bygget med TypeScript og ASP.NET Core.
|
|
||||||
|
|
||||||
## Projekt Information
|
|
||||||
|
|
||||||
- **Projekt ID:** 8ecf2aa3-a2e4-4cc3-aa18-1c4352f00ff1
|
|
||||||
- **Repository:** Calendar (afb8a8ec-cdbc-4c55-8631-fd0285974485)
|
|
||||||
- **Status:** Under aktiv udvikling
|
|
||||||
|
|
||||||
## Teknisk Arkitektur
|
|
||||||
|
|
||||||
- **Frontend:** TypeScript med esbuild som bundler
|
|
||||||
- **Arkitektur:** Event-drevet med CustomEvents (`document.dispatchEvent`/`addEventListener`)
|
|
||||||
- **Backend:** ASP.NET Core Kestrel server
|
|
||||||
- **Styling:** Modulær CSS struktur uden eksterne frameworks
|
|
||||||
- **Bundling:** esbuild for TypeScript transpilering og bundling
|
|
||||||
|
|
||||||
## Arkitekturelle Principper
|
|
||||||
|
|
||||||
- **Ingen global state** - Alt state håndteres i de relevante managers
|
|
||||||
- **Event-drevet kommunikation** - Alle komponenter kommunikerer via DOM CustomEvents
|
|
||||||
- **Modulær opbygning** - Hver manager har et specifikt ansvarsområde
|
|
||||||
- **Ren DOM manipulation** - Ingen eksterne JavaScript frameworks (React, Vue, etc.)
|
|
||||||
- **Custom HTML tags** - Semantisk markup med custom elements
|
|
||||||
|
|
||||||
## Implementerede Komponenter
|
|
||||||
|
|
||||||
Projektet følger en manager-baseret arkitektur, hvor hver manager er ansvarlig for et specifikt aspekt af kalenderen:
|
|
||||||
|
|
||||||
### 1. CalendarManager
|
|
||||||
Hovedkoordinator for alle managers
|
|
||||||
- Initialiserer og koordinerer alle andre managers
|
|
||||||
- Håndterer global konfiguration
|
|
||||||
- Administrerer kalender lifecycle
|
|
||||||
|
|
||||||
### 2. ViewManager
|
|
||||||
Håndterer kalendervisninger
|
|
||||||
- Skifter mellem dag/uge/måned visninger
|
|
||||||
- Opdaterer UI baseret på den valgte visning
|
|
||||||
- Renderer kalender grid struktur
|
|
||||||
|
|
||||||
### 3. NavigationManager
|
|
||||||
Håndterer navigation
|
|
||||||
- Implementerer prev/next/today funktionalitet
|
|
||||||
- Håndterer dato navigation
|
|
||||||
- Opdaterer week info (uge nummer, dato range)
|
|
||||||
|
|
||||||
### 4. EventManager
|
|
||||||
Administrerer events
|
|
||||||
- Håndterer event lifecycle og CRUD operationer
|
|
||||||
- Loader og synkroniserer event data
|
|
||||||
- Administrerer event selection og state
|
|
||||||
|
|
||||||
### 5. EventRenderer
|
|
||||||
Renderer events i DOM
|
|
||||||
- Positionerer events korrekt i kalender grid
|
|
||||||
- Håndterer event styling baseret på type
|
|
||||||
- Implementerer visual feedback for event interactions
|
|
||||||
|
|
||||||
### 6. DataManager
|
|
||||||
Håndterer data operationer
|
|
||||||
- Mock data loading for udvikling
|
|
||||||
- Event data transformation
|
|
||||||
- Data persistence interface
|
|
||||||
|
|
||||||
### 7. GridManager
|
|
||||||
Administrerer kalender grid
|
|
||||||
- Opretter og vedligeholder grid struktur
|
|
||||||
- Håndterer time slots og positioning
|
|
||||||
- Responsive grid layout
|
|
||||||
|
|
||||||
## CSS Struktur
|
|
||||||
|
|
||||||
Projektet har en modulær CSS struktur for bedre organisering:
|
|
||||||
|
|
||||||
- **`calendar-base-css.css`** - Grundlæggende styling og CSS custom properties
|
|
||||||
- **`calendar-components-css.css`** - UI komponenter og controls
|
|
||||||
- **`calendar-events-css.css`** - Event styling og farver
|
|
||||||
- **`calendar-layout-css.css`** - Layout struktur og grid
|
|
||||||
- **`calendar-popup-css.css`** - Popup og modal styling
|
|
||||||
- **`calendar.css`** - Samlet styling fra POC (bruges i øjeblikket)
|
|
||||||
|
|
||||||
## Kommende Funktionalitet
|
|
||||||
|
|
||||||
Baseret på projektstrukturen planlægges følgende komponenter:
|
|
||||||
|
|
||||||
### Utilities
|
|
||||||
- **PositionUtils** - Konvertering mellem pixels og tidspunkter
|
|
||||||
- **SnapUtils** - Snap-to-interval funktionalitet
|
|
||||||
- **DOMUtils** - DOM manipulation utilities
|
|
||||||
|
|
||||||
### Interaction Managers
|
|
||||||
- **DragManager** - Drag & drop funktionalitet for events
|
|
||||||
- **ResizeManager** - Resize funktionalitet for events
|
|
||||||
- **PopupManager** - Håndtering af event detaljer og popups
|
|
||||||
|
|
||||||
### Feature Managers
|
|
||||||
- **SearchManager** - Søgefunktionalitet i events
|
|
||||||
- **TimeManager** - Current time indicator
|
|
||||||
- **LoadingManager** - Loading states og error handling
|
|
||||||
|
|
||||||
### Avancerede Features
|
|
||||||
- Collision detection system for overlappende events
|
|
||||||
- Animation system for smooth transitions
|
|
||||||
- Event creation funktionalitet (double-click, drag-to-create)
|
|
||||||
- Multi-day event support
|
|
||||||
- Touch support for mobile enheder
|
|
||||||
- Keyboard navigation
|
|
||||||
|
|
||||||
## Projekt Struktur
|
|
||||||
|
|
||||||
```
|
|
||||||
Calendar Plantempus/
|
|
||||||
├── src/ # TypeScript source files
|
|
||||||
│ ├── constants/ # Konstanter og enums
|
|
||||||
│ ├── core/ # Core funktionalitet
|
|
||||||
│ ├── managers/ # Manager klasser
|
|
||||||
│ ├── types/ # TypeScript type definitioner
|
|
||||||
│ └── utils/ # Utility funktioner
|
|
||||||
├── wwwroot/ # Static web assets
|
|
||||||
│ ├── css/ # Stylesheets
|
|
||||||
│ ├── js/ # Compiled JavaScript
|
|
||||||
│ └── index.html # Main HTML file
|
|
||||||
├── build.js # esbuild configuration
|
|
||||||
├── tsconfig.json # TypeScript configuration
|
|
||||||
├── package.json # Node.js dependencies
|
|
||||||
└── Program.cs # ASP.NET Core server
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kom i Gang
|
|
||||||
|
|
||||||
### Forudsætninger
|
|
||||||
- .NET 8.0 SDK
|
|
||||||
- Node.js (for esbuild)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
1. Klon repository
|
|
||||||
2. Installer dependencies: `npm install`
|
|
||||||
3. Build TypeScript: `npm run build`
|
|
||||||
4. Start server: `dotnet run`
|
|
||||||
5. Åbn browser på `http://localhost:8000`
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- **Build TypeScript:** `npm run build`
|
|
||||||
- **Watch mode:** `npm run watch` (hvis konfigureret)
|
|
||||||
- **Start server:** `dotnet run`
|
|
||||||
|
|
||||||
## Event System
|
|
||||||
|
|
||||||
Projektet bruger et event-drevet system hvor alle komponenter kommunikerer via DOM CustomEvents:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Dispatch event
|
|
||||||
document.dispatchEvent(new CustomEvent('calendar:view-changed', {
|
|
||||||
detail: { view: 'week', date: new Date() }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Listen for event
|
|
||||||
document.addEventListener('calendar:view-changed', (event) => {
|
|
||||||
// Handle view change
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bidrag
|
|
||||||
|
|
||||||
Dette projekt følger clean code principper og modulær arkitektur. Når du bidrager:
|
|
||||||
|
|
||||||
1. Følg den eksisterende manager-baserede struktur
|
|
||||||
2. Brug event-drevet kommunikation mellem komponenter
|
|
||||||
3. Undgå global state - hold state i relevante managers
|
|
||||||
4. Skriv semantisk HTML med custom tags
|
|
||||||
5. Brug modulær CSS struktur
|
|
||||||
|
|
||||||
## Licens
|
|
||||||
|
|
||||||
[Specificer licens her]
|
|
||||||
|
|
@ -1,772 +0,0 @@
|
||||||
# Event Stacking Concept
|
|
||||||
**Calendar Plantempus - Visual Event Overlap Management**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
**Event Stacking** is a visual technique for displaying overlapping calendar events by offsetting them horizontally with a cascading effect. This creates a clear visual hierarchy showing which events overlap in time.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Visual Concept
|
|
||||||
|
|
||||||
### Basic Stacking
|
|
||||||
|
|
||||||
When multiple events overlap in time, they are "stacked" with increasing left margin:
|
|
||||||
|
|
||||||
```
|
|
||||||
Timeline:
|
|
||||||
08:00 ─────────────────────────────────
|
|
||||||
│
|
|
||||||
09:00 │ Event A starts
|
|
||||||
│ ┌─────────────────────┐
|
|
||||||
│ │ Meeting A │
|
|
||||||
10:00 │ │ │
|
|
||||||
│ │ Event B starts │
|
|
||||||
│ │ ┌─────────────────────┐
|
|
||||||
11:00 │ │ │ Meeting B │
|
|
||||||
│ └──│─────────────────────┘
|
|
||||||
│ │ │
|
|
||||||
12:00 │ │ Event C starts │
|
|
||||||
│ │ ┌─────────────────────┐
|
|
||||||
│ └──│─────────────────────┘
|
|
||||||
13:00 │ │ Meeting C │
|
|
||||||
│ └─────────────────────┘
|
|
||||||
14:00 ─────────────────────────────────
|
|
||||||
|
|
||||||
Visual Result (stacked view):
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ Meeting A │
|
|
||||||
│ ┌─────────────────────┐
|
|
||||||
│ │ Meeting B │
|
|
||||||
└─│─────────────────────┘
|
|
||||||
│ ┌─────────────────────┐
|
|
||||||
│ │ Meeting C │
|
|
||||||
└─│─────────────────────┘
|
|
||||||
└─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Each subsequent event is offset by **15px** to the right.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stack Link Data Structure
|
|
||||||
|
|
||||||
Stack links create a **doubly-linked list** stored directly in DOM elements as data attributes.
|
|
||||||
|
|
||||||
### Interface Definition
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface StackLink {
|
|
||||||
prev?: string; // Event ID of previous event in stack
|
|
||||||
next?: string; // Event ID of next event in stack
|
|
||||||
stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Storage in DOM
|
|
||||||
|
|
||||||
Stack links are stored as JSON in the `data-stack-link` attribute:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<swp-event
|
|
||||||
data-event-id="event-1"
|
|
||||||
data-stack-link='{"stackLevel":0,"next":"event-2"}'>
|
|
||||||
</swp-event>
|
|
||||||
|
|
||||||
<swp-event
|
|
||||||
data-event-id="event-2"
|
|
||||||
data-stack-link='{"stackLevel":1,"prev":"event-1","next":"event-3"}'>
|
|
||||||
</swp-event>
|
|
||||||
|
|
||||||
<swp-event
|
|
||||||
data-event-id="event-3"
|
|
||||||
data-stack-link='{"stackLevel":2,"prev":"event-2"}'>
|
|
||||||
</swp-event>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of DOM Storage
|
|
||||||
|
|
||||||
✅ **State follows the element** - No external state management needed
|
|
||||||
✅ **Survives drag & drop** - Links persist through DOM manipulations
|
|
||||||
✅ **Easy to query** - Can traverse chain using DOM queries
|
|
||||||
✅ **Self-contained** - Each element knows its position in the stack
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overlap Detection
|
|
||||||
|
|
||||||
Events overlap when their time ranges intersect.
|
|
||||||
|
|
||||||
### Time-Based Overlap Algorithm
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function doEventsOverlap(eventA: CalendarEvent, eventB: CalendarEvent): boolean {
|
|
||||||
// Two events overlap if:
|
|
||||||
// - Event A starts before Event B ends AND
|
|
||||||
// - Event A ends after Event B starts
|
|
||||||
return eventA.start < eventB.end && eventA.end > eventB.start;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Cases
|
|
||||||
|
|
||||||
**Case 1: Events Overlap**
|
|
||||||
```
|
|
||||||
Event A: 09:00 ──────── 11:00
|
|
||||||
Event B: 10:00 ──────── 12:00
|
|
||||||
Result: OVERLAP (10:00 to 11:00)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Case 2: No Overlap**
|
|
||||||
```
|
|
||||||
Event A: 09:00 ──── 10:00
|
|
||||||
Event B: 11:00 ──── 12:00
|
|
||||||
Result: NO OVERLAP
|
|
||||||
```
|
|
||||||
|
|
||||||
**Case 3: Complete Containment**
|
|
||||||
```
|
|
||||||
Event A: 09:00 ──────────────── 13:00
|
|
||||||
Event B: 10:00 ─── 11:00
|
|
||||||
Result: OVERLAP (Event B fully inside Event A)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Visual Styling
|
|
||||||
|
|
||||||
### CSS Calculations
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const STACK_OFFSET_PX = 15;
|
|
||||||
|
|
||||||
// For each event in stack:
|
|
||||||
marginLeft = stackLevel * STACK_OFFSET_PX;
|
|
||||||
zIndex = 100 + stackLevel;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example with 3 Stacked Events
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
Event A (stackLevel: 0):
|
|
||||||
marginLeft = 0 * 15 = 0px
|
|
||||||
zIndex = 100 + 0 = 100
|
|
||||||
|
|
||||||
Event B (stackLevel: 1):
|
|
||||||
marginLeft = 1 * 15 = 15px
|
|
||||||
zIndex = 100 + 1 = 101
|
|
||||||
|
|
||||||
Event C (stackLevel: 2):
|
|
||||||
marginLeft = 2 * 15 = 30px
|
|
||||||
zIndex = 100 + 2 = 102
|
|
||||||
```
|
|
||||||
|
|
||||||
Result: Event C appears on top, Event A at the base.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Optimized Stacking (Smart Stacking)
|
|
||||||
|
|
||||||
### The Problem: Naive Stacking vs Optimized Stacking
|
|
||||||
|
|
||||||
**Naive Approach:** Simply stack all overlapping events sequentially.
|
|
||||||
|
|
||||||
```
|
|
||||||
Event A: 09:00 ════════════════════════════ 14:00
|
|
||||||
Event B: 10:00 ═════ 12:00
|
|
||||||
Event C: 12:30 ═══ 13:00
|
|
||||||
|
|
||||||
Naive Result:
|
|
||||||
Event A: stackLevel 0
|
|
||||||
Event B: stackLevel 1
|
|
||||||
Event C: stackLevel 2 ← INEFFICIENT! C doesn't overlap B
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optimized Approach:** Events that don't overlap each other can share the same stack level.
|
|
||||||
|
|
||||||
```
|
|
||||||
Event A: 09:00 ════════════════════════════ 14:00
|
|
||||||
Event B: 10:00 ═════ 12:00
|
|
||||||
Event C: 12:30 ═══ 13:00
|
|
||||||
|
|
||||||
Optimized Result:
|
|
||||||
Event A: stackLevel 0
|
|
||||||
Event B: stackLevel 1 ← Both at level 1
|
|
||||||
Event C: stackLevel 1 ← because they don't overlap!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visual Comparison: The Key Insight
|
|
||||||
|
|
||||||
**Example Timeline:**
|
|
||||||
```
|
|
||||||
Timeline:
|
|
||||||
09:00 ─────────────────────────────────
|
|
||||||
│ Event A starts
|
|
||||||
│ ┌─────────────────────────────┐
|
|
||||||
10:00 │ │ Event A │
|
|
||||||
│ │ │
|
|
||||||
│ │ Event B starts │
|
|
||||||
│ │ ╔═══════════════╗ │
|
|
||||||
11:00 │ │ ║ Event B ║ │
|
|
||||||
│ │ ║ ║ │
|
|
||||||
12:00 │ │ ╚═══════════════╝ │
|
|
||||||
│ │ │
|
|
||||||
│ │ Event C starts │
|
|
||||||
│ │ ╔═══════════╗ │
|
|
||||||
13:00 │ │ ║ Event C ║ │
|
|
||||||
│ └───────╚═══════════╝─────────┘
|
|
||||||
14:00 ─────────────────────────────────
|
|
||||||
|
|
||||||
Key Observation:
|
|
||||||
• Event B (10:00-12:00) and Event C (12:30-13:00) do NOT overlap!
|
|
||||||
• They are separated by 30 minutes (12:00 to 12:30)
|
|
||||||
• Both overlap with Event A, but not with each other
|
|
||||||
```
|
|
||||||
|
|
||||||
**Naive Stacking (Wasteful):**
|
|
||||||
```
|
|
||||||
Visual Result (Naive - Inefficient):
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ Event A │
|
|
||||||
│ ┌─────────────────────┐ │
|
|
||||||
│ │ Event B │ │
|
|
||||||
│ │ ┌─────────────────────┐ │
|
|
||||||
│ └─│─────────────────────┘ │
|
|
||||||
│ │ Event C │ │
|
|
||||||
│ └─────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
0px 15px 30px
|
|
||||||
└──┴────┘
|
|
||||||
Wasted space!
|
|
||||||
|
|
||||||
Stack Levels:
|
|
||||||
• Event A: stackLevel 0 (marginLeft: 0px)
|
|
||||||
• Event B: stackLevel 1 (marginLeft: 15px)
|
|
||||||
• Event C: stackLevel 2 (marginLeft: 30px) ← UNNECESSARY!
|
|
||||||
|
|
||||||
Problem: Event C is pushed 30px to the right even though
|
|
||||||
it doesn't conflict with Event B!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optimized Stacking (Efficient):**
|
|
||||||
```
|
|
||||||
Visual Result (Optimized - Efficient):
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ Event A │
|
|
||||||
│ ┌─────────────────────┐ ┌─────────────────────┐│
|
|
||||||
│ │ Event B │ │ Event C ││
|
|
||||||
│ └─────────────────────┘ └─────────────────────┘│
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
0px 15px 15px
|
|
||||||
└────────────────────┘
|
|
||||||
Same offset for both!
|
|
||||||
|
|
||||||
Stack Levels:
|
|
||||||
• Event A: stackLevel 0 (marginLeft: 0px)
|
|
||||||
• Event B: stackLevel 1 (marginLeft: 15px)
|
|
||||||
• Event C: stackLevel 1 (marginLeft: 15px) ← OPTIMIZED!
|
|
||||||
|
|
||||||
Benefit: Event C reuses stackLevel 1 because Event B
|
|
||||||
has already ended when Event C starts.
|
|
||||||
No visual conflict, saves 15px of horizontal space!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Side-by-Side Comparison:**
|
|
||||||
```
|
|
||||||
Naive (3 levels): Optimized (2 levels):
|
|
||||||
|
|
||||||
A A
|
|
||||||
├─ B ├─ B
|
|
||||||
│ └─ C └─ C
|
|
||||||
|
|
||||||
Uses 45px width Uses 30px width
|
|
||||||
(0 + 15 + 30) (0 + 15 + 15)
|
|
||||||
|
|
||||||
33% space savings! →
|
|
||||||
```
|
|
||||||
|
|
||||||
### Algorithm: Greedy Stack Level Assignment
|
|
||||||
|
|
||||||
The optimized stacking algorithm assigns the lowest available stack level to each event:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
|
|
||||||
// Step 1: Sort events by start time
|
|
||||||
const sorted = events.sort((a, b) => a.start - b.start)
|
|
||||||
|
|
||||||
// Step 2: Track which stack levels are occupied at each time point
|
|
||||||
const stackLinks = new Map<string, StackLink>()
|
|
||||||
|
|
||||||
for (const event of sorted) {
|
|
||||||
// Find the lowest available stack level for this event
|
|
||||||
let stackLevel = 0
|
|
||||||
|
|
||||||
// Check which levels are occupied by overlapping events
|
|
||||||
const overlapping = sorted.filter(other =>
|
|
||||||
other !== event && doEventsOverlap(event, other)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Try each level starting from 0
|
|
||||||
while (true) {
|
|
||||||
const levelOccupied = overlapping.some(other =>
|
|
||||||
stackLinks.get(other.id)?.stackLevel === stackLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!levelOccupied) {
|
|
||||||
break // Found available level
|
|
||||||
}
|
|
||||||
|
|
||||||
stackLevel++ // Try next level
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign the lowest available level
|
|
||||||
stackLinks.set(event.id, { stackLevel })
|
|
||||||
}
|
|
||||||
|
|
||||||
return stackLinks
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Scenarios
|
|
||||||
|
|
||||||
#### Scenario 1: Three Events, Two Parallel Tracks
|
|
||||||
|
|
||||||
```
|
|
||||||
Input:
|
|
||||||
Event A: 09:00-14:00 (long event)
|
|
||||||
Event B: 10:00-12:00
|
|
||||||
Event C: 12:30-13:00
|
|
||||||
|
|
||||||
Analysis:
|
|
||||||
A overlaps with: B, C
|
|
||||||
B overlaps with: A (not C)
|
|
||||||
C overlaps with: A (not B)
|
|
||||||
|
|
||||||
Result:
|
|
||||||
Event A: stackLevel 0 (base)
|
|
||||||
Event B: stackLevel 1 (first available)
|
|
||||||
Event C: stackLevel 1 (level 1 is free, B doesn't conflict)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Scenario 2: Four Events, Three at Same Level
|
|
||||||
|
|
||||||
```
|
|
||||||
Input:
|
|
||||||
Event A: 09:00-15:00 (very long event)
|
|
||||||
Event B: 10:00-11:00
|
|
||||||
Event C: 11:30-12:30
|
|
||||||
Event D: 13:00-14:00
|
|
||||||
|
|
||||||
Analysis:
|
|
||||||
A overlaps with: B, C, D
|
|
||||||
B, C, D don't overlap with each other
|
|
||||||
|
|
||||||
Result:
|
|
||||||
Event A: stackLevel 0
|
|
||||||
Event B: stackLevel 1
|
|
||||||
Event C: stackLevel 1 (B is done, level 1 free)
|
|
||||||
Event D: stackLevel 1 (B and C are done, level 1 free)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Scenario 3: Nested Events with Optimization
|
|
||||||
|
|
||||||
```
|
|
||||||
Input:
|
|
||||||
Event A: 09:00-15:00
|
|
||||||
Event B: 10:00-13:00
|
|
||||||
Event C: 11:00-12:00
|
|
||||||
Event D: 12:30-13:30
|
|
||||||
|
|
||||||
Analysis:
|
|
||||||
A overlaps with: B, C, D
|
|
||||||
B overlaps with: A, C (not D)
|
|
||||||
C overlaps with: A, B (not D)
|
|
||||||
D overlaps with: A (not B, not C)
|
|
||||||
|
|
||||||
Result:
|
|
||||||
Event A: stackLevel 0 (base)
|
|
||||||
Event B: stackLevel 1 (overlaps with A)
|
|
||||||
Event C: stackLevel 2 (overlaps with A and B)
|
|
||||||
Event D: stackLevel 2 (overlaps with A only, level 2 is free)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stack Links with Optimization
|
|
||||||
|
|
||||||
**Important:** With optimized stacking, events at the same stack level are NOT linked via prev/next!
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Traditional chain (naive):
|
|
||||||
Event A: { stackLevel: 0, next: "event-b" }
|
|
||||||
Event B: { stackLevel: 1, prev: "event-a", next: "event-c" }
|
|
||||||
Event C: { stackLevel: 2, prev: "event-b" }
|
|
||||||
|
|
||||||
// Optimized (B and C at same level, no link between them):
|
|
||||||
Event A: { stackLevel: 0 }
|
|
||||||
Event B: { stackLevel: 1 } // No prev/next
|
|
||||||
Event C: { stackLevel: 1 } // No prev/next
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of Optimized Stacking
|
|
||||||
|
|
||||||
✅ **Space Efficiency:** Reduces horizontal space usage by up to 50%
|
|
||||||
✅ **Better Readability:** Events are visually closer, easier to see relationships
|
|
||||||
✅ **Scalability:** Works well with many events in a day
|
|
||||||
✅ **Performance:** Same O(n²) complexity as naive approach
|
|
||||||
|
|
||||||
### Trade-offs
|
|
||||||
|
|
||||||
⚠️ **No Single Chain:** Events at the same level aren't linked, making traversal more complex
|
|
||||||
⚠️ **More Complex Logic:** Requires checking all overlaps, not just sequential ordering
|
|
||||||
⚠️ **Visual Ambiguity:** Users might wonder why some events are at the same level
|
|
||||||
|
|
||||||
## Stack Chain Operations
|
|
||||||
|
|
||||||
### Building a Stack Chain (Naive Approach)
|
|
||||||
|
|
||||||
When events overlap, they form a chain sorted by start time:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Input: Events with overlapping times
|
|
||||||
Event A: 09:00-11:00
|
|
||||||
Event B: 10:00-12:00
|
|
||||||
Event C: 11:30-13:00
|
|
||||||
|
|
||||||
// Step 1: Sort by start time (earliest first)
|
|
||||||
Sorted: [Event A, Event B, Event C]
|
|
||||||
|
|
||||||
// Step 2: Create links
|
|
||||||
Event A: { stackLevel: 0, next: "event-b" }
|
|
||||||
Event B: { stackLevel: 1, prev: "event-a", next: "event-c" }
|
|
||||||
Event C: { stackLevel: 2, prev: "event-b" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Traversing Forward
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Start at any event
|
|
||||||
currentEvent = Event B;
|
|
||||||
|
|
||||||
// Get stack link
|
|
||||||
stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" }
|
|
||||||
|
|
||||||
// Move to next event
|
|
||||||
nextEventId = stackLink.next; // "event-c"
|
|
||||||
nextEvent = document.querySelector(`[data-event-id="${nextEventId}"]`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Traversing Backward
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Start at any event
|
|
||||||
currentEvent = Event B;
|
|
||||||
|
|
||||||
// Get stack link
|
|
||||||
stackLink = currentEvent.dataset.stackLink; // { prev: "event-a", next: "event-c" }
|
|
||||||
|
|
||||||
// Move to previous event
|
|
||||||
prevEventId = stackLink.prev; // "event-a"
|
|
||||||
prevEvent = document.querySelector(`[data-event-id="${prevEventId}"]`);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Finding Stack Root
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function findStackRoot(event: HTMLElement): HTMLElement {
|
|
||||||
let current = event;
|
|
||||||
let stackLink = getStackLink(current);
|
|
||||||
|
|
||||||
// Traverse backward until we find an event with no prev link
|
|
||||||
while (stackLink?.prev) {
|
|
||||||
const prevEvent = document.querySelector(
|
|
||||||
`[data-event-id="${stackLink.prev}"]`
|
|
||||||
);
|
|
||||||
if (!prevEvent) break;
|
|
||||||
|
|
||||||
current = prevEvent;
|
|
||||||
stackLink = getStackLink(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return current; // This is the root (stackLevel 0)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### 1. Adding a New Event to Existing Stack
|
|
||||||
|
|
||||||
```
|
|
||||||
Existing Stack:
|
|
||||||
Event A (09:00-11:00) - stackLevel 0
|
|
||||||
Event B (10:00-12:00) - stackLevel 1
|
|
||||||
|
|
||||||
New Event:
|
|
||||||
Event C (10:30-11:30)
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. Detect overlap with Event A and Event B
|
|
||||||
2. Sort all three by start time: [A, B, C]
|
|
||||||
3. Rebuild stack links:
|
|
||||||
- Event A: { stackLevel: 0, next: "event-b" }
|
|
||||||
- Event B: { stackLevel: 1, prev: "event-a", next: "event-c" }
|
|
||||||
- Event C: { stackLevel: 2, prev: "event-b" }
|
|
||||||
4. Apply visual styling
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Removing Event from Middle of Stack
|
|
||||||
|
|
||||||
```
|
|
||||||
Before:
|
|
||||||
Event A (stackLevel 0) ─→ Event B (stackLevel 1) ─→ Event C (stackLevel 2)
|
|
||||||
|
|
||||||
Remove Event B:
|
|
||||||
|
|
||||||
After:
|
|
||||||
Event A (stackLevel 0) ─→ Event C (stackLevel 1)
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. Get Event B's stack link: { prev: "event-a", next: "event-c" }
|
|
||||||
2. Update Event A's next: "event-c"
|
|
||||||
3. Update Event C's prev: "event-a"
|
|
||||||
4. Update Event C's stackLevel: 1 (was 2)
|
|
||||||
5. Recalculate Event C's marginLeft: 15px (was 30px)
|
|
||||||
6. Remove Event B's stack link
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Moving Event to Different Time
|
|
||||||
|
|
||||||
```
|
|
||||||
Before (events overlap):
|
|
||||||
Event A (09:00-11:00) - stackLevel 0
|
|
||||||
Event B (10:00-12:00) - stackLevel 1
|
|
||||||
|
|
||||||
Move Event B to 14:00-16:00 (no longer overlaps):
|
|
||||||
|
|
||||||
After:
|
|
||||||
Event A (09:00-11:00) - NO STACK LINK (standalone)
|
|
||||||
Event B (14:00-16:00) - NO STACK LINK (standalone)
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
1. Detect that Event B no longer overlaps Event A
|
|
||||||
2. Remove Event B from stack chain
|
|
||||||
3. Clear Event A's next link
|
|
||||||
4. Clear Event B's stack link entirely
|
|
||||||
5. Reset both events' marginLeft to 0px
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
|
|
||||||
### Case 1: Single Event (No Overlap)
|
|
||||||
|
|
||||||
```
|
|
||||||
Event A: 09:00-10:00 (alone in time slot)
|
|
||||||
|
|
||||||
Stack Link: NONE (no data-stack-link attribute)
|
|
||||||
Visual: marginLeft = 0px, zIndex = default
|
|
||||||
```
|
|
||||||
|
|
||||||
### Case 2: Two Events, Same Start Time
|
|
||||||
|
|
||||||
```
|
|
||||||
Event A: 10:00-11:00
|
|
||||||
Event B: 10:00-12:00 (same start, different end)
|
|
||||||
|
|
||||||
Sort by: start time first, then by end time (shortest first)
|
|
||||||
Result: Event A (stackLevel 0), Event B (stackLevel 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Case 3: Multiple Separate Chains in Same Column
|
|
||||||
|
|
||||||
```
|
|
||||||
Chain 1:
|
|
||||||
Event A (09:00-10:00) - stackLevel 0
|
|
||||||
Event B (09:30-10:30) - stackLevel 1
|
|
||||||
|
|
||||||
Chain 2:
|
|
||||||
Event C (14:00-15:00) - stackLevel 0
|
|
||||||
Event D (14:30-15:30) - stackLevel 1
|
|
||||||
|
|
||||||
Note: Two independent chains, each with their own root at stackLevel 0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Case 4: Complete Containment
|
|
||||||
|
|
||||||
```
|
|
||||||
Event A: 09:00-13:00 (large event)
|
|
||||||
Event B: 10:00-11:00 (inside A)
|
|
||||||
Event C: 11:30-12:30 (inside A)
|
|
||||||
|
|
||||||
All three overlap, so they form one chain:
|
|
||||||
Event A - stackLevel 0
|
|
||||||
Event B - stackLevel 1
|
|
||||||
Event C - stackLevel 2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Algorithm Pseudocode
|
|
||||||
|
|
||||||
### Creating Stack for New Event
|
|
||||||
|
|
||||||
```
|
|
||||||
function createStackForNewEvent(newEvent, columnEvents):
|
|
||||||
// Step 1: Find overlapping events
|
|
||||||
overlapping = columnEvents.filter(event =>
|
|
||||||
doEventsOverlap(newEvent, event)
|
|
||||||
)
|
|
||||||
|
|
||||||
if overlapping is empty:
|
|
||||||
// No stack needed
|
|
||||||
return null
|
|
||||||
|
|
||||||
// Step 2: Combine and sort by start time
|
|
||||||
allEvents = [...overlapping, newEvent]
|
|
||||||
allEvents.sort((a, b) => a.start - b.start)
|
|
||||||
|
|
||||||
// Step 3: Create stack links
|
|
||||||
stackLinks = new Map()
|
|
||||||
|
|
||||||
for (i = 0; i < allEvents.length; i++):
|
|
||||||
link = {
|
|
||||||
stackLevel: i,
|
|
||||||
prev: i > 0 ? allEvents[i-1].id : undefined,
|
|
||||||
next: i < allEvents.length-1 ? allEvents[i+1].id : undefined
|
|
||||||
}
|
|
||||||
stackLinks.set(allEvents[i].id, link)
|
|
||||||
|
|
||||||
// Step 4: Apply to DOM
|
|
||||||
for each event in allEvents:
|
|
||||||
element = findElementById(event.id)
|
|
||||||
element.dataset.stackLink = JSON.stringify(stackLinks.get(event.id))
|
|
||||||
element.style.marginLeft = stackLinks.get(event.id).stackLevel * 15 + 'px'
|
|
||||||
element.style.zIndex = 100 + stackLinks.get(event.id).stackLevel
|
|
||||||
|
|
||||||
return stackLinks
|
|
||||||
```
|
|
||||||
|
|
||||||
### Removing Event from Stack
|
|
||||||
|
|
||||||
```
|
|
||||||
function removeEventFromStack(eventId):
|
|
||||||
element = findElementById(eventId)
|
|
||||||
stackLink = JSON.parse(element.dataset.stackLink)
|
|
||||||
|
|
||||||
if not stackLink:
|
|
||||||
return // Not in a stack
|
|
||||||
|
|
||||||
// Update previous element
|
|
||||||
if stackLink.prev:
|
|
||||||
prevElement = findElementById(stackLink.prev)
|
|
||||||
prevLink = JSON.parse(prevElement.dataset.stackLink)
|
|
||||||
prevLink.next = stackLink.next
|
|
||||||
prevElement.dataset.stackLink = JSON.stringify(prevLink)
|
|
||||||
|
|
||||||
// Update next element
|
|
||||||
if stackLink.next:
|
|
||||||
nextElement = findElementById(stackLink.next)
|
|
||||||
nextLink = JSON.parse(nextElement.dataset.stackLink)
|
|
||||||
nextLink.prev = stackLink.prev
|
|
||||||
|
|
||||||
// Shift down stack level
|
|
||||||
nextLink.stackLevel = nextLink.stackLevel - 1
|
|
||||||
nextElement.dataset.stackLink = JSON.stringify(nextLink)
|
|
||||||
|
|
||||||
// Update visual styling
|
|
||||||
nextElement.style.marginLeft = nextLink.stackLevel * 15 + 'px'
|
|
||||||
nextElement.style.zIndex = 100 + nextLink.stackLevel
|
|
||||||
|
|
||||||
// Cascade update to all subsequent events
|
|
||||||
updateSubsequentStackLevels(nextElement, -1)
|
|
||||||
|
|
||||||
// Clear removed element's stack link
|
|
||||||
delete element.dataset.stackLink
|
|
||||||
element.style.marginLeft = '0px'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Time Complexity
|
|
||||||
|
|
||||||
- **Overlap Detection:** O(n) where n = number of events in column
|
|
||||||
- **Stack Creation:** O(n log n) due to sorting
|
|
||||||
- **Chain Traversal:** O(n) worst case (entire chain)
|
|
||||||
- **Stack Removal:** O(n) worst case (update all subsequent)
|
|
||||||
|
|
||||||
### Space Complexity
|
|
||||||
|
|
||||||
- **Stack Links:** O(1) per event (stored in DOM attribute)
|
|
||||||
- **No Global State:** All state is in DOM
|
|
||||||
|
|
||||||
### Optimization Tips
|
|
||||||
|
|
||||||
1. **Batch Updates:** When adding multiple events, batch DOM updates
|
|
||||||
2. **Lazy Evaluation:** Only recalculate stacks when events change
|
|
||||||
3. **Event Delegation:** Use event delegation instead of per-element listeners
|
|
||||||
4. **Virtual Scrolling:** For large calendars, only render visible events
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Guidelines
|
|
||||||
|
|
||||||
### Separation of Concerns
|
|
||||||
|
|
||||||
**Pure Logic (No DOM):**
|
|
||||||
- Overlap detection algorithms
|
|
||||||
- Stack link calculation
|
|
||||||
- Sorting logic
|
|
||||||
|
|
||||||
**DOM Manipulation:**
|
|
||||||
- Applying stack links to elements
|
|
||||||
- Updating visual styles
|
|
||||||
- Chain traversal
|
|
||||||
|
|
||||||
**Event Handling:**
|
|
||||||
- Detecting event changes
|
|
||||||
- Triggering stack recalculation
|
|
||||||
- Cleanup on event removal
|
|
||||||
|
|
||||||
### Testing Strategy
|
|
||||||
|
|
||||||
1. **Unit Tests:** Test overlap detection in isolation
|
|
||||||
2. **Integration Tests:** Test stack creation with DOM
|
|
||||||
3. **Visual Tests:** Test CSS styling calculations
|
|
||||||
4. **Edge Cases:** Test boundary conditions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
|
|
||||||
1. **Smart Stacking:** Detect non-overlapping sub-groups and stack independently
|
|
||||||
2. **Column Sharing:** For events with similar start times, use flexbox columns
|
|
||||||
3. **Compact Mode:** Reduce stack offset for dense calendars
|
|
||||||
4. **Color Coding:** Visual indication of stack depth
|
|
||||||
5. **Stack Preview:** Hover to highlight entire stack chain
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
- **Stack:** Group of overlapping events displayed with horizontal offset
|
|
||||||
- **Stack Link:** Data structure connecting events in a stack (doubly-linked list)
|
|
||||||
- **Stack Level:** Position in stack (0 = base, 1+ = offset)
|
|
||||||
- **Stack Root:** First event in stack (stackLevel 0, no prev link)
|
|
||||||
- **Stack Chain:** Complete sequence of linked events
|
|
||||||
- **Overlap:** Two events with intersecting time ranges
|
|
||||||
- **Offset:** Horizontal margin applied to stacked events (15px per level)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Document Version:** 1.0
|
|
||||||
**Last Updated:** 2025-10-04
|
|
||||||
**Status:** Conceptual Documentation - Ready for TDD Implementation
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue