Some ignored filles was missing
This commit is contained in:
parent
7db22245e2
commit
fd5ab6bc0d
268 changed files with 31970 additions and 4 deletions
91
wwwroot/js/managers/AllDayManager.d.ts
vendored
Normal file
91
wwwroot/js/managers/AllDayManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
||||
import { EventManager } from './EventManager';
|
||||
import { DateService } from '../utils/DateService';
|
||||
/**
|
||||
* AllDayManager - Handles all-day row height animations and management
|
||||
* Uses AllDayLayoutEngine for all overlap detection and layout calculation
|
||||
*/
|
||||
export declare class AllDayManager {
|
||||
private allDayEventRenderer;
|
||||
private eventManager;
|
||||
private dateService;
|
||||
private layoutEngine;
|
||||
private currentAllDayEvents;
|
||||
private currentWeekDates;
|
||||
private isExpanded;
|
||||
private actualRowCount;
|
||||
constructor(eventManager: EventManager, allDayEventRenderer: AllDayEventRenderer, dateService: DateService);
|
||||
/**
|
||||
* Setup event listeners for drag conversions
|
||||
*/
|
||||
private setupEventListeners;
|
||||
private getAllDayContainer;
|
||||
private getCalendarHeader;
|
||||
private getHeaderSpacer;
|
||||
/**
|
||||
* Read current max row from DOM elements
|
||||
* Excludes events marked as removing (data-removing attribute)
|
||||
*/
|
||||
private getMaxRowFromDOM;
|
||||
/**
|
||||
* Get current gridArea for an event from DOM
|
||||
*/
|
||||
private getGridAreaFromDOM;
|
||||
/**
|
||||
* Count events in a specific column by reading DOM
|
||||
*/
|
||||
private countEventsInColumnFromDOM;
|
||||
/**
|
||||
* Calculate all-day height based on number of rows
|
||||
*/
|
||||
private calculateAllDayHeight;
|
||||
/**
|
||||
* Check current all-day events and animate to correct height
|
||||
* Reads max row directly from DOM elements
|
||||
*/
|
||||
checkAndAnimateAllDayHeight(): void;
|
||||
/**
|
||||
* Animate all-day container to specific number of rows
|
||||
*/
|
||||
animateToRows(targetRows: number): void;
|
||||
/**
|
||||
* 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;
|
||||
private handleConvertToAllDay;
|
||||
/**
|
||||
* Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER
|
||||
*/
|
||||
private handleColumnChange;
|
||||
private fadeOutAndRemove;
|
||||
/**
|
||||
* Handle timed → all-day conversion on drop
|
||||
*/
|
||||
private handleTimedToAllDayDrop;
|
||||
/**
|
||||
* Handle all-day → all-day drop (moving within header)
|
||||
*/
|
||||
private handleDragEnd;
|
||||
/**
|
||||
* Update chevron button visibility and state
|
||||
*/
|
||||
private updateChevronButton;
|
||||
/**
|
||||
* Toggle between expanded and collapsed state
|
||||
*/
|
||||
private toggleExpanded;
|
||||
/**
|
||||
* Count number of events in a specific column using IColumnBounds
|
||||
* Reads directly from DOM elements
|
||||
*/
|
||||
private countEventsInColumn;
|
||||
/**
|
||||
* Update overflow indicators for collapsed state
|
||||
*/
|
||||
private updateOverflowIndicators;
|
||||
/**
|
||||
* Clear overflow indicators and restore normal state
|
||||
*/
|
||||
private clearOverflowIndicators;
|
||||
}
|
||||
528
wwwroot/js/managers/AllDayManager.js
Normal file
528
wwwroot/js/managers/AllDayManager.js
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
// All-day row height management and animations
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { ALL_DAY_CONSTANTS } from '../configurations/CalendarConfig';
|
||||
import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine';
|
||||
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
||||
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* AllDayManager - Handles all-day row height animations and management
|
||||
* Uses AllDayLayoutEngine for all overlap detection and layout calculation
|
||||
*/
|
||||
export class AllDayManager {
|
||||
constructor(eventManager, allDayEventRenderer, dateService) {
|
||||
this.layoutEngine = null;
|
||||
// State tracking for layout calculation
|
||||
this.currentAllDayEvents = [];
|
||||
this.currentWeekDates = [];
|
||||
// Expand/collapse state
|
||||
this.isExpanded = false;
|
||||
this.actualRowCount = 0;
|
||||
this.eventManager = eventManager;
|
||||
this.allDayEventRenderer = allDayEventRenderer;
|
||||
this.dateService = dateService;
|
||||
// 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
|
||||
*/
|
||||
setupEventListeners() {
|
||||
eventBus.on('drag:mouseenter-header', (event) => {
|
||||
const payload = event.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.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 = event.detail;
|
||||
if (!payload.draggedClone?.hasAttribute('data-allday')) {
|
||||
return;
|
||||
}
|
||||
this.allDayEventRenderer.handleDragStart(payload);
|
||||
});
|
||||
eventBus.on('drag:column-change', (event) => {
|
||||
let payload = event.detail;
|
||||
if (!payload.draggedClone?.hasAttribute('data-allday')) {
|
||||
return;
|
||||
}
|
||||
this.handleColumnChange(payload);
|
||||
});
|
||||
eventBus.on('drag:end', (event) => {
|
||||
let dragEndPayload = event.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.currentWeekDates);
|
||||
// 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.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) => {
|
||||
let headerReadyEventPayload = event.detail;
|
||||
let startDate = new Date(headerReadyEventPayload.headerElements.at(0).date);
|
||||
let endDate = new Date(headerReadyEventPayload.headerElements.at(-1).date);
|
||||
let events = 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) => {
|
||||
this.allDayEventRenderer.handleViewChanged(event);
|
||||
});
|
||||
}
|
||||
getAllDayContainer() {
|
||||
return document.querySelector('swp-calendar-header swp-allday-container');
|
||||
}
|
||||
getCalendarHeader() {
|
||||
return document.querySelector('swp-calendar-header');
|
||||
}
|
||||
getHeaderSpacer() {
|
||||
return document.querySelector('swp-header-spacer');
|
||||
}
|
||||
/**
|
||||
* Read current max row from DOM elements
|
||||
* Excludes events marked as removing (data-removing attribute)
|
||||
*/
|
||||
getMaxRowFromDOM() {
|
||||
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) => {
|
||||
const htmlElement = element;
|
||||
const row = parseInt(htmlElement.style.gridRow) || 1;
|
||||
maxRow = Math.max(maxRow, row);
|
||||
});
|
||||
return maxRow;
|
||||
}
|
||||
/**
|
||||
* Get current gridArea for an event from DOM
|
||||
*/
|
||||
getGridAreaFromDOM(eventId) {
|
||||
const container = this.getAllDayContainer();
|
||||
if (!container)
|
||||
return null;
|
||||
const element = container.querySelector(`[data-event-id="${eventId}"]`);
|
||||
return element?.style.gridArea || null;
|
||||
}
|
||||
/**
|
||||
* Count events in a specific column by reading DOM
|
||||
*/
|
||||
countEventsInColumnFromDOM(columnIndex) {
|
||||
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) => {
|
||||
const htmlElement = element;
|
||||
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
|
||||
*/
|
||||
calculateAllDayHeight(targetRows) {
|
||||
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
|
||||
*/
|
||||
checkAndAnimateAllDayHeight() {
|
||||
// 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
|
||||
*/
|
||||
animateToRows(targetRows) {
|
||||
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
|
||||
*/
|
||||
calculateAllDayEventsLayout(events, dayHeaders) {
|
||||
// Store current state
|
||||
this.currentAllDayEvents = events;
|
||||
this.currentWeekDates = dayHeaders;
|
||||
// Initialize layout engine with provided week dates
|
||||
let layoutEngine = new AllDayLayoutEngine(dayHeaders.map(column => column.date));
|
||||
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
|
||||
return layoutEngine.calculateLayout(events);
|
||||
}
|
||||
handleConvertToAllDay(payload) {
|
||||
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
|
||||
*/
|
||||
handleColumnChange(dragColumnChangeEventPayload) {
|
||||
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}`;
|
||||
}
|
||||
fadeOutAndRemove(element) {
|
||||
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
|
||||
*/
|
||||
async handleTimedToAllDayDrop(dragEndEvent) {
|
||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column)
|
||||
return;
|
||||
const clone = dragEndEvent.draggedClone;
|
||||
const eventId = clone.eventId.replace('clone-', '');
|
||||
const targetDate = dragEndEvent.finalPosition.column.date;
|
||||
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate });
|
||||
// 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);
|
||||
// Update event in repository
|
||||
await this.eventManager.updateEvent(eventId, {
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
allDay: true
|
||||
});
|
||||
// Remove original timed event
|
||||
this.fadeOutAndRemove(dragEndEvent.originalElement);
|
||||
// Add to current all-day events and recalculate layout
|
||||
const newEvent = {
|
||||
id: eventId,
|
||||
title: clone.title,
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
type: clone.type,
|
||||
allDay: true,
|
||||
syncStatus: 'synced'
|
||||
};
|
||||
const updatedEvents = [...this.currentAllDayEvents, newEvent];
|
||||
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
|
||||
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
||||
// Animate height
|
||||
this.checkAndAnimateAllDayHeight();
|
||||
}
|
||||
/**
|
||||
* Handle all-day → all-day drop (moving within header)
|
||||
*/
|
||||
async handleDragEnd(dragEndEvent) {
|
||||
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column)
|
||||
return;
|
||||
const clone = dragEndEvent.draggedClone;
|
||||
const eventId = clone.eventId.replace('clone-', '');
|
||||
const targetDate = dragEndEvent.finalPosition.column.date;
|
||||
// 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);
|
||||
// Update event in repository
|
||||
await this.eventManager.updateEvent(eventId, {
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
allDay: true
|
||||
});
|
||||
// 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.currentWeekDates);
|
||||
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
|
||||
// Animate height - this also handles overflow classes!
|
||||
this.checkAndAnimateAllDayHeight();
|
||||
}
|
||||
/**
|
||||
* Update chevron button visibility and state
|
||||
*/
|
||||
updateChevronButton(show) {
|
||||
const headerSpacer = this.getHeaderSpacer();
|
||||
if (!headerSpacer)
|
||||
return;
|
||||
let chevron = headerSpacer.querySelector('.allday-chevron');
|
||||
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
|
||||
*/
|
||||
toggleExpanded() {
|
||||
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
|
||||
*/
|
||||
countEventsInColumn(columnBounds) {
|
||||
return this.countEventsInColumnFromDOM(columnBounds.index);
|
||||
}
|
||||
/**
|
||||
* Update overflow indicators for collapsed state
|
||||
*/
|
||||
updateOverflowIndicators() {
|
||||
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}"]`);
|
||||
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
|
||||
*/
|
||||
clearOverflowIndicators() {
|
||||
const container = this.getAllDayContainer();
|
||||
if (!container)
|
||||
return;
|
||||
// Remove all overflow indicator elements
|
||||
container.querySelectorAll('.max-event-indicator').forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=AllDayManager.js.map
|
||||
1
wwwroot/js/managers/AllDayManager.js.map
Normal file
1
wwwroot/js/managers/AllDayManager.js.map
Normal file
File diff suppressed because one or more lines are too long
45
wwwroot/js/managers/CalendarManager.d.ts
vendored
Normal file
45
wwwroot/js/managers/CalendarManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { CalendarView, IEventBus } from '../types/CalendarTypes';
|
||||
import { EventManager } from './EventManager';
|
||||
import { GridManager } from './GridManager';
|
||||
import { EventRenderingService } from '../renderers/EventRendererManager';
|
||||
import { ScrollManager } from './ScrollManager';
|
||||
/**
|
||||
* CalendarManager - Main coordinator for all calendar managers
|
||||
*/
|
||||
export declare class CalendarManager {
|
||||
private eventBus;
|
||||
private eventManager;
|
||||
private gridManager;
|
||||
private eventRenderer;
|
||||
private scrollManager;
|
||||
private config;
|
||||
private currentView;
|
||||
private currentDate;
|
||||
private isInitialized;
|
||||
constructor(eventBus: IEventBus, eventManager: EventManager, gridManager: GridManager, eventRenderingService: EventRenderingService, scrollManager: ScrollManager, config: Configuration);
|
||||
/**
|
||||
* Initialize calendar system using simple direct calls
|
||||
*/
|
||||
initialize(): Promise<void>;
|
||||
/**
|
||||
* Skift calendar view (dag/uge/måned)
|
||||
*/
|
||||
setView(view: CalendarView): void;
|
||||
/**
|
||||
* Sæt aktuel dato
|
||||
*/
|
||||
setCurrentDate(date: Date): void;
|
||||
/**
|
||||
* Setup event listeners for at håndtere events fra andre managers
|
||||
*/
|
||||
private setupEventListeners;
|
||||
/**
|
||||
* Calculate the current period based on view and date
|
||||
*/
|
||||
private calculateCurrentPeriod;
|
||||
/**
|
||||
* Handle workweek configuration changes
|
||||
*/
|
||||
private handleWorkweekChange;
|
||||
}
|
||||
145
wwwroot/js/managers/CalendarManager.js
Normal file
145
wwwroot/js/managers/CalendarManager.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* CalendarManager - Main coordinator for all calendar managers
|
||||
*/
|
||||
export class CalendarManager {
|
||||
constructor(eventBus, eventManager, gridManager, eventRenderingService, scrollManager, config) {
|
||||
this.currentView = 'week';
|
||||
this.currentDate = new Date();
|
||||
this.isInitialized = false;
|
||||
this.eventBus = eventBus;
|
||||
this.eventManager = eventManager;
|
||||
this.gridManager = gridManager;
|
||||
this.eventRenderer = eventRenderingService;
|
||||
this.scrollManager = scrollManager;
|
||||
this.config = config;
|
||||
this.setupEventListeners();
|
||||
}
|
||||
/**
|
||||
* Initialize calendar system using simple direct calls
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Step 1: Load data
|
||||
await this.eventManager.loadData();
|
||||
// Step 2: Render grid structure
|
||||
await this.gridManager.render();
|
||||
this.scrollManager.initialize();
|
||||
this.setView(this.currentView);
|
||||
this.setCurrentDate(this.currentDate);
|
||||
this.isInitialized = true;
|
||||
// Emit initialization complete event
|
||||
this.eventBus.emit(CoreEvents.INITIALIZED, {
|
||||
currentDate: this.currentDate,
|
||||
currentView: this.currentView
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Skift calendar view (dag/uge/måned)
|
||||
*/
|
||||
setView(view) {
|
||||
if (this.currentView === view) {
|
||||
return;
|
||||
}
|
||||
const previousView = this.currentView;
|
||||
this.currentView = view;
|
||||
// Emit view change event
|
||||
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
|
||||
previousView,
|
||||
currentView: view,
|
||||
date: this.currentDate
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sæt aktuel dato
|
||||
*/
|
||||
setCurrentDate(date) {
|
||||
const previousDate = this.currentDate;
|
||||
this.currentDate = new Date(date);
|
||||
// Emit date change event
|
||||
this.eventBus.emit(CoreEvents.DATE_CHANGED, {
|
||||
previousDate,
|
||||
currentDate: this.currentDate,
|
||||
view: this.currentView
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Setup event listeners for at håndtere events fra andre managers
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Listen for workweek changes only
|
||||
this.eventBus.on(CoreEvents.WORKWEEK_CHANGED, (event) => {
|
||||
const customEvent = event;
|
||||
this.handleWorkweekChange();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Calculate the current period based on view and date
|
||||
*/
|
||||
calculateCurrentPeriod() {
|
||||
const current = new Date(this.currentDate);
|
||||
switch (this.currentView) {
|
||||
case 'day':
|
||||
const dayStart = new Date(current);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(current);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
return {
|
||||
start: dayStart.toISOString(),
|
||||
end: dayEnd.toISOString()
|
||||
};
|
||||
case 'week':
|
||||
// Find start of week (Monday)
|
||||
const weekStart = new Date(current);
|
||||
const dayOfWeek = weekStart.getDay();
|
||||
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Sunday = 0, so 6 days back to Monday
|
||||
weekStart.setDate(weekStart.getDate() - daysToMonday);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
// Find end of week (Sunday)
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||
weekEnd.setHours(23, 59, 59, 999);
|
||||
return {
|
||||
start: weekStart.toISOString(),
|
||||
end: weekEnd.toISOString()
|
||||
};
|
||||
case 'month':
|
||||
const monthStart = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||
const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
return {
|
||||
start: monthStart.toISOString(),
|
||||
end: monthEnd.toISOString()
|
||||
};
|
||||
default:
|
||||
// Fallback to week view
|
||||
const fallbackStart = new Date(current);
|
||||
fallbackStart.setDate(fallbackStart.getDate() - 3);
|
||||
fallbackStart.setHours(0, 0, 0, 0);
|
||||
const fallbackEnd = new Date(current);
|
||||
fallbackEnd.setDate(fallbackEnd.getDate() + 3);
|
||||
fallbackEnd.setHours(23, 59, 59, 999);
|
||||
return {
|
||||
start: fallbackStart.toISOString(),
|
||||
end: fallbackEnd.toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle workweek configuration changes
|
||||
*/
|
||||
handleWorkweekChange() {
|
||||
// Simply relay the event - workweek info is in the WORKWEEK_CHANGED event
|
||||
this.eventBus.emit('workweek:header-update', {
|
||||
currentDate: this.currentDate,
|
||||
currentView: this.currentView
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=CalendarManager.js.map
|
||||
1
wwwroot/js/managers/CalendarManager.js.map
Normal file
1
wwwroot/js/managers/CalendarManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"CalendarManager.js","sourceRoot":"","sources":["../../../src/managers/CalendarManager.ts"],"names":[],"mappings":"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"}
|
||||
222
wwwroot/js/managers/DragDropManager.d.ts
vendored
Normal file
222
wwwroot/js/managers/DragDropManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* DragDropManager - Advanced drag-and-drop system with smooth animations and event type conversion
|
||||
*
|
||||
* ARCHITECTURE OVERVIEW:
|
||||
* =====================
|
||||
* DragDropManager provides a sophisticated drag-and-drop system for calendar events that supports:
|
||||
* - Smooth animated dragging with requestAnimationFrame
|
||||
* - Automatic event type conversion (timed events ↔ all-day events)
|
||||
* - Scroll compensation during edge scrolling
|
||||
* - Grid snapping for precise event placement
|
||||
* - Column detection and change tracking
|
||||
*
|
||||
* KEY FEATURES:
|
||||
* =============
|
||||
* 1. DRAG DETECTION
|
||||
* - Movement threshold (5px) to distinguish clicks from drags
|
||||
* - Immediate visual feedback with cloned element
|
||||
* - Mouse offset tracking for natural drag feel
|
||||
*
|
||||
* 2. SMOOTH ANIMATION
|
||||
* - Uses requestAnimationFrame for 60fps animations
|
||||
* - Interpolated movement (30% per frame) for smooth transitions
|
||||
* - Continuous drag:move events for real-time updates
|
||||
*
|
||||
* 3. EVENT TYPE CONVERSION
|
||||
* - Timed → All-day: When dragging into calendar header
|
||||
* - All-day → Timed: When dragging into day columns
|
||||
* - Automatic clone replacement with appropriate element type
|
||||
*
|
||||
* 4. SCROLL COMPENSATION
|
||||
* - Tracks scroll delta during edge-scrolling
|
||||
* - Compensates dragged element position during scroll
|
||||
* - Prevents visual "jumping" when scrolling while dragging
|
||||
*
|
||||
* 5. GRID SNAPPING
|
||||
* - Snaps to time grid on mouse up
|
||||
* - Uses PositionUtils for consistent positioning
|
||||
* - Accounts for mouse offset within event
|
||||
*
|
||||
* STATE MANAGEMENT:
|
||||
* =================
|
||||
* Mouse Tracking:
|
||||
* - mouseDownPosition: Initial click position
|
||||
* - currentMousePosition: Latest mouse position
|
||||
* - mouseOffset: Click offset within event (for natural dragging)
|
||||
*
|
||||
* Drag State:
|
||||
* - originalElement: Source event being dragged
|
||||
* - draggedClone: Animated clone following mouse
|
||||
* - currentColumn: Column mouse is currently over
|
||||
* - previousColumn: Last column (for detecting changes)
|
||||
* - isDragStarted: Whether drag threshold exceeded
|
||||
*
|
||||
* Scroll State:
|
||||
* - scrollDeltaY: Accumulated scroll offset during drag
|
||||
* - lastScrollTop: Previous scroll position
|
||||
* - isScrollCompensating: Whether edge-scroll is active
|
||||
*
|
||||
* Animation State:
|
||||
* - dragAnimationId: requestAnimationFrame ID
|
||||
* - targetY: Desired position for smooth interpolation
|
||||
* - currentY: Current interpolated position
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* 1. Mouse Down (handleMouseDown)
|
||||
* ├─ Store originalElement and mouse offset
|
||||
* └─ Wait for movement
|
||||
*
|
||||
* 2. Mouse Move (handleMouseMove)
|
||||
* ├─ Check movement threshold
|
||||
* ├─ Initialize drag if threshold exceeded (initializeDrag)
|
||||
* │ ├─ Create clone
|
||||
* │ ├─ Emit drag:start
|
||||
* │ └─ Start animation loop
|
||||
* ├─ Continue drag (continueDrag)
|
||||
* │ ├─ Calculate target position with scroll compensation
|
||||
* │ └─ Update animation target
|
||||
* └─ Detect column changes (detectColumnChange)
|
||||
* └─ Emit drag:column-change
|
||||
*
|
||||
* 3. Animation Loop (animateDrag)
|
||||
* ├─ Interpolate currentY toward targetY
|
||||
* ├─ Emit drag:move on each frame
|
||||
* └─ Schedule next frame until target reached
|
||||
*
|
||||
* 4. Event Type Conversion
|
||||
* ├─ Entering header (handleHeaderMouseEnter)
|
||||
* │ ├─ Emit drag:mouseenter-header
|
||||
* │ └─ AllDayManager creates all-day clone
|
||||
* └─ Entering column (handleColumnMouseEnter)
|
||||
* ├─ Emit drag:mouseenter-column
|
||||
* └─ EventRenderingService creates timed clone
|
||||
*
|
||||
* 5. Mouse Up (handleMouseUp)
|
||||
* ├─ Stop animation
|
||||
* ├─ Snap to grid
|
||||
* ├─ Detect drop target (header or column)
|
||||
* ├─ Emit drag:end with final position
|
||||
* └─ Cleanup drag state
|
||||
*
|
||||
* SCROLL COMPENSATION SYSTEM:
|
||||
* ===========================
|
||||
* Problem: When EdgeScrollManager scrolls the grid during drag, the dragged element
|
||||
* can appear to "jump" because the mouse position stays the same but the
|
||||
* coordinate system (scrollable content) has moved.
|
||||
*
|
||||
* Solution: Track cumulative scroll delta and add it to mouse position calculations
|
||||
*
|
||||
* Flow:
|
||||
* 1. EdgeScrollManager starts scrolling → emit edgescroll:started
|
||||
* 2. DragDropManager sets isScrollCompensating = true
|
||||
* 3. On each scroll event:
|
||||
* ├─ Calculate scrollDelta = currentScrollTop - lastScrollTop
|
||||
* ├─ Accumulate into scrollDeltaY
|
||||
* └─ Call continueDrag with adjusted position
|
||||
* 4. continueDrag adds scrollDeltaY to mouse Y coordinate
|
||||
* 5. On event conversion, reset scrollDeltaY (new clone, new coordinate system)
|
||||
*
|
||||
* PERFORMANCE OPTIMIZATIONS:
|
||||
* ==========================
|
||||
* - Uses ColumnDetectionUtils cache for fast column lookups
|
||||
* - Single requestAnimationFrame loop (not per-mousemove)
|
||||
* - Interpolated animation reduces update frequency
|
||||
* - Passive scroll listeners
|
||||
* - Event delegation for header/column detection
|
||||
*
|
||||
* USAGE:
|
||||
* ======
|
||||
* const dragDropManager = new DragDropManager(eventBus, positionUtils);
|
||||
* // Automatically attaches event listeners and manages drag lifecycle
|
||||
* // Other managers listen to drag:start, drag:move, drag:end, etc.
|
||||
*/
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
export declare class DragDropManager {
|
||||
private eventBus;
|
||||
private mouseDownPosition;
|
||||
private currentMousePosition;
|
||||
private mouseOffset;
|
||||
private originalElement;
|
||||
private draggedClone;
|
||||
private currentColumn;
|
||||
private previousColumn;
|
||||
private originalSourceColumn;
|
||||
private isDragStarted;
|
||||
private readonly dragThreshold;
|
||||
private scrollableContent;
|
||||
private scrollDeltaY;
|
||||
private lastScrollTop;
|
||||
private isScrollCompensating;
|
||||
private dragAnimationId;
|
||||
private targetY;
|
||||
private currentY;
|
||||
private targetColumn;
|
||||
private positionUtils;
|
||||
constructor(eventBus: IEventBus, positionUtils: PositionUtils);
|
||||
/**
|
||||
* Initialize with optimized event listener setup
|
||||
*/
|
||||
private init;
|
||||
private handleGridRendered;
|
||||
private handleMouseDown;
|
||||
private handleMouseMove;
|
||||
/**
|
||||
* Try to initialize drag based on movement threshold
|
||||
* Returns true if drag was initialized, false if not enough movement
|
||||
*/
|
||||
private initializeDrag;
|
||||
private continueDrag;
|
||||
/**
|
||||
* Detect column change and emit event
|
||||
*/
|
||||
private detectColumnChange;
|
||||
/**
|
||||
* Optimized mouse up handler with consolidated cleanup
|
||||
*/
|
||||
private handleMouseUp;
|
||||
private cleanupAllClones;
|
||||
/**
|
||||
* Cancel drag operation when mouse leaves grid container
|
||||
* Animates clone back to original position before cleanup
|
||||
*/
|
||||
private cancelDrag;
|
||||
/**
|
||||
* Optimized snap position calculation using PositionUtils
|
||||
*/
|
||||
private calculateSnapPosition;
|
||||
/**
|
||||
* Smooth drag animation using requestAnimationFrame
|
||||
* Emits drag:move events with current draggedClone reference on each frame
|
||||
*/
|
||||
private animateDrag;
|
||||
/**
|
||||
* Handle scroll during drag - update scrollDeltaY and call continueDrag
|
||||
*/
|
||||
private handleScroll;
|
||||
/**
|
||||
* Stop drag animation
|
||||
*/
|
||||
private stopDragAnimation;
|
||||
/**
|
||||
* Clean up drag state
|
||||
*/
|
||||
private cleanupDragState;
|
||||
/**
|
||||
* Detect drop target - whether dropped in swp-day-column or swp-day-header
|
||||
*/
|
||||
private detectDropTarget;
|
||||
/**
|
||||
* Handle mouse enter on calendar header - simplified using native events
|
||||
*/
|
||||
private handleHeaderMouseEnter;
|
||||
/**
|
||||
* Handle mouse enter on day column - for converting all-day to timed events
|
||||
*/
|
||||
private handleColumnMouseEnter;
|
||||
/**
|
||||
* Handle mouse leave from calendar header - simplified using native events
|
||||
*/
|
||||
private handleHeaderMouseLeave;
|
||||
}
|
||||
626
wwwroot/js/managers/DragDropManager.js
Normal file
626
wwwroot/js/managers/DragDropManager.js
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
/**
|
||||
* DragDropManager - Advanced drag-and-drop system with smooth animations and event type conversion
|
||||
*
|
||||
* ARCHITECTURE OVERVIEW:
|
||||
* =====================
|
||||
* DragDropManager provides a sophisticated drag-and-drop system for calendar events that supports:
|
||||
* - Smooth animated dragging with requestAnimationFrame
|
||||
* - Automatic event type conversion (timed events ↔ all-day events)
|
||||
* - Scroll compensation during edge scrolling
|
||||
* - Grid snapping for precise event placement
|
||||
* - Column detection and change tracking
|
||||
*
|
||||
* KEY FEATURES:
|
||||
* =============
|
||||
* 1. DRAG DETECTION
|
||||
* - Movement threshold (5px) to distinguish clicks from drags
|
||||
* - Immediate visual feedback with cloned element
|
||||
* - Mouse offset tracking for natural drag feel
|
||||
*
|
||||
* 2. SMOOTH ANIMATION
|
||||
* - Uses requestAnimationFrame for 60fps animations
|
||||
* - Interpolated movement (30% per frame) for smooth transitions
|
||||
* - Continuous drag:move events for real-time updates
|
||||
*
|
||||
* 3. EVENT TYPE CONVERSION
|
||||
* - Timed → All-day: When dragging into calendar header
|
||||
* - All-day → Timed: When dragging into day columns
|
||||
* - Automatic clone replacement with appropriate element type
|
||||
*
|
||||
* 4. SCROLL COMPENSATION
|
||||
* - Tracks scroll delta during edge-scrolling
|
||||
* - Compensates dragged element position during scroll
|
||||
* - Prevents visual "jumping" when scrolling while dragging
|
||||
*
|
||||
* 5. GRID SNAPPING
|
||||
* - Snaps to time grid on mouse up
|
||||
* - Uses PositionUtils for consistent positioning
|
||||
* - Accounts for mouse offset within event
|
||||
*
|
||||
* STATE MANAGEMENT:
|
||||
* =================
|
||||
* Mouse Tracking:
|
||||
* - mouseDownPosition: Initial click position
|
||||
* - currentMousePosition: Latest mouse position
|
||||
* - mouseOffset: Click offset within event (for natural dragging)
|
||||
*
|
||||
* Drag State:
|
||||
* - originalElement: Source event being dragged
|
||||
* - draggedClone: Animated clone following mouse
|
||||
* - currentColumn: Column mouse is currently over
|
||||
* - previousColumn: Last column (for detecting changes)
|
||||
* - isDragStarted: Whether drag threshold exceeded
|
||||
*
|
||||
* Scroll State:
|
||||
* - scrollDeltaY: Accumulated scroll offset during drag
|
||||
* - lastScrollTop: Previous scroll position
|
||||
* - isScrollCompensating: Whether edge-scroll is active
|
||||
*
|
||||
* Animation State:
|
||||
* - dragAnimationId: requestAnimationFrame ID
|
||||
* - targetY: Desired position for smooth interpolation
|
||||
* - currentY: Current interpolated position
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* 1. Mouse Down (handleMouseDown)
|
||||
* ├─ Store originalElement and mouse offset
|
||||
* └─ Wait for movement
|
||||
*
|
||||
* 2. Mouse Move (handleMouseMove)
|
||||
* ├─ Check movement threshold
|
||||
* ├─ Initialize drag if threshold exceeded (initializeDrag)
|
||||
* │ ├─ Create clone
|
||||
* │ ├─ Emit drag:start
|
||||
* │ └─ Start animation loop
|
||||
* ├─ Continue drag (continueDrag)
|
||||
* │ ├─ Calculate target position with scroll compensation
|
||||
* │ └─ Update animation target
|
||||
* └─ Detect column changes (detectColumnChange)
|
||||
* └─ Emit drag:column-change
|
||||
*
|
||||
* 3. Animation Loop (animateDrag)
|
||||
* ├─ Interpolate currentY toward targetY
|
||||
* ├─ Emit drag:move on each frame
|
||||
* └─ Schedule next frame until target reached
|
||||
*
|
||||
* 4. Event Type Conversion
|
||||
* ├─ Entering header (handleHeaderMouseEnter)
|
||||
* │ ├─ Emit drag:mouseenter-header
|
||||
* │ └─ AllDayManager creates all-day clone
|
||||
* └─ Entering column (handleColumnMouseEnter)
|
||||
* ├─ Emit drag:mouseenter-column
|
||||
* └─ EventRenderingService creates timed clone
|
||||
*
|
||||
* 5. Mouse Up (handleMouseUp)
|
||||
* ├─ Stop animation
|
||||
* ├─ Snap to grid
|
||||
* ├─ Detect drop target (header or column)
|
||||
* ├─ Emit drag:end with final position
|
||||
* └─ Cleanup drag state
|
||||
*
|
||||
* SCROLL COMPENSATION SYSTEM:
|
||||
* ===========================
|
||||
* Problem: When EdgeScrollManager scrolls the grid during drag, the dragged element
|
||||
* can appear to "jump" because the mouse position stays the same but the
|
||||
* coordinate system (scrollable content) has moved.
|
||||
*
|
||||
* Solution: Track cumulative scroll delta and add it to mouse position calculations
|
||||
*
|
||||
* Flow:
|
||||
* 1. EdgeScrollManager starts scrolling → emit edgescroll:started
|
||||
* 2. DragDropManager sets isScrollCompensating = true
|
||||
* 3. On each scroll event:
|
||||
* ├─ Calculate scrollDelta = currentScrollTop - lastScrollTop
|
||||
* ├─ Accumulate into scrollDeltaY
|
||||
* └─ Call continueDrag with adjusted position
|
||||
* 4. continueDrag adds scrollDeltaY to mouse Y coordinate
|
||||
* 5. On event conversion, reset scrollDeltaY (new clone, new coordinate system)
|
||||
*
|
||||
* PERFORMANCE OPTIMIZATIONS:
|
||||
* ==========================
|
||||
* - Uses ColumnDetectionUtils cache for fast column lookups
|
||||
* - Single requestAnimationFrame loop (not per-mousemove)
|
||||
* - Interpolated animation reduces update frequency
|
||||
* - Passive scroll listeners
|
||||
* - Event delegation for header/column detection
|
||||
*
|
||||
* USAGE:
|
||||
* ======
|
||||
* const dragDropManager = new DragDropManager(eventBus, positionUtils);
|
||||
* // Automatically attaches event listeners and manages drag lifecycle
|
||||
* // Other managers listen to drag:start, drag:move, drag:end, etc.
|
||||
*/
|
||||
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
||||
import { SwpEventElement } from '../elements/SwpEventElement';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
export class DragDropManager {
|
||||
constructor(eventBus, positionUtils) {
|
||||
// Mouse tracking with optimized state
|
||||
this.mouseDownPosition = { x: 0, y: 0 };
|
||||
this.currentMousePosition = { x: 0, y: 0 };
|
||||
this.mouseOffset = { x: 0, y: 0 };
|
||||
this.currentColumn = null;
|
||||
this.previousColumn = null;
|
||||
this.originalSourceColumn = null; // Track original start column
|
||||
this.isDragStarted = false;
|
||||
// Movement threshold to distinguish click from drag
|
||||
this.dragThreshold = 5; // pixels
|
||||
// Scroll compensation
|
||||
this.scrollableContent = null;
|
||||
this.scrollDeltaY = 0; // Current scroll delta to apply in continueDrag
|
||||
this.lastScrollTop = 0; // Last scroll position for delta calculation
|
||||
this.isScrollCompensating = false; // Track if scroll compensation is active
|
||||
// Smooth drag animation
|
||||
this.dragAnimationId = null;
|
||||
this.targetY = 0;
|
||||
this.currentY = 0;
|
||||
this.targetColumn = null;
|
||||
this.eventBus = eventBus;
|
||||
this.positionUtils = positionUtils;
|
||||
this.init();
|
||||
}
|
||||
/**
|
||||
* Initialize with optimized event listener setup
|
||||
*/
|
||||
init() {
|
||||
// Add event listeners
|
||||
document.body.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
document.body.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
const calendarContainer = document.querySelector('swp-calendar-container');
|
||||
if (calendarContainer) {
|
||||
calendarContainer.addEventListener('mouseleave', () => {
|
||||
if (this.originalElement && this.isDragStarted) {
|
||||
this.cancelDrag();
|
||||
}
|
||||
});
|
||||
// Event delegation for header enter/leave
|
||||
calendarContainer.addEventListener('mouseenter', (e) => {
|
||||
const target = e.target;
|
||||
if (target.closest('swp-calendar-header')) {
|
||||
this.handleHeaderMouseEnter(e);
|
||||
}
|
||||
else if (target.closest('swp-day-column')) {
|
||||
this.handleColumnMouseEnter(e);
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
calendarContainer.addEventListener('mouseleave', (e) => {
|
||||
const target = e.target;
|
||||
if (target.closest('swp-calendar-header')) {
|
||||
this.handleHeaderMouseLeave(e);
|
||||
}
|
||||
// Don't handle swp-event mouseleave here - let mousemove handle it
|
||||
}, true); // Use capture phase
|
||||
}
|
||||
// Initialize column bounds cache
|
||||
ColumnDetectionUtils.updateColumnBoundsCache();
|
||||
// Listen to resize events to update cache
|
||||
window.addEventListener('resize', () => {
|
||||
ColumnDetectionUtils.updateColumnBoundsCache();
|
||||
});
|
||||
// Listen to navigation events to update cache
|
||||
this.eventBus.on('navigation:completed', () => {
|
||||
ColumnDetectionUtils.updateColumnBoundsCache();
|
||||
});
|
||||
this.eventBus.on(CoreEvents.GRID_RENDERED, (event) => {
|
||||
this.handleGridRendered(event);
|
||||
});
|
||||
// Listen to edge-scroll events to control scroll compensation
|
||||
this.eventBus.on('edgescroll:started', () => {
|
||||
this.isScrollCompensating = true;
|
||||
// Gem nuværende scroll position for delta beregning
|
||||
if (this.scrollableContent) {
|
||||
this.lastScrollTop = this.scrollableContent.scrollTop;
|
||||
}
|
||||
});
|
||||
this.eventBus.on('edgescroll:stopped', () => {
|
||||
this.isScrollCompensating = false;
|
||||
});
|
||||
// Reset scrollDeltaY when event converts (new clone created)
|
||||
this.eventBus.on('drag:mouseenter-header', () => {
|
||||
this.scrollDeltaY = 0;
|
||||
this.lastScrollTop = 0;
|
||||
});
|
||||
this.eventBus.on('drag:mouseenter-column', () => {
|
||||
this.scrollDeltaY = 0;
|
||||
this.lastScrollTop = 0;
|
||||
});
|
||||
}
|
||||
handleGridRendered(event) {
|
||||
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
||||
this.scrollableContent.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
|
||||
}
|
||||
handleMouseDown(event) {
|
||||
// Clean up drag state first
|
||||
this.cleanupDragState();
|
||||
ColumnDetectionUtils.updateColumnBoundsCache();
|
||||
//this.lastMousePosition = { 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;
|
||||
if (target.closest('swp-resize-handle'))
|
||||
return;
|
||||
let eventElement = target;
|
||||
while (eventElement && eventElement.tagName !== 'SWP-GRID-CONTAINER') {
|
||||
if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
|
||||
break;
|
||||
}
|
||||
eventElement = eventElement.parentElement;
|
||||
if (!eventElement)
|
||||
return;
|
||||
}
|
||||
if (eventElement) {
|
||||
// Normal drag - prepare for potential dragging
|
||||
this.originalElement = eventElement;
|
||||
// Calculate mouse offset within event
|
||||
const eventRect = eventElement.getBoundingClientRect();
|
||||
this.mouseOffset = {
|
||||
x: event.clientX - eventRect.left,
|
||||
y: event.clientY - eventRect.top
|
||||
};
|
||||
this.mouseDownPosition = { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
}
|
||||
handleMouseMove(event) {
|
||||
if (event.buttons === 1) {
|
||||
// Always update mouse position from event
|
||||
this.currentMousePosition = { x: event.clientX, y: event.clientY };
|
||||
// Try to initialize drag if not started
|
||||
if (!this.isDragStarted && this.originalElement) {
|
||||
if (!this.initializeDrag(this.currentMousePosition)) {
|
||||
return; // Not enough movement yet
|
||||
}
|
||||
}
|
||||
// Continue drag if started (også under scroll - accumulatedScrollDelta kompenserer)
|
||||
if (this.isDragStarted && this.originalElement && this.draggedClone) {
|
||||
this.continueDrag(this.currentMousePosition);
|
||||
this.detectColumnChange(this.currentMousePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Try to initialize drag based on movement threshold
|
||||
* Returns true if drag was initialized, false if not enough movement
|
||||
*/
|
||||
initializeDrag(currentPosition) {
|
||||
const deltaX = Math.abs(currentPosition.x - this.mouseDownPosition.x);
|
||||
const deltaY = Math.abs(currentPosition.y - this.mouseDownPosition.y);
|
||||
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
if (totalMovement < this.dragThreshold) {
|
||||
return false; // Not enough movement
|
||||
}
|
||||
// Start drag
|
||||
this.isDragStarted = true;
|
||||
// Set high z-index on event-group if exists, otherwise on event itself
|
||||
const eventGroup = this.originalElement.closest('swp-event-group');
|
||||
if (eventGroup) {
|
||||
eventGroup.style.zIndex = '9999';
|
||||
}
|
||||
else {
|
||||
this.originalElement.style.zIndex = '9999';
|
||||
}
|
||||
const originalElement = this.originalElement;
|
||||
this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
||||
this.originalSourceColumn = this.currentColumn; // Store original source column at drag start
|
||||
this.draggedClone = originalElement.createClone();
|
||||
const dragStartPayload = {
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
mousePosition: this.mouseDownPosition,
|
||||
mouseOffset: this.mouseOffset,
|
||||
columnBounds: this.currentColumn
|
||||
};
|
||||
this.eventBus.emit('drag:start', dragStartPayload);
|
||||
return true;
|
||||
}
|
||||
continueDrag(currentPosition) {
|
||||
if (!this.draggedClone.hasAttribute("data-allday")) {
|
||||
// Calculate raw position from mouse (no snapping)
|
||||
const column = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
||||
if (column) {
|
||||
// Calculate raw Y position relative to column (accounting for mouse offset)
|
||||
const columnRect = column.boundingClientRect;
|
||||
// Beregn position fra mus + scroll delta kompensation
|
||||
const adjustedMouseY = currentPosition.y + this.scrollDeltaY;
|
||||
const eventTopY = adjustedMouseY - columnRect.top - this.mouseOffset.y;
|
||||
this.targetY = Math.max(0, eventTopY);
|
||||
this.targetColumn = column;
|
||||
// Start animation loop if not already running
|
||||
if (this.dragAnimationId === null) {
|
||||
this.currentY = parseFloat(this.draggedClone.style.top) || 0;
|
||||
this.animateDrag();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Detect column change and emit event
|
||||
*/
|
||||
detectColumnChange(currentPosition) {
|
||||
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
||||
if (newColumn == null)
|
||||
return;
|
||||
if (newColumn.index !== this.currentColumn?.index) {
|
||||
this.previousColumn = this.currentColumn;
|
||||
this.currentColumn = newColumn;
|
||||
const dragColumnChangePayload = {
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
previousColumn: this.previousColumn,
|
||||
newColumn,
|
||||
mousePosition: currentPosition
|
||||
};
|
||||
this.eventBus.emit('drag:column-change', dragColumnChangePayload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Optimized mouse up handler with consolidated cleanup
|
||||
*/
|
||||
handleMouseUp(event) {
|
||||
this.stopDragAnimation();
|
||||
if (this.originalElement) {
|
||||
// Only emit drag:end if drag was actually started
|
||||
if (this.isDragStarted) {
|
||||
const mousePosition = { x: event.clientX, y: event.clientY };
|
||||
// Snap to grid on mouse up (like ResizeHandleManager)
|
||||
const column = ColumnDetectionUtils.getColumnBounds(mousePosition);
|
||||
if (!column)
|
||||
return;
|
||||
// Get current position and snap it to grid
|
||||
const snappedY = this.calculateSnapPosition(mousePosition.y, column);
|
||||
// Update clone to snapped position immediately
|
||||
if (this.draggedClone) {
|
||||
this.draggedClone.style.top = `${snappedY}px`;
|
||||
}
|
||||
// Detect drop target (swp-day-column or swp-day-header)
|
||||
const dropTarget = this.detectDropTarget(mousePosition);
|
||||
if (!dropTarget)
|
||||
throw "dropTarget is null";
|
||||
const dragEndPayload = {
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
mousePosition,
|
||||
originalSourceColumn: this.originalSourceColumn,
|
||||
finalPosition: { column, snappedY }, // Where drag ended
|
||||
target: dropTarget
|
||||
};
|
||||
this.eventBus.emit('drag:end', dragEndPayload);
|
||||
this.cleanupDragState();
|
||||
}
|
||||
else {
|
||||
// This was just a click - emit click event instead
|
||||
this.eventBus.emit('event:click', {
|
||||
clickedElement: this.originalElement,
|
||||
mousePosition: { x: event.clientX, y: event.clientY }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add a cleanup method that finds and removes ALL clones
|
||||
cleanupAllClones() {
|
||||
// Remove clones from all possible locations
|
||||
const allClones = document.querySelectorAll('[data-event-id^="clone"]');
|
||||
if (allClones.length > 0) {
|
||||
allClones.forEach(clone => clone.remove());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cancel drag operation when mouse leaves grid container
|
||||
* Animates clone back to original position before cleanup
|
||||
*/
|
||||
cancelDrag() {
|
||||
if (!this.originalElement || !this.draggedClone)
|
||||
return;
|
||||
// Get current clone position
|
||||
const cloneRect = this.draggedClone.getBoundingClientRect();
|
||||
// Get original element position
|
||||
const originalRect = this.originalElement.getBoundingClientRect();
|
||||
// Calculate distance to animate
|
||||
const deltaX = originalRect.left - cloneRect.left;
|
||||
const deltaY = originalRect.top - cloneRect.top;
|
||||
// Add transition for smooth animation
|
||||
this.draggedClone.style.transition = 'transform 300ms ease-out';
|
||||
this.draggedClone.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
// Wait for animation to complete, then cleanup
|
||||
setTimeout(() => {
|
||||
this.cleanupAllClones();
|
||||
if (this.originalElement) {
|
||||
this.originalElement.style.opacity = '';
|
||||
this.originalElement.style.cursor = '';
|
||||
}
|
||||
this.eventBus.emit('drag:cancelled', {
|
||||
originalElement: this.originalElement,
|
||||
reason: 'mouse-left-grid'
|
||||
});
|
||||
this.cleanupDragState();
|
||||
this.stopDragAnimation();
|
||||
}, 300);
|
||||
}
|
||||
/**
|
||||
* Optimized snap position calculation using PositionUtils
|
||||
*/
|
||||
calculateSnapPosition(mouseY, column) {
|
||||
// Calculate where the event top would be (accounting for mouse offset)
|
||||
const eventTopY = mouseY - this.mouseOffset.y;
|
||||
// Snap the event top position, not the mouse position
|
||||
const snappedY = this.positionUtils.getPositionFromCoordinate(eventTopY, column);
|
||||
return Math.max(0, snappedY);
|
||||
}
|
||||
/**
|
||||
* Smooth drag animation using requestAnimationFrame
|
||||
* Emits drag:move events with current draggedClone reference on each frame
|
||||
*/
|
||||
animateDrag() {
|
||||
if (!this.isDragStarted || !this.draggedClone || !this.targetColumn) {
|
||||
this.dragAnimationId = null;
|
||||
return;
|
||||
}
|
||||
// Smooth interpolation towards target
|
||||
const diff = this.targetY - this.currentY;
|
||||
const step = diff * 0.3; // 30% of distance per frame
|
||||
// Update if difference is significant
|
||||
if (Math.abs(diff) > 0.5) {
|
||||
this.currentY += step;
|
||||
// Emit drag:move event with current draggedClone reference
|
||||
const dragMovePayload = {
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone, // Always uses current reference
|
||||
mousePosition: this.currentMousePosition, // Use current mouse position!
|
||||
snappedY: this.currentY,
|
||||
columnBounds: this.targetColumn,
|
||||
mouseOffset: this.mouseOffset
|
||||
};
|
||||
this.eventBus.emit('drag:move', dragMovePayload);
|
||||
this.dragAnimationId = requestAnimationFrame(() => this.animateDrag());
|
||||
}
|
||||
else {
|
||||
// Close enough - snap to target
|
||||
this.currentY = this.targetY;
|
||||
// Emit final position
|
||||
const dragMovePayload = {
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
mousePosition: this.currentMousePosition, // Use current mouse position!
|
||||
snappedY: this.currentY,
|
||||
columnBounds: this.targetColumn,
|
||||
mouseOffset: this.mouseOffset
|
||||
};
|
||||
this.eventBus.emit('drag:move', dragMovePayload);
|
||||
this.dragAnimationId = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle scroll during drag - update scrollDeltaY and call continueDrag
|
||||
*/
|
||||
handleScroll() {
|
||||
if (!this.isDragStarted || !this.draggedClone || !this.scrollableContent || !this.isScrollCompensating)
|
||||
return;
|
||||
const currentScrollTop = this.scrollableContent.scrollTop;
|
||||
const scrollDelta = currentScrollTop - this.lastScrollTop;
|
||||
// Gem scroll delta for continueDrag
|
||||
this.scrollDeltaY += scrollDelta;
|
||||
this.lastScrollTop = currentScrollTop;
|
||||
// Kald continueDrag med nuværende mus position
|
||||
this.continueDrag(this.currentMousePosition);
|
||||
}
|
||||
/**
|
||||
* Stop drag animation
|
||||
*/
|
||||
stopDragAnimation() {
|
||||
if (this.dragAnimationId !== null) {
|
||||
cancelAnimationFrame(this.dragAnimationId);
|
||||
this.dragAnimationId = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clean up drag state
|
||||
*/
|
||||
cleanupDragState() {
|
||||
this.previousColumn = null;
|
||||
this.originalElement = null;
|
||||
this.draggedClone = null;
|
||||
this.currentColumn = null;
|
||||
this.originalSourceColumn = null;
|
||||
this.isDragStarted = false;
|
||||
this.scrollDeltaY = 0;
|
||||
this.lastScrollTop = 0;
|
||||
}
|
||||
/**
|
||||
* Detect drop target - whether dropped in swp-day-column or swp-day-header
|
||||
*/
|
||||
detectDropTarget(position) {
|
||||
// Traverse up the DOM tree to find the target container
|
||||
let currentElement = this.draggedClone;
|
||||
while (currentElement && currentElement !== document.body) {
|
||||
if (currentElement.tagName === 'SWP-ALLDAY-CONTAINER') {
|
||||
return 'swp-day-header';
|
||||
}
|
||||
if (currentElement.tagName === 'SWP-DAY-COLUMN') {
|
||||
return 'swp-day-column';
|
||||
}
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Handle mouse enter on calendar header - simplified using native events
|
||||
*/
|
||||
handleHeaderMouseEnter(event) {
|
||||
// Only handle if we're dragging a timed event (not all-day)
|
||||
if (!this.isDragStarted || !this.draggedClone) {
|
||||
return;
|
||||
}
|
||||
const position = { x: event.clientX, y: event.clientY };
|
||||
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
||||
if (targetColumn) {
|
||||
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
|
||||
const dragMouseEnterPayload = {
|
||||
targetColumn: targetColumn,
|
||||
mousePosition: position,
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
calendarEvent: calendarEvent,
|
||||
replaceClone: (newClone) => {
|
||||
this.draggedClone = newClone;
|
||||
this.dragAnimationId === null;
|
||||
}
|
||||
};
|
||||
this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle mouse enter on day column - for converting all-day to timed events
|
||||
*/
|
||||
handleColumnMouseEnter(event) {
|
||||
// Only handle if we're dragging an all-day event
|
||||
if (!this.isDragStarted || !this.draggedClone || !this.draggedClone.hasAttribute('data-allday')) {
|
||||
return;
|
||||
}
|
||||
const position = { x: event.clientX, y: event.clientY };
|
||||
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
||||
if (!targetColumn) {
|
||||
return;
|
||||
}
|
||||
// Calculate snapped Y position
|
||||
const snappedY = this.calculateSnapPosition(position.y, targetColumn);
|
||||
// Extract ICalendarEvent from the dragged clone
|
||||
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
|
||||
const dragMouseEnterPayload = {
|
||||
targetColumn: targetColumn,
|
||||
mousePosition: position,
|
||||
snappedY: snappedY,
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
calendarEvent: calendarEvent,
|
||||
replaceClone: (newClone) => {
|
||||
this.draggedClone = newClone;
|
||||
this.dragAnimationId === null;
|
||||
this.stopDragAnimation();
|
||||
}
|
||||
};
|
||||
this.eventBus.emit('drag:mouseenter-column', dragMouseEnterPayload);
|
||||
}
|
||||
/**
|
||||
* Handle mouse leave from calendar header - simplified using native events
|
||||
*/
|
||||
handleHeaderMouseLeave(event) {
|
||||
// Only handle if we're dragging an all-day event
|
||||
if (!this.isDragStarted || !this.draggedClone || !this.draggedClone.hasAttribute("data-allday")) {
|
||||
return;
|
||||
}
|
||||
const position = { x: event.clientX, y: event.clientY };
|
||||
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
||||
if (!targetColumn) {
|
||||
return;
|
||||
}
|
||||
const dragMouseLeavePayload = {
|
||||
targetDate: targetColumn.date,
|
||||
mousePosition: position,
|
||||
originalElement: this.originalElement,
|
||||
draggedClone: this.draggedClone
|
||||
};
|
||||
this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=DragDropManager.js.map
|
||||
1
wwwroot/js/managers/DragDropManager.js.map
Normal file
1
wwwroot/js/managers/DragDropManager.js.map
Normal file
File diff suppressed because one or more lines are too long
31
wwwroot/js/managers/DragHoverManager.d.ts
vendored
Normal file
31
wwwroot/js/managers/DragHoverManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* DragHoverManager - Handles event hover tracking
|
||||
* Fully autonomous - listens to mouse events and manages hover state independently
|
||||
*/
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
export declare class DragHoverManager {
|
||||
private eventBus;
|
||||
private isHoverTrackingActive;
|
||||
private currentHoveredEvent;
|
||||
private calendarContainer;
|
||||
constructor(eventBus: IEventBus);
|
||||
private init;
|
||||
private setupEventListeners;
|
||||
/**
|
||||
* Handle mouse enter on swp-event - activate hover tracking
|
||||
*/
|
||||
private handleEventMouseEnter;
|
||||
/**
|
||||
* Check if mouse is still over the currently hovered event
|
||||
*/
|
||||
private checkEventHover;
|
||||
/**
|
||||
* Clear hover state
|
||||
*/
|
||||
private clearEventHover;
|
||||
/**
|
||||
* Deactivate hover tracking and clear any current hover
|
||||
* Called via event bus when drag starts
|
||||
*/
|
||||
private deactivateTracking;
|
||||
}
|
||||
101
wwwroot/js/managers/DragHoverManager.js
Normal file
101
wwwroot/js/managers/DragHoverManager.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* DragHoverManager - Handles event hover tracking
|
||||
* Fully autonomous - listens to mouse events and manages hover state independently
|
||||
*/
|
||||
export class DragHoverManager {
|
||||
constructor(eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
this.isHoverTrackingActive = false;
|
||||
this.currentHoveredEvent = null;
|
||||
this.calendarContainer = null;
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(() => {
|
||||
this.calendarContainer = document.querySelector('swp-calendar-container');
|
||||
if (this.calendarContainer) {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
}, 100);
|
||||
// Listen to drag start to deactivate hover tracking
|
||||
this.eventBus.on('drag:start', () => {
|
||||
this.deactivateTracking();
|
||||
});
|
||||
}
|
||||
setupEventListeners() {
|
||||
if (!this.calendarContainer)
|
||||
return;
|
||||
// Listen to mouseenter on events (using event delegation)
|
||||
this.calendarContainer.addEventListener('mouseenter', (e) => {
|
||||
const target = e.target;
|
||||
const eventElement = target.closest('swp-event');
|
||||
if (eventElement) {
|
||||
this.handleEventMouseEnter(e, eventElement);
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
// Listen to mousemove globally to track when mouse leaves event bounds
|
||||
document.body.addEventListener('mousemove', (e) => {
|
||||
if (this.isHoverTrackingActive && e.buttons === 0) {
|
||||
this.checkEventHover(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handle mouse enter on swp-event - activate hover tracking
|
||||
*/
|
||||
handleEventMouseEnter(event, eventElement) {
|
||||
// Only handle hover if mouse button is up
|
||||
if (event.buttons === 0) {
|
||||
// Clear any previous hover first
|
||||
if (this.currentHoveredEvent && this.currentHoveredEvent !== eventElement) {
|
||||
this.currentHoveredEvent.classList.remove('hover');
|
||||
}
|
||||
this.isHoverTrackingActive = true;
|
||||
this.currentHoveredEvent = eventElement;
|
||||
eventElement.classList.add('hover');
|
||||
this.eventBus.emit('event:hover:start', { element: eventElement });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if mouse is still over the currently hovered event
|
||||
*/
|
||||
checkEventHover(event) {
|
||||
// Only track hover when active and mouse button is up
|
||||
if (!this.isHoverTrackingActive || !this.currentHoveredEvent)
|
||||
return;
|
||||
const rect = this.currentHoveredEvent.getBoundingClientRect();
|
||||
const mouseX = event.clientX;
|
||||
const mouseY = event.clientY;
|
||||
// Check if mouse is still within the current hovered event
|
||||
const isStillInside = mouseX >= rect.left && mouseX <= rect.right &&
|
||||
mouseY >= rect.top && mouseY <= rect.bottom;
|
||||
// If mouse left the event
|
||||
if (!isStillInside) {
|
||||
// Only disable tracking and clear if mouse is NOT pressed (allow resize to work)
|
||||
if (event.buttons === 0) {
|
||||
this.isHoverTrackingActive = false;
|
||||
this.clearEventHover();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear hover state
|
||||
*/
|
||||
clearEventHover() {
|
||||
if (this.currentHoveredEvent) {
|
||||
this.currentHoveredEvent.classList.remove('hover');
|
||||
this.eventBus.emit('event:hover:end', { element: this.currentHoveredEvent });
|
||||
this.currentHoveredEvent = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Deactivate hover tracking and clear any current hover
|
||||
* Called via event bus when drag starts
|
||||
*/
|
||||
deactivateTracking() {
|
||||
this.isHoverTrackingActive = false;
|
||||
this.clearEventHover();
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=DragHoverManager.js.map
|
||||
1
wwwroot/js/managers/DragHoverManager.js.map
Normal file
1
wwwroot/js/managers/DragHoverManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"DragHoverManager.js","sourceRoot":"","sources":["../../../src/managers/DragHoverManager.ts"],"names":[],"mappings":"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"}
|
||||
30
wwwroot/js/managers/EdgeScrollManager.d.ts
vendored
Normal file
30
wwwroot/js/managers/EdgeScrollManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* EdgeScrollManager - Auto-scroll when dragging near edges
|
||||
* Uses time-based scrolling with 2-zone system for variable speed
|
||||
*/
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
export declare class EdgeScrollManager {
|
||||
private eventBus;
|
||||
private scrollableContent;
|
||||
private timeGrid;
|
||||
private draggedClone;
|
||||
private scrollRAF;
|
||||
private mouseY;
|
||||
private isDragging;
|
||||
private isScrolling;
|
||||
private lastTs;
|
||||
private rect;
|
||||
private initialScrollTop;
|
||||
private scrollListener;
|
||||
private readonly OUTER_ZONE;
|
||||
private readonly INNER_ZONE;
|
||||
private readonly SLOW_SPEED_PXS;
|
||||
private readonly FAST_SPEED_PXS;
|
||||
constructor(eventBus: IEventBus);
|
||||
private init;
|
||||
private subscribeToEvents;
|
||||
private startDrag;
|
||||
private stopDrag;
|
||||
private handleScroll;
|
||||
private scrollTick;
|
||||
}
|
||||
191
wwwroot/js/managers/EdgeScrollManager.js
Normal file
191
wwwroot/js/managers/EdgeScrollManager.js
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/**
|
||||
* EdgeScrollManager - Auto-scroll when dragging near edges
|
||||
* Uses time-based scrolling with 2-zone system for variable speed
|
||||
*/
|
||||
export class EdgeScrollManager {
|
||||
constructor(eventBus) {
|
||||
this.eventBus = eventBus;
|
||||
this.scrollableContent = null;
|
||||
this.timeGrid = null;
|
||||
this.draggedClone = null;
|
||||
this.scrollRAF = null;
|
||||
this.mouseY = 0;
|
||||
this.isDragging = false;
|
||||
this.isScrolling = false; // Track if edge-scroll is active
|
||||
this.lastTs = 0;
|
||||
this.rect = null;
|
||||
this.initialScrollTop = 0;
|
||||
this.scrollListener = null;
|
||||
// Constants - fixed values as per requirements
|
||||
this.OUTER_ZONE = 100; // px from edge (slow zone)
|
||||
this.INNER_ZONE = 50; // px from edge (fast zone)
|
||||
this.SLOW_SPEED_PXS = 140; // px/sec in outer zone
|
||||
this.FAST_SPEED_PXS = 640; // px/sec in inner zone
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(() => {
|
||||
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
||||
this.timeGrid = document.querySelector('swp-time-grid');
|
||||
if (this.scrollableContent) {
|
||||
// Disable smooth scroll for instant auto-scroll
|
||||
this.scrollableContent.style.scrollBehavior = 'auto';
|
||||
// Add scroll listener to detect actual scrolling
|
||||
this.scrollListener = this.handleScroll.bind(this);
|
||||
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
|
||||
}
|
||||
}, 100);
|
||||
// Listen to mousemove directly from document to always get mouse coords
|
||||
document.body.addEventListener('mousemove', (e) => {
|
||||
if (this.isDragging) {
|
||||
this.mouseY = e.clientY;
|
||||
}
|
||||
});
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
subscribeToEvents() {
|
||||
// Listen to drag events from DragDropManager
|
||||
this.eventBus.on('drag:start', (event) => {
|
||||
const payload = event.detail;
|
||||
this.draggedClone = payload.draggedClone;
|
||||
this.startDrag();
|
||||
});
|
||||
this.eventBus.on('drag:end', () => this.stopDrag());
|
||||
this.eventBus.on('drag:cancelled', () => this.stopDrag());
|
||||
// Stop scrolling when event converts to/from all-day
|
||||
this.eventBus.on('drag:mouseenter-header', () => {
|
||||
console.log('🔄 EdgeScrollManager: Event converting to all-day - stopping scroll');
|
||||
this.stopDrag();
|
||||
});
|
||||
this.eventBus.on('drag:mouseenter-column', () => {
|
||||
this.startDrag();
|
||||
});
|
||||
}
|
||||
startDrag() {
|
||||
console.log('🎬 EdgeScrollManager: Starting drag');
|
||||
this.isDragging = true;
|
||||
this.isScrolling = false; // Reset scroll state
|
||||
this.lastTs = performance.now();
|
||||
// Save initial scroll position
|
||||
if (this.scrollableContent) {
|
||||
this.initialScrollTop = this.scrollableContent.scrollTop;
|
||||
}
|
||||
if (this.scrollRAF === null) {
|
||||
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||
}
|
||||
}
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
// Emit stopped event if we were scrolling
|
||||
if (this.isScrolling) {
|
||||
this.isScrolling = false;
|
||||
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (drag ended)');
|
||||
this.eventBus.emit('edgescroll:stopped', {});
|
||||
}
|
||||
if (this.scrollRAF !== null) {
|
||||
cancelAnimationFrame(this.scrollRAF);
|
||||
this.scrollRAF = null;
|
||||
}
|
||||
this.rect = null;
|
||||
this.lastTs = 0;
|
||||
this.initialScrollTop = 0;
|
||||
}
|
||||
handleScroll() {
|
||||
if (!this.isDragging || !this.scrollableContent)
|
||||
return;
|
||||
const currentScrollTop = this.scrollableContent.scrollTop;
|
||||
const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop);
|
||||
// Only emit started event if we've actually scrolled more than 1px
|
||||
if (scrollDelta > 1 && !this.isScrolling) {
|
||||
this.isScrolling = true;
|
||||
console.log('💾 EdgeScrollManager: Edge-scroll started (actual scroll detected)', {
|
||||
initialScrollTop: this.initialScrollTop,
|
||||
currentScrollTop,
|
||||
scrollDelta
|
||||
});
|
||||
this.eventBus.emit('edgescroll:started', {});
|
||||
}
|
||||
}
|
||||
scrollTick(ts) {
|
||||
const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
|
||||
this.lastTs = ts;
|
||||
if (!this.scrollableContent) {
|
||||
this.stopDrag();
|
||||
return;
|
||||
}
|
||||
// Cache rect for performance (only measure once per frame)
|
||||
if (!this.rect) {
|
||||
this.rect = this.scrollableContent.getBoundingClientRect();
|
||||
}
|
||||
let vy = 0;
|
||||
if (this.isDragging) {
|
||||
const distTop = this.mouseY - this.rect.top;
|
||||
const distBot = this.rect.bottom - this.mouseY;
|
||||
// Check top edge
|
||||
if (distTop < this.INNER_ZONE) {
|
||||
vy = -this.FAST_SPEED_PXS;
|
||||
}
|
||||
else if (distTop < this.OUTER_ZONE) {
|
||||
vy = -this.SLOW_SPEED_PXS;
|
||||
}
|
||||
// Check bottom edge
|
||||
else if (distBot < this.INNER_ZONE) {
|
||||
vy = this.FAST_SPEED_PXS;
|
||||
}
|
||||
else if (distBot < this.OUTER_ZONE) {
|
||||
vy = this.SLOW_SPEED_PXS;
|
||||
}
|
||||
}
|
||||
if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedClone) {
|
||||
// Check if we can scroll in the requested direction
|
||||
const currentScrollTop = this.scrollableContent.scrollTop;
|
||||
const scrollableHeight = this.scrollableContent.clientHeight;
|
||||
const timeGridHeight = this.timeGrid.clientHeight;
|
||||
// Get dragged element position and height
|
||||
const cloneRect = this.draggedClone.getBoundingClientRect();
|
||||
const cloneBottom = cloneRect.bottom;
|
||||
const timeGridRect = this.timeGrid.getBoundingClientRect();
|
||||
const timeGridBottom = timeGridRect.bottom;
|
||||
// Check boundaries
|
||||
const atTop = currentScrollTop <= 0 && vy < 0;
|
||||
const atBottom = (cloneBottom >= timeGridBottom) && vy > 0;
|
||||
if (atTop || atBottom) {
|
||||
// At boundary - stop scrolling
|
||||
if (this.isScrolling) {
|
||||
this.isScrolling = false;
|
||||
this.initialScrollTop = this.scrollableContent.scrollTop;
|
||||
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (reached boundary)');
|
||||
this.eventBus.emit('edgescroll:stopped', {});
|
||||
}
|
||||
// Continue RAF loop to detect when mouse moves away from boundary
|
||||
if (this.isDragging) {
|
||||
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not at boundary - apply scroll
|
||||
this.scrollableContent.scrollTop += vy * dt;
|
||||
this.rect = null; // Invalidate cache for next frame
|
||||
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Mouse moved away from edge - stop scrolling
|
||||
if (this.isScrolling) {
|
||||
this.isScrolling = false;
|
||||
this.initialScrollTop = this.scrollableContent.scrollTop; // Reset for next scroll
|
||||
console.log('🛑 EdgeScrollManager: Edge-scroll stopped (mouse left edge)');
|
||||
this.eventBus.emit('edgescroll:stopped', {});
|
||||
}
|
||||
// Continue RAF loop even if not scrolling, to detect edge entry
|
||||
if (this.isDragging) {
|
||||
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||
}
|
||||
else {
|
||||
this.stopDrag();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=EdgeScrollManager.js.map
|
||||
1
wwwroot/js/managers/EdgeScrollManager.js.map
Normal file
1
wwwroot/js/managers/EdgeScrollManager.js.map
Normal file
File diff suppressed because one or more lines are too long
32
wwwroot/js/managers/EventFilterManager.d.ts
vendored
Normal file
32
wwwroot/js/managers/EventFilterManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* EventFilterManager - Handles fuzzy search filtering of calendar events
|
||||
* Uses Fuse.js for fuzzy matching (Apache 2.0 License)
|
||||
*/
|
||||
export declare class EventFilterManager {
|
||||
private searchInput;
|
||||
private allEvents;
|
||||
private matchingEventIds;
|
||||
private isFilterActive;
|
||||
private frameRequest;
|
||||
private fuse;
|
||||
constructor();
|
||||
private init;
|
||||
private setupSearchListeners;
|
||||
private subscribeToEvents;
|
||||
private updateEventsList;
|
||||
private handleSearchInput;
|
||||
private applyFilter;
|
||||
private clearFilter;
|
||||
private updateVisualState;
|
||||
/**
|
||||
* Check if an event matches the current filter
|
||||
*/
|
||||
eventMatchesFilter(eventId: string): boolean;
|
||||
/**
|
||||
* Get current filter state
|
||||
*/
|
||||
getFilterState(): {
|
||||
active: boolean;
|
||||
matchingIds: string[];
|
||||
};
|
||||
}
|
||||
192
wwwroot/js/managers/EventFilterManager.js
Normal file
192
wwwroot/js/managers/EventFilterManager.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* EventFilterManager - Handles fuzzy search filtering of calendar events
|
||||
* Uses Fuse.js for fuzzy matching (Apache 2.0 License)
|
||||
*/
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
// Import Fuse.js from npm
|
||||
import Fuse from 'fuse.js';
|
||||
export class EventFilterManager {
|
||||
constructor() {
|
||||
this.searchInput = null;
|
||||
this.allEvents = [];
|
||||
this.matchingEventIds = new Set();
|
||||
this.isFilterActive = false;
|
||||
this.frameRequest = null;
|
||||
this.fuse = null;
|
||||
// Wait for DOM to be ready before initializing
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
this.init();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
init() {
|
||||
// Find search input
|
||||
this.searchInput = document.querySelector('swp-search-container input[type="search"]');
|
||||
if (!this.searchInput) {
|
||||
return;
|
||||
}
|
||||
// Set up event listeners
|
||||
this.setupSearchListeners();
|
||||
this.subscribeToEvents();
|
||||
// Initialization complete
|
||||
}
|
||||
setupSearchListeners() {
|
||||
if (!this.searchInput)
|
||||
return;
|
||||
// Listen for input changes
|
||||
this.searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value;
|
||||
this.handleSearchInput(query);
|
||||
});
|
||||
// Listen for escape key
|
||||
this.searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.clearFilter();
|
||||
}
|
||||
});
|
||||
}
|
||||
subscribeToEvents() {
|
||||
// Listen for events data updates
|
||||
eventBus.on(CoreEvents.EVENTS_RENDERED, (e) => {
|
||||
const detail = e.detail;
|
||||
if (detail?.events) {
|
||||
this.updateEventsList(detail.events);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateEventsList(events) {
|
||||
this.allEvents = events;
|
||||
// Initialize Fuse with the new events list
|
||||
this.fuse = new Fuse(this.allEvents, {
|
||||
keys: ['title', 'description'],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
minMatchCharLength: 2, // Minimum 2 characters for a match
|
||||
shouldSort: true,
|
||||
ignoreLocation: true // Search anywhere in the string
|
||||
});
|
||||
// Re-apply filter if active
|
||||
if (this.isFilterActive && this.searchInput) {
|
||||
this.applyFilter(this.searchInput.value);
|
||||
}
|
||||
}
|
||||
handleSearchInput(query) {
|
||||
// Cancel any pending filter
|
||||
if (this.frameRequest) {
|
||||
cancelAnimationFrame(this.frameRequest);
|
||||
}
|
||||
// Debounce with requestAnimationFrame
|
||||
this.frameRequest = requestAnimationFrame(() => {
|
||||
if (query.length === 0) {
|
||||
// Only clear when input is completely empty
|
||||
this.clearFilter();
|
||||
}
|
||||
else {
|
||||
// Let Fuse.js handle minimum character length via minMatchCharLength
|
||||
this.applyFilter(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
applyFilter(query) {
|
||||
if (!this.fuse) {
|
||||
return;
|
||||
}
|
||||
// Perform fuzzy search
|
||||
const results = this.fuse.search(query);
|
||||
// Extract matching event IDs
|
||||
this.matchingEventIds.clear();
|
||||
results.forEach((result) => {
|
||||
if (result.item && result.item.id) {
|
||||
this.matchingEventIds.add(result.item.id);
|
||||
}
|
||||
});
|
||||
// Update filter state
|
||||
this.isFilterActive = true;
|
||||
// Update visual state
|
||||
this.updateVisualState();
|
||||
// Emit filter changed event
|
||||
eventBus.emit(CoreEvents.FILTER_CHANGED, {
|
||||
active: true,
|
||||
query: query,
|
||||
matchingIds: Array.from(this.matchingEventIds)
|
||||
});
|
||||
}
|
||||
clearFilter() {
|
||||
this.isFilterActive = false;
|
||||
this.matchingEventIds.clear();
|
||||
// Clear search input
|
||||
if (this.searchInput) {
|
||||
this.searchInput.value = '';
|
||||
}
|
||||
// Update visual state
|
||||
this.updateVisualState();
|
||||
// Emit filter cleared event
|
||||
eventBus.emit(CoreEvents.FILTER_CHANGED, {
|
||||
active: false,
|
||||
query: '',
|
||||
matchingIds: []
|
||||
});
|
||||
}
|
||||
updateVisualState() {
|
||||
// Update search container styling
|
||||
const searchContainer = document.querySelector('swp-search-container');
|
||||
if (searchContainer) {
|
||||
if (this.isFilterActive) {
|
||||
searchContainer.classList.add('filter-active');
|
||||
}
|
||||
else {
|
||||
searchContainer.classList.remove('filter-active');
|
||||
}
|
||||
}
|
||||
// Update all events layers
|
||||
const eventsLayers = document.querySelectorAll('swp-events-layer');
|
||||
eventsLayers.forEach(layer => {
|
||||
if (this.isFilterActive) {
|
||||
layer.setAttribute('data-filter-active', 'true');
|
||||
// Mark matching events
|
||||
const events = layer.querySelectorAll('swp-event');
|
||||
events.forEach(event => {
|
||||
const eventId = event.getAttribute('data-event-id');
|
||||
if (eventId && this.matchingEventIds.has(eventId)) {
|
||||
event.setAttribute('data-matches', 'true');
|
||||
}
|
||||
else {
|
||||
event.removeAttribute('data-matches');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
layer.removeAttribute('data-filter-active');
|
||||
// Remove all match attributes
|
||||
const events = layer.querySelectorAll('swp-event');
|
||||
events.forEach(event => {
|
||||
event.removeAttribute('data-matches');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Check if an event matches the current filter
|
||||
*/
|
||||
eventMatchesFilter(eventId) {
|
||||
if (!this.isFilterActive) {
|
||||
return true; // No filter active, all events match
|
||||
}
|
||||
return this.matchingEventIds.has(eventId);
|
||||
}
|
||||
/**
|
||||
* Get current filter state
|
||||
*/
|
||||
getFilterState() {
|
||||
return {
|
||||
active: this.isFilterActive,
|
||||
matchingIds: Array.from(this.matchingEventIds)
|
||||
};
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=EventFilterManager.js.map
|
||||
1
wwwroot/js/managers/EventFilterManager.js.map
Normal file
1
wwwroot/js/managers/EventFilterManager.js.map
Normal file
File diff suppressed because one or more lines are too long
78
wwwroot/js/managers/EventLayoutCoordinator.d.ts
vendored
Normal file
78
wwwroot/js/managers/EventLayoutCoordinator.d.ts
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* EventLayoutCoordinator - Coordinates event layout calculations
|
||||
*
|
||||
* Separates layout logic from rendering concerns.
|
||||
* Calculates stack levels, groups events, and determines rendering strategy.
|
||||
*/
|
||||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { EventStackManager, IStackLink } from './EventStackManager';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
export interface IGridGroupLayout {
|
||||
events: ICalendarEvent[];
|
||||
stackLevel: number;
|
||||
position: {
|
||||
top: number;
|
||||
};
|
||||
columns: ICalendarEvent[][];
|
||||
}
|
||||
export interface IStackedEventLayout {
|
||||
event: ICalendarEvent;
|
||||
stackLink: IStackLink;
|
||||
position: {
|
||||
top: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
export interface IColumnLayout {
|
||||
gridGroups: IGridGroupLayout[];
|
||||
stackedEvents: IStackedEventLayout[];
|
||||
}
|
||||
export declare class EventLayoutCoordinator {
|
||||
private stackManager;
|
||||
private config;
|
||||
private positionUtils;
|
||||
constructor(stackManager: EventStackManager, config: Configuration, positionUtils: PositionUtils);
|
||||
/**
|
||||
* Calculate complete layout for a column of events (recursive approach)
|
||||
*/
|
||||
calculateColumnLayout(columnEvents: ICalendarEvent[]): IColumnLayout;
|
||||
/**
|
||||
* Calculate stack level for a grid group based on already rendered events
|
||||
*/
|
||||
private calculateGridGroupStackLevelFromRendered;
|
||||
/**
|
||||
* Calculate stack level for a single stacked event based on already rendered events
|
||||
*/
|
||||
private calculateStackLevelFromRendered;
|
||||
/**
|
||||
* Detect if two events have a conflict based on threshold
|
||||
*
|
||||
* @param event1 - First event
|
||||
* @param event2 - Second event
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns true if events conflict
|
||||
*/
|
||||
private detectConflict;
|
||||
/**
|
||||
* Expand grid candidates to find all events connected by conflict chains
|
||||
*
|
||||
* Uses expanding search to find chains (A→B→C where each conflicts with next)
|
||||
*
|
||||
* @param firstEvent - The first event to start with
|
||||
* @param remaining - Remaining events to check
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns Array of all events in the conflict chain
|
||||
*/
|
||||
private expandGridCandidates;
|
||||
/**
|
||||
* Allocate events to columns within a grid group
|
||||
*
|
||||
* Events that don't overlap can share the same column.
|
||||
* Uses a greedy algorithm to minimize the number of columns.
|
||||
*
|
||||
* @param events - Events in the grid group (should already be sorted by start time)
|
||||
* @returns Array of columns, where each column is an array of events
|
||||
*/
|
||||
private allocateColumns;
|
||||
}
|
||||
201
wwwroot/js/managers/EventLayoutCoordinator.js
Normal file
201
wwwroot/js/managers/EventLayoutCoordinator.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* EventLayoutCoordinator - Coordinates event layout calculations
|
||||
*
|
||||
* Separates layout logic from rendering concerns.
|
||||
* Calculates stack levels, groups events, and determines rendering strategy.
|
||||
*/
|
||||
export class EventLayoutCoordinator {
|
||||
constructor(stackManager, config, positionUtils) {
|
||||
this.stackManager = stackManager;
|
||||
this.config = config;
|
||||
this.positionUtils = positionUtils;
|
||||
}
|
||||
/**
|
||||
* Calculate complete layout for a column of events (recursive approach)
|
||||
*/
|
||||
calculateColumnLayout(columnEvents) {
|
||||
if (columnEvents.length === 0) {
|
||||
return { gridGroups: [], stackedEvents: [] };
|
||||
}
|
||||
const gridGroupLayouts = [];
|
||||
const stackedEventLayouts = [];
|
||||
const renderedEventsWithLevels = [];
|
||||
let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
// Process events recursively
|
||||
while (remaining.length > 0) {
|
||||
// Take first event
|
||||
const firstEvent = remaining[0];
|
||||
// Find events that could be in GRID with first event
|
||||
// Use expanding search to find chains (A→B→C where each conflicts with next)
|
||||
const gridSettings = this.config.gridSettings;
|
||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||
// Use refactored method for expanding grid candidates
|
||||
const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes);
|
||||
// Decide: should this group be GRID or STACK?
|
||||
const group = {
|
||||
events: gridCandidates,
|
||||
containerType: 'NONE',
|
||||
startTime: firstEvent.start
|
||||
};
|
||||
const containerType = this.stackManager.decideContainerType(group);
|
||||
if (containerType === 'GRID' && gridCandidates.length > 1) {
|
||||
// Render as GRID
|
||||
const gridStackLevel = this.calculateGridGroupStackLevelFromRendered(gridCandidates, renderedEventsWithLevels);
|
||||
// Ensure we get the earliest event (explicit sort for robustness)
|
||||
const earliestEvent = [...gridCandidates].sort((a, b) => a.start.getTime() - b.start.getTime())[0];
|
||||
const position = this.positionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
const columns = this.allocateColumns(gridCandidates);
|
||||
gridGroupLayouts.push({
|
||||
events: gridCandidates,
|
||||
stackLevel: gridStackLevel,
|
||||
position: { top: position.top + 1 },
|
||||
columns
|
||||
});
|
||||
// Mark all events in grid with their stack level
|
||||
gridCandidates.forEach(e => renderedEventsWithLevels.push({ event: e, level: gridStackLevel }));
|
||||
// Remove all events in this grid from remaining
|
||||
remaining = remaining.filter(e => !gridCandidates.includes(e));
|
||||
}
|
||||
else {
|
||||
// Render first event as STACKED
|
||||
const stackLevel = this.calculateStackLevelFromRendered(firstEvent, renderedEventsWithLevels);
|
||||
const position = this.positionUtils.calculateEventPosition(firstEvent.start, firstEvent.end);
|
||||
stackedEventLayouts.push({
|
||||
event: firstEvent,
|
||||
stackLink: { stackLevel },
|
||||
position: { top: position.top + 1, height: position.height - 3 }
|
||||
});
|
||||
// Mark this event with its stack level
|
||||
renderedEventsWithLevels.push({ event: firstEvent, level: stackLevel });
|
||||
// Remove only first event from remaining
|
||||
remaining = remaining.slice(1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
gridGroups: gridGroupLayouts,
|
||||
stackedEvents: stackedEventLayouts
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate stack level for a grid group based on already rendered events
|
||||
*/
|
||||
calculateGridGroupStackLevelFromRendered(gridEvents, renderedEventsWithLevels) {
|
||||
// Find highest stack level of any rendered event that overlaps with this grid
|
||||
let maxOverlappingLevel = -1;
|
||||
for (const gridEvent of gridEvents) {
|
||||
for (const rendered of renderedEventsWithLevels) {
|
||||
if (this.stackManager.doEventsOverlap(gridEvent, rendered.event)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level);
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
/**
|
||||
* Calculate stack level for a single stacked event based on already rendered events
|
||||
*/
|
||||
calculateStackLevelFromRendered(event, renderedEventsWithLevels) {
|
||||
// Find highest stack level of any rendered event that overlaps with this event
|
||||
let maxOverlappingLevel = -1;
|
||||
for (const rendered of renderedEventsWithLevels) {
|
||||
if (this.stackManager.doEventsOverlap(event, rendered.event)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level);
|
||||
}
|
||||
}
|
||||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
/**
|
||||
* Detect if two events have a conflict based on threshold
|
||||
*
|
||||
* @param event1 - First event
|
||||
* @param event2 - Second event
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns true if events conflict
|
||||
*/
|
||||
detectConflict(event1, event2, thresholdMinutes) {
|
||||
// Check 1: Start-to-start conflict (starts within threshold)
|
||||
const startToStartDiff = Math.abs(event1.start.getTime() - event2.start.getTime()) / (1000 * 60);
|
||||
if (startToStartDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(event1, event2)) {
|
||||
return true;
|
||||
}
|
||||
// Check 2: End-to-start conflict (event1 starts within threshold before event2 ends)
|
||||
const endToStartMinutes = (event2.end.getTime() - event1.start.getTime()) / (1000 * 60);
|
||||
if (endToStartMinutes > 0 && endToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
// Check 3: Reverse end-to-start (event2 starts within threshold before event1 ends)
|
||||
const reverseEndToStart = (event1.end.getTime() - event2.start.getTime()) / (1000 * 60);
|
||||
if (reverseEndToStart > 0 && reverseEndToStart <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Expand grid candidates to find all events connected by conflict chains
|
||||
*
|
||||
* Uses expanding search to find chains (A→B→C where each conflicts with next)
|
||||
*
|
||||
* @param firstEvent - The first event to start with
|
||||
* @param remaining - Remaining events to check
|
||||
* @param thresholdMinutes - Threshold in minutes
|
||||
* @returns Array of all events in the conflict chain
|
||||
*/
|
||||
expandGridCandidates(firstEvent, remaining, thresholdMinutes) {
|
||||
const gridCandidates = [firstEvent];
|
||||
let candidatesChanged = true;
|
||||
// Keep expanding until no new candidates can be added
|
||||
while (candidatesChanged) {
|
||||
candidatesChanged = false;
|
||||
for (let i = 1; i < remaining.length; i++) {
|
||||
const candidate = remaining[i];
|
||||
// Skip if already in candidates
|
||||
if (gridCandidates.includes(candidate))
|
||||
continue;
|
||||
// Check if candidate conflicts with ANY event in gridCandidates
|
||||
for (const existingCandidate of gridCandidates) {
|
||||
if (this.detectConflict(candidate, existingCandidate, thresholdMinutes)) {
|
||||
gridCandidates.push(candidate);
|
||||
candidatesChanged = true;
|
||||
break; // Found conflict, move to next candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return gridCandidates;
|
||||
}
|
||||
/**
|
||||
* Allocate events to columns within a grid group
|
||||
*
|
||||
* Events that don't overlap can share the same column.
|
||||
* Uses a greedy algorithm to minimize the number of columns.
|
||||
*
|
||||
* @param events - Events in the grid group (should already be sorted by start time)
|
||||
* @returns Array of columns, where each column is an array of events
|
||||
*/
|
||||
allocateColumns(events) {
|
||||
if (events.length === 0)
|
||||
return [];
|
||||
if (events.length === 1)
|
||||
return [[events[0]]];
|
||||
const columns = [];
|
||||
// For each event, try to place it in an existing column where it doesn't overlap
|
||||
for (const event of events) {
|
||||
let placed = false;
|
||||
// Try to find a column where this event doesn't overlap with any existing event
|
||||
for (const column of columns) {
|
||||
const hasOverlap = column.some(colEvent => this.stackManager.doEventsOverlap(event, colEvent));
|
||||
if (!hasOverlap) {
|
||||
column.push(event);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If no suitable column found, create a new one
|
||||
if (!placed) {
|
||||
columns.push([event]);
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=EventLayoutCoordinator.js.map
|
||||
1
wwwroot/js/managers/EventLayoutCoordinator.js.map
Normal file
1
wwwroot/js/managers/EventLayoutCoordinator.js.map
Normal file
File diff suppressed because one or more lines are too long
69
wwwroot/js/managers/EventManager.d.ts
vendored
Normal file
69
wwwroot/js/managers/EventManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { IEventBus, ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { IEventRepository } from '../repositories/IEventRepository';
|
||||
/**
|
||||
* EventManager - Event lifecycle and CRUD operations
|
||||
* Delegates all data operations to IEventRepository
|
||||
* No longer maintains in-memory cache - repository is single source of truth
|
||||
*/
|
||||
export declare class EventManager {
|
||||
private eventBus;
|
||||
private dateService;
|
||||
private config;
|
||||
private repository;
|
||||
constructor(eventBus: IEventBus, dateService: DateService, config: Configuration, repository: IEventRepository);
|
||||
/**
|
||||
* Load event data from repository
|
||||
* No longer caches - delegates to repository
|
||||
*/
|
||||
loadData(): Promise<void>;
|
||||
/**
|
||||
* Get all events from repository
|
||||
*/
|
||||
getEvents(copy?: boolean): Promise<ICalendarEvent[]>;
|
||||
/**
|
||||
* Get event by ID from repository
|
||||
*/
|
||||
getEventById(id: string): Promise<ICalendarEvent | undefined>;
|
||||
/**
|
||||
* Get event by ID and return event info for navigation
|
||||
* @param id Event ID to find
|
||||
* @returns Event with navigation info or null if not found
|
||||
*/
|
||||
getEventForNavigation(id: string): Promise<{
|
||||
event: ICalendarEvent;
|
||||
eventDate: Date;
|
||||
} | null>;
|
||||
/**
|
||||
* Navigate to specific event by ID
|
||||
* Emits navigation events for other managers to handle
|
||||
* @param eventId Event ID to navigate to
|
||||
* @returns true if event found and navigation initiated, false otherwise
|
||||
*/
|
||||
navigateToEvent(eventId: string): Promise<boolean>;
|
||||
/**
|
||||
* Get events that overlap with a given time period
|
||||
*/
|
||||
getEventsForPeriod(startDate: Date, endDate: Date): Promise<ICalendarEvent[]>;
|
||||
/**
|
||||
* Create a new event and add it to the calendar
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
addEvent(event: Omit<ICalendarEvent, 'id'>): Promise<ICalendarEvent>;
|
||||
/**
|
||||
* Update an existing event
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
updateEvent(id: string, updates: Partial<ICalendarEvent>): Promise<ICalendarEvent | null>;
|
||||
/**
|
||||
* Delete an event
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
deleteEvent(id: string): Promise<boolean>;
|
||||
/**
|
||||
* Handle remote update from SignalR
|
||||
* Delegates to repository with source='remote'
|
||||
*/
|
||||
handleRemoteUpdate(event: ICalendarEvent): Promise<void>;
|
||||
}
|
||||
164
wwwroot/js/managers/EventManager.js
Normal file
164
wwwroot/js/managers/EventManager.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* EventManager - Event lifecycle and CRUD operations
|
||||
* Delegates all data operations to IEventRepository
|
||||
* No longer maintains in-memory cache - repository is single source of truth
|
||||
*/
|
||||
export class EventManager {
|
||||
constructor(eventBus, dateService, config, repository) {
|
||||
this.eventBus = eventBus;
|
||||
this.dateService = dateService;
|
||||
this.config = config;
|
||||
this.repository = repository;
|
||||
}
|
||||
/**
|
||||
* Load event data from repository
|
||||
* No longer caches - delegates to repository
|
||||
*/
|
||||
async loadData() {
|
||||
try {
|
||||
// Just ensure repository is ready - no caching
|
||||
await this.repository.loadEvents();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to load event data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get all events from repository
|
||||
*/
|
||||
async getEvents(copy = false) {
|
||||
const events = await this.repository.loadEvents();
|
||||
return copy ? [...events] : events;
|
||||
}
|
||||
/**
|
||||
* Get event by ID from repository
|
||||
*/
|
||||
async getEventById(id) {
|
||||
const events = await this.repository.loadEvents();
|
||||
return events.find(event => event.id === id);
|
||||
}
|
||||
/**
|
||||
* Get event by ID and return event info for navigation
|
||||
* @param id Event ID to find
|
||||
* @returns Event with navigation info or null if not found
|
||||
*/
|
||||
async getEventForNavigation(id) {
|
||||
const event = await this.getEventById(id);
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
// Validate event dates
|
||||
const validation = this.dateService.validateDate(event.start);
|
||||
if (!validation.valid) {
|
||||
console.warn(`EventManager: Invalid event start date for event ${id}:`, validation.error);
|
||||
return null;
|
||||
}
|
||||
// Validate date range
|
||||
if (!this.dateService.isValidRange(event.start, event.end)) {
|
||||
console.warn(`EventManager: Invalid date range for event ${id}: start must be before end`);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
event,
|
||||
eventDate: event.start
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Navigate to specific event by ID
|
||||
* Emits navigation events for other managers to handle
|
||||
* @param eventId Event ID to navigate to
|
||||
* @returns true if event found and navigation initiated, false otherwise
|
||||
*/
|
||||
async navigateToEvent(eventId) {
|
||||
const eventInfo = await this.getEventForNavigation(eventId);
|
||||
if (!eventInfo) {
|
||||
console.warn(`EventManager: Event with ID ${eventId} not found`);
|
||||
return false;
|
||||
}
|
||||
const { event, eventDate } = eventInfo;
|
||||
// Emit navigation request event
|
||||
this.eventBus.emit(CoreEvents.NAVIGATE_TO_EVENT, {
|
||||
eventId,
|
||||
event,
|
||||
eventDate,
|
||||
eventStartTime: event.start
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Get events that overlap with a given time period
|
||||
*/
|
||||
async getEventsForPeriod(startDate, endDate) {
|
||||
const events = await this.repository.loadEvents();
|
||||
// Event overlaps period if it starts before period ends AND ends after period starts
|
||||
return events.filter(event => {
|
||||
return event.start <= endDate && event.end >= startDate;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create a new event and add it to the calendar
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
async addEvent(event) {
|
||||
const newEvent = await this.repository.createEvent(event, 'local');
|
||||
this.eventBus.emit(CoreEvents.EVENT_CREATED, {
|
||||
event: newEvent
|
||||
});
|
||||
return newEvent;
|
||||
}
|
||||
/**
|
||||
* Update an existing event
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
async updateEvent(id, updates) {
|
||||
try {
|
||||
const updatedEvent = await this.repository.updateEvent(id, updates, 'local');
|
||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
||||
event: updatedEvent
|
||||
});
|
||||
return updatedEvent;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to update event ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete an event
|
||||
* Delegates to repository with source='local'
|
||||
*/
|
||||
async deleteEvent(id) {
|
||||
try {
|
||||
await this.repository.deleteEvent(id, 'local');
|
||||
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
|
||||
eventId: id
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to delete event ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle remote update from SignalR
|
||||
* Delegates to repository with source='remote'
|
||||
*/
|
||||
async handleRemoteUpdate(event) {
|
||||
try {
|
||||
await this.repository.updateEvent(event.id, event, 'remote');
|
||||
this.eventBus.emit(CoreEvents.REMOTE_UPDATE_RECEIVED, {
|
||||
event
|
||||
});
|
||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
||||
event
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to handle remote update for event ${event.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=EventManager.js.map
|
||||
1
wwwroot/js/managers/EventManager.js.map
Normal file
1
wwwroot/js/managers/EventManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"EventManager.js","sourceRoot":"","sources":["../../../src/managers/EventManager.ts"],"names":[],"mappings":"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"}
|
||||
91
wwwroot/js/managers/EventStackManager.d.ts
vendored
Normal file
91
wwwroot/js/managers/EventStackManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* EventStackManager - Manages visual stacking of overlapping calendar events
|
||||
*
|
||||
* This class handles the creation and maintenance of "stack chains" - doubly-linked
|
||||
* lists of overlapping events stored directly in DOM elements via data attributes.
|
||||
*
|
||||
* Implements 3-phase algorithm for grid + nested stacking:
|
||||
* Phase 1: Group events by start time proximity (configurable threshold)
|
||||
* Phase 2: Decide container type (GRID vs STACKING)
|
||||
* Phase 3: Handle late arrivals (nested stacking - NOT IMPLEMENTED)
|
||||
*
|
||||
* @see STACKING_CONCEPT.md for detailed documentation
|
||||
* @see stacking-visualization.html for visual examples
|
||||
*/
|
||||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
export interface IStackLink {
|
||||
prev?: string;
|
||||
next?: string;
|
||||
stackLevel: number;
|
||||
}
|
||||
export interface IEventGroup {
|
||||
events: ICalendarEvent[];
|
||||
containerType: 'NONE' | 'GRID' | 'STACKING';
|
||||
startTime: Date;
|
||||
}
|
||||
export declare class EventStackManager {
|
||||
private static readonly STACK_OFFSET_PX;
|
||||
private config;
|
||||
constructor(config: Configuration);
|
||||
/**
|
||||
* Group events by time conflicts (both start-to-start and end-to-start within threshold)
|
||||
*
|
||||
* Events are grouped if:
|
||||
* 1. They start within ±threshold minutes of each other (start-to-start)
|
||||
* 2. One event starts within threshold minutes before another ends (end-to-start conflict)
|
||||
*/
|
||||
groupEventsByStartTime(events: ICalendarEvent[]): IEventGroup[];
|
||||
/**
|
||||
* Decide container type for a group of events
|
||||
*
|
||||
* Rule: Events starting simultaneously (within threshold) should ALWAYS use GRID,
|
||||
* even if they overlap each other. This provides better visual indication that
|
||||
* events start at the same time.
|
||||
*/
|
||||
decideContainerType(group: IEventGroup): 'NONE' | 'GRID' | 'STACKING';
|
||||
/**
|
||||
* Check if two events overlap in time
|
||||
*/
|
||||
doEventsOverlap(event1: ICalendarEvent, event2: ICalendarEvent): boolean;
|
||||
/**
|
||||
* Create optimized stack links (events share levels when possible)
|
||||
*/
|
||||
createOptimizedStackLinks(events: ICalendarEvent[]): Map<string, IStackLink>;
|
||||
/**
|
||||
* Calculate marginLeft based on stack level
|
||||
*/
|
||||
calculateMarginLeft(stackLevel: number): number;
|
||||
/**
|
||||
* Calculate zIndex based on stack level
|
||||
*/
|
||||
calculateZIndex(stackLevel: number): number;
|
||||
/**
|
||||
* Serialize stack link to JSON string
|
||||
*/
|
||||
serializeStackLink(stackLink: IStackLink): string;
|
||||
/**
|
||||
* Deserialize JSON string to stack link
|
||||
*/
|
||||
deserializeStackLink(json: string): IStackLink | null;
|
||||
/**
|
||||
* Apply stack link to DOM element
|
||||
*/
|
||||
applyStackLinkToElement(element: HTMLElement, stackLink: IStackLink): void;
|
||||
/**
|
||||
* Get stack link from DOM element
|
||||
*/
|
||||
getStackLinkFromElement(element: HTMLElement): IStackLink | null;
|
||||
/**
|
||||
* Apply visual styling to element based on stack level
|
||||
*/
|
||||
applyVisualStyling(element: HTMLElement, stackLevel: number): void;
|
||||
/**
|
||||
* Clear stack link from element
|
||||
*/
|
||||
clearStackLinkFromElement(element: HTMLElement): void;
|
||||
/**
|
||||
* Clear visual styling from element
|
||||
*/
|
||||
clearVisualStyling(element: HTMLElement): void;
|
||||
}
|
||||
217
wwwroot/js/managers/EventStackManager.js
Normal file
217
wwwroot/js/managers/EventStackManager.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/**
|
||||
* EventStackManager - Manages visual stacking of overlapping calendar events
|
||||
*
|
||||
* This class handles the creation and maintenance of "stack chains" - doubly-linked
|
||||
* lists of overlapping events stored directly in DOM elements via data attributes.
|
||||
*
|
||||
* Implements 3-phase algorithm for grid + nested stacking:
|
||||
* Phase 1: Group events by start time proximity (configurable threshold)
|
||||
* Phase 2: Decide container type (GRID vs STACKING)
|
||||
* Phase 3: Handle late arrivals (nested stacking - NOT IMPLEMENTED)
|
||||
*
|
||||
* @see STACKING_CONCEPT.md for detailed documentation
|
||||
* @see stacking-visualization.html for visual examples
|
||||
*/
|
||||
export class EventStackManager {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
}
|
||||
// ============================================
|
||||
// PHASE 1: Start Time Grouping
|
||||
// ============================================
|
||||
/**
|
||||
* Group events by time conflicts (both start-to-start and end-to-start within threshold)
|
||||
*
|
||||
* Events are grouped if:
|
||||
* 1. They start within ±threshold minutes of each other (start-to-start)
|
||||
* 2. One event starts within threshold minutes before another ends (end-to-start conflict)
|
||||
*/
|
||||
groupEventsByStartTime(events) {
|
||||
if (events.length === 0)
|
||||
return [];
|
||||
// Get threshold from config
|
||||
const gridSettings = this.config.gridSettings;
|
||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||
// Sort events by start time
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const groups = [];
|
||||
for (const event of sorted) {
|
||||
// Find existing group that this event conflicts with
|
||||
const existingGroup = groups.find(group => {
|
||||
// Check if event conflicts with ANY event in the group
|
||||
return group.events.some(groupEvent => {
|
||||
// Start-to-start conflict: events start within threshold
|
||||
const startToStartMinutes = Math.abs(event.start.getTime() - groupEvent.start.getTime()) / (1000 * 60);
|
||||
if (startToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
// End-to-start conflict: event starts within threshold before groupEvent ends
|
||||
const endToStartMinutes = (groupEvent.end.getTime() - event.start.getTime()) / (1000 * 60);
|
||||
if (endToStartMinutes > 0 && endToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
// Also check reverse: groupEvent starts within threshold before event ends
|
||||
const reverseEndToStart = (event.end.getTime() - groupEvent.start.getTime()) / (1000 * 60);
|
||||
if (reverseEndToStart > 0 && reverseEndToStart <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
if (existingGroup) {
|
||||
existingGroup.events.push(event);
|
||||
}
|
||||
else {
|
||||
groups.push({
|
||||
events: [event],
|
||||
containerType: 'NONE',
|
||||
startTime: event.start
|
||||
});
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
// ============================================
|
||||
// PHASE 2: Container Type Decision
|
||||
// ============================================
|
||||
/**
|
||||
* Decide container type for a group of events
|
||||
*
|
||||
* Rule: Events starting simultaneously (within threshold) should ALWAYS use GRID,
|
||||
* even if they overlap each other. This provides better visual indication that
|
||||
* events start at the same time.
|
||||
*/
|
||||
decideContainerType(group) {
|
||||
if (group.events.length === 1) {
|
||||
return 'NONE';
|
||||
}
|
||||
// If events are grouped together (start within threshold), they should share columns (GRID)
|
||||
// This is true EVEN if they overlap, because the visual priority is to show
|
||||
// that they start simultaneously.
|
||||
return 'GRID';
|
||||
}
|
||||
/**
|
||||
* Check if two events overlap in time
|
||||
*/
|
||||
doEventsOverlap(event1, event2) {
|
||||
return event1.start < event2.end && event1.end > event2.start;
|
||||
}
|
||||
// ============================================
|
||||
// Stack Level Calculation
|
||||
// ============================================
|
||||
/**
|
||||
* Create optimized stack links (events share levels when possible)
|
||||
*/
|
||||
createOptimizedStackLinks(events) {
|
||||
const stackLinks = new Map();
|
||||
if (events.length === 0)
|
||||
return stackLinks;
|
||||
// Sort by start time
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
// Step 1: Assign stack levels
|
||||
for (const event of sorted) {
|
||||
// Find all events this event overlaps with
|
||||
const overlapping = sorted.filter(other => other !== event && this.doEventsOverlap(event, other));
|
||||
// Find the MINIMUM required level (must be above all overlapping events)
|
||||
let minRequiredLevel = 0;
|
||||
for (const other of overlapping) {
|
||||
const otherLink = stackLinks.get(other.id);
|
||||
if (otherLink) {
|
||||
// Must be at least one level above the overlapping event
|
||||
minRequiredLevel = Math.max(minRequiredLevel, otherLink.stackLevel + 1);
|
||||
}
|
||||
}
|
||||
stackLinks.set(event.id, { stackLevel: minRequiredLevel });
|
||||
}
|
||||
// Step 2: Build prev/next chains for overlapping events at adjacent stack levels
|
||||
for (const event of sorted) {
|
||||
const currentLink = stackLinks.get(event.id);
|
||||
// Find overlapping events that are directly below (stackLevel - 1)
|
||||
const overlapping = sorted.filter(other => other !== event && this.doEventsOverlap(event, other));
|
||||
const directlyBelow = overlapping.filter(other => {
|
||||
const otherLink = stackLinks.get(other.id);
|
||||
return otherLink && otherLink.stackLevel === currentLink.stackLevel - 1;
|
||||
});
|
||||
if (directlyBelow.length > 0) {
|
||||
// Use the first one in sorted order as prev
|
||||
currentLink.prev = directlyBelow[0].id;
|
||||
}
|
||||
// Find overlapping events that are directly above (stackLevel + 1)
|
||||
const directlyAbove = overlapping.filter(other => {
|
||||
const otherLink = stackLinks.get(other.id);
|
||||
return otherLink && otherLink.stackLevel === currentLink.stackLevel + 1;
|
||||
});
|
||||
if (directlyAbove.length > 0) {
|
||||
// Use the first one in sorted order as next
|
||||
currentLink.next = directlyAbove[0].id;
|
||||
}
|
||||
}
|
||||
return stackLinks;
|
||||
}
|
||||
/**
|
||||
* Calculate marginLeft based on stack level
|
||||
*/
|
||||
calculateMarginLeft(stackLevel) {
|
||||
return stackLevel * EventStackManager.STACK_OFFSET_PX;
|
||||
}
|
||||
/**
|
||||
* Calculate zIndex based on stack level
|
||||
*/
|
||||
calculateZIndex(stackLevel) {
|
||||
return 100 + stackLevel;
|
||||
}
|
||||
/**
|
||||
* Serialize stack link to JSON string
|
||||
*/
|
||||
serializeStackLink(stackLink) {
|
||||
return JSON.stringify(stackLink);
|
||||
}
|
||||
/**
|
||||
* Deserialize JSON string to stack link
|
||||
*/
|
||||
deserializeStackLink(json) {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Apply stack link to DOM element
|
||||
*/
|
||||
applyStackLinkToElement(element, stackLink) {
|
||||
element.dataset.stackLink = this.serializeStackLink(stackLink);
|
||||
}
|
||||
/**
|
||||
* Get stack link from DOM element
|
||||
*/
|
||||
getStackLinkFromElement(element) {
|
||||
const data = element.dataset.stackLink;
|
||||
if (!data)
|
||||
return null;
|
||||
return this.deserializeStackLink(data);
|
||||
}
|
||||
/**
|
||||
* Apply visual styling to element based on stack level
|
||||
*/
|
||||
applyVisualStyling(element, stackLevel) {
|
||||
element.style.marginLeft = `${this.calculateMarginLeft(stackLevel)}px`;
|
||||
element.style.zIndex = `${this.calculateZIndex(stackLevel)}`;
|
||||
}
|
||||
/**
|
||||
* Clear stack link from element
|
||||
*/
|
||||
clearStackLinkFromElement(element) {
|
||||
delete element.dataset.stackLink;
|
||||
}
|
||||
/**
|
||||
* Clear visual styling from element
|
||||
*/
|
||||
clearVisualStyling(element) {
|
||||
element.style.marginLeft = '';
|
||||
element.style.zIndex = '';
|
||||
}
|
||||
}
|
||||
EventStackManager.STACK_OFFSET_PX = 15;
|
||||
//# sourceMappingURL=EventStackManager.js.map
|
||||
1
wwwroot/js/managers/EventStackManager.js.map
Normal file
1
wwwroot/js/managers/EventStackManager.js.map
Normal file
File diff suppressed because one or more lines are too long
30
wwwroot/js/managers/GridManager.d.ts
vendored
Normal file
30
wwwroot/js/managers/GridManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* GridManager - Simplified grid manager using centralized GridRenderer
|
||||
* Delegates DOM rendering to GridRenderer, focuses on coordination
|
||||
*/
|
||||
import { GridRenderer } from '../renderers/GridRenderer';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { EventManager } from './EventManager';
|
||||
/**
|
||||
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer
|
||||
*/
|
||||
export declare class GridManager {
|
||||
private container;
|
||||
private currentDate;
|
||||
private currentView;
|
||||
private gridRenderer;
|
||||
private dateService;
|
||||
private config;
|
||||
private dataSource;
|
||||
private eventManager;
|
||||
constructor(gridRenderer: GridRenderer, dateService: DateService, config: Configuration, eventManager: EventManager);
|
||||
private init;
|
||||
private findElements;
|
||||
private subscribeToEvents;
|
||||
/**
|
||||
* Main render method - delegates to GridRenderer
|
||||
* Note: CSS variables are automatically updated by ConfigManager when config changes
|
||||
*/
|
||||
render(): Promise<void>;
|
||||
}
|
||||
77
wwwroot/js/managers/GridManager.js
Normal file
77
wwwroot/js/managers/GridManager.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* GridManager - Simplified grid manager using centralized GridRenderer
|
||||
* Delegates DOM rendering to GridRenderer, focuses on coordination
|
||||
*/
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { DateColumnDataSource } from '../datasources/DateColumnDataSource';
|
||||
/**
|
||||
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer
|
||||
*/
|
||||
export class GridManager {
|
||||
constructor(gridRenderer, dateService, config, eventManager) {
|
||||
this.container = null;
|
||||
this.currentDate = new Date();
|
||||
this.currentView = 'week';
|
||||
this.gridRenderer = gridRenderer;
|
||||
this.dateService = dateService;
|
||||
this.config = config;
|
||||
this.eventManager = eventManager;
|
||||
this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView);
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
this.findElements();
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
findElements() {
|
||||
this.container = document.querySelector('swp-calendar-container');
|
||||
}
|
||||
subscribeToEvents() {
|
||||
// Listen for view changes
|
||||
eventBus.on(CoreEvents.VIEW_CHANGED, (e) => {
|
||||
const detail = e.detail;
|
||||
this.currentView = detail.currentView;
|
||||
this.dataSource.setCurrentView(this.currentView);
|
||||
this.render();
|
||||
});
|
||||
// Listen for navigation events from NavigationButtons
|
||||
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e) => {
|
||||
const detail = e.detail;
|
||||
this.currentDate = detail.newDate;
|
||||
this.dataSource.setCurrentDate(this.currentDate);
|
||||
this.render();
|
||||
});
|
||||
// Listen for config changes that affect rendering
|
||||
eventBus.on(CoreEvents.REFRESH_REQUESTED, (e) => {
|
||||
this.render();
|
||||
});
|
||||
eventBus.on(CoreEvents.WORKWEEK_CHANGED, () => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Main render method - delegates to GridRenderer
|
||||
* Note: CSS variables are automatically updated by ConfigManager when config changes
|
||||
*/
|
||||
async render() {
|
||||
if (!this.container) {
|
||||
return;
|
||||
}
|
||||
// Get dates from datasource - single source of truth
|
||||
const dates = this.dataSource.getColumns();
|
||||
// Get events for the period from EventManager
|
||||
const startDate = dates[0];
|
||||
const endDate = dates[dates.length - 1];
|
||||
const events = await this.eventManager.getEventsForPeriod(startDate, endDate);
|
||||
// Delegate to GridRenderer with dates and events
|
||||
this.gridRenderer.renderGrid(this.container, this.currentDate, this.currentView, dates, events);
|
||||
// Emit grid rendered event
|
||||
eventBus.emit(CoreEvents.GRID_RENDERED, {
|
||||
container: this.container,
|
||||
currentDate: this.currentDate,
|
||||
dates: dates
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=GridManager.js.map
|
||||
1
wwwroot/js/managers/GridManager.js.map
Normal file
1
wwwroot/js/managers/GridManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"GridManager.js","sourceRoot":"","sources":["../../../src/managers/GridManager.ts"],"names":[],"mappings":"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"}
|
||||
32
wwwroot/js/managers/HeaderManager.d.ts
vendored
Normal file
32
wwwroot/js/managers/HeaderManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { IHeaderRenderer } from '../renderers/DateHeaderRenderer';
|
||||
/**
|
||||
* HeaderManager - Handles all header-related event logic
|
||||
* Separates event handling from rendering concerns
|
||||
* Uses dependency injection for renderer strategy
|
||||
*/
|
||||
export declare class HeaderManager {
|
||||
private headerRenderer;
|
||||
private config;
|
||||
constructor(headerRenderer: IHeaderRenderer, config: Configuration);
|
||||
/**
|
||||
* Setup header drag event listeners - Listen to DragDropManager events
|
||||
*/
|
||||
setupHeaderDragListeners(): void;
|
||||
/**
|
||||
* Handle drag mouse enter header event
|
||||
*/
|
||||
private handleDragMouseEnterHeader;
|
||||
/**
|
||||
* Handle drag mouse leave header event
|
||||
*/
|
||||
private handleDragMouseLeaveHeader;
|
||||
/**
|
||||
* Setup navigation event listener
|
||||
*/
|
||||
private setupNavigationListener;
|
||||
/**
|
||||
* Update header content for navigation
|
||||
*/
|
||||
private updateHeader;
|
||||
}
|
||||
103
wwwroot/js/managers/HeaderManager.js
Normal file
103
wwwroot/js/managers/HeaderManager.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { eventBus } from '../core/EventBus';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
||||
/**
|
||||
* HeaderManager - Handles all header-related event logic
|
||||
* Separates event handling from rendering concerns
|
||||
* Uses dependency injection for renderer strategy
|
||||
*/
|
||||
export class HeaderManager {
|
||||
constructor(headerRenderer, config) {
|
||||
this.headerRenderer = headerRenderer;
|
||||
this.config = config;
|
||||
// Bind handler methods for event listeners
|
||||
this.handleDragMouseEnterHeader = this.handleDragMouseEnterHeader.bind(this);
|
||||
this.handleDragMouseLeaveHeader = this.handleDragMouseLeaveHeader.bind(this);
|
||||
// Listen for navigation events to update header
|
||||
this.setupNavigationListener();
|
||||
}
|
||||
/**
|
||||
* Setup header drag event listeners - Listen to DragDropManager events
|
||||
*/
|
||||
setupHeaderDragListeners() {
|
||||
console.log('🎯 HeaderManager: Setting up drag event listeners');
|
||||
// Subscribe to drag events from DragDropManager
|
||||
eventBus.on('drag:mouseenter-header', this.handleDragMouseEnterHeader);
|
||||
eventBus.on('drag:mouseleave-header', this.handleDragMouseLeaveHeader);
|
||||
console.log('✅ HeaderManager: Drag event listeners attached');
|
||||
}
|
||||
/**
|
||||
* Handle drag mouse enter header event
|
||||
*/
|
||||
handleDragMouseEnterHeader(event) {
|
||||
const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } = event.detail;
|
||||
console.log('🎯 HeaderManager: Received drag:mouseenter-header', {
|
||||
targetDate,
|
||||
originalElement: !!originalElement,
|
||||
cloneElement: !!cloneElement
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handle drag mouse leave header event
|
||||
*/
|
||||
handleDragMouseLeaveHeader(event) {
|
||||
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = event.detail;
|
||||
console.log('🚪 HeaderManager: Received drag:mouseleave-header', {
|
||||
targetDate,
|
||||
originalElement: !!originalElement,
|
||||
cloneElement: !!cloneElement
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Setup navigation event listener
|
||||
*/
|
||||
setupNavigationListener() {
|
||||
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (event) => {
|
||||
const { currentDate } = event.detail;
|
||||
this.updateHeader(currentDate);
|
||||
});
|
||||
// Also listen for date changes (including initial setup)
|
||||
eventBus.on(CoreEvents.DATE_CHANGED, (event) => {
|
||||
const { currentDate } = event.detail;
|
||||
this.updateHeader(currentDate);
|
||||
});
|
||||
// Listen for workweek header updates after grid rebuild
|
||||
//currentDate: this.currentDate,
|
||||
//currentView: this.currentView,
|
||||
//workweek: this.config.currentWorkWeek
|
||||
eventBus.on('workweek:header-update', (event) => {
|
||||
const { currentDate } = event.detail;
|
||||
this.updateHeader(currentDate);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update header content for navigation
|
||||
*/
|
||||
updateHeader(currentDate) {
|
||||
console.log('🎯 HeaderManager.updateHeader called', {
|
||||
currentDate,
|
||||
rendererType: this.headerRenderer.constructor.name
|
||||
});
|
||||
const calendarHeader = document.querySelector('swp-calendar-header');
|
||||
if (!calendarHeader) {
|
||||
console.warn('❌ HeaderManager: No calendar header found!');
|
||||
return;
|
||||
}
|
||||
// Clear existing content
|
||||
calendarHeader.innerHTML = '';
|
||||
// Render new header content using injected renderer
|
||||
const context = {
|
||||
currentWeek: currentDate,
|
||||
config: this.config
|
||||
};
|
||||
this.headerRenderer.render(calendarHeader, context);
|
||||
// Setup event listeners on the new content
|
||||
this.setupHeaderDragListeners();
|
||||
// Notify other managers that header is ready with period data
|
||||
const payload = {
|
||||
headerElements: ColumnDetectionUtils.getHeaderColumns(),
|
||||
};
|
||||
eventBus.emit('header:ready', payload);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=HeaderManager.js.map
|
||||
1
wwwroot/js/managers/HeaderManager.js.map
Normal file
1
wwwroot/js/managers/HeaderManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"HeaderManager.js","sourceRoot":"","sources":["../../../src/managers/HeaderManager.ts"],"names":[],"mappings":"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"}
|
||||
40
wwwroot/js/managers/NavigationButtonsManager.d.ts
vendored
Normal file
40
wwwroot/js/managers/NavigationButtonsManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
/**
|
||||
* NavigationButtonsManager - Manages navigation button UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-nav-group> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Handles button clicks on swp-nav-button elements
|
||||
* - Validates navigation actions (prev, next, today)
|
||||
* - Emits NAV_BUTTON_CLICKED events
|
||||
* - Manages button UI listeners
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → validateAction() → emit event → NavigationManager handles navigation
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - NavigationManager: Performs actual navigation logic (animations, grid updates, week calculations)
|
||||
*/
|
||||
export declare class NavigationButtonsManager {
|
||||
private eventBus;
|
||||
private buttonListeners;
|
||||
constructor(eventBus: IEventBus);
|
||||
/**
|
||||
* Setup click listeners on all navigation buttons
|
||||
*/
|
||||
private setupButtonListeners;
|
||||
/**
|
||||
* Handle navigation action
|
||||
*/
|
||||
private handleNavigation;
|
||||
/**
|
||||
* Validate if string is a valid navigation action
|
||||
*/
|
||||
private isValidAction;
|
||||
}
|
||||
63
wwwroot/js/managers/NavigationButtonsManager.js
Normal file
63
wwwroot/js/managers/NavigationButtonsManager.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* NavigationButtonsManager - Manages navigation button UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-nav-group> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Handles button clicks on swp-nav-button elements
|
||||
* - Validates navigation actions (prev, next, today)
|
||||
* - Emits NAV_BUTTON_CLICKED events
|
||||
* - Manages button UI listeners
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → validateAction() → emit event → NavigationManager handles navigation
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - NavigationManager: Performs actual navigation logic (animations, grid updates, week calculations)
|
||||
*/
|
||||
export class NavigationButtonsManager {
|
||||
constructor(eventBus) {
|
||||
this.buttonListeners = new Map();
|
||||
this.eventBus = eventBus;
|
||||
this.setupButtonListeners();
|
||||
}
|
||||
/**
|
||||
* Setup click listeners on all navigation buttons
|
||||
*/
|
||||
setupButtonListeners() {
|
||||
const buttons = document.querySelectorAll('swp-nav-button[data-action]');
|
||||
buttons.forEach(button => {
|
||||
const clickHandler = (event) => {
|
||||
event.preventDefault();
|
||||
const action = button.getAttribute('data-action');
|
||||
if (action && this.isValidAction(action)) {
|
||||
this.handleNavigation(action);
|
||||
}
|
||||
};
|
||||
button.addEventListener('click', clickHandler);
|
||||
this.buttonListeners.set(button, clickHandler);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handle navigation action
|
||||
*/
|
||||
handleNavigation(action) {
|
||||
// Emit navigation button clicked event
|
||||
this.eventBus.emit(CoreEvents.NAV_BUTTON_CLICKED, {
|
||||
action: action
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validate if string is a valid navigation action
|
||||
*/
|
||||
isValidAction(action) {
|
||||
return ['prev', 'next', 'today'].includes(action);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=NavigationButtonsManager.js.map
|
||||
1
wwwroot/js/managers/NavigationButtonsManager.js.map
Normal file
1
wwwroot/js/managers/NavigationButtonsManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"NavigationButtonsManager.js","sourceRoot":"","sources":["../../../src/managers/NavigationButtonsManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,wBAAwB;IAInC,YAAY,QAAmB;QAFvB,oBAAe,GAAgC,IAAI,GAAG,EAAE,CAAC;QAG/D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,oBAAoB,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACK,oBAAoB;QAC1B,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,6BAA6B,CAAC,CAAC;QAEzE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YACvB,MAAM,YAAY,GAAG,CAAC,KAAY,EAAE,EAAE;gBACpC,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;gBAClD,IAAI,MAAM,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;oBACzC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAC/C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,gBAAgB,CAAC,MAAc;QACrC,uCAAuC;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,kBAAkB,EAAE;YAChD,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,aAAa,CAAC,MAAc;QAClC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpD,CAAC;CACF"}
|
||||
32
wwwroot/js/managers/NavigationManager.d.ts
vendored
Normal file
32
wwwroot/js/managers/NavigationManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { EventRenderingService } from '../renderers/EventRendererManager';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { WeekInfoRenderer } from '../renderers/WeekInfoRenderer';
|
||||
import { GridRenderer } from '../renderers/GridRenderer';
|
||||
export declare class NavigationManager {
|
||||
private eventBus;
|
||||
private weekInfoRenderer;
|
||||
private gridRenderer;
|
||||
private dateService;
|
||||
private currentWeek;
|
||||
private targetWeek;
|
||||
private animationQueue;
|
||||
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService, gridRenderer: GridRenderer, dateService: DateService, weekInfoRenderer: WeekInfoRenderer);
|
||||
private init;
|
||||
/**
|
||||
* Get the start of the ISO week (Monday) for a given date
|
||||
* @param date - Any date in the week
|
||||
* @returns The Monday of the ISO week
|
||||
*/
|
||||
private getISOWeekStart;
|
||||
private setupEventListeners;
|
||||
/**
|
||||
* Navigate to specific event date and emit scroll event after navigation
|
||||
*/
|
||||
private navigateToEventDate;
|
||||
private navigateToDate;
|
||||
/**
|
||||
* Animation transition using pre-rendered containers when available
|
||||
*/
|
||||
private animateTransition;
|
||||
}
|
||||
188
wwwroot/js/managers/NavigationManager.js
Normal file
188
wwwroot/js/managers/NavigationManager.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
export class NavigationManager {
|
||||
constructor(eventBus, eventRenderer, gridRenderer, dateService, weekInfoRenderer) {
|
||||
this.animationQueue = 0;
|
||||
this.eventBus = eventBus;
|
||||
this.dateService = dateService;
|
||||
this.weekInfoRenderer = weekInfoRenderer;
|
||||
this.gridRenderer = gridRenderer;
|
||||
this.currentWeek = this.getISOWeekStart(new Date());
|
||||
this.targetWeek = new Date(this.currentWeek);
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
/**
|
||||
* Get the start of the ISO week (Monday) for a given date
|
||||
* @param date - Any date in the week
|
||||
* @returns The Monday of the ISO week
|
||||
*/
|
||||
getISOWeekStart(date) {
|
||||
const weekBounds = this.dateService.getWeekBounds(date);
|
||||
return this.dateService.startOfDay(weekBounds.start);
|
||||
}
|
||||
setupEventListeners() {
|
||||
// Listen for filter changes and apply to pre-rendered grids
|
||||
this.eventBus.on(CoreEvents.FILTER_CHANGED, (e) => {
|
||||
const detail = e.detail;
|
||||
this.weekInfoRenderer.applyFilterToPreRenderedGrids(detail);
|
||||
});
|
||||
// Listen for navigation button clicks from NavigationButtons
|
||||
this.eventBus.on(CoreEvents.NAV_BUTTON_CLICKED, (event) => {
|
||||
const { direction, newDate } = event.detail;
|
||||
// Navigate to the new date with animation
|
||||
this.navigateToDate(newDate, direction);
|
||||
});
|
||||
// Listen for external navigation requests
|
||||
this.eventBus.on(CoreEvents.DATE_CHANGED, (event) => {
|
||||
const customEvent = event;
|
||||
const dateFromEvent = customEvent.detail.currentDate;
|
||||
// Validate date before processing
|
||||
if (!dateFromEvent) {
|
||||
console.warn('NavigationManager: No date provided in DATE_CHANGED event');
|
||||
return;
|
||||
}
|
||||
const targetDate = new Date(dateFromEvent);
|
||||
// Use DateService validation
|
||||
const validation = this.dateService.validateDate(targetDate);
|
||||
if (!validation.valid) {
|
||||
console.warn('NavigationManager: Invalid date received:', validation.error);
|
||||
return;
|
||||
}
|
||||
this.navigateToDate(targetDate);
|
||||
});
|
||||
// Listen for event navigation requests
|
||||
this.eventBus.on(CoreEvents.NAVIGATE_TO_EVENT, (event) => {
|
||||
const customEvent = event;
|
||||
const { eventDate, eventStartTime } = customEvent.detail;
|
||||
if (!eventDate || !eventStartTime) {
|
||||
console.warn('NavigationManager: Invalid event navigation data');
|
||||
return;
|
||||
}
|
||||
this.navigateToEventDate(eventDate, eventStartTime);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Navigate to specific event date and emit scroll event after navigation
|
||||
*/
|
||||
navigateToEventDate(eventDate, eventStartTime) {
|
||||
const weekStart = this.getISOWeekStart(eventDate);
|
||||
this.targetWeek = new Date(weekStart);
|
||||
const currentTime = this.currentWeek.getTime();
|
||||
const targetTime = weekStart.getTime();
|
||||
// Store event start time for scrolling after navigation
|
||||
const scrollAfterNavigation = () => {
|
||||
// Emit scroll request after navigation is complete
|
||||
this.eventBus.emit('scroll:to-event-time', {
|
||||
eventStartTime
|
||||
});
|
||||
};
|
||||
if (currentTime < targetTime) {
|
||||
this.animationQueue++;
|
||||
this.animateTransition('next', weekStart);
|
||||
// Listen for navigation completion to trigger scroll
|
||||
this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation);
|
||||
}
|
||||
else if (currentTime > targetTime) {
|
||||
this.animationQueue++;
|
||||
this.animateTransition('prev', weekStart);
|
||||
// Listen for navigation completion to trigger scroll
|
||||
this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation);
|
||||
}
|
||||
else {
|
||||
// Already on correct week, just scroll
|
||||
scrollAfterNavigation();
|
||||
}
|
||||
}
|
||||
navigateToDate(date, direction) {
|
||||
const weekStart = this.getISOWeekStart(date);
|
||||
this.targetWeek = new Date(weekStart);
|
||||
const currentTime = this.currentWeek.getTime();
|
||||
const targetTime = weekStart.getTime();
|
||||
// Use provided direction or calculate based on time comparison
|
||||
let animationDirection;
|
||||
if (direction === 'next') {
|
||||
animationDirection = 'next';
|
||||
}
|
||||
else if (direction === 'previous') {
|
||||
animationDirection = 'prev';
|
||||
}
|
||||
else if (direction === 'today') {
|
||||
// For "today", determine direction based on current position
|
||||
animationDirection = currentTime < targetTime ? 'next' : 'prev';
|
||||
}
|
||||
else {
|
||||
// Fallback: calculate direction
|
||||
animationDirection = currentTime < targetTime ? 'next' : 'prev';
|
||||
}
|
||||
if (currentTime !== targetTime) {
|
||||
this.animationQueue++;
|
||||
this.animateTransition(animationDirection, weekStart);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Animation transition using pre-rendered containers when available
|
||||
*/
|
||||
animateTransition(direction, targetWeek) {
|
||||
const container = document.querySelector('swp-calendar-container');
|
||||
const currentGrid = document.querySelector('swp-calendar-container swp-grid-container:not([data-prerendered])');
|
||||
if (!container || !currentGrid) {
|
||||
return;
|
||||
}
|
||||
// Reset all-day height BEFORE creating new grid to ensure base height
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--all-day-row-height', '0px');
|
||||
let newGrid;
|
||||
console.group('🔧 NavigationManager.refactored');
|
||||
console.log('Calling GridRenderer instead of NavigationRenderer');
|
||||
console.log('Target week:', targetWeek);
|
||||
// Always create a fresh container for consistent behavior
|
||||
newGrid = this.gridRenderer.createNavigationGrid(container, targetWeek);
|
||||
console.groupEnd();
|
||||
// Clear any existing transforms before animation
|
||||
newGrid.style.transform = '';
|
||||
currentGrid.style.transform = '';
|
||||
// Animate transition using Web Animations API
|
||||
const slideOutAnimation = currentGrid.animate([
|
||||
{ transform: 'translateX(0)', opacity: '1' },
|
||||
{ transform: direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)', opacity: '0.5' }
|
||||
], {
|
||||
duration: 400,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards'
|
||||
});
|
||||
const slideInAnimation = newGrid.animate([
|
||||
{ transform: direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)' },
|
||||
{ transform: 'translateX(0)' }
|
||||
], {
|
||||
duration: 400,
|
||||
easing: 'ease-in-out',
|
||||
fill: 'forwards'
|
||||
});
|
||||
// Handle animation completion
|
||||
slideInAnimation.addEventListener('finish', () => {
|
||||
// Cleanup: Remove all old grids except the new one
|
||||
const allGrids = container.querySelectorAll('swp-grid-container');
|
||||
for (let i = 0; i < allGrids.length - 1; i++) {
|
||||
allGrids[i].remove();
|
||||
}
|
||||
// Reset positioning
|
||||
newGrid.style.position = 'relative';
|
||||
newGrid.removeAttribute('data-prerendered');
|
||||
// Update state
|
||||
this.currentWeek = new Date(targetWeek);
|
||||
this.animationQueue--;
|
||||
// If this was the last queued animation, ensure we're in sync
|
||||
if (this.animationQueue === 0) {
|
||||
this.currentWeek = new Date(this.targetWeek);
|
||||
}
|
||||
// Emit navigation completed event
|
||||
this.eventBus.emit(CoreEvents.NAVIGATION_COMPLETED, {
|
||||
direction,
|
||||
newDate: this.currentWeek
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=NavigationManager.js.map
|
||||
1
wwwroot/js/managers/NavigationManager.js.map
Normal file
1
wwwroot/js/managers/NavigationManager.js.map
Normal file
File diff suppressed because one or more lines are too long
42
wwwroot/js/managers/ResizeHandleManager.d.ts
vendored
Normal file
42
wwwroot/js/managers/ResizeHandleManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
export declare class ResizeHandleManager {
|
||||
private config;
|
||||
private positionUtils;
|
||||
private isResizing;
|
||||
private targetEl;
|
||||
private startY;
|
||||
private startDurationMin;
|
||||
private snapMin;
|
||||
private minDurationMin;
|
||||
private animationId;
|
||||
private currentHeight;
|
||||
private targetHeight;
|
||||
private pointerCaptured;
|
||||
private prevZ?;
|
||||
private readonly ANIMATION_SPEED;
|
||||
private readonly Z_INDEX_RESIZING;
|
||||
private readonly EVENT_REFRESH_THRESHOLD;
|
||||
constructor(config: Configuration, positionUtils: PositionUtils);
|
||||
initialize(): void;
|
||||
destroy(): void;
|
||||
private removeEventListeners;
|
||||
private createResizeHandle;
|
||||
private attachGlobalListeners;
|
||||
private onMouseOver;
|
||||
private onPointerDown;
|
||||
private startResizing;
|
||||
private setZIndexForResizing;
|
||||
private capturePointer;
|
||||
private onPointerMove;
|
||||
private updateResizeHeight;
|
||||
private animate;
|
||||
private finalizeAnimation;
|
||||
private onPointerUp;
|
||||
private cleanupAnimation;
|
||||
private snapToGrid;
|
||||
private emitResizeEndEvent;
|
||||
private cleanupResizing;
|
||||
private restoreZIndex;
|
||||
private releasePointer;
|
||||
}
|
||||
194
wwwroot/js/managers/ResizeHandleManager.js
Normal file
194
wwwroot/js/managers/ResizeHandleManager.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { eventBus } from '../core/EventBus';
|
||||
export class ResizeHandleManager {
|
||||
constructor(config, positionUtils) {
|
||||
this.config = config;
|
||||
this.positionUtils = positionUtils;
|
||||
this.isResizing = false;
|
||||
this.targetEl = null;
|
||||
this.startY = 0;
|
||||
this.startDurationMin = 0;
|
||||
this.animationId = null;
|
||||
this.currentHeight = 0;
|
||||
this.targetHeight = 0;
|
||||
this.pointerCaptured = false;
|
||||
// Constants for better maintainability
|
||||
this.ANIMATION_SPEED = 0.35;
|
||||
this.Z_INDEX_RESIZING = '1000';
|
||||
this.EVENT_REFRESH_THRESHOLD = 0.5;
|
||||
this.onMouseOver = (e) => {
|
||||
const target = e.target;
|
||||
const eventElement = target.closest('swp-event');
|
||||
if (eventElement && !this.isResizing) {
|
||||
// Check if handle already exists
|
||||
if (!eventElement.querySelector(':scope > swp-resize-handle')) {
|
||||
const handle = this.createResizeHandle();
|
||||
eventElement.appendChild(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.onPointerDown = (e) => {
|
||||
const handle = e.target.closest('swp-resize-handle');
|
||||
if (!handle)
|
||||
return;
|
||||
const element = handle.parentElement;
|
||||
this.startResizing(element, e);
|
||||
};
|
||||
this.onPointerMove = (e) => {
|
||||
if (!this.isResizing || !this.targetEl)
|
||||
return;
|
||||
this.updateResizeHeight(e.clientY);
|
||||
};
|
||||
this.animate = () => {
|
||||
if (!this.isResizing || !this.targetEl) {
|
||||
this.animationId = null;
|
||||
return;
|
||||
}
|
||||
const diff = this.targetHeight - this.currentHeight;
|
||||
if (Math.abs(diff) > this.EVENT_REFRESH_THRESHOLD) {
|
||||
this.currentHeight += diff * this.ANIMATION_SPEED;
|
||||
this.targetEl.updateHeight?.(this.currentHeight);
|
||||
this.animationId = requestAnimationFrame(this.animate);
|
||||
}
|
||||
else {
|
||||
this.finalizeAnimation();
|
||||
}
|
||||
};
|
||||
this.onPointerUp = (e) => {
|
||||
if (!this.isResizing || !this.targetEl)
|
||||
return;
|
||||
this.cleanupAnimation();
|
||||
this.snapToGrid();
|
||||
this.emitResizeEndEvent();
|
||||
this.cleanupResizing(e);
|
||||
};
|
||||
const grid = this.config.gridSettings;
|
||||
this.snapMin = grid.snapInterval;
|
||||
this.minDurationMin = this.snapMin;
|
||||
}
|
||||
initialize() {
|
||||
this.attachGlobalListeners();
|
||||
}
|
||||
destroy() {
|
||||
this.removeEventListeners();
|
||||
}
|
||||
removeEventListeners() {
|
||||
const calendarContainer = document.querySelector('swp-calendar-container');
|
||||
if (calendarContainer) {
|
||||
calendarContainer.removeEventListener('mouseover', this.onMouseOver, true);
|
||||
}
|
||||
document.removeEventListener('pointerdown', this.onPointerDown, true);
|
||||
document.removeEventListener('pointermove', this.onPointerMove, true);
|
||||
document.removeEventListener('pointerup', this.onPointerUp, true);
|
||||
}
|
||||
createResizeHandle() {
|
||||
const handle = document.createElement('swp-resize-handle');
|
||||
handle.setAttribute('aria-label', 'Resize event');
|
||||
handle.setAttribute('role', 'separator');
|
||||
return handle;
|
||||
}
|
||||
attachGlobalListeners() {
|
||||
const calendarContainer = document.querySelector('swp-calendar-container');
|
||||
if (calendarContainer) {
|
||||
calendarContainer.addEventListener('mouseover', this.onMouseOver, true);
|
||||
}
|
||||
document.addEventListener('pointerdown', this.onPointerDown, true);
|
||||
document.addEventListener('pointermove', this.onPointerMove, true);
|
||||
document.addEventListener('pointerup', this.onPointerUp, true);
|
||||
}
|
||||
startResizing(element, event) {
|
||||
this.targetEl = element;
|
||||
this.isResizing = true;
|
||||
this.startY = event.clientY;
|
||||
const startHeight = element.offsetHeight;
|
||||
this.startDurationMin = Math.max(this.minDurationMin, Math.round(this.positionUtils.pixelsToMinutes(startHeight)));
|
||||
this.setZIndexForResizing(element);
|
||||
this.capturePointer(event);
|
||||
document.documentElement.classList.add('swp--resizing');
|
||||
event.preventDefault();
|
||||
}
|
||||
setZIndexForResizing(element) {
|
||||
const container = element.closest('swp-event-group') ?? element;
|
||||
this.prevZ = container.style.zIndex;
|
||||
container.style.zIndex = this.Z_INDEX_RESIZING;
|
||||
}
|
||||
capturePointer(event) {
|
||||
try {
|
||||
event.target.setPointerCapture?.(event.pointerId);
|
||||
this.pointerCaptured = true;
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Pointer capture failed:', error);
|
||||
}
|
||||
}
|
||||
updateResizeHeight(currentY) {
|
||||
const deltaY = currentY - this.startY;
|
||||
const startHeight = this.positionUtils.minutesToPixels(this.startDurationMin);
|
||||
const rawHeight = startHeight + deltaY;
|
||||
const minHeight = this.positionUtils.minutesToPixels(this.minDurationMin);
|
||||
this.targetHeight = Math.max(minHeight, rawHeight);
|
||||
if (this.animationId == null) {
|
||||
this.currentHeight = this.targetEl?.offsetHeight;
|
||||
this.animate();
|
||||
}
|
||||
}
|
||||
finalizeAnimation() {
|
||||
if (!this.targetEl)
|
||||
return;
|
||||
this.currentHeight = this.targetHeight;
|
||||
this.targetEl.updateHeight?.(this.currentHeight);
|
||||
this.animationId = null;
|
||||
}
|
||||
cleanupAnimation() {
|
||||
if (this.animationId != null) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
}
|
||||
snapToGrid() {
|
||||
if (!this.targetEl)
|
||||
return;
|
||||
const currentHeight = this.targetEl.offsetHeight;
|
||||
const snapDistancePx = this.positionUtils.minutesToPixels(this.snapMin);
|
||||
const snappedHeight = Math.round(currentHeight / snapDistancePx) * snapDistancePx;
|
||||
const minHeight = this.positionUtils.minutesToPixels(this.minDurationMin);
|
||||
const finalHeight = Math.max(minHeight, snappedHeight) - 3; // Small gap to grid lines
|
||||
this.targetEl.updateHeight?.(finalHeight);
|
||||
}
|
||||
emitResizeEndEvent() {
|
||||
if (!this.targetEl)
|
||||
return;
|
||||
const eventId = this.targetEl.dataset.eventId || '';
|
||||
const resizeEndPayload = {
|
||||
eventId,
|
||||
element: this.targetEl,
|
||||
finalHeight: this.targetEl.offsetHeight
|
||||
};
|
||||
eventBus.emit('resize:end', resizeEndPayload);
|
||||
}
|
||||
cleanupResizing(event) {
|
||||
this.restoreZIndex();
|
||||
this.releasePointer(event);
|
||||
this.isResizing = false;
|
||||
this.targetEl = null;
|
||||
document.documentElement.classList.remove('swp--resizing');
|
||||
}
|
||||
restoreZIndex() {
|
||||
if (!this.targetEl || this.prevZ === undefined)
|
||||
return;
|
||||
const container = this.targetEl.closest('swp-event-group') ?? this.targetEl;
|
||||
container.style.zIndex = this.prevZ;
|
||||
this.prevZ = undefined;
|
||||
}
|
||||
releasePointer(event) {
|
||||
if (!this.pointerCaptured)
|
||||
return;
|
||||
try {
|
||||
event.target.releasePointerCapture?.(event.pointerId);
|
||||
this.pointerCaptured = false;
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Pointer release failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=ResizeHandleManager.js.map
|
||||
1
wwwroot/js/managers/ResizeHandleManager.js.map
Normal file
1
wwwroot/js/managers/ResizeHandleManager.js.map
Normal file
File diff suppressed because one or more lines are too long
64
wwwroot/js/managers/ScrollManager.d.ts
vendored
Normal file
64
wwwroot/js/managers/ScrollManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
/**
|
||||
* Manages scrolling functionality for the calendar using native scrollbars
|
||||
*/
|
||||
export declare class ScrollManager {
|
||||
private scrollableContent;
|
||||
private calendarContainer;
|
||||
private timeAxis;
|
||||
private calendarHeader;
|
||||
private resizeObserver;
|
||||
private positionUtils;
|
||||
constructor(positionUtils: PositionUtils);
|
||||
private init;
|
||||
/**
|
||||
* Public method to initialize scroll after grid is rendered
|
||||
*/
|
||||
initialize(): void;
|
||||
private subscribeToEvents;
|
||||
/**
|
||||
* Setup scrolling functionality after grid is rendered
|
||||
*/
|
||||
private setupScrolling;
|
||||
/**
|
||||
* Find DOM elements needed for scrolling
|
||||
*/
|
||||
private findElements;
|
||||
/**
|
||||
* Scroll to specific position
|
||||
*/
|
||||
scrollTo(scrollTop: number): void;
|
||||
/**
|
||||
* Scroll to specific hour using PositionUtils
|
||||
*/
|
||||
scrollToHour(hour: number): void;
|
||||
/**
|
||||
* Scroll to specific event time
|
||||
* @param eventStartTime ISO string of event start time
|
||||
*/
|
||||
scrollToEventTime(eventStartTime: string): void;
|
||||
/**
|
||||
* Setup ResizeObserver to monitor container size changes
|
||||
*/
|
||||
private setupResizeObserver;
|
||||
/**
|
||||
* Calculate and update scrollable content height dynamically
|
||||
*/
|
||||
private updateScrollableHeight;
|
||||
/**
|
||||
* Setup scroll synchronization between scrollable content and time axis
|
||||
*/
|
||||
private setupScrollSynchronization;
|
||||
/**
|
||||
* Synchronize time axis position with scrollable content
|
||||
*/
|
||||
private syncTimeAxisPosition;
|
||||
/**
|
||||
* Setup horizontal scroll synchronization between scrollable content and calendar header
|
||||
*/
|
||||
private setupHorizontalScrollSynchronization;
|
||||
/**
|
||||
* Synchronize calendar header position with scrollable content horizontal scroll
|
||||
*/
|
||||
private syncCalendarHeaderPosition;
|
||||
}
|
||||
217
wwwroot/js/managers/ScrollManager.js
Normal file
217
wwwroot/js/managers/ScrollManager.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// Custom scroll management for calendar week container
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* Manages scrolling functionality for the calendar using native scrollbars
|
||||
*/
|
||||
export class ScrollManager {
|
||||
constructor(positionUtils) {
|
||||
this.scrollableContent = null;
|
||||
this.calendarContainer = null;
|
||||
this.timeAxis = null;
|
||||
this.calendarHeader = null;
|
||||
this.resizeObserver = null;
|
||||
this.positionUtils = positionUtils;
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
/**
|
||||
* Public method to initialize scroll after grid is rendered
|
||||
*/
|
||||
initialize() {
|
||||
this.setupScrolling();
|
||||
}
|
||||
subscribeToEvents() {
|
||||
// Handle navigation animation completion - sync time axis position
|
||||
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
||||
this.syncTimeAxisPosition();
|
||||
this.setupScrolling();
|
||||
});
|
||||
// Handle all-day row height changes
|
||||
eventBus.on('header:height-changed', () => {
|
||||
this.updateScrollableHeight();
|
||||
});
|
||||
// Handle header ready - refresh header reference and re-sync
|
||||
eventBus.on('header:ready', () => {
|
||||
this.calendarHeader = document.querySelector('swp-calendar-header');
|
||||
if (this.scrollableContent && this.calendarHeader) {
|
||||
this.setupHorizontalScrollSynchronization();
|
||||
this.syncCalendarHeaderPosition(); // Immediately sync position
|
||||
}
|
||||
this.updateScrollableHeight(); // Update height calculations
|
||||
});
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateScrollableHeight();
|
||||
});
|
||||
// Listen for scroll to event time requests
|
||||
eventBus.on('scroll:to-event-time', (event) => {
|
||||
const customEvent = event;
|
||||
const { eventStartTime } = customEvent.detail;
|
||||
if (eventStartTime) {
|
||||
this.scrollToEventTime(eventStartTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Setup scrolling functionality after grid is rendered
|
||||
*/
|
||||
setupScrolling() {
|
||||
this.findElements();
|
||||
if (this.scrollableContent && this.calendarContainer) {
|
||||
this.setupResizeObserver();
|
||||
this.updateScrollableHeight();
|
||||
this.setupScrollSynchronization();
|
||||
}
|
||||
// Setup horizontal scrolling synchronization
|
||||
if (this.scrollableContent && this.calendarHeader) {
|
||||
this.setupHorizontalScrollSynchronization();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Find DOM elements needed for scrolling
|
||||
*/
|
||||
findElements() {
|
||||
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
||||
this.calendarContainer = document.querySelector('swp-calendar-container');
|
||||
this.timeAxis = document.querySelector('swp-time-axis');
|
||||
this.calendarHeader = document.querySelector('swp-calendar-header');
|
||||
}
|
||||
/**
|
||||
* Scroll to specific position
|
||||
*/
|
||||
scrollTo(scrollTop) {
|
||||
if (!this.scrollableContent)
|
||||
return;
|
||||
this.scrollableContent.scrollTop = scrollTop;
|
||||
}
|
||||
/**
|
||||
* Scroll to specific hour using PositionUtils
|
||||
*/
|
||||
scrollToHour(hour) {
|
||||
// Create time string for the hour
|
||||
const timeString = `${hour.toString().padStart(2, '0')}:00`;
|
||||
const scrollTop = this.positionUtils.timeToPixels(timeString);
|
||||
this.scrollTo(scrollTop);
|
||||
}
|
||||
/**
|
||||
* Scroll to specific event time
|
||||
* @param eventStartTime ISO string of event start time
|
||||
*/
|
||||
scrollToEventTime(eventStartTime) {
|
||||
try {
|
||||
const eventDate = new Date(eventStartTime);
|
||||
const eventHour = eventDate.getHours();
|
||||
const eventMinutes = eventDate.getMinutes();
|
||||
// Convert to decimal hour (e.g., 14:30 becomes 14.5)
|
||||
const decimalHour = eventHour + (eventMinutes / 60);
|
||||
this.scrollToHour(decimalHour);
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('ScrollManager: Failed to scroll to event time:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Setup ResizeObserver to monitor container size changes
|
||||
*/
|
||||
setupResizeObserver() {
|
||||
if (!this.calendarContainer)
|
||||
return;
|
||||
// Clean up existing observer
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
this.updateScrollableHeight();
|
||||
}
|
||||
});
|
||||
this.resizeObserver.observe(this.calendarContainer);
|
||||
}
|
||||
/**
|
||||
* Calculate and update scrollable content height dynamically
|
||||
*/
|
||||
updateScrollableHeight() {
|
||||
if (!this.scrollableContent || !this.calendarContainer)
|
||||
return;
|
||||
// Get calendar container height
|
||||
const containerRect = this.calendarContainer.getBoundingClientRect();
|
||||
// Find navigation height
|
||||
const navigation = document.querySelector('swp-calendar-nav');
|
||||
const navHeight = navigation ? navigation.getBoundingClientRect().height : 0;
|
||||
// Find calendar header height
|
||||
const calendarHeaderElement = document.querySelector('swp-calendar-header');
|
||||
const headerHeight = calendarHeaderElement ? calendarHeaderElement.getBoundingClientRect().height : 80;
|
||||
// Calculate available height for scrollable content
|
||||
const availableHeight = containerRect.height - headerHeight;
|
||||
// Calculate available width (container width minus time-axis)
|
||||
const availableWidth = containerRect.width - 60; // 60px time-axis
|
||||
// Set the height and width on scrollable content
|
||||
if (availableHeight > 0) {
|
||||
this.scrollableContent.style.height = `${availableHeight}px`;
|
||||
}
|
||||
if (availableWidth > 0) {
|
||||
this.scrollableContent.style.width = `${availableWidth}px`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Setup scroll synchronization between scrollable content and time axis
|
||||
*/
|
||||
setupScrollSynchronization() {
|
||||
if (!this.scrollableContent || !this.timeAxis)
|
||||
return;
|
||||
// Throttle scroll events for better performance
|
||||
let scrollTimeout = null;
|
||||
this.scrollableContent.addEventListener('scroll', () => {
|
||||
if (scrollTimeout) {
|
||||
cancelAnimationFrame(scrollTimeout);
|
||||
}
|
||||
scrollTimeout = requestAnimationFrame(() => {
|
||||
this.syncTimeAxisPosition();
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Synchronize time axis position with scrollable content
|
||||
*/
|
||||
syncTimeAxisPosition() {
|
||||
if (!this.scrollableContent || !this.timeAxis)
|
||||
return;
|
||||
const scrollTop = this.scrollableContent.scrollTop;
|
||||
const timeAxisContent = this.timeAxis.querySelector('swp-time-axis-content');
|
||||
if (timeAxisContent) {
|
||||
// Use transform for smooth performance
|
||||
timeAxisContent.style.transform = `translateY(-${scrollTop}px)`;
|
||||
// Debug logging (can be removed later)
|
||||
if (scrollTop % 100 === 0) { // Only log every 100px to avoid spam
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Setup horizontal scroll synchronization between scrollable content and calendar header
|
||||
*/
|
||||
setupHorizontalScrollSynchronization() {
|
||||
if (!this.scrollableContent || !this.calendarHeader)
|
||||
return;
|
||||
// Listen to horizontal scroll events
|
||||
this.scrollableContent.addEventListener('scroll', () => {
|
||||
this.syncCalendarHeaderPosition();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Synchronize calendar header position with scrollable content horizontal scroll
|
||||
*/
|
||||
syncCalendarHeaderPosition() {
|
||||
if (!this.scrollableContent || !this.calendarHeader)
|
||||
return;
|
||||
const scrollLeft = this.scrollableContent.scrollLeft;
|
||||
// Use transform for smooth performance
|
||||
this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`;
|
||||
// Debug logging (can be removed later)
|
||||
if (scrollLeft % 100 === 0) { // Only log every 100px to avoid spam
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=ScrollManager.js.map
|
||||
1
wwwroot/js/managers/ScrollManager.js.map
Normal file
1
wwwroot/js/managers/ScrollManager.js.map
Normal file
File diff suppressed because one or more lines are too long
80
wwwroot/js/managers/SimpleEventOverlapManager.d.ts
vendored
Normal file
80
wwwroot/js/managers/SimpleEventOverlapManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* SimpleEventOverlapManager - Clean, focused overlap management
|
||||
* Eliminates complex state tracking in favor of direct DOM manipulation
|
||||
*/
|
||||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
export declare enum OverlapType {
|
||||
NONE = "none",
|
||||
COLUMN_SHARING = "column_sharing",
|
||||
STACKING = "stacking"
|
||||
}
|
||||
export interface OverlapGroup {
|
||||
type: OverlapType;
|
||||
events: CalendarEvent[];
|
||||
position: {
|
||||
top: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
export interface StackLink {
|
||||
prev?: string;
|
||||
next?: string;
|
||||
stackLevel: number;
|
||||
}
|
||||
export declare class SimpleEventOverlapManager {
|
||||
private static readonly STACKING_WIDTH_REDUCTION_PX;
|
||||
/**
|
||||
* Detect overlap type between two DOM elements - pixel-based logic
|
||||
*/
|
||||
resolveOverlapType(element1: HTMLElement, element2: HTMLElement): OverlapType;
|
||||
/**
|
||||
* Group overlapping elements - pixel-based algorithm
|
||||
*/
|
||||
groupOverlappingElements(elements: HTMLElement[]): HTMLElement[][];
|
||||
/**
|
||||
* Create flexbox container for column sharing - clean and simple
|
||||
*/
|
||||
createEventGroup(events: CalendarEvent[], position: {
|
||||
top: number;
|
||||
height: number;
|
||||
}): HTMLElement;
|
||||
/**
|
||||
* Add event to flexbox group - simple relative positioning
|
||||
*/
|
||||
addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void;
|
||||
/**
|
||||
* Create stacked event with data-attribute tracking
|
||||
*/
|
||||
createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement, stackLevel: number): void;
|
||||
/**
|
||||
* Remove stacked styling with proper stack re-linking
|
||||
*/
|
||||
removeStackedStyling(eventElement: HTMLElement): void;
|
||||
/**
|
||||
* Update stack levels for all events following a given event ID
|
||||
*/
|
||||
private updateSubsequentStackLevels;
|
||||
/**
|
||||
* Check if element is stacked - check both style and data-stack-link
|
||||
*/
|
||||
isStackedEvent(element: HTMLElement): boolean;
|
||||
/**
|
||||
* Remove event from group with proper cleanup
|
||||
*/
|
||||
removeFromEventGroup(container: HTMLElement, eventId: string): boolean;
|
||||
/**
|
||||
* Restack events in container - respects separate stack chains
|
||||
*/
|
||||
restackEventsInContainer(container: HTMLElement): void;
|
||||
/**
|
||||
* Utility methods - simple DOM traversal
|
||||
*/
|
||||
getEventGroup(eventElement: HTMLElement): HTMLElement | null;
|
||||
isInEventGroup(element: HTMLElement): boolean;
|
||||
/**
|
||||
* Helper methods for data-attribute based stack tracking
|
||||
*/
|
||||
getStackLink(element: HTMLElement): StackLink | null;
|
||||
private setStackLink;
|
||||
private findElementById;
|
||||
}
|
||||
399
wwwroot/js/managers/SimpleEventOverlapManager.js
Normal file
399
wwwroot/js/managers/SimpleEventOverlapManager.js
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
/**
|
||||
* 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
|
||||
1
wwwroot/js/managers/SimpleEventOverlapManager.js.map
Normal file
1
wwwroot/js/managers/SimpleEventOverlapManager.js.map
Normal file
File diff suppressed because one or more lines are too long
23
wwwroot/js/managers/ViewManager.d.ts
vendored
Normal file
23
wwwroot/js/managers/ViewManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
export declare class ViewManager {
|
||||
private eventBus;
|
||||
private config;
|
||||
private currentView;
|
||||
private buttonListeners;
|
||||
constructor(eventBus: IEventBus, config: Configuration);
|
||||
private setupEventListeners;
|
||||
private setupEventBusListeners;
|
||||
private setupButtonHandlers;
|
||||
private setupButtonGroup;
|
||||
private getViewButtons;
|
||||
private getWorkweekButtons;
|
||||
private initializeView;
|
||||
private changeView;
|
||||
private changeWorkweek;
|
||||
private updateAllButtons;
|
||||
private updateButtonGroup;
|
||||
private emitViewRendered;
|
||||
private refreshCurrentView;
|
||||
private isValidView;
|
||||
}
|
||||
106
wwwroot/js/managers/ViewManager.js
Normal file
106
wwwroot/js/managers/ViewManager.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { ConfigManager } from '../configurations/ConfigManager';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
export class ViewManager {
|
||||
constructor(eventBus, config) {
|
||||
this.currentView = 'week';
|
||||
this.buttonListeners = new Map();
|
||||
this.eventBus = eventBus;
|
||||
this.config = config;
|
||||
this.setupEventListeners();
|
||||
}
|
||||
setupEventListeners() {
|
||||
this.setupEventBusListeners();
|
||||
this.setupButtonHandlers();
|
||||
}
|
||||
setupEventBusListeners() {
|
||||
this.eventBus.on(CoreEvents.INITIALIZED, () => {
|
||||
this.initializeView();
|
||||
});
|
||||
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
|
||||
this.refreshCurrentView();
|
||||
});
|
||||
}
|
||||
setupButtonHandlers() {
|
||||
this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => {
|
||||
if (this.isValidView(value)) {
|
||||
this.changeView(value);
|
||||
}
|
||||
});
|
||||
this.setupButtonGroup('swp-preset-button[data-workweek]', 'data-workweek', (value) => {
|
||||
this.changeWorkweek(value);
|
||||
});
|
||||
}
|
||||
setupButtonGroup(selector, attribute, handler) {
|
||||
const buttons = document.querySelectorAll(selector);
|
||||
buttons.forEach(button => {
|
||||
const clickHandler = (event) => {
|
||||
event.preventDefault();
|
||||
const value = button.getAttribute(attribute);
|
||||
if (value) {
|
||||
handler(value);
|
||||
}
|
||||
};
|
||||
button.addEventListener('click', clickHandler);
|
||||
this.buttonListeners.set(button, clickHandler);
|
||||
});
|
||||
}
|
||||
getViewButtons() {
|
||||
return document.querySelectorAll('swp-view-button[data-view]');
|
||||
}
|
||||
getWorkweekButtons() {
|
||||
return document.querySelectorAll('swp-preset-button[data-workweek]');
|
||||
}
|
||||
initializeView() {
|
||||
this.updateAllButtons();
|
||||
this.emitViewRendered();
|
||||
}
|
||||
changeView(newView) {
|
||||
if (newView === this.currentView)
|
||||
return;
|
||||
const previousView = this.currentView;
|
||||
this.currentView = newView;
|
||||
this.updateAllButtons();
|
||||
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
|
||||
previousView,
|
||||
currentView: newView
|
||||
});
|
||||
}
|
||||
changeWorkweek(workweekId) {
|
||||
this.config.setWorkWeek(workweekId);
|
||||
// Update all CSS properties to match new configuration
|
||||
ConfigManager.updateCSSProperties(this.config);
|
||||
this.updateAllButtons();
|
||||
const settings = this.config.getWorkWeekSettings();
|
||||
this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, {
|
||||
workWeekId: workweekId,
|
||||
settings: settings
|
||||
});
|
||||
}
|
||||
updateAllButtons() {
|
||||
this.updateButtonGroup(this.getViewButtons(), 'data-view', this.currentView);
|
||||
this.updateButtonGroup(this.getWorkweekButtons(), 'data-workweek', this.config.currentWorkWeek);
|
||||
}
|
||||
updateButtonGroup(buttons, attribute, activeValue) {
|
||||
buttons.forEach(button => {
|
||||
const buttonValue = button.getAttribute(attribute);
|
||||
if (buttonValue === activeValue) {
|
||||
button.setAttribute('data-active', 'true');
|
||||
}
|
||||
else {
|
||||
button.removeAttribute('data-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
emitViewRendered() {
|
||||
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
|
||||
view: this.currentView
|
||||
});
|
||||
}
|
||||
refreshCurrentView() {
|
||||
this.emitViewRendered();
|
||||
}
|
||||
isValidView(view) {
|
||||
return ['day', 'week', 'month'].includes(view);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=ViewManager.js.map
|
||||
1
wwwroot/js/managers/ViewManager.js.map
Normal file
1
wwwroot/js/managers/ViewManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"ViewManager.js","sourceRoot":"","sources":["../../../src/managers/ViewManager.ts"],"names":[],"mappings":"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"}
|
||||
70
wwwroot/js/managers/ViewSelectorManager.d.ts
vendored
Normal file
70
wwwroot/js/managers/ViewSelectorManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
/**
|
||||
* ViewSelectorManager - Manages view selector UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-view-selector> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Handles button clicks on swp-view-button elements
|
||||
* - Manages current view state (day/week/month)
|
||||
* - Validates view values
|
||||
* - Emits VIEW_CHANGED and VIEW_RENDERED events
|
||||
* - Updates button UI states (data-active attributes)
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → changeView() → validate → update state → emit event → update UI
|
||||
*
|
||||
* IMPLEMENTATION STATUS:
|
||||
* ======================
|
||||
* - Week view: FULLY IMPLEMENTED
|
||||
* - Day view: NOT IMPLEMENTED (button exists but no rendering)
|
||||
* - Month view: NOT IMPLEMENTED (button exists but no rendering)
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - GridRenderer: Uses view parameter (currently only supports 'week')
|
||||
* - Future: DayRenderer, MonthRenderer when implemented
|
||||
*/
|
||||
export declare class ViewSelectorManager {
|
||||
private eventBus;
|
||||
private config;
|
||||
private buttonListeners;
|
||||
constructor(eventBus: IEventBus, config: Configuration);
|
||||
/**
|
||||
* Setup click listeners on all view selector buttons
|
||||
*/
|
||||
private setupButtonListeners;
|
||||
/**
|
||||
* Setup event bus listeners
|
||||
*/
|
||||
private setupEventListeners;
|
||||
/**
|
||||
* Change the active view
|
||||
*/
|
||||
private changeView;
|
||||
/**
|
||||
* Update button states (data-active attributes)
|
||||
*/
|
||||
private updateButtonStates;
|
||||
/**
|
||||
* Initialize view on INITIALIZED event
|
||||
*/
|
||||
private initializeView;
|
||||
/**
|
||||
* Emit VIEW_RENDERED event
|
||||
*/
|
||||
private emitViewRendered;
|
||||
/**
|
||||
* Refresh current view on DATE_CHANGED event
|
||||
*/
|
||||
private refreshCurrentView;
|
||||
/**
|
||||
* Validate if string is a valid CalendarView type
|
||||
*/
|
||||
private isValidView;
|
||||
}
|
||||
130
wwwroot/js/managers/ViewSelectorManager.js
Normal file
130
wwwroot/js/managers/ViewSelectorManager.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
/**
|
||||
* ViewSelectorManager - Manages view selector UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-view-selector> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Handles button clicks on swp-view-button elements
|
||||
* - Manages current view state (day/week/month)
|
||||
* - Validates view values
|
||||
* - Emits VIEW_CHANGED and VIEW_RENDERED events
|
||||
* - Updates button UI states (data-active attributes)
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → changeView() → validate → update state → emit event → update UI
|
||||
*
|
||||
* IMPLEMENTATION STATUS:
|
||||
* ======================
|
||||
* - Week view: FULLY IMPLEMENTED
|
||||
* - Day view: NOT IMPLEMENTED (button exists but no rendering)
|
||||
* - Month view: NOT IMPLEMENTED (button exists but no rendering)
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - GridRenderer: Uses view parameter (currently only supports 'week')
|
||||
* - Future: DayRenderer, MonthRenderer when implemented
|
||||
*/
|
||||
export class ViewSelectorManager {
|
||||
constructor(eventBus, config) {
|
||||
this.buttonListeners = new Map();
|
||||
this.eventBus = eventBus;
|
||||
this.config = config;
|
||||
this.setupButtonListeners();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
/**
|
||||
* Setup click listeners on all view selector buttons
|
||||
*/
|
||||
setupButtonListeners() {
|
||||
const buttons = document.querySelectorAll('swp-view-button[data-view]');
|
||||
buttons.forEach(button => {
|
||||
const clickHandler = (event) => {
|
||||
event.preventDefault();
|
||||
const view = button.getAttribute('data-view');
|
||||
if (view && this.isValidView(view)) {
|
||||
this.changeView(view);
|
||||
}
|
||||
};
|
||||
button.addEventListener('click', clickHandler);
|
||||
this.buttonListeners.set(button, clickHandler);
|
||||
});
|
||||
// Initialize button states
|
||||
this.updateButtonStates();
|
||||
}
|
||||
/**
|
||||
* Setup event bus listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
this.eventBus.on(CoreEvents.INITIALIZED, () => {
|
||||
this.initializeView();
|
||||
});
|
||||
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
|
||||
this.refreshCurrentView();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Change the active view
|
||||
*/
|
||||
changeView(newView) {
|
||||
if (newView === this.config.currentView) {
|
||||
return; // No change
|
||||
}
|
||||
const previousView = this.config.currentView;
|
||||
this.config.currentView = newView;
|
||||
// Update button UI states
|
||||
this.updateButtonStates();
|
||||
// Emit event for subscribers
|
||||
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
|
||||
previousView,
|
||||
currentView: newView
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update button states (data-active attributes)
|
||||
*/
|
||||
updateButtonStates() {
|
||||
const buttons = document.querySelectorAll('swp-view-button[data-view]');
|
||||
buttons.forEach(button => {
|
||||
const buttonView = button.getAttribute('data-view');
|
||||
if (buttonView === this.config.currentView) {
|
||||
button.setAttribute('data-active', 'true');
|
||||
}
|
||||
else {
|
||||
button.removeAttribute('data-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Initialize view on INITIALIZED event
|
||||
*/
|
||||
initializeView() {
|
||||
this.updateButtonStates();
|
||||
this.emitViewRendered();
|
||||
}
|
||||
/**
|
||||
* Emit VIEW_RENDERED event
|
||||
*/
|
||||
emitViewRendered() {
|
||||
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
|
||||
view: this.config.currentView
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Refresh current view on DATE_CHANGED event
|
||||
*/
|
||||
refreshCurrentView() {
|
||||
this.emitViewRendered();
|
||||
}
|
||||
/**
|
||||
* Validate if string is a valid CalendarView type
|
||||
*/
|
||||
isValidView(view) {
|
||||
return ['day', 'week', 'month'].includes(view);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=ViewSelectorManager.js.map
|
||||
1
wwwroot/js/managers/ViewSelectorManager.js.map
Normal file
1
wwwroot/js/managers/ViewSelectorManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"ViewSelectorManager.js","sourceRoot":"","sources":["../../../src/managers/ViewSelectorManager.ts"],"names":[],"mappings":"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"}
|
||||
71
wwwroot/js/managers/WorkHoursManager.d.ts
vendored
Normal file
71
wwwroot/js/managers/WorkHoursManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { DateService } from '../utils/DateService';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
/**
|
||||
* Work hours for a specific day
|
||||
*/
|
||||
export interface IDayWorkHours {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
/**
|
||||
* Work schedule configuration
|
||||
*/
|
||||
export interface IWorkScheduleConfig {
|
||||
weeklyDefault: {
|
||||
monday: IDayWorkHours | 'off';
|
||||
tuesday: IDayWorkHours | 'off';
|
||||
wednesday: IDayWorkHours | 'off';
|
||||
thursday: IDayWorkHours | 'off';
|
||||
friday: IDayWorkHours | 'off';
|
||||
saturday: IDayWorkHours | 'off';
|
||||
sunday: IDayWorkHours | 'off';
|
||||
};
|
||||
dateOverrides: {
|
||||
[dateString: string]: IDayWorkHours | 'off';
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Manages work hours scheduling with weekly defaults and date-specific overrides
|
||||
*/
|
||||
export declare class WorkHoursManager {
|
||||
private dateService;
|
||||
private config;
|
||||
private positionUtils;
|
||||
private workSchedule;
|
||||
constructor(dateService: DateService, config: Configuration, positionUtils: PositionUtils);
|
||||
/**
|
||||
* Get work hours for a specific date
|
||||
*/
|
||||
getWorkHoursForDate(date: Date): IDayWorkHours | 'off';
|
||||
/**
|
||||
* Get work hours for multiple dates (used by GridManager)
|
||||
*/
|
||||
getWorkHoursForDateRange(dates: Date[]): Map<string, IDayWorkHours | 'off'>;
|
||||
/**
|
||||
* Calculate CSS custom properties for non-work hour overlays using PositionUtils
|
||||
*/
|
||||
calculateNonWorkHoursStyle(workHours: IDayWorkHours | 'off'): {
|
||||
beforeWorkHeight: number;
|
||||
afterWorkTop: number;
|
||||
} | null;
|
||||
/**
|
||||
* Calculate CSS custom properties for work hours overlay using PositionUtils
|
||||
*/
|
||||
calculateWorkHoursStyle(workHours: IDayWorkHours | 'off'): {
|
||||
top: number;
|
||||
height: number;
|
||||
} | null;
|
||||
/**
|
||||
* Load work schedule from JSON (future implementation)
|
||||
*/
|
||||
loadWorkSchedule(jsonData: IWorkScheduleConfig): Promise<void>;
|
||||
/**
|
||||
* Get current work schedule configuration
|
||||
*/
|
||||
getWorkSchedule(): IWorkScheduleConfig;
|
||||
/**
|
||||
* Convert Date to day name key
|
||||
*/
|
||||
private getDayName;
|
||||
}
|
||||
108
wwwroot/js/managers/WorkHoursManager.js
Normal file
108
wwwroot/js/managers/WorkHoursManager.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Work hours management for per-column scheduling
|
||||
/**
|
||||
* Manages work hours scheduling with weekly defaults and date-specific overrides
|
||||
*/
|
||||
export class WorkHoursManager {
|
||||
constructor(dateService, config, positionUtils) {
|
||||
this.dateService = dateService;
|
||||
this.config = config;
|
||||
this.positionUtils = positionUtils;
|
||||
// Default work schedule - will be loaded from JSON later
|
||||
this.workSchedule = {
|
||||
weeklyDefault: {
|
||||
monday: { start: 9, end: 17 },
|
||||
tuesday: { start: 9, end: 17 },
|
||||
wednesday: { start: 9, end: 17 },
|
||||
thursday: { start: 9, end: 17 },
|
||||
friday: { start: 9, end: 15 },
|
||||
saturday: 'off',
|
||||
sunday: 'off'
|
||||
},
|
||||
dateOverrides: {
|
||||
'2025-01-20': { start: 10, end: 16 },
|
||||
'2025-01-21': { start: 8, end: 14 },
|
||||
'2025-01-22': 'off'
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get work hours for a specific date
|
||||
*/
|
||||
getWorkHoursForDate(date) {
|
||||
const dateString = this.dateService.formatISODate(date);
|
||||
// Check for date-specific override first
|
||||
if (this.workSchedule.dateOverrides[dateString]) {
|
||||
return this.workSchedule.dateOverrides[dateString];
|
||||
}
|
||||
// Fall back to weekly default
|
||||
const dayName = this.getDayName(date);
|
||||
return this.workSchedule.weeklyDefault[dayName];
|
||||
}
|
||||
/**
|
||||
* Get work hours for multiple dates (used by GridManager)
|
||||
*/
|
||||
getWorkHoursForDateRange(dates) {
|
||||
const workHoursMap = new Map();
|
||||
dates.forEach(date => {
|
||||
const dateString = this.dateService.formatISODate(date);
|
||||
const workHours = this.getWorkHoursForDate(date);
|
||||
workHoursMap.set(dateString, workHours);
|
||||
});
|
||||
return workHoursMap;
|
||||
}
|
||||
/**
|
||||
* Calculate CSS custom properties for non-work hour overlays using PositionUtils
|
||||
*/
|
||||
calculateNonWorkHoursStyle(workHours) {
|
||||
if (workHours === 'off') {
|
||||
return null; // Full day will be colored via CSS background
|
||||
}
|
||||
const gridSettings = this.config.gridSettings;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
// Before work: from day start to work start
|
||||
const beforeWorkHeight = (workHours.start - dayStartHour) * hourHeight;
|
||||
// After work: from work end to day end
|
||||
const afterWorkTop = (workHours.end - dayStartHour) * hourHeight;
|
||||
return {
|
||||
beforeWorkHeight: Math.max(0, beforeWorkHeight),
|
||||
afterWorkTop: Math.max(0, afterWorkTop)
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate CSS custom properties for work hours overlay using PositionUtils
|
||||
*/
|
||||
calculateWorkHoursStyle(workHours) {
|
||||
if (workHours === 'off') {
|
||||
return null;
|
||||
}
|
||||
// Create dummy time strings for start and end of work hours
|
||||
const startTime = `${workHours.start.toString().padStart(2, '0')}:00`;
|
||||
const endTime = `${workHours.end.toString().padStart(2, '0')}:00`;
|
||||
// Use PositionUtils for consistent position calculation
|
||||
const position = this.positionUtils.calculateEventPosition(startTime, endTime);
|
||||
return { top: position.top, height: position.height };
|
||||
}
|
||||
/**
|
||||
* Load work schedule from JSON (future implementation)
|
||||
*/
|
||||
async loadWorkSchedule(jsonData) {
|
||||
this.workSchedule = jsonData;
|
||||
}
|
||||
/**
|
||||
* Get current work schedule configuration
|
||||
*/
|
||||
getWorkSchedule() {
|
||||
return this.workSchedule;
|
||||
}
|
||||
/**
|
||||
* Convert Date to day name key
|
||||
*/
|
||||
getDayName(date) {
|
||||
const dayNames = [
|
||||
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'
|
||||
];
|
||||
return dayNames[date.getDay()];
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=WorkHoursManager.js.map
|
||||
1
wwwroot/js/managers/WorkHoursManager.js.map
Normal file
1
wwwroot/js/managers/WorkHoursManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"WorkHoursManager.js","sourceRoot":"","sources":["../../../src/managers/WorkHoursManager.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAgClD;;GAEG;AACH,MAAM,OAAO,gBAAgB;IAM3B,YAAY,WAAwB,EAAE,MAAqB,EAAE,aAA4B;QACvF,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QAEnC,yDAAyD;QACzD,IAAI,CAAC,YAAY,GAAG;YAClB,aAAa,EAAE;gBACb,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAC7B,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAC9B,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAChC,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAC/B,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBAC7B,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,KAAK;aACd;YACD,aAAa,EAAE;gBACb,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;gBACpC,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE;gBACnC,YAAY,EAAE,KAAK;aACpB;SACF,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,mBAAmB,CAAC,IAAU;QAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAExD,yCAAyC;QACzC,IAAI,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QACrD,CAAC;QAED,8BAA8B;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAClD,CAAC;IAED;;OAEG;IACH,wBAAwB,CAAC,KAAa;QACpC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAiC,CAAC;QAE9D,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACnB,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YACxD,MAAM,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;YACjD,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;OAEG;IACH,0BAA0B,CAAC,SAAgC;QACzD,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,CAAC,8CAA8C;QAC7D,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;QAC9C,MAAM,YAAY,GAAG,YAAY,CAAC,YAAY,CAAC;QAC/C,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC;QAE3C,4CAA4C;QAC5C,MAAM,gBAAgB,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,UAAU,CAAC;QAEvE,uCAAuC;QACvC,MAAM,YAAY,GAAG,CAAC,SAAS,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,UAAU,CAAC;QAEjE,OAAO;YACL,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,CAAC;YAC/C,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC;SACxC,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,uBAAuB,CAAC,SAAgC;QACtD,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,4DAA4D;QAC5D,MAAM,SAAS,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC;QACtE,MAAM,OAAO,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC;QAElE,wDAAwD;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,sBAAsB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAE/E,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACxD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,gBAAgB,CAAC,QAA6B;QAClD,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAU;QAC3B,MAAM,QAAQ,GAAmD;YAC/D,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU;SAC7E,CAAC;QACF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACjC,CAAC;CACF"}
|
||||
47
wwwroot/js/managers/WorkweekPresetsManager.d.ts
vendored
Normal file
47
wwwroot/js/managers/WorkweekPresetsManager.d.ts
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { Configuration } from '../configurations/CalendarConfig';
|
||||
/**
|
||||
* WorkweekPresetsManager - Manages workweek preset UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-workweek-presets> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Owns WORK_WEEK_PRESETS data
|
||||
* - Handles button clicks on swp-preset-button elements
|
||||
* - Manages current workweek preset state
|
||||
* - Validates preset IDs
|
||||
* - Emits WORKWEEK_CHANGED events
|
||||
* - Updates button UI states (data-active attributes)
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → changePreset() → validate → update state → emit event → update UI
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - ConfigManager: Updates CSS variables (--grid-columns)
|
||||
* - GridManager: Re-renders grid with new column count
|
||||
* - CalendarManager: Relays to header update (via workweek:header-update)
|
||||
* - HeaderManager: Updates date headers
|
||||
*/
|
||||
export declare class WorkweekPresetsManager {
|
||||
private eventBus;
|
||||
private config;
|
||||
private buttonListeners;
|
||||
constructor(eventBus: IEventBus, config: Configuration);
|
||||
/**
|
||||
* Setup click listeners on all workweek preset buttons
|
||||
*/
|
||||
private setupButtonListeners;
|
||||
/**
|
||||
* Change the active workweek preset
|
||||
*/
|
||||
private changePreset;
|
||||
/**
|
||||
* Update button states (data-active attributes)
|
||||
*/
|
||||
private updateButtonStates;
|
||||
}
|
||||
95
wwwroot/js/managers/WorkweekPresetsManager.js
Normal file
95
wwwroot/js/managers/WorkweekPresetsManager.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { WORK_WEEK_PRESETS } from '../configurations/CalendarConfig';
|
||||
/**
|
||||
* WorkweekPresetsManager - Manages workweek preset UI and state
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* ===============
|
||||
* This manager owns all logic related to the <swp-workweek-presets> UI element.
|
||||
* It follows the principle that each functional UI element has its own manager.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Owns WORK_WEEK_PRESETS data
|
||||
* - Handles button clicks on swp-preset-button elements
|
||||
* - Manages current workweek preset state
|
||||
* - Validates preset IDs
|
||||
* - Emits WORKWEEK_CHANGED events
|
||||
* - Updates button UI states (data-active attributes)
|
||||
*
|
||||
* EVENT FLOW:
|
||||
* ===========
|
||||
* User clicks button → changePreset() → validate → update state → emit event → update UI
|
||||
*
|
||||
* SUBSCRIBERS:
|
||||
* ============
|
||||
* - ConfigManager: Updates CSS variables (--grid-columns)
|
||||
* - GridManager: Re-renders grid with new column count
|
||||
* - CalendarManager: Relays to header update (via workweek:header-update)
|
||||
* - HeaderManager: Updates date headers
|
||||
*/
|
||||
export class WorkweekPresetsManager {
|
||||
constructor(eventBus, config) {
|
||||
this.buttonListeners = new Map();
|
||||
this.eventBus = eventBus;
|
||||
this.config = config;
|
||||
this.setupButtonListeners();
|
||||
}
|
||||
/**
|
||||
* Setup click listeners on all workweek preset buttons
|
||||
*/
|
||||
setupButtonListeners() {
|
||||
const buttons = document.querySelectorAll('swp-preset-button[data-workweek]');
|
||||
buttons.forEach(button => {
|
||||
const clickHandler = (event) => {
|
||||
event.preventDefault();
|
||||
const presetId = button.getAttribute('data-workweek');
|
||||
if (presetId) {
|
||||
this.changePreset(presetId);
|
||||
}
|
||||
};
|
||||
button.addEventListener('click', clickHandler);
|
||||
this.buttonListeners.set(button, clickHandler);
|
||||
});
|
||||
// Initialize button states
|
||||
this.updateButtonStates();
|
||||
}
|
||||
/**
|
||||
* Change the active workweek preset
|
||||
*/
|
||||
changePreset(presetId) {
|
||||
if (!WORK_WEEK_PRESETS[presetId]) {
|
||||
console.warn(`Invalid preset ID "${presetId}"`);
|
||||
return;
|
||||
}
|
||||
if (presetId === this.config.currentWorkWeek) {
|
||||
return; // No change
|
||||
}
|
||||
const previousPresetId = this.config.currentWorkWeek;
|
||||
this.config.currentWorkWeek = presetId;
|
||||
const settings = WORK_WEEK_PRESETS[presetId];
|
||||
// Update button UI states
|
||||
this.updateButtonStates();
|
||||
// Emit event for subscribers
|
||||
this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, {
|
||||
workWeekId: presetId,
|
||||
previousWorkWeekId: previousPresetId,
|
||||
settings: settings
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update button states (data-active attributes)
|
||||
*/
|
||||
updateButtonStates() {
|
||||
const buttons = document.querySelectorAll('swp-preset-button[data-workweek]');
|
||||
buttons.forEach(button => {
|
||||
const buttonPresetId = button.getAttribute('data-workweek');
|
||||
if (buttonPresetId === this.config.currentWorkWeek) {
|
||||
button.setAttribute('data-active', 'true');
|
||||
}
|
||||
else {
|
||||
button.removeAttribute('data-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=WorkweekPresetsManager.js.map
|
||||
1
wwwroot/js/managers/WorkweekPresetsManager.js.map
Normal file
1
wwwroot/js/managers/WorkweekPresetsManager.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"WorkweekPresetsManager.js","sourceRoot":"","sources":["../../../src/managers/WorkweekPresetsManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,iBAAiB,EAAiB,MAAM,kCAAkC,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,OAAO,sBAAsB;IAKjC,YAAY,QAAmB,EAAE,MAAqB;QAF9C,oBAAe,GAAgC,IAAI,GAAG,EAAE,CAAC;QAG/D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,IAAI,CAAC,oBAAoB,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACK,oBAAoB;QAC1B,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,kCAAkC,CAAC,CAAC;QAE9E,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YACvB,MAAM,YAAY,GAAG,CAAC,KAAY,EAAE,EAAE;gBACpC,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;gBACtD,IAAI,QAAQ,EAAE,CAAC;oBACb,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;gBAC9B,CAAC;YACH,CAAC,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YAC/C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,2BAA2B;QAC3B,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAED;;OAEG;IACK,YAAY,CAAC,QAAgB;QACnC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,sBAAsB,QAAQ,GAAG,CAAC,CAAC;YAChD,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,KAAK,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC7C,OAAO,CAAC,YAAY;QACtB,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,eAAe,GAAG,QAAQ,CAAC;QAEvC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAE7C,0BAA0B;QAC1B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAE1B,6BAA6B;QAC7B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE;YAC9C,UAAU,EAAE,QAAQ;YACpB,kBAAkB,EAAE,gBAAgB;YACpC,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,kBAAkB;QACxB,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,kCAAkC,CAAC,CAAC;QAE9E,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;YACvB,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;YAE5D,IAAI,cAAc,KAAK,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;gBACnD,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;YAC7C,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;YACxC,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CAEF"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue