/** * SimpleEventOverlapManager - Clean, focused overlap management * Eliminates complex state tracking in favor of direct DOM manipulation */ import { calendarConfig } from '../core/CalendarConfig'; export var OverlapType; (function (OverlapType) { OverlapType["NONE"] = "none"; OverlapType["COLUMN_SHARING"] = "column_sharing"; OverlapType["STACKING"] = "stacking"; })(OverlapType || (OverlapType = {})); export class SimpleEventOverlapManager { /** * Detect overlap type between two DOM elements - pixel-based logic */ resolveOverlapType(element1, element2) { const top1 = parseInt(element1.style.top) || 0; const height1 = parseInt(element1.style.height) || 0; const bottom1 = top1 + height1; const top2 = parseInt(element2.style.top) || 0; const height2 = parseInt(element2.style.height) || 0; const bottom2 = top2 + height2; // Check if events overlap in pixel space const tolerance = 2; if (bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance)) { return OverlapType.NONE; } // Events overlap - check start position difference for overlap type const startDifference = Math.abs(top1 - top2); // Over 40px start difference = stacking if (startDifference > 40) { return OverlapType.STACKING; } // Within 40px start difference = column sharing return OverlapType.COLUMN_SHARING; } /** * Group overlapping elements - pixel-based algorithm */ groupOverlappingElements(elements) { const groups = []; const processed = new Set(); for (const element of elements) { if (processed.has(element)) continue; // Find all elements that overlap with this one const overlapping = elements.filter(other => { if (processed.has(other)) return false; return other === element || this.resolveOverlapType(element, other) !== OverlapType.NONE; }); // Mark all as processed overlapping.forEach(e => processed.add(e)); groups.push(overlapping); } return groups; } /** * Create flexbox container for column sharing - clean and simple */ createEventGroup(events, position) { const container = document.createElement('swp-event-group'); return container; } /** * Add event to flexbox group - simple relative positioning */ addToEventGroup(container, eventElement) { // Set duration-based height const duration = eventElement.dataset.duration; if (duration) { const durationMinutes = parseInt(duration); const gridSettings = calendarConfig.getGridSettings(); const height = (durationMinutes / 60) * gridSettings.hourHeight; eventElement.style.height = `${height - 3}px`; } // Flexbox styling eventElement.style.position = 'relative'; eventElement.style.flex = '1'; eventElement.style.minWidth = '50px'; container.appendChild(eventElement); } /** * Create stacked event with data-attribute tracking */ createStackedEvent(eventElement, underlyingElement, stackLevel) { const marginLeft = stackLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; // Apply visual styling eventElement.style.marginLeft = `${marginLeft}px`; eventElement.style.left = '2px'; eventElement.style.right = '2px'; eventElement.style.zIndex = `${100 + stackLevel}`; // Set up stack linking via data attributes const eventId = eventElement.dataset.eventId; const underlyingId = underlyingElement.dataset.eventId; if (!eventId || !underlyingId) { console.warn('Missing event IDs for stack linking:', eventId, underlyingId); return; } // Find the last event in the stack chain let lastElement = underlyingElement; let lastLink = this.getStackLink(lastElement); // If underlying doesn't have stack link yet, create it if (!lastLink) { this.setStackLink(lastElement, { stackLevel: 0 }); lastLink = { stackLevel: 0 }; } // Traverse to find the end of the chain while (lastLink?.next) { const nextElement = this.findElementById(lastLink.next); if (!nextElement) break; lastElement = nextElement; lastLink = this.getStackLink(lastElement); } // Link the new event to the end of the chain const lastElementId = lastElement.dataset.eventId; this.setStackLink(lastElement, { ...lastLink, next: eventId }); this.setStackLink(eventElement, { prev: lastElementId, stackLevel: stackLevel }); } /** * Remove stacked styling with proper stack re-linking */ removeStackedStyling(eventElement) { // Clear visual styling eventElement.style.marginLeft = ''; eventElement.style.zIndex = ''; eventElement.style.left = '2px'; eventElement.style.right = '2px'; // Handle stack chain re-linking const link = this.getStackLink(eventElement); if (link) { // Re-link prev and next events if (link.prev && link.next) { // Middle element - link prev to next const prevElement = this.findElementById(link.prev); const nextElement = this.findElementById(link.next); if (prevElement && nextElement) { const prevLink = this.getStackLink(prevElement); const nextLink = this.getStackLink(nextElement); // CRITICAL: Check if prev and next actually overlap without the middle element const actuallyOverlap = this.resolveOverlapType(prevElement, nextElement); if (!actuallyOverlap) { // CHAIN BREAKING: prev and next don't overlap - break the chain console.log('Breaking stack chain - events do not overlap directly'); // Prev element: remove next link (becomes end of its own chain) this.setStackLink(prevElement, { ...prevLink, next: undefined }); // Next element: becomes standalone (remove all stack links and styling) this.setStackLink(nextElement, null); nextElement.style.marginLeft = ''; nextElement.style.zIndex = ''; // If next element had subsequent events, they also become standalone if (nextLink?.next) { let subsequentId = nextLink.next; while (subsequentId) { const subsequentElement = this.findElementById(subsequentId); if (!subsequentElement) break; const subsequentLink = this.getStackLink(subsequentElement); this.setStackLink(subsequentElement, null); subsequentElement.style.marginLeft = ''; subsequentElement.style.zIndex = ''; subsequentId = subsequentLink?.next; } } } else { // NORMAL STACKING: they overlap, maintain the chain this.setStackLink(prevElement, { ...prevLink, next: link.next }); const correctStackLevel = (prevLink?.stackLevel ?? 0) + 1; this.setStackLink(nextElement, { ...nextLink, prev: link.prev, stackLevel: correctStackLevel }); // Update visual styling to match new stackLevel const marginLeft = correctStackLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; nextElement.style.marginLeft = `${marginLeft}px`; nextElement.style.zIndex = `${100 + correctStackLevel}`; } } } else if (link.prev) { // Last element - remove next link from prev const prevElement = this.findElementById(link.prev); if (prevElement) { const prevLink = this.getStackLink(prevElement); this.setStackLink(prevElement, { ...prevLink, next: undefined }); } } else if (link.next) { // First element - remove prev link from next const nextElement = this.findElementById(link.next); if (nextElement) { const nextLink = this.getStackLink(nextElement); this.setStackLink(nextElement, { ...nextLink, prev: undefined, stackLevel: 0 // Next becomes the base event }); } } // Only update subsequent stack levels if we didn't break the chain if (link.prev && link.next) { const nextElement = this.findElementById(link.next); const nextLink = nextElement ? this.getStackLink(nextElement) : null; // If next element still has a stack link, the chain wasn't broken if (nextLink && nextLink.next) { this.updateSubsequentStackLevels(nextLink.next, -1); } // If nextLink is null, chain was broken - no subsequent updates needed } else { // First or last removal - update all subsequent this.updateSubsequentStackLevels(link.next, -1); } // Clear this element's stack link this.setStackLink(eventElement, null); } } /** * Update stack levels for all events following a given event ID */ updateSubsequentStackLevels(startEventId, levelDelta) { let currentId = startEventId; while (currentId) { const currentElement = this.findElementById(currentId); if (!currentElement) break; const currentLink = this.getStackLink(currentElement); if (!currentLink) break; // Update stack level const newLevel = Math.max(0, currentLink.stackLevel + levelDelta); this.setStackLink(currentElement, { ...currentLink, stackLevel: newLevel }); // Update visual styling const marginLeft = newLevel * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; currentElement.style.marginLeft = `${marginLeft}px`; currentElement.style.zIndex = `${100 + newLevel}`; currentId = currentLink.next; } } /** * Check if element is stacked - check both style and data-stack-link */ isStackedEvent(element) { const marginLeft = element.style.marginLeft; const hasMarginLeft = marginLeft !== '' && marginLeft !== '0px'; const hasStackLink = this.getStackLink(element) !== null; return hasMarginLeft || hasStackLink; } /** * Remove event from group with proper cleanup */ removeFromEventGroup(container, eventId) { const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`); if (!eventElement) return false; // Simply remove the element - no position calculation needed since it's being removed eventElement.remove(); // Handle remaining events const remainingEvents = container.querySelectorAll('swp-event'); const remainingCount = remainingEvents.length; if (remainingCount === 0) { container.remove(); return true; } if (remainingCount === 1) { const remainingEvent = remainingEvents[0]; // Convert last event back to absolute positioning - use current pixel position const currentTop = parseInt(remainingEvent.style.top) || 0; remainingEvent.style.position = 'absolute'; remainingEvent.style.top = `${currentTop}px`; remainingEvent.style.left = '2px'; remainingEvent.style.right = '2px'; remainingEvent.style.flex = ''; remainingEvent.style.minWidth = ''; container.parentElement?.insertBefore(remainingEvent, container); container.remove(); return true; } return false; } /** * Restack events in container - respects separate stack chains */ restackEventsInContainer(container) { const stackedEvents = Array.from(container.querySelectorAll('swp-event')) .filter(el => this.isStackedEvent(el)); if (stackedEvents.length === 0) return; // Group events by their stack chains const processedEventIds = new Set(); const stackChains = []; for (const element of stackedEvents) { const eventId = element.dataset.eventId; if (!eventId || processedEventIds.has(eventId)) continue; // Find the root of this stack chain (stackLevel 0 or no prev link) let rootElement = element; let rootLink = this.getStackLink(rootElement); while (rootLink?.prev) { const prevElement = this.findElementById(rootLink.prev); if (!prevElement) break; rootElement = prevElement; rootLink = this.getStackLink(rootElement); } // Collect all elements in this chain const chain = []; let currentElement = rootElement; while (currentElement) { chain.push(currentElement); processedEventIds.add(currentElement.dataset.eventId); const currentLink = this.getStackLink(currentElement); if (!currentLink?.next) break; const nextElement = this.findElementById(currentLink.next); if (!nextElement) break; currentElement = nextElement; } if (chain.length > 1) { // Only add chains with multiple events stackChains.push(chain); } } // Re-stack each chain separately stackChains.forEach(chain => { chain.forEach((element, index) => { const marginLeft = index * SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX; element.style.marginLeft = `${marginLeft}px`; element.style.zIndex = `${100 + index}`; // Update the data-stack-link with correct stackLevel const link = this.getStackLink(element); if (link) { this.setStackLink(element, { ...link, stackLevel: index }); } }); }); } /** * Utility methods - simple DOM traversal */ getEventGroup(eventElement) { return eventElement.closest('swp-event-group'); } isInEventGroup(element) { return this.getEventGroup(element) !== null; } /** * Helper methods for data-attribute based stack tracking */ getStackLink(element) { const linkData = element.dataset.stackLink; if (!linkData) return null; try { return JSON.parse(linkData); } catch (e) { console.warn('Failed to parse stack link data:', linkData, e); return null; } } setStackLink(element, link) { if (link === null) { delete element.dataset.stackLink; } else { element.dataset.stackLink = JSON.stringify(link); } } findElementById(eventId) { return document.querySelector(`swp-event[data-event-id="${eventId}"]`); } } SimpleEventOverlapManager.STACKING_WIDTH_REDUCTION_PX = 15; //# sourceMappingURL=SimpleEventOverlapManager.js.map