wip
This commit is contained in:
parent
727a6ec53a
commit
72019a3d9a
15 changed files with 1056 additions and 1230 deletions
|
|
@ -27,11 +27,16 @@ export class DragDropManager {
|
|||
private lastLoggedPosition: Position = { x: 0, y: 0 };
|
||||
private currentMouseY = 0;
|
||||
private mouseOffset: Position = { x: 0, y: 0 };
|
||||
private initialMousePosition: Position = { x: 0, y: 0 };
|
||||
|
||||
// Drag state
|
||||
private draggedEventId: string | null = null;
|
||||
private originalElement: HTMLElement | null = null;
|
||||
private currentColumn: string | null = null;
|
||||
private isDragStarted = false;
|
||||
|
||||
// Movement threshold to distinguish click from drag
|
||||
private readonly dragThreshold = 5; // pixels
|
||||
|
||||
// Cached DOM elements for performance
|
||||
private cachedElements: CachedElements = {
|
||||
|
|
@ -105,8 +110,10 @@ export class DragDropManager {
|
|||
|
||||
private handleMouseDown(event: MouseEvent): void {
|
||||
this.isMouseDown = true;
|
||||
this.isDragStarted = false;
|
||||
this.lastMousePosition = { x: event.clientX, y: event.clientY };
|
||||
this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
|
||||
this.initialMousePosition = { x: event.clientX, y: event.clientY };
|
||||
|
||||
// Check if mousedown is on an event
|
||||
const target = event.target as HTMLElement;
|
||||
|
|
@ -125,7 +132,7 @@ export class DragDropManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Found an event - start dragging
|
||||
// Found an event - prepare for potential dragging
|
||||
if (eventElement) {
|
||||
this.originalElement = eventElement;
|
||||
this.draggedEventId = eventElement.dataset.eventId || null;
|
||||
|
|
@ -143,15 +150,7 @@ export class DragDropManager {
|
|||
this.currentColumn = column;
|
||||
}
|
||||
|
||||
// Emit drag start event
|
||||
this.eventBus.emit('drag:start', {
|
||||
originalElement: eventElement,
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: { x: event.clientX, y: event.clientY },
|
||||
mouseOffset: this.mouseOffset,
|
||||
column: this.currentColumn
|
||||
});
|
||||
|
||||
// Don't emit drag:start yet - wait for movement threshold
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,40 +162,66 @@ export class DragDropManager {
|
|||
|
||||
if (this.isMouseDown && this.draggedEventId) {
|
||||
const currentPosition: Position = { x: event.clientX, y: event.clientY };
|
||||
const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y);
|
||||
|
||||
// Check for snap interval vertical movement (normal drag behavior)
|
||||
if (deltaY >= this.snapDistancePx) {
|
||||
this.lastLoggedPosition = currentPosition;
|
||||
// Check if we need to start drag (movement threshold)
|
||||
if (!this.isDragStarted) {
|
||||
const deltaX = Math.abs(currentPosition.x - this.initialMousePosition.x);
|
||||
const deltaY = Math.abs(currentPosition.y - this.initialMousePosition.y);
|
||||
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
// Consolidated position calculations with snapping for normal drag
|
||||
const positionData = this.calculateDragPosition(currentPosition);
|
||||
|
||||
// Emit drag move event with snapped position (normal behavior)
|
||||
this.eventBus.emit('drag:move', {
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: currentPosition,
|
||||
snappedY: positionData.snappedY,
|
||||
column: positionData.column,
|
||||
mouseOffset: this.mouseOffset
|
||||
});
|
||||
if (totalMovement >= this.dragThreshold) {
|
||||
// Start drag - emit drag:start event
|
||||
this.isDragStarted = true;
|
||||
this.eventBus.emit('drag:start', {
|
||||
originalElement: this.originalElement,
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: this.initialMousePosition,
|
||||
mouseOffset: this.mouseOffset,
|
||||
column: this.currentColumn
|
||||
});
|
||||
} else {
|
||||
// Not enough movement yet - don't start drag
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for auto-scroll
|
||||
this.checkAutoScroll(event);
|
||||
|
||||
// Check for column change using cached data
|
||||
const newColumn = this.getColumnFromCache(currentPosition);
|
||||
if (newColumn && newColumn !== this.currentColumn) {
|
||||
const previousColumn = this.currentColumn;
|
||||
this.currentColumn = newColumn;
|
||||
// Continue with normal drag behavior only if drag has started
|
||||
if (this.isDragStarted) {
|
||||
const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y);
|
||||
|
||||
this.eventBus.emit('drag:column-change', {
|
||||
eventId: this.draggedEventId,
|
||||
previousColumn,
|
||||
newColumn,
|
||||
mousePosition: currentPosition
|
||||
});
|
||||
// Check for snap interval vertical movement (normal drag behavior)
|
||||
if (deltaY >= this.snapDistancePx) {
|
||||
this.lastLoggedPosition = currentPosition;
|
||||
|
||||
// Consolidated position calculations with snapping for normal drag
|
||||
const positionData = this.calculateDragPosition(currentPosition);
|
||||
|
||||
// Emit drag move event with snapped position (normal behavior)
|
||||
this.eventBus.emit('drag:move', {
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: currentPosition,
|
||||
snappedY: positionData.snappedY,
|
||||
column: positionData.column,
|
||||
mouseOffset: this.mouseOffset
|
||||
});
|
||||
}
|
||||
|
||||
// Check for auto-scroll
|
||||
this.checkAutoScroll(event);
|
||||
|
||||
// Check for column change using cached data
|
||||
const newColumn = this.getColumnFromCache(currentPosition);
|
||||
if (newColumn && newColumn !== this.currentColumn) {
|
||||
const previousColumn = this.currentColumn;
|
||||
this.currentColumn = newColumn;
|
||||
|
||||
this.eventBus.emit('drag:column-change', {
|
||||
eventId: this.draggedEventId,
|
||||
previousColumn,
|
||||
newColumn,
|
||||
mousePosition: currentPosition
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,19 +236,29 @@ export class DragDropManager {
|
|||
this.stopAutoScroll();
|
||||
|
||||
if (this.draggedEventId && this.originalElement) {
|
||||
const finalPosition: Position = { x: event.clientX, y: event.clientY };
|
||||
|
||||
// Use consolidated position calculation
|
||||
const positionData = this.calculateDragPosition(finalPosition);
|
||||
|
||||
// Emit drag end event
|
||||
this.eventBus.emit('drag:end', {
|
||||
eventId: this.draggedEventId,
|
||||
originalElement: this.originalElement,
|
||||
finalPosition,
|
||||
finalColumn: positionData.column,
|
||||
finalY: positionData.snappedY
|
||||
});
|
||||
// Only emit drag:end if drag was actually started
|
||||
if (this.isDragStarted) {
|
||||
const finalPosition: Position = { x: event.clientX, y: event.clientY };
|
||||
|
||||
// Use consolidated position calculation
|
||||
const positionData = this.calculateDragPosition(finalPosition);
|
||||
|
||||
// Emit drag end event
|
||||
this.eventBus.emit('drag:end', {
|
||||
eventId: this.draggedEventId,
|
||||
originalElement: this.originalElement,
|
||||
finalPosition,
|
||||
finalColumn: positionData.column,
|
||||
finalY: positionData.snappedY
|
||||
});
|
||||
} else {
|
||||
// This was just a click - emit click event instead
|
||||
this.eventBus.emit('event:click', {
|
||||
eventId: this.draggedEventId,
|
||||
originalElement: this.originalElement,
|
||||
mousePosition: { x: event.clientX, y: event.clientY }
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up drag state
|
||||
this.cleanupDragState();
|
||||
|
|
@ -424,6 +459,7 @@ export class DragDropManager {
|
|||
this.draggedEventId = null;
|
||||
this.originalElement = null;
|
||||
this.currentColumn = null;
|
||||
this.isDragStarted = false;
|
||||
|
||||
// Clear cached elements
|
||||
this.cachedElements.currentColumn = null;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export class EventManager {
|
|||
return resourceData.resources.flatMap(resource =>
|
||||
resource.events.map(event => ({
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end),
|
||||
resourceName: resource.name,
|
||||
resourceDisplayName: resource.displayName,
|
||||
resourceEmployeeId: resource.employeeId
|
||||
|
|
@ -70,7 +72,11 @@ export class EventManager {
|
|||
);
|
||||
}
|
||||
|
||||
return data as CalendarEvent[];
|
||||
return data.map((event: any) => ({
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -115,15 +121,14 @@ export class EventManager {
|
|||
}
|
||||
|
||||
try {
|
||||
const eventDate = new Date(event.start);
|
||||
if (isNaN(eventDate.getTime())) {
|
||||
if (isNaN(event.start.getTime())) {
|
||||
console.warn(`EventManager: Invalid event start date for event ${id}:`, event.start);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
eventDate
|
||||
eventDate: event.start
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`EventManager: Failed to parse event date for event ${id}:`, error);
|
||||
|
|
@ -171,12 +176,8 @@ export class EventManager {
|
|||
|
||||
// Filter events using optimized date operations
|
||||
const filteredEvents = this.events.filter(event => {
|
||||
// Use DateCalculator for consistent date parsing
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
// Event overlaps period if it starts before period ends AND ends after period starts
|
||||
return eventStart <= endDate && eventEnd >= startDate;
|
||||
return event.start <= endDate && event.end >= startDate;
|
||||
});
|
||||
|
||||
// Cache the result
|
||||
|
|
|
|||
|
|
@ -1,451 +0,0 @@
|
|||
/**
|
||||
* EventOverlapManager - Håndterer overlap detection og DOM manipulation for overlapping events
|
||||
* Implementerer både column sharing (flexbox) og stacking patterns
|
||||
*/
|
||||
|
||||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
import { DateCalculator } from '../utils/DateCalculator';
|
||||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
|
||||
export enum OverlapType {
|
||||
NONE = 'none',
|
||||
COLUMN_SHARING = 'column_sharing',
|
||||
STACKING = 'stacking'
|
||||
}
|
||||
|
||||
export interface OverlapGroup {
|
||||
type: OverlapType;
|
||||
events: CalendarEvent[];
|
||||
position: { top: number; height: number };
|
||||
container?: HTMLElement;
|
||||
}
|
||||
|
||||
export class EventOverlapManager {
|
||||
private static readonly STACKING_TIME_THRESHOLD_MINUTES = 30;
|
||||
private static readonly STACKING_WIDTH_REDUCTION_PX = 15;
|
||||
private nextZIndex = 100;
|
||||
|
||||
// Linked list til at holde styr på stacked events
|
||||
private stackChains = new Map<string, { next?: string, prev?: string, stackLevel: number }>();
|
||||
|
||||
|
||||
/**
|
||||
* Detect overlap mellem events baseret på faktisk time overlap og start tid forskel
|
||||
*/
|
||||
public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
|
||||
// Først: Tjek om events overlapper i tid
|
||||
if (!this.eventsOverlapInTime(event1, event2)) {
|
||||
return OverlapType.NONE;
|
||||
}
|
||||
|
||||
// Events overlapper i tid - nu tjek start tid forskel
|
||||
const start1 = new Date(event1.start).getTime();
|
||||
const start2 = new Date(event2.start).getTime();
|
||||
const timeDiffMinutes = Math.abs(start1 - start2) / (1000 * 60);
|
||||
|
||||
// Over 30 min start forskel = stacking
|
||||
if (timeDiffMinutes > EventOverlapManager.STACKING_TIME_THRESHOLD_MINUTES) {
|
||||
return OverlapType.STACKING;
|
||||
}
|
||||
|
||||
// Indenfor 30 min start forskel = column sharing
|
||||
return OverlapType.COLUMN_SHARING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tjek om to events faktisk overlapper i tid
|
||||
*/
|
||||
private eventsOverlapInTime(event1: CalendarEvent, event2: CalendarEvent): boolean {
|
||||
const start1 = new Date(event1.start).getTime();
|
||||
const end1 = new Date(event1.end).getTime();
|
||||
const start2 = new Date(event2.start).getTime();
|
||||
const end2 = new Date(event2.end).getTime();
|
||||
|
||||
// Events overlapper hvis de deler mindst ét tidspunkt
|
||||
return !(end1 <= start2 || end2 <= start1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppér events baseret på overlap type
|
||||
*/
|
||||
public groupOverlappingEvents(events: CalendarEvent[]): OverlapGroup[] {
|
||||
const groups: OverlapGroup[] = [];
|
||||
const processedEvents = new Set<string>();
|
||||
|
||||
for (const event of events) {
|
||||
if (processedEvents.has(event.id)) continue;
|
||||
|
||||
const overlappingEvents = [event];
|
||||
processedEvents.add(event.id);
|
||||
|
||||
// Find alle events der overlapper med dette event
|
||||
for (const otherEvent of events) {
|
||||
if (otherEvent.id === event.id || processedEvents.has(otherEvent.id)) continue;
|
||||
|
||||
const overlapType = this.detectOverlap(event, otherEvent);
|
||||
if (overlapType !== OverlapType.NONE) {
|
||||
overlappingEvents.push(otherEvent);
|
||||
processedEvents.add(otherEvent.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Opret gruppe hvis der er overlap
|
||||
if (overlappingEvents.length > 1) {
|
||||
const overlapType = this.detectOverlap(overlappingEvents[0], overlappingEvents[1]);
|
||||
groups.push({
|
||||
type: overlapType,
|
||||
events: overlappingEvents,
|
||||
position: this.calculateGroupPosition(overlappingEvents)
|
||||
});
|
||||
} else {
|
||||
// Single event - ingen overlap
|
||||
groups.push({
|
||||
type: OverlapType.NONE,
|
||||
events: [event],
|
||||
position: this.calculateGroupPosition([event])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opret flexbox container for column sharing events
|
||||
*/
|
||||
public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement {
|
||||
const container = document.createElement('swp-event-group');
|
||||
container.style.position = 'absolute';
|
||||
container.style.top = `${position.top}px`;
|
||||
// Ingen højde på gruppen - kun på individuelle events
|
||||
container.style.left = '2px';
|
||||
container.style.right = '2px';
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tilføj event til eksisterende event group
|
||||
*/
|
||||
public addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void {
|
||||
// Sørg for at event har korrekt højde baseret på varighed
|
||||
const duration = eventElement.dataset.duration;
|
||||
if (duration) {
|
||||
const durationMinutes = parseInt(duration);
|
||||
const gridSettings = { hourHeight: 80 }; // Fra config
|
||||
const height = (durationMinutes / 60) * gridSettings.hourHeight;
|
||||
eventElement.style.height = `${height - 3}px`; // -3px som andre events
|
||||
}
|
||||
|
||||
// Events i flexbox grupper skal bruge relative positioning
|
||||
eventElement.style.position = 'relative';
|
||||
|
||||
container.appendChild(eventElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fjern event fra event group og cleanup hvis tom
|
||||
*/
|
||||
public removeFromEventGroup(container: HTMLElement, eventId: string): boolean {
|
||||
const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||
if (!eventElement) return false;
|
||||
|
||||
// Tjek om det fjernede event var stacked
|
||||
const wasStacked = this.isStackedEvent(eventElement);
|
||||
|
||||
// Beregn korrekt top position baseret på event data
|
||||
const startTime = eventElement.dataset.start;
|
||||
if (startTime) {
|
||||
const startDate = new Date(startTime);
|
||||
const gridSettings = { dayStartHour: 6, hourHeight: 80 }; // Fra config
|
||||
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
// Gendan absolute positioning med korrekt top position
|
||||
eventElement.style.position = 'absolute';
|
||||
eventElement.style.top = `${top + 1}px`; // +1px som andre events
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
// Fjern stacking styling
|
||||
eventElement.style.marginLeft = '';
|
||||
eventElement.style.zIndex = '';
|
||||
}
|
||||
|
||||
eventElement.remove();
|
||||
|
||||
// Tæl resterende events
|
||||
const remainingEvents = container.querySelectorAll('swp-event');
|
||||
const remainingCount = remainingEvents.length;
|
||||
|
||||
// Cleanup hvis tom container
|
||||
if (remainingCount === 0) {
|
||||
container.remove();
|
||||
return true; // Container blev fjernet
|
||||
}
|
||||
|
||||
// Hvis kun ét event tilbage, konvertér tilbage til normal event
|
||||
if (remainingCount === 1) {
|
||||
const remainingEvent = remainingEvents[0] as HTMLElement;
|
||||
|
||||
// Beregn korrekt top position for remaining event
|
||||
const remainingStartTime = remainingEvent.dataset.start;
|
||||
if (remainingStartTime) {
|
||||
const remainingStartDate = new Date(remainingStartTime);
|
||||
const gridSettings = { dayStartHour: 6, hourHeight: 80 };
|
||||
const remainingStartMinutes = remainingStartDate.getHours() * 60 + remainingStartDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
const remainingTop = ((remainingStartMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
// Gendan normal event positioning (absolute for standalone events)
|
||||
remainingEvent.style.position = 'absolute';
|
||||
remainingEvent.style.top = `${remainingTop + 1}px`; // +1px som andre events
|
||||
remainingEvent.style.left = '2px';
|
||||
remainingEvent.style.right = '2px';
|
||||
// Fjern eventuel stacking styling
|
||||
remainingEvent.style.marginLeft = '';
|
||||
remainingEvent.style.zIndex = '';
|
||||
}
|
||||
|
||||
// Indsæt før container og fjern container
|
||||
container.parentElement?.insertBefore(remainingEvent, container);
|
||||
container.remove();
|
||||
return true; // Container blev fjernet
|
||||
}
|
||||
|
||||
// Altid tjek for stack chain cleanup, uanset wasStacked flag
|
||||
const removedEventId = eventElement.dataset.eventId;
|
||||
console.log('Checking stack chain for removed event:', removedEventId, 'Has chain:', this.stackChains.has(removedEventId || ''));
|
||||
|
||||
if (removedEventId && this.stackChains.has(removedEventId)) {
|
||||
console.log('Removing from stack chain:', removedEventId);
|
||||
const affectedEventIds = this.removeFromStackChain(removedEventId);
|
||||
console.log('Affected events:', affectedEventIds);
|
||||
|
||||
// Opdater margin-left for påvirkede events
|
||||
affectedEventIds.forEach((affectedId: string) => {
|
||||
const affectedElement = container.querySelector(`swp-event[data-event-id="${affectedId}"]`) as HTMLElement;
|
||||
console.log('Found affected element:', affectedId, !!affectedElement);
|
||||
|
||||
if (affectedElement) {
|
||||
const chainInfo = this.stackChains.get(affectedId);
|
||||
if (chainInfo) {
|
||||
const newMarginLeft = chainInfo.stackLevel * EventOverlapManager.STACKING_WIDTH_REDUCTION_PX;
|
||||
console.log('Updating margin-left for', affectedId, 'from', affectedElement.style.marginLeft, 'to', newMarginLeft + 'px');
|
||||
affectedElement.style.marginLeft = `${newMarginLeft}px`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false; // Container blev ikke fjernet
|
||||
}
|
||||
|
||||
/**
|
||||
* Opret stacked event med margin-left offset
|
||||
*/
|
||||
public createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement, stackLevel: number = 1): void {
|
||||
// Brug margin-left i stedet for width manipulation
|
||||
const marginLeft = stackLevel * EventOverlapManager.STACKING_WIDTH_REDUCTION_PX;
|
||||
|
||||
eventElement.style.marginLeft = `${marginLeft}px`;
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
eventElement.style.width = '';
|
||||
eventElement.style.zIndex = this.getNextZIndex().toString();
|
||||
|
||||
// Tilføj til stack chain
|
||||
const eventId = eventElement.dataset.eventId;
|
||||
const underlyingId = underlyingElement.dataset.eventId;
|
||||
|
||||
console.log('STACK CHAIN ADD: Adding', eventId, 'to chain with underlying', underlyingId, 'at stackLevel', stackLevel);
|
||||
|
||||
if (eventId && underlyingId) {
|
||||
// Find sidste event i chain
|
||||
let lastEventId = underlyingId;
|
||||
while (this.stackChains.has(lastEventId) && this.stackChains.get(lastEventId)?.next) {
|
||||
lastEventId = this.stackChains.get(lastEventId)!.next!;
|
||||
}
|
||||
|
||||
console.log('STACK CHAIN ADD: Last event in chain is', lastEventId);
|
||||
|
||||
// Link det nye event til chain
|
||||
if (!this.stackChains.has(lastEventId)) {
|
||||
this.stackChains.set(lastEventId, { stackLevel: 0 });
|
||||
console.log('STACK CHAIN ADD: Created chain entry for underlying event', lastEventId);
|
||||
}
|
||||
this.stackChains.get(lastEventId)!.next = eventId;
|
||||
this.stackChains.set(eventId, { prev: lastEventId, stackLevel });
|
||||
|
||||
console.log('STACK CHAIN ADD: Linked', lastEventId, '->', eventId);
|
||||
console.log('STACK CHAIN STATE:', Array.from(this.stackChains.entries()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fjern stacking styling fra event
|
||||
*/
|
||||
public removeStackedStyling(eventElement: HTMLElement): void {
|
||||
const eventId = eventElement.dataset.eventId;
|
||||
console.log('removeStackedStyling called for:', eventId);
|
||||
|
||||
eventElement.style.marginLeft = '';
|
||||
eventElement.style.width = '';
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
eventElement.style.zIndex = '';
|
||||
|
||||
// Fjern fra stack chain og opdater andre events
|
||||
if (eventId && this.stackChains.has(eventId)) {
|
||||
console.log('Removing from stack chain and updating affected events:', eventId);
|
||||
const affectedEventIds = this.removeFromStackChain(eventId);
|
||||
console.log('Affected events from removeFromStackChain:', affectedEventIds);
|
||||
|
||||
// Find den kolonne hvor eventet var placeret
|
||||
const columnElement = eventElement.closest('swp-events-layer');
|
||||
if (columnElement) {
|
||||
console.log('Found column element, updating affected events');
|
||||
// Opdater margin-left for ALLE resterende events baseret på deres index
|
||||
affectedEventIds.forEach((affectedId: string, index: number) => {
|
||||
const affectedElement = columnElement.querySelector(`swp-event[data-event-id="${affectedId}"]`) as HTMLElement;
|
||||
console.log('Looking for affected element:', affectedId, 'found:', !!affectedElement);
|
||||
|
||||
if (affectedElement) {
|
||||
// Index 0 = 0px margin, index 1 = 15px margin, index 2 = 30px margin, osv.
|
||||
const newMarginLeft = index * EventOverlapManager.STACKING_WIDTH_REDUCTION_PX;
|
||||
console.log('Updating margin-left for', affectedId, 'at index', index, 'from', affectedElement.style.marginLeft, 'to', newMarginLeft + 'px');
|
||||
affectedElement.style.marginLeft = `${newMarginLeft}px`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('No column element found for updating affected events');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fjern event fra stack chain og re-stack resterende events
|
||||
*/
|
||||
private removeFromStackChain(eventId: string): string[] {
|
||||
console.log('STACK CHAIN REMOVE: Removing', eventId, 'from chain');
|
||||
console.log('STACK CHAIN STATE BEFORE:', Array.from(this.stackChains.entries()));
|
||||
|
||||
// Fjern eventet fra chain
|
||||
this.stackChains.delete(eventId);
|
||||
|
||||
// Find ALLE resterende events i stackChains og returner dem
|
||||
const allRemainingEventIds = Array.from(this.stackChains.keys());
|
||||
console.log('STACK CHAIN REMOVE: All remaining events to re-stack:', allRemainingEventIds);
|
||||
|
||||
// Re-assign stackLevel baseret på position (0 = underlying, 1 = første stacked, osv.)
|
||||
allRemainingEventIds.forEach((remainingId, index) => {
|
||||
const chainInfo = this.stackChains.get(remainingId);
|
||||
if (chainInfo) {
|
||||
chainInfo.stackLevel = index;
|
||||
console.log('STACK CHAIN REMOVE: Set stackLevel for', remainingId, 'to', index);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('STACK CHAIN STATE AFTER:', Array.from(this.stackChains.entries()));
|
||||
|
||||
return allRemainingEventIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-stack events efter fjernelse af et stacked event
|
||||
*/
|
||||
private restackRemainingEvents(container: HTMLElement): void {
|
||||
// Find alle stacked events (events med margin-left)
|
||||
const stackedEvents = Array.from(container.querySelectorAll('swp-event'))
|
||||
.filter(el => {
|
||||
const element = el as HTMLElement;
|
||||
return element.style.marginLeft && element.style.marginLeft !== '0px';
|
||||
}) as HTMLElement[];
|
||||
|
||||
if (stackedEvents.length === 0) return;
|
||||
|
||||
// Sort events by current margin-left (ascending)
|
||||
stackedEvents.sort((a, b) => {
|
||||
const marginA = parseInt(a.style.marginLeft) || 0;
|
||||
const marginB = parseInt(b.style.marginLeft) || 0;
|
||||
return marginA - marginB;
|
||||
});
|
||||
|
||||
// Re-assign margin-left values starting from 15px
|
||||
stackedEvents.forEach((element, index) => {
|
||||
const newMarginLeft = (index + 1) * EventOverlapManager.STACKING_WIDTH_REDUCTION_PX;
|
||||
element.style.marginLeft = `${newMarginLeft}px`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Beregn position for event gruppe
|
||||
*/
|
||||
private calculateGroupPosition(events: CalendarEvent[]): { top: number; height: number } {
|
||||
if (events.length === 0) return { top: 0, height: 0 };
|
||||
|
||||
// Find tidligste start og seneste slut
|
||||
const startTimes = events.map(e => new Date(e.start).getTime());
|
||||
const endTimes = events.map(e => new Date(e.end).getTime());
|
||||
|
||||
const earliestStart = Math.min(...startTimes);
|
||||
const latestEnd = Math.max(...endTimes);
|
||||
|
||||
// Konvertér til pixel positions (dette skal matches med EventRenderer logik)
|
||||
const startDate = new Date(earliestStart);
|
||||
const endDate = new Date(latestEnd);
|
||||
|
||||
// Brug samme logik som EventRenderer.calculateEventPosition
|
||||
const gridSettings = { dayStartHour: 6, hourHeight: 80 }; // Fra config
|
||||
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
||||
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
|
||||
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
const height = ((endMinutes - startMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
return { top, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next available z-index for stacked events
|
||||
*/
|
||||
private getNextZIndex(): number {
|
||||
return ++this.nextZIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset z-index counter
|
||||
*/
|
||||
public resetZIndex(): void {
|
||||
this.nextZIndex = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is part of an event group
|
||||
*/
|
||||
public isInEventGroup(element: HTMLElement): boolean {
|
||||
return element.closest('swp-event-group') !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is a stacked event
|
||||
*/
|
||||
public isStackedEvent(element: HTMLElement): boolean {
|
||||
const eventId = element.dataset.eventId;
|
||||
const hasMarginLeft = element.style.marginLeft !== '' && element.style.marginLeft !== '0px';
|
||||
const isInStackChain = eventId ? this.stackChains.has(eventId) : false;
|
||||
|
||||
console.log('isStackedEvent check:', eventId, 'hasMarginLeft:', hasMarginLeft, 'isInStackChain:', isInStackChain);
|
||||
|
||||
// Et event er stacked hvis det enten har margin-left ELLER er i en stack chain
|
||||
return hasMarginLeft || isInStackChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event group container for an event element
|
||||
*/
|
||||
public getEventGroup(eventElement: HTMLElement): HTMLElement | null {
|
||||
return eventElement.closest('swp-event-group') as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,67 +25,59 @@ export interface StackLink {
|
|||
}
|
||||
|
||||
export class SimpleEventOverlapManager {
|
||||
private static readonly STACKING_TIME_THRESHOLD_MINUTES = 30;
|
||||
private static readonly STACKING_WIDTH_REDUCTION_PX = 15;
|
||||
|
||||
/**
|
||||
* Detect overlap type between two events - simplified logic
|
||||
* Detect overlap type between two DOM elements - pixel-based logic
|
||||
*/
|
||||
public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
|
||||
if (!this.eventsOverlapInTime(event1, event2)) {
|
||||
public resolveOverlapType(element1: HTMLElement, element2: HTMLElement): OverlapType {
|
||||
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;
|
||||
}
|
||||
|
||||
const timeDiffMinutes = Math.abs(
|
||||
new Date(event1.start).getTime() - new Date(event2.start).getTime()
|
||||
) / (1000 * 60);
|
||||
// Events overlap - check start position difference for overlap type
|
||||
const startDifference = Math.abs(top1 - top2);
|
||||
|
||||
return timeDiffMinutes > SimpleEventOverlapManager.STACKING_TIME_THRESHOLD_MINUTES
|
||||
? OverlapType.STACKING
|
||||
: OverlapType.COLUMN_SHARING;
|
||||
// Over 40px start difference = stacking
|
||||
if (startDifference > 40) {
|
||||
return OverlapType.STACKING;
|
||||
}
|
||||
|
||||
// Within 40px start difference = column sharing
|
||||
return OverlapType.COLUMN_SHARING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple time overlap check
|
||||
*/
|
||||
private eventsOverlapInTime(event1: CalendarEvent, event2: CalendarEvent): boolean {
|
||||
const start1 = new Date(event1.start).getTime();
|
||||
const end1 = new Date(event1.end).getTime();
|
||||
const start2 = new Date(event2.start).getTime();
|
||||
const end2 = new Date(event2.end).getTime();
|
||||
|
||||
return !(end1 <= start2 || end2 <= start1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group overlapping events - much cleaner algorithm
|
||||
* Group overlapping elements - pixel-based algorithm
|
||||
*/
|
||||
public groupOverlappingEvents(events: CalendarEvent[]): OverlapGroup[] {
|
||||
const groups: OverlapGroup[] = [];
|
||||
const processed = new Set<string>();
|
||||
public groupOverlappingElements(elements: HTMLElement[]): HTMLElement[][] {
|
||||
const groups: HTMLElement[][] = [];
|
||||
const processed = new Set<HTMLElement>();
|
||||
|
||||
for (const event of events) {
|
||||
if (processed.has(event.id)) continue;
|
||||
for (const element of elements) {
|
||||
if (processed.has(element)) continue;
|
||||
|
||||
// Find all events that overlap with this one
|
||||
const overlapping = events.filter(other => {
|
||||
if (processed.has(other.id)) return false;
|
||||
return other.id === event.id || this.detectOverlap(event, other) !== OverlapType.NONE;
|
||||
// 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.id));
|
||||
overlapping.forEach(e => processed.add(e));
|
||||
|
||||
// Determine group type
|
||||
const overlapType = overlapping.length > 1
|
||||
? this.detectOverlap(overlapping[0], overlapping[1])
|
||||
: OverlapType.NONE;
|
||||
|
||||
groups.push({
|
||||
type: overlapType,
|
||||
events: overlapping,
|
||||
position: this.calculateGroupPosition(overlapping)
|
||||
});
|
||||
groups.push(overlapping);
|
||||
}
|
||||
|
||||
return groups;
|
||||
|
|
@ -96,14 +88,6 @@ export class SimpleEventOverlapManager {
|
|||
*/
|
||||
public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement {
|
||||
const container = document.createElement('swp-event-group');
|
||||
container.style.cssText = `
|
||||
position: absolute;
|
||||
top: ${position.top}px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
`;
|
||||
return container;
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +188,7 @@ export class SimpleEventOverlapManager {
|
|||
const nextLink = this.getStackLink(nextElement);
|
||||
|
||||
// CRITICAL: Check if prev and next actually overlap without the middle element
|
||||
const actuallyOverlap = this.checkPixelOverlap(prevElement, nextElement);
|
||||
const actuallyOverlap = this.resolveOverlapType(prevElement, nextElement);
|
||||
|
||||
if (!actuallyOverlap) {
|
||||
// CHAIN BREAKING: prev and next don't overlap - break the chain
|
||||
|
|
@ -346,24 +330,7 @@ export class SimpleEventOverlapManager {
|
|||
const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||
if (!eventElement) return false;
|
||||
|
||||
// Calculate correct absolute position for standalone event
|
||||
const startTime = eventElement.dataset.start;
|
||||
if (startTime) {
|
||||
const startDate = new Date(startTime);
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
// Convert back to absolute positioning
|
||||
eventElement.style.position = 'absolute';
|
||||
eventElement.style.top = `${top + 1}px`;
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
eventElement.style.flex = '';
|
||||
eventElement.style.minWidth = '';
|
||||
}
|
||||
|
||||
// Simply remove the element - no position calculation needed since it's being removed
|
||||
eventElement.remove();
|
||||
|
||||
// Handle remaining events
|
||||
|
|
@ -378,22 +345,15 @@ export class SimpleEventOverlapManager {
|
|||
if (remainingCount === 1) {
|
||||
const remainingEvent = remainingEvents[0] as HTMLElement;
|
||||
|
||||
// Convert last event back to absolute positioning
|
||||
const remainingStartTime = remainingEvent.dataset.start;
|
||||
if (remainingStartTime) {
|
||||
const remainingStartDate = new Date(remainingStartTime);
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const remainingStartMinutes = remainingStartDate.getHours() * 60 + remainingStartDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
const remainingTop = ((remainingStartMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
remainingEvent.style.position = 'absolute';
|
||||
remainingEvent.style.top = `${remainingTop + 1}px`;
|
||||
remainingEvent.style.left = '2px';
|
||||
remainingEvent.style.right = '2px';
|
||||
remainingEvent.style.flex = '';
|
||||
remainingEvent.style.minWidth = '';
|
||||
}
|
||||
// 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();
|
||||
|
|
@ -471,33 +431,6 @@ export class SimpleEventOverlapManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position for group - simplified calculation
|
||||
*/
|
||||
private calculateGroupPosition(events: CalendarEvent[]): { top: number; height: number } {
|
||||
if (events.length === 0) return { top: 0, height: 0 };
|
||||
|
||||
const times = events.flatMap(e => [
|
||||
new Date(e.start).getTime(),
|
||||
new Date(e.end).getTime()
|
||||
]);
|
||||
|
||||
const earliestStart = Math.min(...times);
|
||||
const latestEnd = Math.max(...times);
|
||||
|
||||
const startDate = new Date(earliestStart);
|
||||
const endDate = new Date(latestEnd);
|
||||
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
||||
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
||||
const dayStartMinutes = gridSettings.dayStartHour * 60;
|
||||
|
||||
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
|
||||
const height = ((endMinutes - startMinutes) / 60) * gridSettings.hourHeight;
|
||||
|
||||
return { top, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility methods - simple DOM traversal
|
||||
|
|
@ -537,22 +470,4 @@ export class SimpleEventOverlapManager {
|
|||
return document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two elements overlap in pixel space
|
||||
*/
|
||||
private checkPixelOverlap(element1: HTMLElement, element2: HTMLElement): boolean {
|
||||
if (!element1 || !element2) return false;
|
||||
|
||||
const top1 = parseFloat(element1.style.top) || 0;
|
||||
const height1 = parseFloat(element1.style.height) || 0;
|
||||
const bottom1 = top1 + height1;
|
||||
|
||||
const top2 = parseFloat(element2.style.top) || 0;
|
||||
const height2 = parseFloat(element2.style.height) || 0;
|
||||
const bottom2 = top2 + height2;
|
||||
|
||||
// Add tolerance for small gaps (borders, etc)
|
||||
const tolerance = 2;
|
||||
return !(bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue