Calendar/data_attribute_solution.md
Janus Knudsen 5bdb2f578d Simplifies event overlap management
Refactors event overlap handling to use a DOM-centric approach with data attributes for stack tracking. This eliminates complex state management, reduces code complexity, and improves maintainability. Removes the previous Map-based linked list implementation.

The new approach offers better debugging, automatic memory management, and eliminates state synchronization bugs.

The solution maintains identical functionality with a significantly simpler implementation, focusing on DOM manipulation for visual stacking and column sharing.

Addresses potential performance concerns of DOM queries by scoping them to specific containers.
2025-09-04 23:35:19 +02:00

6.6 KiB

Data-Attribute Stack Tracking Solution

Implementation Summary

Vi har nu implementeret stack tracking via data attributes i stedet for komplekse Map-baserede linked lists.

🎯 How it works:

<!-- Base event -->
<swp-event 
  data-event-id="event_123"
  data-stack-link='{"stackLevel":0,"next":"event_456"}'
  style="margin-left: 0px;">
</swp-event>

<!-- Stacked event -->
<swp-event 
  data-event-id="event_456"
  data-stack-link='{"prev":"event_123","stackLevel":1,"next":"event_789"}'
  style="margin-left: 15px;">
</swp-event>

<!-- Top stacked event -->
<swp-event 
  data-event-id="event_789"
  data-stack-link='{"prev":"event_456","stackLevel":2}'
  style="margin-left: 30px;">
</swp-event>

🔧 Key Methods:

createStackedEvent()

// Links new event to end of chain
let lastElement = underlyingElement;
while (lastLink?.next) {
  lastElement = this.findElementById(lastLink.next);
  lastLink = this.getStackLink(lastElement);
}

// Create bidirectional link
this.setStackLink(lastElement, { ...lastLink, next: eventId });
this.setStackLink(eventElement, { prev: lastElementId, stackLevel });

removeStackedStyling()

// Re-link prev and next
if (link.prev && link.next) {
  this.setStackLink(prevElement, { ...prevLink, next: link.next });
  this.setStackLink(nextElement, { ...nextLink, prev: link.prev });
}

// Update subsequent stack levels
this.updateSubsequentStackLevels(link.next, -1);

restackEventsInContainer()

// Group by stack chains (not all stacked events together!)
for (const element of stackedEvents) {
  // Find root of chain
  while (rootLink?.prev) {
    rootElement = this.findElementById(rootLink.prev);
  }
  
  // Collect entire chain
  // Re-stack each chain separately
}

🏆 Advantages vs Map Solution:

Simplified State Management

Aspect Map + Linked List Data Attributes
State Location Separate Map object In DOM elements
Synchronization Manual sync required Automatic with DOM
Memory Cleanup Manual Map cleanup Automatic with element removal
Debugging Console logs only DevTools inspection
State Consistency Possible sync bugs Always consistent

Code Complexity Reduction

// OLD: Complex Map management
private stackChains = new Map<string, { next?: string, prev?: string, stackLevel: number }>();

// Find last event in chain - complex iteration
let lastEventId = underlyingId;
while (this.stackChains.has(lastEventId) && this.stackChains.get(lastEventId)?.next) {
  lastEventId = this.stackChains.get(lastEventId)!.next!;
}

// Link events - error prone
this.stackChains.get(lastEventId)!.next = eventId;
this.stackChains.set(eventId, { prev: lastEventId, stackLevel });

// NEW: Simple data attribute management
let lastElement = underlyingElement;
while (lastLink?.next) {
  lastElement = this.findElementById(lastLink.next);
}

this.setStackLink(lastElement, { ...lastLink, next: eventId });
this.setStackLink(eventElement, { prev: lastElementId, stackLevel });

Better Error Handling

// DOM elements can't get out of sync with their own attributes
// When element is removed, its state automatically disappears
// No orphaned Map entries

🧪 Test Scenarios:

Scenario 1: Multiple Separate Stacks

Column has:
Stack A: Event1 → Event2 → Event3  (times: 09:00-10:00, 09:15-10:15, 09:30-10:30)
Stack B: Event4 → Event5           (times: 14:00-15:00, 14:10-15:10)

Remove Event2 (middle of Stack A):
✅ Expected: Event1 → Event3 (Event3 moves to 15px margin)
✅ Expected: Stack B unchanged (Event4→Event5 still at 0px→15px)
❌ Old naive approach: Would group all events together

Scenario 2: Remove Base Event

Stack: EventA(base) → EventB → EventC

Remove EventA:
✅ Expected: EventB becomes base (0px), EventC moves to 15px
✅ Data-attribute solution: EventB.stackLevel = 0, EventC.stackLevel = 1

Scenario 3: Drag and Drop

Drag Event2 from Stack A to new position:
✅ removeStackedStyling() handles re-linking
✅ Other stack events maintain their relationships
✅ No Map synchronization issues

🔍 Debugging Benefits:

Browser DevTools Inspection:

<!-- Easy to see stack relationships directly in HTML -->
<swp-event data-stack-link='{"prev":"123","next":"789","stackLevel":1}'>
  <!-- Event content -->
</swp-event>

Console Debugging:

// Easy to inspect stack chains
const element = document.querySelector('[data-event-id="456"]');
const link = JSON.parse(element.dataset.stackLink);
console.log('Stack chain:', link);

📊 Performance Comparison:

Operation Map Solution Data-Attribute Solution
Create Stack Map.set() + element.style JSON.stringify() + element.style
Remove Stack Map manipulation + DOM queries JSON.parse/stringify + DOM queries
Find Chain Map iteration DOM traversal
Memory Usage Map + DOM DOM only
Sync Overhead High (keep Map in sync) None (DOM is source)

Performance Notes:

  • JSON.parse/stringify: Very fast for small objects (~10 properties max)
  • DOM traversal: Limited by chain length (typically 2-5 events)
  • Memory: Significant reduction (no separate Map)
  • Garbage collection: Better (automatic cleanup)

Solution Status:

Completed:

  • StackLink interface definition
  • Helper methods (getStackLink, setStackLink, findElementById)
  • createStackedEvent with data-attribute linking
  • removeStackedStyling with proper re-linking
  • restackEventsInContainer respects separate chains
  • isStackedEvent checks both style and data-attributes
  • Compilation successful

Ready for Testing:

  • Manual UI testing of stack behavior
  • Drag and drop stacked events
  • Multiple stacks in same column
  • Edge cases (remove first/middle/last)

🎉 Conclusion:

This data-attribute solution provides:

  1. Same functionality as the Map-based approach
  2. Simpler implementation (DOM as single source of truth)
  3. Better debugging experience (DevTools visibility)
  4. Automatic memory management (no manual cleanup)
  5. No synchronization bugs (state follows element)

The solution maintains all the precision of the original complex system while dramatically simplifying the implementation and eliminating entire classes of potential bugs.