Introduces type-safe EventId with centralized normalization logic for clone and standard event IDs Refactors event ID management across multiple components to use consistent ID transformation methods Improves type safety and reduces potential ID-related bugs in drag-and-drop and event rendering
744 lines
No EOL
25 KiB
TypeScript
744 lines
No EOL
25 KiB
TypeScript
// All-day row height management and animations
|
|
|
|
import { eventBus } from '../core/EventBus';
|
|
import { ALL_DAY_CONSTANTS } from '../configurations/CalendarConfig';
|
|
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
|
import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
|
|
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
|
import { IColumnDataSource } from '../types/ColumnDataSource';
|
|
import { ICalendarEvent } from '../types/CalendarTypes';
|
|
import { CalendarEventType } from '../types/BookingTypes';
|
|
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
|
import {
|
|
IDragMouseEnterHeaderEventPayload,
|
|
IDragMouseEnterColumnEventPayload,
|
|
IDragStartEventPayload,
|
|
IDragMoveEventPayload,
|
|
IDragEndEventPayload,
|
|
IDragColumnChangeEventPayload,
|
|
IHeaderReadyEventPayload
|
|
} from '../types/EventTypes';
|
|
import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
|
import { EventManager } from './EventManager';
|
|
import { DateService } from '../utils/DateService';
|
|
import { EventId } from '../types/EventId';
|
|
|
|
/**
|
|
* AllDayManager - Handles all-day row height animations and management
|
|
* Uses AllDayLayoutEngine for all overlap detection and layout calculation
|
|
*/
|
|
export class AllDayManager {
|
|
private allDayEventRenderer: AllDayEventRenderer;
|
|
private eventManager: EventManager;
|
|
private dateService: DateService;
|
|
private dataSource: IColumnDataSource;
|
|
|
|
private layoutEngine: AllDayLayoutEngine | null = null;
|
|
|
|
// State tracking for layout calculation
|
|
private currentAllDayEvents: ICalendarEvent[] = [];
|
|
private currentColumns: IColumnBounds[] = [];
|
|
|
|
// Expand/collapse state
|
|
private isExpanded: boolean = false;
|
|
private actualRowCount: number = 0;
|
|
|
|
|
|
constructor(
|
|
eventManager: EventManager,
|
|
allDayEventRenderer: AllDayEventRenderer,
|
|
dateService: DateService,
|
|
dataSource: IColumnDataSource
|
|
) {
|
|
this.eventManager = eventManager;
|
|
this.allDayEventRenderer = allDayEventRenderer;
|
|
this.dateService = dateService;
|
|
this.dataSource = dataSource;
|
|
|
|
// Sync CSS variable with TypeScript constant to ensure consistency
|
|
document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`);
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners for drag conversions
|
|
*/
|
|
private setupEventListeners(): void {
|
|
eventBus.on('drag:mouseenter-header', (event) => {
|
|
const payload = (event as CustomEvent<IDragMouseEnterHeaderEventPayload>).detail;
|
|
|
|
if (payload.draggedClone.hasAttribute('data-allday'))
|
|
return;
|
|
|
|
console.log('🔄 AllDayManager: Received drag:mouseenter-header', {
|
|
targetDate: payload.targetColumn,
|
|
originalElementId: payload.originalElement?.dataset?.eventId,
|
|
originalElementTag: payload.originalElement?.tagName
|
|
});
|
|
|
|
this.handleConvertToAllDay(payload);
|
|
});
|
|
|
|
eventBus.on('drag:mouseleave-header', (event) => {
|
|
const { originalElement, cloneElement } = (event as CustomEvent).detail;
|
|
|
|
console.log('🚪 AllDayManager: Received drag:mouseleave-header', {
|
|
originalElementId: originalElement?.dataset?.eventId
|
|
});
|
|
|
|
});
|
|
|
|
// Listen for drag operations on all-day events
|
|
eventBus.on('drag:start', (event) => {
|
|
let payload: IDragStartEventPayload = (event as CustomEvent<IDragStartEventPayload>).detail;
|
|
|
|
if (!payload.draggedClone?.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
|
|
this.allDayEventRenderer.handleDragStart(payload);
|
|
});
|
|
|
|
eventBus.on('drag:column-change', (event) => {
|
|
let payload: IDragColumnChangeEventPayload = (event as CustomEvent<IDragColumnChangeEventPayload>).detail;
|
|
|
|
if (!payload.draggedClone?.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
|
|
this.handleColumnChange(payload);
|
|
});
|
|
|
|
eventBus.on('drag:end', (event) => {
|
|
let dragEndPayload: IDragEndEventPayload = (event as CustomEvent<IDragEndEventPayload>).detail;
|
|
|
|
console.log('🎯 AllDayManager: drag:end received', {
|
|
target: dragEndPayload.target,
|
|
originalElementTag: dragEndPayload.originalElement?.tagName,
|
|
hasAllDayAttribute: dragEndPayload.originalElement?.hasAttribute('data-allday'),
|
|
eventId: dragEndPayload.originalElement?.dataset.eventId
|
|
});
|
|
|
|
// Handle all-day → all-day drops (within header)
|
|
if (dragEndPayload.target === 'swp-day-header' && dragEndPayload.originalElement?.hasAttribute('data-allday')) {
|
|
console.log('✅ AllDayManager: Handling all-day → all-day drop');
|
|
this.handleDragEnd(dragEndPayload);
|
|
return;
|
|
}
|
|
|
|
// Handle timed → all-day conversion (dropped in header)
|
|
if (dragEndPayload.target === 'swp-day-header' && !dragEndPayload.originalElement?.hasAttribute('data-allday')) {
|
|
console.log('🔄 AllDayManager: Timed → all-day conversion on drop');
|
|
this.handleTimedToAllDayDrop(dragEndPayload);
|
|
return;
|
|
}
|
|
|
|
// Handle all-day → timed conversion (dropped in column)
|
|
if (dragEndPayload.target === 'swp-day-column' && dragEndPayload.originalElement?.hasAttribute('data-allday')) {
|
|
const eventId = dragEndPayload.originalElement.dataset.eventId;
|
|
|
|
console.log('🔄 AllDayManager: All-day → timed conversion', { eventId });
|
|
|
|
// Mark for removal (sets data-removing attribute)
|
|
this.fadeOutAndRemove(dragEndPayload.originalElement);
|
|
|
|
// Recalculate layout WITHOUT the removed event to compress gaps
|
|
const remainingEvents = this.currentAllDayEvents.filter(e => e.id !== eventId);
|
|
const newLayouts = this.calculateAllDayEventsLayout(remainingEvents, this.currentColumns);
|
|
|
|
// Re-render all-day events with compressed layout
|
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
|
|
|
// NOW animate height with compressed layout
|
|
this.checkAndAnimateAllDayHeight();
|
|
}
|
|
});
|
|
|
|
// Listen for drag cancellation to recalculate height
|
|
eventBus.on('drag:cancelled', (event) => {
|
|
const { draggedElement, reason } = (event as CustomEvent).detail;
|
|
|
|
console.log('🚫 AllDayManager: Drag cancelled', {
|
|
eventId: draggedElement?.dataset?.eventId,
|
|
reason
|
|
});
|
|
|
|
});
|
|
|
|
// Listen for header ready - when dates are populated with period data
|
|
eventBus.on('header:ready', async (event: Event) => {
|
|
let headerReadyEventPayload = (event as CustomEvent<IHeaderReadyEventPayload>).detail;
|
|
|
|
let startDate = this.dateService.parseISO(headerReadyEventPayload.headerElements.at(0)!.identifier);
|
|
let endDate = this.dateService.parseISO(headerReadyEventPayload.headerElements.at(-1)!.identifier);
|
|
|
|
let events: ICalendarEvent[] = await this.eventManager.getEventsForPeriod(startDate, endDate);
|
|
// Filter for all-day events
|
|
const allDayEvents = events.filter(event => event.allDay);
|
|
|
|
const layouts = this.calculateAllDayEventsLayout(allDayEvents, headerReadyEventPayload.headerElements);
|
|
|
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(layouts);
|
|
this.checkAndAnimateAllDayHeight();
|
|
});
|
|
|
|
eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
|
|
this.allDayEventRenderer.handleViewChanged(event as CustomEvent);
|
|
});
|
|
}
|
|
|
|
private getAllDayContainer(): HTMLElement | null {
|
|
return document.querySelector('swp-calendar-header swp-allday-container');
|
|
}
|
|
|
|
private getCalendarHeader(): HTMLElement | null {
|
|
return document.querySelector('swp-calendar-header');
|
|
}
|
|
|
|
private getHeaderSpacer(): HTMLElement | null {
|
|
return document.querySelector('swp-header-spacer');
|
|
}
|
|
|
|
/**
|
|
* Read current max row from DOM elements
|
|
* Excludes events marked as removing (data-removing attribute)
|
|
*/
|
|
private getMaxRowFromDOM(): number {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return 0;
|
|
|
|
let maxRow = 0;
|
|
const allDayEvents = container.querySelectorAll('swp-allday-event:not(.max-event-indicator):not([data-removing])');
|
|
|
|
allDayEvents.forEach((element: Element) => {
|
|
const htmlElement = element as HTMLElement;
|
|
const row = parseInt(htmlElement.style.gridRow) || 1;
|
|
maxRow = Math.max(maxRow, row);
|
|
});
|
|
|
|
return maxRow;
|
|
}
|
|
|
|
/**
|
|
* Get current gridArea for an event from DOM
|
|
*/
|
|
private getGridAreaFromDOM(eventId: string): string | null {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return null;
|
|
|
|
const element = container.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement;
|
|
return element?.style.gridArea || null;
|
|
}
|
|
|
|
/**
|
|
* Count events in a specific column by reading DOM
|
|
*/
|
|
private countEventsInColumnFromDOM(columnIndex: number): number {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return 0;
|
|
|
|
let count = 0;
|
|
const allDayEvents = container.querySelectorAll('swp-allday-event:not(.max-event-indicator)');
|
|
|
|
allDayEvents.forEach((element: Element) => {
|
|
const htmlElement = element as HTMLElement;
|
|
const gridColumn = htmlElement.style.gridColumn;
|
|
|
|
// Parse "1 / 3" format
|
|
const match = gridColumn.match(/(\d+)\s*\/\s*(\d+)/);
|
|
if (match) {
|
|
const startCol = parseInt(match[1]);
|
|
const endCol = parseInt(match[2]) - 1; // End is exclusive in CSS
|
|
|
|
if (startCol <= columnIndex && endCol >= columnIndex) {
|
|
count++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Calculate all-day height based on number of rows
|
|
*/
|
|
private calculateAllDayHeight(targetRows: number): {
|
|
targetHeight: number;
|
|
currentHeight: number;
|
|
heightDifference: number;
|
|
} {
|
|
const root = document.documentElement;
|
|
const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
|
|
// Read CSS variable directly from style property or default to 0
|
|
const currentHeightStr = root.style.getPropertyValue('--all-day-row-height') || '0px';
|
|
const currentHeight = parseInt(currentHeightStr) || 0;
|
|
const heightDifference = targetHeight - currentHeight;
|
|
|
|
return { targetHeight, currentHeight, heightDifference };
|
|
}
|
|
|
|
/**
|
|
* Check current all-day events and animate to correct height
|
|
* Reads max row directly from DOM elements
|
|
*/
|
|
public checkAndAnimateAllDayHeight(): void {
|
|
// Read max row directly from DOM
|
|
const maxRows = this.getMaxRowFromDOM();
|
|
|
|
console.log('📊 AllDayManager: Height calculation', {
|
|
maxRows,
|
|
isExpanded: this.isExpanded
|
|
});
|
|
|
|
// Store actual row count
|
|
this.actualRowCount = maxRows;
|
|
|
|
// Determine what to display
|
|
let displayRows = maxRows;
|
|
|
|
if (maxRows > ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS) {
|
|
// Show chevron button
|
|
this.updateChevronButton(true);
|
|
|
|
// Show 4 rows when collapsed (3 events + indicators)
|
|
if (!this.isExpanded) {
|
|
|
|
displayRows = ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS;
|
|
this.updateOverflowIndicators();
|
|
|
|
} else {
|
|
|
|
this.clearOverflowIndicators();
|
|
|
|
}
|
|
} else {
|
|
|
|
// Hide chevron - not needed
|
|
this.updateChevronButton(false);
|
|
this.clearOverflowIndicators();
|
|
}
|
|
|
|
console.log('🎬 AllDayManager: Will animate to', {
|
|
displayRows,
|
|
maxRows,
|
|
willAnimate: displayRows !== this.actualRowCount
|
|
});
|
|
|
|
console.log(`🎯 AllDayManager: Animating to ${displayRows} rows`);
|
|
|
|
// Animate to required rows (0 = collapse, >0 = expand)
|
|
this.animateToRows(displayRows);
|
|
}
|
|
|
|
/**
|
|
* Animate all-day container to specific number of rows
|
|
*/
|
|
public animateToRows(targetRows: number): void {
|
|
const { targetHeight, currentHeight, heightDifference } = this.calculateAllDayHeight(targetRows);
|
|
|
|
if (targetHeight === currentHeight) return; // No animation needed
|
|
|
|
console.log(`🎬 All-day height animation: ${currentHeight}px → ${targetHeight}px (${Math.ceil(currentHeight / ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT)} → ${targetRows} rows)`);
|
|
|
|
// Get cached elements
|
|
const calendarHeader = this.getCalendarHeader();
|
|
const headerSpacer = this.getHeaderSpacer();
|
|
const allDayContainer = this.getAllDayContainer();
|
|
|
|
if (!calendarHeader || !allDayContainer) return;
|
|
|
|
// Get current parent height for animation
|
|
const currentParentHeight = parseFloat(getComputedStyle(calendarHeader).height);
|
|
const targetParentHeight = currentParentHeight + heightDifference;
|
|
|
|
const animations = [
|
|
calendarHeader.animate([
|
|
{ height: `${currentParentHeight}px` },
|
|
{ height: `${targetParentHeight}px` }
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease-out',
|
|
fill: 'forwards'
|
|
})
|
|
];
|
|
|
|
// Add spacer animation if spacer exists, but don't use fill: 'forwards'
|
|
if (headerSpacer) {
|
|
const root = document.documentElement;
|
|
const headerHeightStr = root.style.getPropertyValue('--header-height');
|
|
const headerHeight = parseInt(headerHeightStr);
|
|
const currentSpacerHeight = headerHeight + currentHeight;
|
|
const targetSpacerHeight = headerHeight + targetHeight;
|
|
|
|
animations.push(
|
|
headerSpacer.animate([
|
|
{ height: `${currentSpacerHeight}px` },
|
|
{ height: `${targetSpacerHeight}px` }
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease-out'
|
|
// No fill: 'forwards' - let CSS calc() take over after animation
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update CSS variable after animation
|
|
Promise.all(animations.map(anim => anim.finished)).then(() => {
|
|
const root = document.documentElement;
|
|
root.style.setProperty('--all-day-row-height', `${targetHeight}px`);
|
|
eventBus.emit('header:height-changed');
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculate layout for ALL all-day events using AllDayLayoutEngine
|
|
* This is the correct method that processes all events together for proper overlap detection
|
|
*/
|
|
private calculateAllDayEventsLayout(events: ICalendarEvent[], dayHeaders: IColumnBounds[]): IEventLayout[] {
|
|
|
|
// Store current state
|
|
this.currentAllDayEvents = events;
|
|
this.currentColumns = dayHeaders;
|
|
|
|
// Map IColumnBounds to IColumnInfo structure (identifier + groupId)
|
|
const columns = dayHeaders.map(column => ({
|
|
identifier: column.identifier,
|
|
groupId: column.element.dataset.groupId || column.identifier,
|
|
data: new Date(), // Not used by AllDayLayoutEngine
|
|
events: [] // Not used by AllDayLayoutEngine
|
|
}));
|
|
|
|
// Initialize layout engine with column info including groupId
|
|
let layoutEngine = new AllDayLayoutEngine(columns);
|
|
|
|
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
|
|
return layoutEngine.calculateLayout(events);
|
|
|
|
}
|
|
|
|
private handleConvertToAllDay(payload: IDragMouseEnterHeaderEventPayload): void {
|
|
|
|
let allDayContainer = this.getAllDayContainer();
|
|
if (!allDayContainer) return;
|
|
|
|
// Create SwpAllDayEventElement from ICalendarEvent
|
|
const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent);
|
|
|
|
// Apply grid positioning
|
|
allDayElement.style.gridRow = '1';
|
|
allDayElement.style.gridColumn = payload.targetColumn.index.toString();
|
|
|
|
// Remove old swp-event clone
|
|
payload.draggedClone.remove();
|
|
|
|
// Call delegate to update DragDropManager's draggedClone reference
|
|
payload.replaceClone(allDayElement);
|
|
|
|
// Append to container
|
|
allDayContainer.appendChild(allDayElement);
|
|
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
|
|
// Recalculate height after adding all-day event
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER
|
|
*/
|
|
private handleColumnChange(dragColumnChangeEventPayload: IDragColumnChangeEventPayload): void {
|
|
|
|
let allDayContainer = this.getAllDayContainer();
|
|
if (!allDayContainer) return;
|
|
|
|
let targetColumn = ColumnDetectionUtils.getColumnBounds(dragColumnChangeEventPayload.mousePosition);
|
|
|
|
if (targetColumn == null)
|
|
return;
|
|
|
|
if (!dragColumnChangeEventPayload.draggedClone)
|
|
return;
|
|
|
|
// Calculate event span from original grid positioning
|
|
const computedStyle = window.getComputedStyle(dragColumnChangeEventPayload.draggedClone);
|
|
const gridColumnStart = parseInt(computedStyle.gridColumnStart) || targetColumn.index;
|
|
const gridColumnEnd = parseInt(computedStyle.gridColumnEnd) || targetColumn.index + 1;
|
|
const span = gridColumnEnd - gridColumnStart;
|
|
|
|
// Update clone position maintaining the span
|
|
const newStartColumn = targetColumn.index;
|
|
const newEndColumn = newStartColumn + span;
|
|
dragColumnChangeEventPayload.draggedClone.style.gridColumn = `${newStartColumn} / ${newEndColumn}`;
|
|
|
|
}
|
|
private fadeOutAndRemove(element: HTMLElement): void {
|
|
console.log('🗑️ AllDayManager: About to remove all-day event', {
|
|
eventId: element.dataset.eventId,
|
|
element: element.tagName
|
|
});
|
|
|
|
// Mark element as removing so it's excluded from height calculations
|
|
element.setAttribute('data-removing', 'true');
|
|
|
|
element.style.transition = 'opacity 0.3s ease-out';
|
|
element.style.opacity = '0';
|
|
|
|
setTimeout(() => {
|
|
element.remove();
|
|
console.log('✅ AllDayManager: All-day event removed from DOM');
|
|
}, 300);
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle timed → all-day conversion on drop
|
|
*/
|
|
private async handleTimedToAllDayDrop(dragEndEvent: IDragEndEventPayload): Promise<void> {
|
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
|
|
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
|
const eventId = EventId.from(clone.eventId);
|
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
|
|
|
// Determine target date based on mode
|
|
let targetDate: Date;
|
|
let resourceId: string | undefined;
|
|
|
|
if (this.dataSource.isResource()) {
|
|
// Resource mode: keep event's existing date, set resourceId
|
|
targetDate = clone.start;
|
|
resourceId = columnIdentifier;
|
|
} else {
|
|
// Date mode: parse date from column identifier
|
|
targetDate = this.dateService.parseISO(columnIdentifier);
|
|
}
|
|
|
|
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate, resourceId });
|
|
|
|
// Create new dates preserving time
|
|
const newStart = new Date(targetDate);
|
|
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
|
|
|
const newEnd = new Date(targetDate);
|
|
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
|
|
|
// Build update payload
|
|
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
|
|
start: newStart,
|
|
end: newEnd,
|
|
allDay: true
|
|
};
|
|
|
|
if (resourceId) {
|
|
updatePayload.resourceId = resourceId;
|
|
}
|
|
|
|
// Update event in repository
|
|
await this.eventManager.updateEvent(eventId, updatePayload);
|
|
|
|
// Remove original timed event
|
|
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
|
|
|
// Add to current all-day events and recalculate layout
|
|
const newEvent: ICalendarEvent = {
|
|
id: eventId,
|
|
title: clone.title,
|
|
start: newStart,
|
|
end: newEnd,
|
|
type: clone.type as CalendarEventType,
|
|
allDay: true,
|
|
syncStatus: 'synced'
|
|
};
|
|
|
|
const updatedEvents = [...this.currentAllDayEvents, newEvent];
|
|
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentColumns);
|
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
|
|
|
// Animate height
|
|
this.checkAndAnimateAllDayHeight();
|
|
}
|
|
|
|
/**
|
|
* Handle all-day → all-day drop (moving within header)
|
|
*/
|
|
private async handleDragEnd(dragEndEvent: IDragEndEventPayload): Promise<void> {
|
|
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
|
|
|
|
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
|
|
const eventId = EventId.from(clone.eventId);
|
|
const columnIdentifier = dragEndEvent.finalPosition.column.identifier;
|
|
|
|
// Determine target date based on mode
|
|
let targetDate: Date;
|
|
let resourceId: string | undefined;
|
|
|
|
if (this.dataSource.isResource()) {
|
|
// Resource mode: keep event's existing date, set resourceId
|
|
targetDate = clone.start;
|
|
resourceId = columnIdentifier;
|
|
} else {
|
|
// Date mode: parse date from column identifier
|
|
targetDate = this.dateService.parseISO(columnIdentifier);
|
|
}
|
|
|
|
// Calculate duration in days
|
|
const durationDays = this.dateService.differenceInCalendarDays(clone.end, clone.start);
|
|
|
|
// Create new dates preserving time
|
|
const newStart = new Date(targetDate);
|
|
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
|
|
|
|
const newEnd = new Date(targetDate);
|
|
newEnd.setDate(newEnd.getDate() + durationDays);
|
|
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
|
|
|
|
// Build update payload
|
|
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
|
|
start: newStart,
|
|
end: newEnd,
|
|
allDay: true
|
|
};
|
|
|
|
if (resourceId) {
|
|
updatePayload.resourceId = resourceId;
|
|
}
|
|
|
|
// Update event in repository
|
|
await this.eventManager.updateEvent(eventId, updatePayload);
|
|
|
|
// Remove original and fade out
|
|
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
|
|
|
// Recalculate and re-render ALL events
|
|
const updatedEvents = this.currentAllDayEvents.map(e =>
|
|
e.id === eventId ? { ...e, start: newStart, end: newEnd } : e
|
|
);
|
|
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentColumns);
|
|
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
|
|
|
// Animate height - this also handles overflow classes!
|
|
this.checkAndAnimateAllDayHeight();
|
|
}
|
|
|
|
/**
|
|
* Update chevron button visibility and state
|
|
*/
|
|
private updateChevronButton(show: boolean): void {
|
|
const headerSpacer = this.getHeaderSpacer();
|
|
if (!headerSpacer) return;
|
|
|
|
let chevron = headerSpacer.querySelector('.allday-chevron') as HTMLElement;
|
|
|
|
if (show && !chevron) {
|
|
|
|
chevron = document.createElement('button');
|
|
chevron.className = 'allday-chevron collapsed';
|
|
chevron.innerHTML = `
|
|
<svg width="12" height="8" viewBox="0 0 12 8">
|
|
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
</svg>
|
|
`;
|
|
chevron.onclick = () => this.toggleExpanded();
|
|
headerSpacer.appendChild(chevron);
|
|
|
|
} else if (!show && chevron) {
|
|
|
|
chevron.remove();
|
|
|
|
} else if (chevron) {
|
|
|
|
chevron.classList.toggle('collapsed', !this.isExpanded);
|
|
chevron.classList.toggle('expanded', this.isExpanded);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle between expanded and collapsed state
|
|
*/
|
|
private toggleExpanded(): void {
|
|
this.isExpanded = !this.isExpanded;
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
const elements = document.querySelectorAll('swp-allday-container swp-allday-event.max-event-overflow-hide, swp-allday-container swp-allday-event.max-event-overflow-show');
|
|
|
|
elements.forEach((element) => {
|
|
if (this.isExpanded) {
|
|
// ALTID vis når expanded=true
|
|
element.classList.remove('max-event-overflow-hide');
|
|
element.classList.add('max-event-overflow-show');
|
|
} else {
|
|
// ALTID skjul når expanded=false
|
|
element.classList.remove('max-event-overflow-show');
|
|
element.classList.add('max-event-overflow-hide');
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Count number of events in a specific column using IColumnBounds
|
|
* Reads directly from DOM elements
|
|
*/
|
|
private countEventsInColumn(columnBounds: IColumnBounds): number {
|
|
return this.countEventsInColumnFromDOM(columnBounds.index);
|
|
}
|
|
|
|
/**
|
|
* Update overflow indicators for collapsed state
|
|
*/
|
|
private updateOverflowIndicators(): void {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return;
|
|
|
|
// Create overflow indicators for each column that needs them
|
|
let columns = ColumnDetectionUtils.getColumns();
|
|
|
|
columns.forEach((columnBounds) => {
|
|
let totalEventsInColumn = this.countEventsInColumn(columnBounds);
|
|
let overflowCount = totalEventsInColumn - ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS
|
|
|
|
if (overflowCount > 0) {
|
|
// Check if indicator already exists in this column
|
|
let existingIndicator = container.querySelector(`.max-event-indicator[data-column="${columnBounds.index}"]`) as HTMLElement;
|
|
|
|
if (existingIndicator) {
|
|
// Update existing indicator
|
|
existingIndicator.innerHTML = `<span>+${overflowCount + 1} more</span>`;
|
|
} else {
|
|
// Create new overflow indicator element
|
|
let overflowElement = document.createElement('swp-allday-event');
|
|
overflowElement.className = 'max-event-indicator';
|
|
overflowElement.setAttribute('data-column', columnBounds.index.toString());
|
|
overflowElement.style.gridRow = ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS.toString();
|
|
overflowElement.style.gridColumn = columnBounds.index.toString();
|
|
overflowElement.innerHTML = `<span>+${overflowCount + 1} more</span>`;
|
|
overflowElement.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.toggleExpanded();
|
|
};
|
|
|
|
container.appendChild(overflowElement);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear overflow indicators and restore normal state
|
|
*/
|
|
private clearOverflowIndicators(): void {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return;
|
|
|
|
// Remove all overflow indicator elements
|
|
container.querySelectorAll('.max-event-indicator').forEach((element) => {
|
|
element.remove();
|
|
});
|
|
|
|
|
|
}
|
|
|
|
} |