Refactors calendar managers to components

Reorganizes navigation, view selector, and workweek preset managers into separate component files

Improves code organization by moving specialized manager classes to a more descriptive components directory

Adds enhanced logging for all-day event management and drag-and-drop interactions
This commit is contained in:
Janus C. H. Knudsen 2025-11-08 14:30:18 +01:00
parent bd8f5ae6c6
commit b566aafb19
5 changed files with 73 additions and 15 deletions

View file

@ -9,6 +9,7 @@ import { ICalendarEvent } from '../types/CalendarTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
import {
IDragMouseEnterHeaderEventPayload,
IDragMouseEnterColumnEventPayload,
IDragStartEventPayload,
IDragMoveEventPayload,
IDragEndEventPayload,
@ -107,12 +108,45 @@ export class AllDayManager {
});
eventBus.on('drag:end', (event) => {
let draggedElement: IDragEndEventPayload = (event as CustomEvent<IDragEndEventPayload>).detail;
let dragEndPayload: IDragEndEventPayload = (event as CustomEvent<IDragEndEventPayload>).detail;
if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
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') {
console.log('✅ AllDayManager: Handling all-day → all-day drop');
this.handleDragEnd(dragEndPayload);
return;
}
this.handleDragEnd(draggedElement);
// 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,
currentLayoutsCount: this.currentLayouts.length,
layoutsBeforeFilter: this.currentLayouts.map(l => l.calenderEvent.id)
});
// Remove event from currentLayouts since it's now a timed event
this.currentLayouts = this.currentLayouts.filter(
layout => layout.calenderEvent.id !== eventId
);
console.log('📊 AllDayManager: After filter', {
currentLayoutsCount: this.currentLayouts.length,
layoutsAfterFilter: this.currentLayouts.map(l => l.calenderEvent.id)
});
// Recalculate and animate header height
this.checkAndAnimateAllDayHeight();
}
});
// Listen for drag cancellation to recalculate height
@ -189,6 +223,15 @@ export class AllDayManager {
* Check current all-day events and animate to correct height
*/
public checkAndAnimateAllDayHeight(): void {
console.log('📏 AllDayManager: checkAndAnimateAllDayHeight called', {
currentLayoutsCount: this.currentLayouts.length,
layouts: this.currentLayouts.map(l => ({
id: l.calenderEvent.id,
row: l.row,
title: l.calenderEvent.title
}))
});
// Calculate required rows - 0 if no events (will collapse)
let maxRows = 0;
@ -205,6 +248,12 @@ export class AllDayManager {
}
console.log('📊 AllDayManager: Height calculation', {
maxRows,
currentLayoutsLength: this.currentLayouts.length,
isExpanded: this.isExpanded
});
// Store actual row count
this.actualRowCount = maxRows;
@ -233,6 +282,12 @@ export class AllDayManager {
this.clearOverflowIndicators();
}
console.log('🎬 AllDayManager: Will animate to', {
displayRows,
maxRows,
willAnimate: displayRows !== this.actualRowCount
});
// Animate to required rows (0 = collapse, >0 = expand)
this.animateToRows(displayRows);
}
@ -339,6 +394,9 @@ export class AllDayManager {
ColumnDetectionUtils.updateColumnBoundsCache();
// Recalculate height after adding all-day event
this.checkAndAnimateAllDayHeight();
}

View file

@ -1,71 +0,0 @@
import { IEventBus } from '../types/CalendarTypes';
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 {
private eventBus: IEventBus;
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
this.setupButtonListeners();
}
/**
* Setup click listeners on all navigation buttons
*/
private setupButtonListeners(): void {
const buttons = document.querySelectorAll('swp-nav-button[data-action]');
buttons.forEach(button => {
const clickHandler = (event: 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
*/
private handleNavigation(action: string): void {
// Emit navigation button clicked event
this.eventBus.emit(CoreEvents.NAV_BUTTON_CLICKED, {
action: action
});
}
/**
* Validate if string is a valid navigation action
*/
private isValidAction(action: string): boolean {
return ['prev', 'next', 'today'].includes(action);
}
}

View file

@ -1,152 +0,0 @@
import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
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 class ViewSelectorManager {
private eventBus: IEventBus;
private config: Configuration;
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus, config: Configuration) {
this.eventBus = eventBus;
this.config = config;
this.setupButtonListeners();
this.setupEventListeners();
}
/**
* Setup click listeners on all view selector buttons
*/
private setupButtonListeners(): void {
const buttons = document.querySelectorAll('swp-view-button[data-view]');
buttons.forEach(button => {
const clickHandler = (event: Event) => {
event.preventDefault();
const view = button.getAttribute('data-view');
if (view && this.isValidView(view)) {
this.changeView(view as CalendarView);
}
};
button.addEventListener('click', clickHandler);
this.buttonListeners.set(button, clickHandler);
});
// Initialize button states
this.updateButtonStates();
}
/**
* Setup event bus listeners
*/
private setupEventListeners(): void {
this.eventBus.on(CoreEvents.INITIALIZED, () => {
this.initializeView();
});
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
this.refreshCurrentView();
});
}
/**
* Change the active view
*/
private changeView(newView: CalendarView): void {
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)
*/
private updateButtonStates(): void {
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
*/
private initializeView(): void {
this.updateButtonStates();
this.emitViewRendered();
}
/**
* Emit VIEW_RENDERED event
*/
private emitViewRendered(): void {
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
view: this.config.currentView
});
}
/**
* Refresh current view on DATE_CHANGED event
*/
private refreshCurrentView(): void {
this.emitViewRendered();
}
/**
* Validate if string is a valid CalendarView type
*/
private isValidView(view: string): view is CalendarView {
return ['day', 'week', 'month'].includes(view);
}
}

View file

@ -1,114 +0,0 @@
import { IEventBus } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { IWorkWeekSettings } from '../configurations/WorkWeekSettings';
import { WORK_WEEK_PRESETS, 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 class WorkweekPresetsManager {
private eventBus: IEventBus;
private config: Configuration;
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus, config: Configuration) {
this.eventBus = eventBus;
this.config = config;
this.setupButtonListeners();
}
/**
* Setup click listeners on all workweek preset buttons
*/
private setupButtonListeners(): void {
const buttons = document.querySelectorAll('swp-preset-button[data-workweek]');
buttons.forEach(button => {
const clickHandler = (event: 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
*/
private changePreset(presetId: string): void {
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)
*/
private updateButtonStates(): void {
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');
}
});
}
}