# IndexedDB-Only DOM Optimization Plan **Date:** 2025-11-12 **Status:** Planning Phase **Goal:** Reduce DOM data-attributes to only event ID, using IndexedDB as single source of truth ## Current Problem Events currently store all data in DOM attributes: ```html ``` **Issues:** - Data duplication (IndexedDB + DOM) - Synchronization complexity - Large DOM size with descriptions - Memory overhead ## Proposed Solution ### Architecture Principle **Single Source of Truth: IndexedDB** ```mermaid graph TB A[IndexedDB] -->|getEvent| B[SwpEventElement] B -->|Only stores| C[data-event-id] B -->|Renders from| D[ICalendarEvent] A -->|Provides| D ``` ### Target DOM Structure ```html ``` Only 1 attribute instead of 8+. ## Implementation Plan ### Phase 1: Refactor SwpEventElement **File:** `src/elements/SwpEventElement.ts` #### 1.1 Remove Getters/Setters Remove all property getters/setters except `eventId`: - ❌ Remove: `start`, `end`, `title`, `description`, `type` - ✅ Keep: `eventId` #### 1.2 Add IndexedDB Reference ```typescript export class SwpEventElement extends BaseSwpEventElement { private static indexedDB: IndexedDBService; static setIndexedDB(db: IndexedDBService): void { SwpEventElement.indexedDB = db; } } ``` #### 1.3 Implement Async Data Loading ```typescript async connectedCallback() { const event = await this.loadEventData(); if (event) { await this.renderFromEvent(event); } } private async loadEventData(): Promise { return await SwpEventElement.indexedDB.getEvent(this.eventId); } ``` #### 1.4 Update Render Method ```typescript private async renderFromEvent(event: ICalendarEvent): Promise { const timeRange = TimeFormatter.formatTimeRange(event.start, event.end); const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60); this.innerHTML = ` ${timeRange} ${event.title} ${event.description ? `${event.description}` : ''} `; } ``` ### Phase 2: Update Factory Method **File:** `src/elements/SwpEventElement.ts` (line 284) ```typescript public static fromCalendarEvent(event: ICalendarEvent): SwpEventElement { const element = document.createElement('swp-event') as SwpEventElement; // Only set event ID - all other data comes from IndexedDB element.dataset.eventId = event.id; return element; } ``` ### Phase 3: Update Extract Method **File:** `src/elements/SwpEventElement.ts` (line 303) ```typescript public static async extractCalendarEventFromElement(element: HTMLElement): Promise { const eventId = element.dataset.eventId; if (!eventId) return null; // Load from IndexedDB instead of reading from DOM return await SwpEventElement.indexedDB.getEvent(eventId); } ``` ### Phase 4: Update Position Updates **File:** `src/elements/SwpEventElement.ts` (line 117) ```typescript public async updatePosition(columnDate: Date, snappedY: number): Promise { // 1. Update visual position this.style.top = `${snappedY + 1}px`; // 2. Load current event data from IndexedDB const event = await this.loadEventData(); if (!event) return; // 3. Calculate new timestamps const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY, event); // 4. Create new dates const startDate = this.dateService.createDateAtTime(columnDate, startMinutes); let endDate = this.dateService.createDateAtTime(columnDate, endMinutes); // Handle cross-midnight if (endMinutes >= 1440) { const extraDays = Math.floor(endMinutes / 1440); endDate = this.dateService.addDays(endDate, extraDays); } // 5. Update in IndexedDB const updatedEvent = { ...event, start: startDate, end: endDate }; await SwpEventElement.indexedDB.saveEvent(updatedEvent); // 6. Re-render from updated data await this.renderFromEvent(updatedEvent); } ``` ### Phase 5: Update Height Updates **File:** `src/elements/SwpEventElement.ts` (line 142) ```typescript public async updateHeight(newHeight: number): Promise { // 1. Update visual height this.style.height = `${newHeight}px`; // 2. Load current event const event = await this.loadEventData(); if (!event) return; // 3. Calculate new end time const gridSettings = this.config.gridSettings; const { hourHeight, snapInterval } = gridSettings; const rawDurationMinutes = (newHeight / hourHeight) * 60; const snappedDurationMinutes = Math.round(rawDurationMinutes / snapInterval) * snapInterval; const endDate = this.dateService.addMinutes(event.start, snappedDurationMinutes); // 4. Update in IndexedDB const updatedEvent = { ...event, end: endDate }; await SwpEventElement.indexedDB.saveEvent(updatedEvent); // 5. Re-render await this.renderFromEvent(updatedEvent); } ``` ### Phase 6: Update Calculate Times **File:** `src/elements/SwpEventElement.ts` (line 255) ```typescript private calculateTimesFromPosition(snappedY: number, event: ICalendarEvent): { startMinutes: number; endMinutes: number } { const gridSettings = this.config.gridSettings; const { hourHeight, dayStartHour, snapInterval } = gridSettings; // Calculate original duration from event data const originalDuration = (event.end.getTime() - event.start.getTime()) / (1000 * 60); // Calculate snapped start minutes const minutesFromGridStart = (snappedY / hourHeight) * 60; const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart; const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval; // Calculate end minutes const endMinutes = snappedStartMinutes + originalDuration; return { startMinutes: snappedStartMinutes, endMinutes }; } ``` ### Phase 7: Update DragDropManager **File:** `src/managers/DragDropManager.ts` All places reading from `element.dataset.start`, `element.dataset.end` etc. must change to: ```typescript // Before: const start = new Date(element.dataset.start); const end = new Date(element.dataset.end); // After: const event = await SwpEventElement.extractCalendarEventFromElement(element); if (!event) return; const start = event.start; const end = event.end; ``` ### Phase 8: Update Clone Method **File:** `src/elements/SwpEventElement.ts` (line 169) ```typescript public async createClone(): Promise { const clone = this.cloneNode(true) as SwpEventElement; // Apply "clone-" prefix to ID clone.dataset.eventId = `clone-${this.eventId}`; // Disable pointer events clone.style.pointerEvents = 'none'; // Load event data to get duration const event = await this.loadEventData(); if (event) { const duration = (event.end.getTime() - event.start.getTime()) / (1000 * 60); clone.dataset.originalDuration = duration.toString(); } // Set height from original clone.style.height = this.style.height || `${this.getBoundingClientRect().height}px`; return clone; } ``` ### Phase 9: Initialize IndexedDB Reference **File:** `src/index.ts` ```typescript // After IndexedDB initialization const indexedDB = new IndexedDBService(); await indexedDB.initialize(); // Set reference in SwpEventElement SwpEventElement.setIndexedDB(indexedDB); ``` ## Data Flow ```mermaid sequenceDiagram participant DOM as SwpEventElement participant IDB as IndexedDBService participant User User->>DOM: Drag event DOM->>IDB: getEvent(id) IDB-->>DOM: ICalendarEvent DOM->>DOM: Calculate new position DOM->>IDB: saveEvent(updated) IDB-->>DOM: Success DOM->>DOM: renderFromEvent() ``` ## Benefits ✅ **Minimal DOM**: Only 1 attribute instead of 8+ ✅ **Single Source of Truth**: IndexedDB is authoritative ✅ **No Duplication**: Data only in one place ✅ **Scalability**: Large descriptions no problem ✅ **Simpler Sync**: No DOM/IndexedDB mismatch ## Potential Challenges ⚠️ **Async Complexity**: All data operations become async ⚠️ **Performance**: More IndexedDB lookups ⚠️ **Drag Smoothness**: Async lookup during drag ## Solutions to Challenges 1. **Async Complexity**: Use `async/await` consistently throughout 2. **Performance**: IndexedDB is fast enough for our use case 3. **Drag Smoothness**: Store `data-original-duration` during drag to avoid lookup ## Files to Modify 1. ✏️ `src/elements/SwpEventElement.ts` - Main refactoring 2. ✏️ `src/managers/DragDropManager.ts` - Update to use async lookups 3. ✏️ `src/index.ts` - Initialize IndexedDB reference 4. ✏️ `src/renderers/EventRenderer.ts` - May need async updates 5. ✏️ `src/managers/AllDayManager.ts` - May need async updates ## Testing Strategy 1. Test event rendering with only ID in DOM 2. Test drag & drop with async data loading 3. Test resize with async data loading 4. Test performance with many events 5. Test offline functionality 6. Test sync after reconnection ## Next Steps 1. Review this plan 2. Discuss any concerns or modifications 3. Switch to Code mode for implementation 4. Implement phase by phase 5. Test thoroughly after each phase --- **Note:** This is a significant architectural change. We should implement it carefully and test thoroughly at each phase.