Adds I-prefix to all interfaces

This commit is contained in:
Janus C. H. Knudsen 2025-11-03 21:30:50 +01:00
parent 80aaab46f2
commit 8ec5f52872
44 changed files with 1731 additions and 1949 deletions

View file

@ -1,21 +1,21 @@
// All-day row height management and animations
import { eventBus } from '../core/EventBus';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { ALL_DAY_CONSTANTS } from '../configuration/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { CalendarEvent } from '../types/CalendarTypes';
import { AllDayLayoutEngine, IEventLayout } from '../utils/AllDayLayoutEngine';
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { ICalendarEvent } from '../types/CalendarTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
import {
DragMouseEnterHeaderEventPayload,
DragStartEventPayload,
DragMoveEventPayload,
DragEndEventPayload,
DragColumnChangeEventPayload,
HeaderReadyEventPayload
IDragMouseEnterHeaderEventPayload,
IDragStartEventPayload,
IDragMoveEventPayload,
IDragEndEventPayload,
IDragColumnChangeEventPayload,
IHeaderReadyEventPayload
} from '../types/EventTypes';
import { DragOffset, MousePosition } from '../types/DragDropTypes';
import { IDragOffset, IMousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from './EventManager';
import { differenceInCalendarDays } from 'date-fns';
@ -33,10 +33,10 @@ export class AllDayManager {
private layoutEngine: AllDayLayoutEngine | null = null;
// State tracking for differential updates
private currentLayouts: EventLayout[] = [];
private currentAllDayEvents: CalendarEvent[] = [];
private currentWeekDates: ColumnBounds[] = [];
private newLayouts: EventLayout[] = [];
private currentLayouts: IEventLayout[] = [];
private currentAllDayEvents: ICalendarEvent[] = [];
private currentWeekDates: IColumnBounds[] = [];
private newLayouts: IEventLayout[] = [];
// Expand/collapse state
private isExpanded: boolean = false;
@ -62,7 +62,7 @@ export class AllDayManager {
*/
private setupEventListeners(): void {
eventBus.on('drag:mouseenter-header', (event) => {
const payload = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
const payload = (event as CustomEvent<IDragMouseEnterHeaderEventPayload>).detail;
if (payload.draggedClone.hasAttribute('data-allday'))
return;
@ -87,7 +87,7 @@ export class AllDayManager {
// Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => {
let payload: DragStartEventPayload = (event as CustomEvent<DragStartEventPayload>).detail;
let payload: IDragStartEventPayload = (event as CustomEvent<IDragStartEventPayload>).detail;
if (!payload.draggedClone?.hasAttribute('data-allday')) {
return;
@ -97,7 +97,7 @@ export class AllDayManager {
});
eventBus.on('drag:column-change', (event) => {
let payload: DragColumnChangeEventPayload = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
let payload: IDragColumnChangeEventPayload = (event as CustomEvent<IDragColumnChangeEventPayload>).detail;
if (!payload.draggedClone?.hasAttribute('data-allday')) {
return;
@ -107,7 +107,7 @@ export class AllDayManager {
});
eventBus.on('drag:end', (event) => {
let draggedElement: DragEndEventPayload = (event as CustomEvent<DragEndEventPayload>).detail;
let draggedElement: IDragEndEventPayload = (event as CustomEvent<IDragEndEventPayload>).detail;
if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
return;
@ -128,12 +128,12 @@ export class AllDayManager {
// Listen for header ready - when dates are populated with period data
eventBus.on('header:ready', (event: Event) => {
let headerReadyEventPayload = (event as CustomEvent<HeaderReadyEventPayload>).detail;
let headerReadyEventPayload = (event as CustomEvent<IHeaderReadyEventPayload>).detail;
let startDate = new Date(headerReadyEventPayload.headerElements.at(0)!.date);
let endDate = new Date(headerReadyEventPayload.headerElements.at(-1)!.date);
let events: CalendarEvent[] = this.eventManager.getEventsForPeriod(startDate, endDate);
let events: ICalendarEvent[] = this.eventManager.getEventsForPeriod(startDate, endDate);
// Filter for all-day events
const allDayEvents = events.filter(event => event.allDay);
@ -302,7 +302,7 @@ export class AllDayManager {
* Calculate layout for ALL all-day events using AllDayLayoutEngine
* This is the correct method that processes all events together for proper overlap detection
*/
private calculateAllDayEventsLayout(events: CalendarEvent[], dayHeaders: ColumnBounds[]): EventLayout[] {
private calculateAllDayEventsLayout(events: ICalendarEvent[], dayHeaders: IColumnBounds[]): IEventLayout[] {
// Store current state
this.currentAllDayEvents = events;
@ -316,12 +316,12 @@ export class AllDayManager {
}
private handleConvertToAllDay(payload: DragMouseEnterHeaderEventPayload): void {
private handleConvertToAllDay(payload: IDragMouseEnterHeaderEventPayload): void {
let allDayContainer = this.getAllDayContainer();
if (!allDayContainer) return;
// Create SwpAllDayEventElement from CalendarEvent
// Create SwpAllDayEventElement from ICalendarEvent
const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent);
// Apply grid positioning
@ -345,7 +345,7 @@ export class AllDayManager {
/**
* Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER
*/
private handleColumnChange(dragColumnChangeEventPayload: DragColumnChangeEventPayload): void {
private handleColumnChange(dragColumnChangeEventPayload: IDragColumnChangeEventPayload): void {
let allDayContainer = this.getAllDayContainer();
if (!allDayContainer) return;
@ -380,7 +380,7 @@ export class AllDayManager {
}
private handleDragEnd(dragEndEvent: DragEndEventPayload): void {
private handleDragEnd(dragEndEvent: IDragEndEventPayload): void {
const getEventDurationDays = (start: string | undefined, end: string | undefined): number => {
@ -433,7 +433,7 @@ export class AllDayManager {
dragEndEvent.draggedClone.dataset.start = this.dateService.toUTC(newStartDate);
dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate);
const droppedEvent: CalendarEvent = {
const droppedEvent: ICalendarEvent = {
id: eventId,
title: dragEndEvent.draggedClone.dataset.title || '',
start: newStartDate,
@ -557,9 +557,9 @@ export class AllDayManager {
});
}
/**
* Count number of events in a specific column using ColumnBounds
* Count number of events in a specific column using IColumnBounds
*/
private countEventsInColumn(columnBounds: ColumnBounds): number {
private countEventsInColumn(columnBounds: IColumnBounds): number {
let columnIndex = columnBounds.index;
let count = 0;

View file

@ -1,5 +1,5 @@
import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { EventManager } from './EventManager';
import { GridManager } from './GridManager';
@ -15,7 +15,7 @@ export class CalendarManager {
private gridManager: GridManager;
private eventRenderer: EventRenderingService;
private scrollManager: ScrollManager;
private config: CalendarConfig;
private config: Configuration;
private currentView: CalendarView = 'week';
private currentDate: Date = new Date();
private isInitialized: boolean = false;
@ -26,7 +26,7 @@ export class CalendarManager {
gridManager: GridManager,
eventRenderingService: EventRenderingService,
scrollManager: ScrollManager,
config: CalendarConfig
config: Configuration
) {
this.eventBus = eventBus;
this.eventManager = eventManager;
@ -232,4 +232,4 @@ export class CalendarManager {
});
}
}
}

View file

@ -1,174 +0,0 @@
// Configuration manager - handles config updates with event emission
// Uses static CalendarConfig internally but adds event-driven updates
import { IEventBus, ICalendarConfig } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig';
/**
* Grid display settings interface (re-export from CalendarConfig)
*/
interface GridSettings {
dayStartHour: number;
dayEndHour: number;
workStartHour: number;
workEndHour: number;
hourHeight: number;
snapInterval: number;
fitToWidth: boolean;
scrollToHour: number | null;
gridStartThresholdMinutes: number;
showCurrentTime: boolean;
showWorkHours: boolean;
}
/**
* ConfigManager - Handles configuration updates with event emission
* Wraps static CalendarConfig with event-driven functionality for DI system
* Also manages CSS custom properties that reflect config values
*/
export class ConfigManager {
constructor(private eventBus: IEventBus) {}
/**
* Initialize CSS variables on startup
* Must be called after DOM is ready but before any rendering
*/
public initialize(): void {
this.updateCSSVariables();
}
/**
* Set a config value and emit event
*/
set<K extends keyof ICalendarConfig>(key: K, value: ICalendarConfig[K]): void {
const oldValue = CalendarConfig.get(key);
CalendarConfig.set(key, value);
// Update CSS variables to reflect config change
this.updateCSSVariables();
// Emit config update event
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
key,
value,
oldValue
});
}
/**
* Update multiple config values and emit event
*/
update(updates: Partial<ICalendarConfig>): void {
Object.entries(updates).forEach(([key, value]) => {
this.set(key as keyof ICalendarConfig, value);
});
}
/**
* Update grid display settings and emit event
*/
updateGridSettings(updates: Partial<GridSettings>): void {
CalendarConfig.updateGridSettings(updates);
// Update CSS variables to reflect config change
this.updateCSSVariables();
// Emit event after update
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
key: 'gridSettings',
value: CalendarConfig.getGridSettings()
});
}
/**
* Set selected date and emit event
*/
setSelectedDate(date: Date): void {
const oldDate = CalendarConfig.getSelectedDate();
CalendarConfig.setSelectedDate(date);
// Emit date change event if it actually changed
if (!oldDate || oldDate.getTime() !== date.getTime()) {
this.eventBus.emit(CoreEvents.DATE_CHANGED, {
date,
oldDate
});
}
}
/**
* Set work week and emit event
*/
setWorkWeek(workWeekId: string): void {
const oldWorkWeek = CalendarConfig.getCurrentWorkWeek();
CalendarConfig.setWorkWeek(workWeekId);
// Update CSS variables to reflect config change
this.updateCSSVariables();
// Emit event if changed
if (oldWorkWeek !== workWeekId) {
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {
key: 'workWeek',
value: workWeekId,
oldValue: oldWorkWeek
});
}
}
/**
* Update all CSS custom properties based on current config
* This keeps the DOM in sync with config values
*/
private updateCSSVariables(): void {
const root = document.documentElement;
const gridSettings = CalendarConfig.getGridSettings();
const calendar = document.querySelector('swp-calendar') as HTMLElement;
// Set time-related CSS variables
root.style.setProperty('--header-height', '80px'); // Fixed header height
root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`);
root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`);
root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString());
root.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString());
root.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString());
root.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString());
root.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString());
// Set column count based on view
const columnCount = this.calculateColumnCount();
root.style.setProperty('--grid-columns', columnCount.toString());
// Set column width based on fitToWidth setting
if (gridSettings.fitToWidth) {
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
} else {
root.style.setProperty('--day-column-min-width', '250px'); // Default min-width for horizontal scroll mode
}
// Set fitToWidth data attribute for CSS targeting
if (calendar) {
calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString());
}
}
/**
* Calculate number of columns based on view
*/
private calculateColumnCount(): number {
const dateSettings = CalendarConfig.getDateViewSettings();
const workWeekSettings = CalendarConfig.getWorkWeekSettings();
switch (dateSettings.period) {
case 'day':
return 1;
case 'week':
return workWeekSettings.totalDays;
case 'month':
return workWeekSettings.totalDays; // Use work week for month view too
default:
return workWeekSettings.totalDays;
}
}
}

View file

@ -134,33 +134,33 @@
import { IEventBus } from '../types/CalendarTypes';
import { PositionUtils } from '../utils/PositionUtils';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement';
import {
DragStartEventPayload,
DragMoveEventPayload,
DragEndEventPayload,
DragMouseEnterHeaderEventPayload,
DragMouseLeaveHeaderEventPayload,
DragMouseEnterColumnEventPayload,
DragColumnChangeEventPayload
IDragStartEventPayload,
IDragMoveEventPayload,
IDragEndEventPayload,
IDragMouseEnterHeaderEventPayload,
IDragMouseLeaveHeaderEventPayload,
IDragMouseEnterColumnEventPayload,
IDragColumnChangeEventPayload
} from '../types/EventTypes';
import { MousePosition } from '../types/DragDropTypes';
import { IMousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents';
export class DragDropManager {
private eventBus: IEventBus;
// Mouse tracking with optimized state
private mouseDownPosition: MousePosition = { x: 0, y: 0 };
private currentMousePosition: MousePosition = { x: 0, y: 0 };
private mouseOffset: MousePosition = { x: 0, y: 0 };
private mouseDownPosition: IMousePosition = { x: 0, y: 0 };
private currentMousePosition: IMousePosition = { x: 0, y: 0 };
private mouseOffset: IMousePosition = { x: 0, y: 0 };
// Drag state
private originalElement!: HTMLElement | null;
private draggedClone!: HTMLElement | null;
private currentColumn: ColumnBounds | null = null;
private previousColumn: ColumnBounds | null = null;
private currentColumn: IColumnBounds | null = null;
private previousColumn: IColumnBounds | null = null;
private isDragStarted = false;
// Movement threshold to distinguish click from drag
@ -176,7 +176,7 @@ export class DragDropManager {
private dragAnimationId: number | null = null;
private targetY = 0;
private currentY = 0;
private targetColumn: ColumnBounds | null = null;
private targetColumn: IColumnBounds | null = null;
private positionUtils: PositionUtils;
constructor(eventBus: IEventBus, positionUtils: PositionUtils) {
@ -336,7 +336,7 @@ export class DragDropManager {
* Try to initialize drag based on movement threshold
* Returns true if drag was initialized, false if not enough movement
*/
private initializeDrag(currentPosition: MousePosition): boolean {
private initializeDrag(currentPosition: IMousePosition): boolean {
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);
@ -362,7 +362,7 @@ export class DragDropManager {
this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
this.draggedClone = originalElement.createClone();
const dragStartPayload: DragStartEventPayload = {
const dragStartPayload: IDragStartEventPayload = {
originalElement: this.originalElement!,
draggedClone: this.draggedClone,
mousePosition: this.mouseDownPosition,
@ -375,7 +375,7 @@ export class DragDropManager {
}
private continueDrag(currentPosition: MousePosition): void {
private continueDrag(currentPosition: IMousePosition): void {
if (!this.draggedClone!.hasAttribute("data-allday")) {
// Calculate raw position from mouse (no snapping)
@ -405,7 +405,7 @@ export class DragDropManager {
/**
* Detect column change and emit event
*/
private detectColumnChange(currentPosition: MousePosition): void {
private detectColumnChange(currentPosition: IMousePosition): void {
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
if (newColumn == null) return;
@ -413,7 +413,7 @@ export class DragDropManager {
this.previousColumn = this.currentColumn;
this.currentColumn = newColumn;
const dragColumnChangePayload: DragColumnChangeEventPayload = {
const dragColumnChangePayload: IDragColumnChangeEventPayload = {
originalElement: this.originalElement!,
draggedClone: this.draggedClone!,
previousColumn: this.previousColumn,
@ -434,7 +434,7 @@ export class DragDropManager {
// Only emit drag:end if drag was actually started
if (this.isDragStarted) {
const mousePosition: MousePosition = { x: event.clientX, y: event.clientY };
const mousePosition: IMousePosition = { x: event.clientX, y: event.clientY };
// Snap to grid on mouse up (like ResizeHandleManager)
const column = ColumnDetectionUtils.getColumnBounds(mousePosition);
@ -455,7 +455,7 @@ export class DragDropManager {
if (!dropTarget)
throw "dropTarget is null";
const dragEndPayload: DragEndEventPayload = {
const dragEndPayload: IDragEndEventPayload = {
originalElement: this.originalElement,
draggedClone: this.draggedClone,
mousePosition,
@ -530,7 +530,7 @@ export class DragDropManager {
/**
* Optimized snap position calculation using PositionUtils
*/
private calculateSnapPosition(mouseY: number, column: ColumnBounds): number {
private calculateSnapPosition(mouseY: number, column: IColumnBounds): number {
// Calculate where the event top would be (accounting for mouse offset)
const eventTopY = mouseY - this.mouseOffset.y;
@ -560,7 +560,7 @@ export class DragDropManager {
this.currentY += step;
// Emit drag:move event with current draggedClone reference
const dragMovePayload: DragMoveEventPayload = {
const dragMovePayload: IDragMoveEventPayload = {
originalElement: this.originalElement!,
draggedClone: this.draggedClone, // Always uses current reference
mousePosition: this.currentMousePosition, // Use current mouse position!
@ -576,7 +576,7 @@ export class DragDropManager {
this.currentY = this.targetY;
// Emit final position
const dragMovePayload: DragMoveEventPayload = {
const dragMovePayload: IDragMoveEventPayload = {
originalElement: this.originalElement!,
draggedClone: this.draggedClone,
mousePosition: this.currentMousePosition, // Use current mouse position!
@ -633,7 +633,7 @@ export class DragDropManager {
/**
* Detect drop target - whether dropped in swp-day-column or swp-day-header
*/
private detectDropTarget(position: MousePosition): 'swp-day-column' | 'swp-day-header' | null {
private detectDropTarget(position: IMousePosition): 'swp-day-column' | 'swp-day-header' | null {
// Traverse up the DOM tree to find the target container
let currentElement = this.draggedClone;
@ -659,13 +659,13 @@ export class DragDropManager {
return;
}
const position: MousePosition = { x: event.clientX, y: event.clientY };
const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (targetColumn) {
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = {
const dragMouseEnterPayload: IDragMouseEnterHeaderEventPayload = {
targetColumn: targetColumn,
mousePosition: position,
originalElement: this.originalElement,
@ -689,7 +689,7 @@ export class DragDropManager {
return;
}
const position: MousePosition = { x: event.clientX, y: event.clientY };
const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) {
@ -699,10 +699,10 @@ export class DragDropManager {
// Calculate snapped Y position
const snappedY = this.calculateSnapPosition(position.y, targetColumn);
// Extract CalendarEvent from the dragged clone
// Extract ICalendarEvent from the dragged clone
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
const dragMouseEnterPayload: DragMouseEnterColumnEventPayload = {
const dragMouseEnterPayload: IDragMouseEnterColumnEventPayload = {
targetColumn: targetColumn,
mousePosition: position,
snappedY: snappedY,
@ -727,14 +727,14 @@ export class DragDropManager {
return;
}
const position: MousePosition = { x: event.clientX, y: event.clientY };
const position: IMousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) {
return;
}
const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = {
const dragMouseLeavePayload: IDragMouseLeaveHeaderEventPayload = {
targetDate: targetColumn.date,
mousePosition: position,
originalElement: this.originalElement,

View file

@ -1,230 +1,230 @@
/**
* EdgeScrollManager - Auto-scroll when dragging near edges
* Uses time-based scrolling with 2-zone system for variable speed
*/
import { IEventBus } from '../types/CalendarTypes';
import { DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
export class EdgeScrollManager {
private scrollableContent: HTMLElement | null = null;
private timeGrid: HTMLElement | null = null;
private draggedClone: HTMLElement | null = null;
private scrollRAF: number | null = null;
private mouseY = 0;
private isDragging = false;
private isScrolling = false; // Track if edge-scroll is active
private lastTs = 0;
private rect: DOMRect | null = null;
private initialScrollTop = 0;
private scrollListener: ((e: Event) => void) | null = null;
// Constants - fixed values as per requirements
private readonly OUTER_ZONE = 100; // px from edge (slow zone)
private readonly INNER_ZONE = 50; // px from edge (fast zone)
private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone
constructor(private eventBus: IEventBus) {
this.init();
}
private init(): void {
// 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: MouseEvent) => {
if (this.isDragging) {
this.mouseY = e.clientY;
}
});
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen to drag events from DragDropManager
this.eventBus.on('drag:start', (event: Event) => {
const payload = (event as CustomEvent).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();
});
}
private startDrag(): void {
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));
}
}
private stopDrag(): void {
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;
}
private handleScroll(): void {
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', {});
}
}
private scrollTick(ts: number): void {
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;
console.log('📊 Scroll check:', {
currentScrollTop,
scrollableHeight,
timeGridHeight,
cloneBottom,
timeGridBottom,
atTop,
atBottom,
vy
});
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();
}
}
}
/**
* EdgeScrollManager - Auto-scroll when dragging near edges
* Uses time-based scrolling with 2-zone system for variable speed
*/
import { IEventBus } from '../types/CalendarTypes';
import { IDragMoveEventPayload, IDragStartEventPayload } from '../types/EventTypes';
export class EdgeScrollManager {
private scrollableContent: HTMLElement | null = null;
private timeGrid: HTMLElement | null = null;
private draggedClone: HTMLElement | null = null;
private scrollRAF: number | null = null;
private mouseY = 0;
private isDragging = false;
private isScrolling = false; // Track if edge-scroll is active
private lastTs = 0;
private rect: DOMRect | null = null;
private initialScrollTop = 0;
private scrollListener: ((e: Event) => void) | null = null;
// Constants - fixed values as per requirements
private readonly OUTER_ZONE = 100; // px from edge (slow zone)
private readonly INNER_ZONE = 50; // px from edge (fast zone)
private readonly SLOW_SPEED_PXS = 140; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 640; // px/sec in inner zone
constructor(private eventBus: IEventBus) {
this.init();
}
private init(): void {
// 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: MouseEvent) => {
if (this.isDragging) {
this.mouseY = e.clientY;
}
});
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen to drag events from DragDropManager
this.eventBus.on('drag:start', (event: Event) => {
const payload = (event as CustomEvent).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();
});
}
private startDrag(): void {
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));
}
}
private stopDrag(): void {
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;
}
private handleScroll(): void {
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', {});
}
}
private scrollTick(ts: number): void {
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;
console.log('📊 Scroll check:', {
currentScrollTop,
scrollableHeight,
timeGridHeight,
cloneBottom,
timeGridBottom,
atTop,
atBottom,
vy
});
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();
}
}
}
}

View file

@ -5,24 +5,24 @@
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
import { CalendarEvent } from '../types/CalendarTypes';
import { ICalendarEvent } from '../types/CalendarTypes';
// Import Fuse.js from npm
import Fuse from 'fuse.js';
interface FuseResult {
item: CalendarEvent;
item: ICalendarEvent;
refIndex: number;
score?: number;
}
export class EventFilterManager {
private searchInput: HTMLInputElement | null = null;
private allEvents: CalendarEvent[] = [];
private allEvents: ICalendarEvent[] = [];
private matchingEventIds: Set<string> = new Set();
private isFilterActive: boolean = false;
private frameRequest: number | null = null;
private fuse: Fuse<CalendarEvent> | null = null;
private fuse: Fuse<ICalendarEvent> | null = null;
constructor() {
// Wait for DOM to be ready before initializing
@ -77,7 +77,7 @@ export class EventFilterManager {
});
}
private updateEventsList(events: CalendarEvent[]): void {
private updateEventsList(events: ICalendarEvent[]): void {
this.allEvents = events;
// Initialize Fuse with the new events list

View file

@ -5,35 +5,35 @@
* Calculates stack levels, groups events, and determines rendering strategy.
*/
import { CalendarEvent } from '../types/CalendarTypes';
import { EventStackManager, EventGroup, StackLink } from './EventStackManager';
import { ICalendarEvent } from '../types/CalendarTypes';
import { EventStackManager, IEventGroup, IStackLink } from './EventStackManager';
import { PositionUtils } from '../utils/PositionUtils';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
export interface GridGroupLayout {
events: CalendarEvent[];
export interface IGridGroupLayout {
events: ICalendarEvent[];
stackLevel: number;
position: { top: number };
columns: CalendarEvent[][]; // Events grouped by column (events in same array share a column)
columns: ICalendarEvent[][]; // Events grouped by column (events in same array share a column)
}
export interface StackedEventLayout {
event: CalendarEvent;
stackLink: StackLink;
export interface IStackedEventLayout {
event: ICalendarEvent;
stackLink: IStackLink;
position: { top: number; height: number };
}
export interface ColumnLayout {
gridGroups: GridGroupLayout[];
stackedEvents: StackedEventLayout[];
export interface IColumnLayout {
gridGroups: IGridGroupLayout[];
stackedEvents: IStackedEventLayout[];
}
export class EventLayoutCoordinator {
private stackManager: EventStackManager;
private config: CalendarConfig;
private config: Configuration;
private positionUtils: PositionUtils;
constructor(stackManager: EventStackManager, config: CalendarConfig, positionUtils: PositionUtils) {
constructor(stackManager: EventStackManager, config: Configuration, positionUtils: PositionUtils) {
this.stackManager = stackManager;
this.config = config;
this.positionUtils = positionUtils;
@ -42,14 +42,14 @@ export class EventLayoutCoordinator {
/**
* Calculate complete layout for a column of events (recursive approach)
*/
public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout {
public calculateColumnLayout(columnEvents: ICalendarEvent[]): IColumnLayout {
if (columnEvents.length === 0) {
return { gridGroups: [], stackedEvents: [] };
}
const gridGroupLayouts: GridGroupLayout[] = [];
const stackedEventLayouts: StackedEventLayout[] = [];
const renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> = [];
const gridGroupLayouts: IGridGroupLayout[] = [];
const stackedEventLayouts: IStackedEventLayout[] = [];
const renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }> = [];
let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
// Process events recursively
@ -66,7 +66,7 @@ export class EventLayoutCoordinator {
const gridCandidates = this.expandGridCandidates(firstEvent, remaining, thresholdMinutes);
// Decide: should this group be GRID or STACK?
const group: EventGroup = {
const group: IEventGroup = {
events: gridCandidates,
containerType: 'NONE',
startTime: firstEvent.start
@ -129,8 +129,8 @@ export class EventLayoutCoordinator {
* Calculate stack level for a grid group based on already rendered events
*/
private calculateGridGroupStackLevelFromRendered(
gridEvents: CalendarEvent[],
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }>
gridEvents: ICalendarEvent[],
renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }>
): number {
// Find highest stack level of any rendered event that overlaps with this grid
let maxOverlappingLevel = -1;
@ -150,8 +150,8 @@ export class EventLayoutCoordinator {
* Calculate stack level for a single stacked event based on already rendered events
*/
private calculateStackLevelFromRendered(
event: CalendarEvent,
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }>
event: ICalendarEvent,
renderedEventsWithLevels: Array<{ event: ICalendarEvent; level: number }>
): number {
// Find highest stack level of any rendered event that overlaps with this event
let maxOverlappingLevel = -1;
@ -173,7 +173,7 @@ export class EventLayoutCoordinator {
* @param thresholdMinutes - Threshold in minutes
* @returns true if events conflict
*/
private detectConflict(event1: CalendarEvent, event2: CalendarEvent, thresholdMinutes: number): boolean {
private detectConflict(event1: ICalendarEvent, event2: ICalendarEvent, thresholdMinutes: number): boolean {
// 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)) {
@ -206,10 +206,10 @@ export class EventLayoutCoordinator {
* @returns Array of all events in the conflict chain
*/
private expandGridCandidates(
firstEvent: CalendarEvent,
remaining: CalendarEvent[],
firstEvent: ICalendarEvent,
remaining: ICalendarEvent[],
thresholdMinutes: number
): CalendarEvent[] {
): ICalendarEvent[] {
const gridCandidates = [firstEvent];
let candidatesChanged = true;
@ -246,11 +246,11 @@ export class EventLayoutCoordinator {
* @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(events: CalendarEvent[]): CalendarEvent[][] {
private allocateColumns(events: ICalendarEvent[]): ICalendarEvent[][] {
if (events.length === 0) return [];
if (events.length === 1) return [[events[0]]];
const columns: CalendarEvent[][] = [];
const columns: ICalendarEvent[][] = [];
// For each event, try to place it in an existing column where it doesn't overlap
for (const event of events) {

View file

@ -1,6 +1,6 @@
import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
import { IEventBus, ICalendarEvent } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
import { DateService } from '../utils/DateService';
import { IEventRepository } from '../repositories/IEventRepository';
@ -10,15 +10,15 @@ import { IEventRepository } from '../repositories/IEventRepository';
*/
export class EventManager {
private events: CalendarEvent[] = [];
private events: ICalendarEvent[] = [];
private dateService: DateService;
private config: CalendarConfig;
private config: Configuration;
private repository: IEventRepository;
constructor(
private eventBus: IEventBus,
dateService: DateService,
config: CalendarConfig,
config: Configuration,
repository: IEventRepository
) {
this.dateService = dateService;
@ -42,14 +42,14 @@ export class EventManager {
/**
* Get events with optional copying for performance
*/
public getEvents(copy: boolean = false): CalendarEvent[] {
public getEvents(copy: boolean = false): ICalendarEvent[] {
return copy ? [...this.events] : this.events;
}
/**
* Optimized event lookup with early return
*/
public getEventById(id: string): CalendarEvent | undefined {
public getEventById(id: string): ICalendarEvent | undefined {
// Use find for better performance than filter + first
return this.events.find(event => event.id === id);
}
@ -59,7 +59,7 @@ export class EventManager {
* @param id Event ID to find
* @returns Event with navigation info or null if not found
*/
public getEventForNavigation(id: string): { event: CalendarEvent; eventDate: Date } | null {
public getEventForNavigation(id: string): { event: ICalendarEvent; eventDate: Date } | null {
const event = this.getEventById(id);
if (!event) {
return null;
@ -113,7 +113,7 @@ export class EventManager {
/**
* Get events that overlap with a given time period
*/
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
public getEventsForPeriod(startDate: Date, endDate: Date): ICalendarEvent[] {
// Event overlaps period if it starts before period ends AND ends after period starts
return this.events.filter(event => {
return event.start <= endDate && event.end >= startDate;
@ -123,8 +123,8 @@ export class EventManager {
/**
* Create a new event and add it to the calendar
*/
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent {
const newEvent: CalendarEvent = {
public addEvent(event: Omit<ICalendarEvent, 'id'>): ICalendarEvent {
const newEvent: ICalendarEvent = {
...event,
id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
};
@ -141,7 +141,7 @@ export class EventManager {
/**
* Update an existing event
*/
public updateEvent(id: string, updates: Partial<CalendarEvent>): CalendarEvent | null {
public updateEvent(id: string, updates: Partial<ICalendarEvent>): ICalendarEvent | null {
const eventIndex = this.events.findIndex(event => event.id === id);
if (eventIndex === -1) return null;

View file

@ -13,26 +13,26 @@
* @see stacking-visualization.html for visual examples
*/
import { CalendarEvent } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig';
import { ICalendarEvent } from '../types/CalendarTypes';
import { Configuration } from '../configuration/CalendarConfig';
export interface StackLink {
export interface IStackLink {
prev?: string; // Event ID of previous event in stack
next?: string; // Event ID of next event in stack
stackLevel: number; // Position in stack (0 = base, 1 = first offset, etc.)
}
export interface EventGroup {
events: CalendarEvent[];
export interface IEventGroup {
events: ICalendarEvent[];
containerType: 'NONE' | 'GRID' | 'STACKING';
startTime: Date;
}
export class EventStackManager {
private static readonly STACK_OFFSET_PX = 15;
private config: CalendarConfig;
private config: Configuration;
constructor(config: CalendarConfig) {
constructor(config: Configuration) {
this.config = config;
}
@ -47,7 +47,7 @@ export class EventStackManager {
* 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)
*/
public groupEventsByStartTime(events: CalendarEvent[]): EventGroup[] {
public groupEventsByStartTime(events: ICalendarEvent[]): IEventGroup[] {
if (events.length === 0) return [];
// Get threshold from config
@ -57,7 +57,7 @@ export class EventStackManager {
// Sort events by start time
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
const groups: EventGroup[] = [];
const groups: IEventGroup[] = [];
for (const event of sorted) {
// Find existing group that this event conflicts with
@ -112,7 +112,7 @@ export class EventStackManager {
* even if they overlap each other. This provides better visual indication that
* events start at the same time.
*/
public decideContainerType(group: EventGroup): 'NONE' | 'GRID' | 'STACKING' {
public decideContainerType(group: IEventGroup): 'NONE' | 'GRID' | 'STACKING' {
if (group.events.length === 1) {
return 'NONE';
}
@ -127,7 +127,7 @@ export class EventStackManager {
/**
* Check if two events overlap in time
*/
public doEventsOverlap(event1: CalendarEvent, event2: CalendarEvent): boolean {
public doEventsOverlap(event1: ICalendarEvent, event2: ICalendarEvent): boolean {
return event1.start < event2.end && event1.end > event2.start;
}
@ -139,8 +139,8 @@ export class EventStackManager {
/**
* Create optimized stack links (events share levels when possible)
*/
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
const stackLinks = new Map<string, StackLink>();
public createOptimizedStackLinks(events: ICalendarEvent[]): Map<string, IStackLink> {
const stackLinks = new Map<string, IStackLink>();
if (events.length === 0) return stackLinks;
@ -218,14 +218,14 @@ export class EventStackManager {
/**
* Serialize stack link to JSON string
*/
public serializeStackLink(stackLink: StackLink): string {
public serializeStackLink(stackLink: IStackLink): string {
return JSON.stringify(stackLink);
}
/**
* Deserialize JSON string to stack link
*/
public deserializeStackLink(json: string): StackLink | null {
public deserializeStackLink(json: string): IStackLink | null {
try {
return JSON.parse(json);
} catch (e) {
@ -236,14 +236,14 @@ export class EventStackManager {
/**
* Apply stack link to DOM element
*/
public applyStackLinkToElement(element: HTMLElement, stackLink: StackLink): void {
public applyStackLinkToElement(element: HTMLElement, stackLink: IStackLink): void {
element.dataset.stackLink = this.serializeStackLink(stackLink);
}
/**
* Get stack link from DOM element
*/
public getStackLinkFromElement(element: HTMLElement): StackLink | null {
public getStackLinkFromElement(element: HTMLElement): IStackLink | null {
const data = element.dataset.stackLink;
if (!data) return null;
return this.deserializeStackLink(data);

View file

@ -1,8 +1,8 @@
import { eventBus } from '../core/EventBus';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { IHeaderRenderer, HeaderRenderContext } from '../renderers/DateHeaderRenderer';
import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes';
import { IHeaderRenderer, IHeaderRenderContext } from '../renderers/DateHeaderRenderer';
import { IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IHeaderReadyEventPayload } from '../types/EventTypes';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
/**
@ -12,9 +12,9 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
*/
export class HeaderManager {
private headerRenderer: IHeaderRenderer;
private config: CalendarConfig;
private config: Configuration;
constructor(headerRenderer: IHeaderRenderer, config: CalendarConfig) {
constructor(headerRenderer: IHeaderRenderer, config: Configuration) {
this.headerRenderer = headerRenderer;
this.config = config;
@ -44,7 +44,7 @@ export class HeaderManager {
*/
private handleDragMouseEnterHeader(event: Event): void {
const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } =
(event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
(event as CustomEvent<IDragMouseEnterHeaderEventPayload>).detail;
console.log('🎯 HeaderManager: Received drag:mouseenter-header', {
targetDate,
@ -58,7 +58,7 @@ export class HeaderManager {
*/
private handleDragMouseLeaveHeader(event: Event): void {
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } =
(event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
(event as CustomEvent<IDragMouseLeaveHeaderEventPayload>).detail;
console.log('🚪 HeaderManager: Received drag:mouseleave-header', {
targetDate,
@ -109,7 +109,7 @@ export class HeaderManager {
calendarHeader.innerHTML = '';
// Render new header content using injected renderer
const context: HeaderRenderContext = {
const context: IHeaderRenderContext = {
currentWeek: currentDate,
config: this.config
};
@ -120,9 +120,9 @@ export class HeaderManager {
this.setupHeaderDragListeners();
// Notify other managers that header is ready with period data
const payload: HeaderReadyEventPayload = {
const payload: IHeaderReadyEventPayload = {
headerElements: ColumnDetectionUtils.getHeaderColumns(),
};
eventBus.emit('header:ready', payload);
}
}
}

View file

@ -1,7 +1,7 @@
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig } from '../core/CalendarConfig';
import { ResizeEndEventPayload } from '../types/EventTypes';
import { Configuration } from '../configuration/CalendarConfig';
import { IResizeEndEventPayload } from '../types/EventTypes';
type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void };
@ -29,9 +29,9 @@ export class ResizeHandleManager {
private unsubscribers: Array<() => void> = [];
private pointerCaptured = false;
private prevZ?: string;
private config: CalendarConfig;
private config: Configuration;
constructor(config: CalendarConfig) {
constructor(config: Configuration) {
this.config = config;
const grid = this.config.getGridSettings();
this.hourHeightPx = grid.hourHeight;
@ -237,7 +237,7 @@ export class ResizeHandleManager {
// Emit resize:end event for re-stacking
const eventId = this.targetEl.dataset.eventId || '';
const resizeEndPayload: ResizeEndEventPayload = {
const resizeEndPayload: IResizeEndEventPayload = {
eventId,
element: this.targetEl,
finalHeight

View file

@ -1,15 +1,15 @@
import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
export class ViewManager {
private eventBus: IEventBus;
private config: CalendarConfig;
private config: Configuration;
private currentView: CalendarView = 'week';
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus, config: CalendarConfig) {
constructor(eventBus: IEventBus, config: Configuration) {
this.eventBus = eventBus;
this.config = config;
this.setupEventListeners();
@ -143,4 +143,4 @@ export class ViewManager {
}
}
}

View file

@ -1,13 +1,13 @@
// Work hours management for per-column scheduling
import { DateService } from '../utils/DateService';
import { CalendarConfig } from '../core/CalendarConfig';
import { Configuration } from '../configuration/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils';
/**
* Work hours for a specific day
*/
export interface DayWorkHours {
export interface IDayWorkHours {
start: number; // Hour (0-23)
end: number; // Hour (0-23)
}
@ -15,18 +15,18 @@ export interface DayWorkHours {
/**
* Work schedule configuration
*/
export interface WorkScheduleConfig {
export interface IWorkScheduleConfig {
weeklyDefault: {
monday: DayWorkHours | 'off';
tuesday: DayWorkHours | 'off';
wednesday: DayWorkHours | 'off';
thursday: DayWorkHours | 'off';
friday: DayWorkHours | 'off';
saturday: DayWorkHours | 'off';
sunday: DayWorkHours | 'off';
monday: IDayWorkHours | 'off';
tuesday: IDayWorkHours | 'off';
wednesday: IDayWorkHours | 'off';
thursday: IDayWorkHours | 'off';
friday: IDayWorkHours | 'off';
saturday: IDayWorkHours | 'off';
sunday: IDayWorkHours | 'off';
};
dateOverrides: {
[dateString: string]: DayWorkHours | 'off'; // YYYY-MM-DD format
[dateString: string]: IDayWorkHours | 'off'; // YYYY-MM-DD format
};
}
@ -35,11 +35,11 @@ export interface WorkScheduleConfig {
*/
export class WorkHoursManager {
private dateService: DateService;
private config: CalendarConfig;
private config: Configuration;
private positionUtils: PositionUtils;
private workSchedule: WorkScheduleConfig;
private workSchedule: IWorkScheduleConfig;
constructor(dateService: DateService, config: CalendarConfig, positionUtils: PositionUtils) {
constructor(dateService: DateService, config: Configuration, positionUtils: PositionUtils) {
this.dateService = dateService;
this.config = config;
this.positionUtils = positionUtils;
@ -66,7 +66,7 @@ export class WorkHoursManager {
/**
* Get work hours for a specific date
*/
getWorkHoursForDate(date: Date): DayWorkHours | 'off' {
getWorkHoursForDate(date: Date): IDayWorkHours | 'off' {
const dateString = this.dateService.formatISODate(date);
// Check for date-specific override first
@ -82,8 +82,8 @@ export class WorkHoursManager {
/**
* Get work hours for multiple dates (used by GridManager)
*/
getWorkHoursForDateRange(dates: Date[]): Map<string, DayWorkHours | 'off'> {
const workHoursMap = new Map<string, DayWorkHours | 'off'>();
getWorkHoursForDateRange(dates: Date[]): Map<string, IDayWorkHours | 'off'> {
const workHoursMap = new Map<string, IDayWorkHours | 'off'>();
dates.forEach(date => {
const dateString = this.dateService.formatISODate(date);
@ -97,7 +97,7 @@ export class WorkHoursManager {
/**
* Calculate CSS custom properties for non-work hour overlays using PositionUtils
*/
calculateNonWorkHoursStyle(workHours: DayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null {
calculateNonWorkHoursStyle(workHours: IDayWorkHours | 'off'): { beforeWorkHeight: number; afterWorkTop: number } | null {
if (workHours === 'off') {
return null; // Full day will be colored via CSS background
}
@ -121,7 +121,7 @@ export class WorkHoursManager {
/**
* Calculate CSS custom properties for work hours overlay using PositionUtils
*/
calculateWorkHoursStyle(workHours: DayWorkHours | 'off'): { top: number; height: number } | null {
calculateWorkHoursStyle(workHours: IDayWorkHours | 'off'): { top: number; height: number } | null {
if (workHours === 'off') {
return null;
}
@ -139,24 +139,24 @@ export class WorkHoursManager {
/**
* Load work schedule from JSON (future implementation)
*/
async loadWorkSchedule(jsonData: WorkScheduleConfig): Promise<void> {
async loadWorkSchedule(jsonData: IWorkScheduleConfig): Promise<void> {
this.workSchedule = jsonData;
}
/**
* Get current work schedule configuration
*/
getWorkSchedule(): WorkScheduleConfig {
getWorkSchedule(): IWorkScheduleConfig {
return this.workSchedule;
}
/**
* Convert Date to day name key
*/
private getDayName(date: Date): keyof WorkScheduleConfig['weeklyDefault'] {
const dayNames: (keyof WorkScheduleConfig['weeklyDefault'])[] = [
private getDayName(date: Date): keyof IWorkScheduleConfig['weeklyDefault'] {
const dayNames: (keyof IWorkScheduleConfig['weeklyDefault'])[] = [
'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'
];
return dayNames[date.getDay()];
}
}
}