Major refactor into type safe TS
With a risk oof rolling it all back
This commit is contained in:
parent
c08fa02c29
commit
48d1fd681c
19 changed files with 449 additions and 81 deletions
|
|
@ -59,14 +59,14 @@ export class EventBus implements IEventBus {
|
||||||
/**
|
/**
|
||||||
* Emit an event via DOM CustomEvent
|
* Emit an event via DOM CustomEvent
|
||||||
*/
|
*/
|
||||||
emit(eventType: string, detail: any = {}): boolean {
|
emit(eventType: string, detail: unknown = {}): boolean {
|
||||||
// Validate eventType
|
// Validate eventType
|
||||||
if (!eventType || typeof eventType !== 'string') {
|
if (!eventType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = new CustomEvent(eventType, {
|
const event = new CustomEvent(eventType, {
|
||||||
detail,
|
detail: detail ?? {},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true
|
cancelable: true
|
||||||
});
|
});
|
||||||
|
|
@ -78,7 +78,7 @@ export class EventBus implements IEventBus {
|
||||||
|
|
||||||
this.eventLog.push({
|
this.eventLog.push({
|
||||||
type: eventType,
|
type: eventType,
|
||||||
detail,
|
detail: detail ?? {},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ export class EventBus implements IEventBus {
|
||||||
/**
|
/**
|
||||||
* Log event with console grouping
|
* Log event with console grouping
|
||||||
*/
|
*/
|
||||||
private logEventWithGrouping(eventType: string, detail: any): void {
|
private logEventWithGrouping(eventType: string, detail: unknown): void {
|
||||||
// Extract category from event type (e.g., 'calendar:datechanged' → 'calendar')
|
// Extract category from event type (e.g., 'calendar:datechanged' → 'calendar')
|
||||||
const category = this.extractCategory(eventType);
|
const category = this.extractCategory(eventType);
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ export class EventBus implements IEventBus {
|
||||||
* Extract category from event type
|
* Extract category from event type
|
||||||
*/
|
*/
|
||||||
private extractCategory(eventType: string): string {
|
private extractCategory(eventType: string): string {
|
||||||
if (!eventType || typeof eventType !== 'string') {
|
if (!eventType) {
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { ViewManager } from '../managers/ViewManager';
|
||||||
import { CalendarManager } from '../managers/CalendarManager';
|
import { CalendarManager } from '../managers/CalendarManager';
|
||||||
import { DragDropManager } from '../managers/DragDropManager';
|
import { DragDropManager } from '../managers/DragDropManager';
|
||||||
import { AllDayManager } from '../managers/AllDayManager';
|
import { AllDayManager } from '../managers/AllDayManager';
|
||||||
|
import { CalendarManagers } from '../types/ManagerTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating and managing calendar managers with proper dependency injection
|
* Factory for creating and managing calendar managers with proper dependency injection
|
||||||
|
|
@ -27,17 +28,7 @@ export class ManagerFactory {
|
||||||
/**
|
/**
|
||||||
* Create all managers with proper dependency injection
|
* Create all managers with proper dependency injection
|
||||||
*/
|
*/
|
||||||
public createManagers(eventBus: IEventBus): {
|
public createManagers(eventBus: IEventBus): CalendarManagers {
|
||||||
eventManager: EventManager;
|
|
||||||
eventRenderer: EventRenderingService;
|
|
||||||
gridManager: GridManager;
|
|
||||||
scrollManager: ScrollManager;
|
|
||||||
navigationManager: NavigationManager;
|
|
||||||
viewManager: ViewManager;
|
|
||||||
calendarManager: CalendarManager;
|
|
||||||
dragDropManager: DragDropManager;
|
|
||||||
allDayManager: AllDayManager;
|
|
||||||
} {
|
|
||||||
|
|
||||||
// Create managers in dependency order
|
// Create managers in dependency order
|
||||||
const eventManager = new EventManager(eventBus);
|
const eventManager = new EventManager(eventBus);
|
||||||
|
|
@ -75,13 +66,10 @@ export class ManagerFactory {
|
||||||
/**
|
/**
|
||||||
* Initialize all managers in the correct order
|
* Initialize all managers in the correct order
|
||||||
*/
|
*/
|
||||||
public async initializeManagers(managers: {
|
public async initializeManagers(managers: CalendarManagers): Promise<void> {
|
||||||
calendarManager: CalendarManager;
|
|
||||||
[key: string]: any;
|
|
||||||
}): Promise<void> {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await managers.calendarManager.initialize();
|
await managers.calendarManager.initialize?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/index.ts
23
src/index.ts
|
|
@ -1,15 +1,16 @@
|
||||||
// Main entry point for Calendar Plantempus
|
// Main entry point for Calendar Plantempus
|
||||||
import { eventBus } from './core/EventBus.js';
|
import { eventBus } from './core/EventBus';
|
||||||
import { calendarConfig } from './core/CalendarConfig.js';
|
import { calendarConfig } from './core/CalendarConfig';
|
||||||
import { CalendarTypeFactory } from './factories/CalendarTypeFactory.js';
|
import { CalendarTypeFactory } from './factories/CalendarTypeFactory';
|
||||||
import { ManagerFactory } from './factories/ManagerFactory.js';
|
import { ManagerFactory } from './factories/ManagerFactory';
|
||||||
import { DateCalculator } from './utils/DateCalculator.js';
|
import { DateCalculator } from './utils/DateCalculator';
|
||||||
import { URLManager } from './utils/URLManager.js';
|
import { URLManager } from './utils/URLManager';
|
||||||
|
import { CalendarManagers } from './types/ManagerTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle deep linking functionality after managers are initialized
|
* Handle deep linking functionality after managers are initialized
|
||||||
*/
|
*/
|
||||||
async function handleDeepLinking(managers: any): Promise<void> {
|
async function handleDeepLinking(managers: CalendarManagers): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const urlManager = new URLManager(eventBus);
|
const urlManager = new URLManager(eventBus);
|
||||||
const eventId = urlManager.parseEventIdFromURL();
|
const eventId = urlManager.parseEventIdFromURL();
|
||||||
|
|
@ -58,8 +59,12 @@ async function initializeCalendar(): Promise<void> {
|
||||||
// Handle deep linking after managers are initialized
|
// Handle deep linking after managers are initialized
|
||||||
await handleDeepLinking(managers);
|
await handleDeepLinking(managers);
|
||||||
|
|
||||||
// Expose to window for debugging
|
// Expose to window for debugging (with proper typing)
|
||||||
(window as any).calendarDebug = {
|
(window as Window & {
|
||||||
|
calendarDebug?: {
|
||||||
|
eventBus: typeof eventBus;
|
||||||
|
} & CalendarManagers;
|
||||||
|
}).calendarDebug = {
|
||||||
eventBus,
|
eventBus,
|
||||||
...managers
|
...managers
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base interface for all managers
|
* Base interface for all managers
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,8 +25,8 @@ export interface IManager {
|
||||||
*/
|
*/
|
||||||
export interface IEventManager extends IManager {
|
export interface IEventManager extends IManager {
|
||||||
loadData(): Promise<void>;
|
loadData(): Promise<void>;
|
||||||
getEvents(): any[];
|
getEvents(): CalendarEvent[];
|
||||||
getEventsForPeriod(startDate: Date, endDate: Date): any[];
|
getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
DragMoveEventPayload,
|
DragMoveEventPayload,
|
||||||
DragEndEventPayload
|
DragEndEventPayload
|
||||||
} from '../types/EventTypes';
|
} from '../types/EventTypes';
|
||||||
|
import { DragOffset, MousePosition } from '../types/DragDropTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AllDayManager - Handles all-day row height animations and management
|
* AllDayManager - Handles all-day row height animations and management
|
||||||
|
|
@ -102,7 +103,7 @@ export class AllDayManager {
|
||||||
|
|
||||||
|
|
||||||
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
||||||
this.handleDragEnd(draggedElement, dragClone as HTMLElement, finalPosition.column);
|
this.handleDragEnd(draggedElement, dragClone as HTMLElement, { column: finalPosition.column || '', y: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for drag cancellation to recalculate height
|
// Listen for drag cancellation to recalculate height
|
||||||
|
|
@ -374,7 +375,7 @@ export class AllDayManager {
|
||||||
/**
|
/**
|
||||||
* Handle drag start for all-day events
|
* Handle drag start for all-day events
|
||||||
*/
|
*/
|
||||||
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any): void {
|
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset): void {
|
||||||
// Create clone
|
// Create clone
|
||||||
const clone = originalElement.cloneNode(true) as HTMLElement;
|
const clone = originalElement.cloneNode(true) as HTMLElement;
|
||||||
clone.dataset.eventId = `clone-${eventId}`;
|
clone.dataset.eventId = `clone-${eventId}`;
|
||||||
|
|
@ -409,7 +410,7 @@ export class AllDayManager {
|
||||||
/**
|
/**
|
||||||
* Handle drag move for all-day events
|
* Handle drag move for all-day events
|
||||||
*/
|
*/
|
||||||
private handleDragMove(dragClone: HTMLElement, mousePosition: any): void {
|
private handleDragMove(dragClone: HTMLElement, mousePosition: MousePosition): void {
|
||||||
// Calculate grid column based on mouse position
|
// Calculate grid column based on mouse position
|
||||||
const dayHeaders = document.querySelectorAll('swp-day-header');
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
||||||
let targetColumn = 1;
|
let targetColumn = 1;
|
||||||
|
|
@ -434,7 +435,7 @@ export class AllDayManager {
|
||||||
/**
|
/**
|
||||||
* Handle drag end for all-day events
|
* Handle drag end for all-day events
|
||||||
*/
|
*/
|
||||||
private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: any): void {
|
private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void {
|
||||||
|
|
||||||
// Normalize clone
|
// Normalize clone
|
||||||
const cloneId = dragClone.dataset.eventId;
|
const cloneId = dragClone.dataset.eventId;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { EventBus } from '../core/EventBus.js';
|
import { EventBus } from '../core/EventBus';
|
||||||
import { CoreEvents } from '../constants/CoreEvents.js';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { calendarConfig } from '../core/CalendarConfig.js';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
|
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes';
|
||||||
import { EventManager } from './EventManager.js';
|
import { EventManager } from './EventManager';
|
||||||
import { GridManager } from './GridManager.js';
|
import { GridManager } from './GridManager';
|
||||||
import { HeaderManager } from './HeaderManager.js';
|
import { HeaderManager } from './HeaderManager';
|
||||||
import { EventRenderingService } from '../renderers/EventRendererManager.js';
|
import { EventRenderingService } from '../renderers/EventRendererManager';
|
||||||
import { ScrollManager } from './ScrollManager.js';
|
import { ScrollManager } from './ScrollManager';
|
||||||
import { DateCalculator } from '../utils/DateCalculator.js';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { EventFilterManager } from './EventFilterManager.js';
|
import { EventFilterManager } from './EventFilterManager';
|
||||||
|
import { InitializationReport } from '../types/ManagerTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CalendarManager - Main coordinator for all calendar managers
|
* CalendarManager - Main coordinator for all calendar managers
|
||||||
|
|
@ -65,7 +66,7 @@ export class CalendarManager {
|
||||||
// Step 2: Pass data to GridManager and render grid structure
|
// Step 2: Pass data to GridManager and render grid structure
|
||||||
if (calendarType === 'resource') {
|
if (calendarType === 'resource') {
|
||||||
const resourceData = this.eventManager.getResourceData();
|
const resourceData = this.eventManager.getResourceData();
|
||||||
this.gridManager.setResourceData(resourceData);
|
this.gridManager.setResourceData(this.eventManager.getRawData() as import('../types/CalendarTypes').ResourceCalendarData);
|
||||||
}
|
}
|
||||||
await this.gridManager.render();
|
await this.gridManager.render();
|
||||||
|
|
||||||
|
|
@ -211,12 +212,17 @@ export class CalendarManager {
|
||||||
/**
|
/**
|
||||||
* Get initialization report for debugging
|
* Get initialization report for debugging
|
||||||
*/
|
*/
|
||||||
public getInitializationReport(): any {
|
public getInitializationReport(): InitializationReport {
|
||||||
return {
|
return {
|
||||||
isInitialized: this.isInitialized,
|
initialized: this.isInitialized,
|
||||||
currentView: this.currentView,
|
timestamp: Date.now(),
|
||||||
currentDate: this.currentDate,
|
managers: {
|
||||||
initializationTime: 'N/A - simple initialization'
|
calendar: { initialized: this.isInitialized },
|
||||||
|
event: { initialized: true },
|
||||||
|
grid: { initialized: true },
|
||||||
|
header: { initialized: true },
|
||||||
|
scroll: { initialized: true }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,19 @@ import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
// Import Fuse.js from npm
|
// Import Fuse.js from npm
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
interface FuseResult {
|
||||||
|
item: CalendarEvent;
|
||||||
|
refIndex: number;
|
||||||
|
score?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class EventFilterManager {
|
export class EventFilterManager {
|
||||||
private searchInput: HTMLInputElement | null = null;
|
private searchInput: HTMLInputElement | null = null;
|
||||||
private allEvents: CalendarEvent[] = [];
|
private allEvents: CalendarEvent[] = [];
|
||||||
private matchingEventIds: Set<string> = new Set();
|
private matchingEventIds: Set<string> = new Set();
|
||||||
private isFilterActive: boolean = false;
|
private isFilterActive: boolean = false;
|
||||||
private frameRequest: number | null = null;
|
private frameRequest: number | null = null;
|
||||||
private fuse: any = null;
|
private fuse: Fuse<CalendarEvent> | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Wait for DOM to be ready before initializing
|
// Wait for DOM to be ready before initializing
|
||||||
|
|
@ -119,7 +125,7 @@ export class EventFilterManager {
|
||||||
|
|
||||||
// Extract matching event IDs
|
// Extract matching event IDs
|
||||||
this.matchingEventIds.clear();
|
this.matchingEventIds.clear();
|
||||||
results.forEach((result: any) => {
|
results.forEach((result: FuseResult) => {
|
||||||
if (result.item && result.item.id) {
|
if (result.item && result.item.id) {
|
||||||
this.matchingEventIds.add(result.item.id);
|
this.matchingEventIds.add(result.item.id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,17 @@ import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/Calenda
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
import { ResourceData } from '../types/ManagerTypes';
|
||||||
|
|
||||||
|
interface RawEventData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: string | Date;
|
||||||
|
end: string | Date;
|
||||||
|
color?: string;
|
||||||
|
allDay?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventManager - Optimized event lifecycle and CRUD operations
|
* EventManager - Optimized event lifecycle and CRUD operations
|
||||||
|
|
@ -11,7 +22,7 @@ import { DateCalculator } from '../utils/DateCalculator';
|
||||||
export class EventManager {
|
export class EventManager {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
private events: CalendarEvent[] = [];
|
private events: CalendarEvent[] = [];
|
||||||
private rawData: any = null;
|
private rawData: ResourceCalendarData | RawEventData[] | null = null;
|
||||||
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
|
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
|
||||||
private lastCacheKey: string = '';
|
private lastCacheKey: string = '';
|
||||||
|
|
||||||
|
|
@ -57,7 +68,7 @@ export class EventManager {
|
||||||
/**
|
/**
|
||||||
* Optimized data processing with better type safety
|
* Optimized data processing with better type safety
|
||||||
*/
|
*/
|
||||||
private processCalendarData(calendarType: string, data: any): CalendarEvent[] {
|
private processCalendarData(calendarType: string, data: ResourceCalendarData | RawEventData[]): CalendarEvent[] {
|
||||||
if (calendarType === 'resource') {
|
if (calendarType === 'resource') {
|
||||||
const resourceData = data as ResourceCalendarData;
|
const resourceData = data as ResourceCalendarData;
|
||||||
return resourceData.resources.flatMap(resource =>
|
return resourceData.resources.flatMap(resource =>
|
||||||
|
|
@ -72,10 +83,14 @@ export class EventManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.map((event: any) => ({
|
const eventData = data as RawEventData[];
|
||||||
|
return eventData.map((event): CalendarEvent => ({
|
||||||
...event,
|
...event,
|
||||||
start: new Date(event.start),
|
start: new Date(event.start),
|
||||||
end: new Date(event.end)
|
end: new Date(event.end),
|
||||||
|
type: 'event',
|
||||||
|
allDay: event.allDay || false,
|
||||||
|
syncStatus: 'synced' as const
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +112,24 @@ export class EventManager {
|
||||||
/**
|
/**
|
||||||
* Get raw resource data for resource calendar mode
|
* Get raw resource data for resource calendar mode
|
||||||
*/
|
*/
|
||||||
public getResourceData(): any {
|
public getResourceData(): ResourceData | null {
|
||||||
|
if (!this.rawData || !('resources' in this.rawData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resources: this.rawData.resources.map(r => ({
|
||||||
|
id: r.employeeId || r.name, // Use employeeId as id, fallback to name
|
||||||
|
name: r.name,
|
||||||
|
type: r.employeeId ? 'employee' : 'resource',
|
||||||
|
color: 'blue' // Default color since Resource interface doesn't have color
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw data for compatibility
|
||||||
|
*/
|
||||||
|
public getRawData(): ResourceCalendarData | RawEventData[] | null {
|
||||||
return this.rawData;
|
return this.rawData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ export class GridManager {
|
||||||
/**
|
/**
|
||||||
* Get layout config for current view
|
* Get layout config for current view
|
||||||
*/
|
*/
|
||||||
private getLayoutConfig(): any {
|
private getLayoutConfig(): { columnCount: number; type: string } {
|
||||||
switch (this.currentView) {
|
switch (this.currentView) {
|
||||||
case 'week':
|
case 'week':
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { IEventBus } from '../types/CalendarTypes.js';
|
import { IEventBus } from '../types/CalendarTypes';
|
||||||
import { EventRenderingService } from '../renderers/EventRendererManager.js';
|
import { EventRenderingService } from '../renderers/EventRendererManager';
|
||||||
import { DateCalculator } from '../utils/DateCalculator.js';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { CoreEvents } from '../constants/CoreEvents.js';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { NavigationRenderer } from '../renderers/NavigationRenderer.js';
|
import { NavigationRenderer } from '../renderers/NavigationRenderer';
|
||||||
import { calendarConfig } from '../core/CalendarConfig.js';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector';
|
||||||
import { SwpEventElement } from '../elements/SwpEventElement';
|
import { SwpEventElement } from '../elements/SwpEventElement';
|
||||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||||
import { PositionUtils } from '../utils/PositionUtils';
|
import { PositionUtils } from '../utils/PositionUtils';
|
||||||
|
import { DragOffset, StackLinkData } from '../types/DragDropTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -15,8 +16,8 @@ import { PositionUtils } from '../utils/PositionUtils';
|
||||||
export interface EventRendererStrategy {
|
export interface EventRendererStrategy {
|
||||||
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
|
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
|
||||||
clearEvents(container?: HTMLElement): void;
|
clearEvents(container?: HTMLElement): void;
|
||||||
handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void;
|
handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void;
|
||||||
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: any): void;
|
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void;
|
||||||
handleDragAutoScroll?(eventId: string, snappedY: number): void;
|
handleDragAutoScroll?(eventId: string, snappedY: number): void;
|
||||||
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
|
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
|
||||||
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
|
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
|
||||||
|
|
@ -159,7 +160,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
/**
|
/**
|
||||||
* Handle drag start event
|
* Handle drag start event
|
||||||
*/
|
*/
|
||||||
public handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
public handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void {
|
||||||
this.originalEvent = originalElement;
|
this.originalEvent = originalElement;
|
||||||
|
|
||||||
// Remove stacking styling during drag will be handled by new system
|
// Remove stacking styling during drag will be handled by new system
|
||||||
|
|
@ -195,7 +196,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
/**
|
/**
|
||||||
* Handle drag move event
|
* Handle drag move event
|
||||||
*/
|
*/
|
||||||
public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void {
|
public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void {
|
||||||
if (!this.draggedClone) return;
|
if (!this.draggedClone) return;
|
||||||
|
|
||||||
// Update position
|
// Update position
|
||||||
|
|
@ -259,7 +260,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
const allStackEventIds: Set<string> = new Set();
|
const allStackEventIds: Set<string> = new Set();
|
||||||
|
|
||||||
// Recursive funktion til at traversere stack chain
|
// Recursive funktion til at traversere stack chain
|
||||||
const traverseStack = (linkData: any, visitedIds: Set<string>) => {
|
const traverseStack = (linkData: StackLinkData, visitedIds: Set<string>) => {
|
||||||
if (linkData.prev && !visitedIds.has(linkData.prev)) {
|
if (linkData.prev && !visitedIds.has(linkData.prev)) {
|
||||||
visitedIds.add(linkData.prev);
|
visitedIds.add(linkData.prev);
|
||||||
const prevElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.prev}"]`) as HTMLElement;
|
const prevElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.prev}"]`) as HTMLElement;
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ export class EventRenderingService {
|
||||||
/**
|
/**
|
||||||
* Handle conversion from all-day event to time event
|
* Handle conversion from all-day event to time event
|
||||||
*/
|
*/
|
||||||
private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: any, column: string): void {
|
private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: { x: number; y: number }, column: string): void {
|
||||||
// Use the provided draggedElement directly
|
// Use the provided draggedElement directly
|
||||||
const allDayClone = draggedElement;
|
const allDayClone = draggedElement;
|
||||||
const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || '';
|
const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || '';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
import { ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
|
|
||||||
|
interface GridSettings {
|
||||||
|
hourHeight: number;
|
||||||
|
snapInterval: number;
|
||||||
|
dayStartHour: number;
|
||||||
|
dayEndHour: number;
|
||||||
|
workStartHour: number;
|
||||||
|
workEndHour: number;
|
||||||
|
fitToWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GridStyleManager - Manages CSS variables and styling for the grid
|
* GridStyleManager - Manages CSS variables and styling for the grid
|
||||||
* Separated from GridManager to follow Single Responsibility Principle
|
* Separated from GridManager to follow Single Responsibility Principle
|
||||||
|
|
@ -38,7 +48,7 @@ export class GridStyleManager {
|
||||||
/**
|
/**
|
||||||
* Set time-related CSS variables
|
* Set time-related CSS variables
|
||||||
*/
|
*/
|
||||||
private setTimeVariables(root: HTMLElement, gridSettings: any): void {
|
private setTimeVariables(root: HTMLElement, gridSettings: GridSettings): void {
|
||||||
root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`);
|
root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`);
|
||||||
root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`);
|
root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`);
|
||||||
root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString());
|
root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString());
|
||||||
|
|
@ -76,7 +86,7 @@ export class GridStyleManager {
|
||||||
/**
|
/**
|
||||||
* Set column width based on fitToWidth setting
|
* Set column width based on fitToWidth setting
|
||||||
*/
|
*/
|
||||||
private setColumnWidth(root: HTMLElement, gridSettings: any): void {
|
private setColumnWidth(root: HTMLElement, gridSettings: GridSettings): void {
|
||||||
if (gridSettings.fitToWidth) {
|
if (gridSettings.fitToWidth) {
|
||||||
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
|
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
|
import { ViewStrategy, ViewContext, ViewLayoutConfig } from './ViewStrategy';
|
||||||
import { DateCalculator } from '../utils/DateCalculator';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { CalendarEvent } from '../types/CalendarTypes';
|
||||||
|
|
||||||
export class MonthViewStrategy implements ViewStrategy {
|
export class MonthViewStrategy implements ViewStrategy {
|
||||||
private dateCalculator: DateCalculator;
|
private dateCalculator: DateCalculator;
|
||||||
|
|
@ -113,7 +114,7 @@ export class MonthViewStrategy implements ViewStrategy {
|
||||||
return dates;
|
return dates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderMonthEvents(container: HTMLElement, events: any[]): void {
|
private renderMonthEvents(container: HTMLElement, events: CalendarEvent[]): void {
|
||||||
// TODO: Implement month event rendering
|
// TODO: Implement month event rendering
|
||||||
// Events will be small blocks in day cells
|
// Events will be small blocks in day cells
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export interface CalendarConfig {
|
||||||
|
|
||||||
export interface EventLogEntry {
|
export interface EventLogEntry {
|
||||||
type: string;
|
type: string;
|
||||||
detail: any;
|
detail: unknown;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ export interface IEventBus {
|
||||||
on(eventType: string, handler: EventListener, options?: AddEventListenerOptions): () => void;
|
on(eventType: string, handler: EventListener, options?: AddEventListenerOptions): () => void;
|
||||||
once(eventType: string, handler: EventListener): () => void;
|
once(eventType: string, handler: EventListener): () => void;
|
||||||
off(eventType: string, handler: EventListener): void;
|
off(eventType: string, handler: EventListener): void;
|
||||||
emit(eventType: string, detail?: any): boolean;
|
emit(eventType: string, detail?: unknown): boolean;
|
||||||
getEventLog(eventType?: string): EventLogEntry[];
|
getEventLog(eventType?: string): EventLogEntry[];
|
||||||
setDebug(enabled: boolean): void;
|
setDebug(enabled: boolean): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
|
|
|
||||||
47
src/types/DragDropTypes.ts
Normal file
47
src/types/DragDropTypes.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Type definitions for drag and drop functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MousePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
clientX?: number;
|
||||||
|
clientY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragOffset {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
offsetX?: number;
|
||||||
|
offsetY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragState {
|
||||||
|
isDragging: boolean;
|
||||||
|
draggedElement: HTMLElement | null;
|
||||||
|
draggedClone: HTMLElement | null;
|
||||||
|
eventId: string | null;
|
||||||
|
startColumn: string | null;
|
||||||
|
currentColumn: string | null;
|
||||||
|
mouseOffset: DragOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragEndPosition {
|
||||||
|
column: string;
|
||||||
|
y: number;
|
||||||
|
snappedY: number;
|
||||||
|
time?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackLinkData {
|
||||||
|
prev?: string;
|
||||||
|
next?: string;
|
||||||
|
isFirst?: boolean;
|
||||||
|
isLast?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragEventHandlers {
|
||||||
|
handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void;
|
||||||
|
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void;
|
||||||
|
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
|
||||||
|
}
|
||||||
177
src/types/EventPayloadMap.ts
Normal file
177
src/types/EventPayloadMap.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { CalendarEvent, CalendarView } from './CalendarTypes';
|
||||||
|
import {
|
||||||
|
DragStartEventPayload,
|
||||||
|
DragMoveEventPayload,
|
||||||
|
DragEndEventPayload,
|
||||||
|
DragMouseEnterHeaderEventPayload,
|
||||||
|
DragMouseLeaveHeaderEventPayload,
|
||||||
|
HeaderReadyEventPayload
|
||||||
|
} from './EventTypes';
|
||||||
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete type mapping for all calendar events
|
||||||
|
* This enables type-safe event emission and handling
|
||||||
|
*/
|
||||||
|
export interface CalendarEventPayloadMap {
|
||||||
|
// Lifecycle events
|
||||||
|
[CoreEvents.INITIALIZED]: {
|
||||||
|
initialized: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
[CoreEvents.READY]: undefined;
|
||||||
|
[CoreEvents.DESTROYED]: undefined;
|
||||||
|
|
||||||
|
// View events
|
||||||
|
[CoreEvents.VIEW_CHANGED]: {
|
||||||
|
view: CalendarView;
|
||||||
|
previousView?: CalendarView;
|
||||||
|
};
|
||||||
|
[CoreEvents.VIEW_RENDERED]: {
|
||||||
|
view: CalendarView;
|
||||||
|
};
|
||||||
|
[CoreEvents.WORKWEEK_CHANGED]: {
|
||||||
|
settings: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation events
|
||||||
|
[CoreEvents.DATE_CHANGED]: {
|
||||||
|
date: Date;
|
||||||
|
view?: CalendarView;
|
||||||
|
};
|
||||||
|
[CoreEvents.NAVIGATION_COMPLETED]: {
|
||||||
|
direction: 'previous' | 'next' | 'today';
|
||||||
|
};
|
||||||
|
[CoreEvents.PERIOD_INFO_UPDATE]: {
|
||||||
|
label: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
};
|
||||||
|
[CoreEvents.NAVIGATE_TO_EVENT]: {
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Data events
|
||||||
|
[CoreEvents.DATA_LOADING]: undefined;
|
||||||
|
[CoreEvents.DATA_LOADED]: {
|
||||||
|
events: CalendarEvent[];
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
[CoreEvents.DATA_ERROR]: {
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
[CoreEvents.EVENTS_FILTERED]: {
|
||||||
|
filteredEvents: CalendarEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grid events
|
||||||
|
[CoreEvents.GRID_RENDERED]: {
|
||||||
|
container: HTMLElement;
|
||||||
|
currentDate: Date;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
columnCount: number;
|
||||||
|
};
|
||||||
|
[CoreEvents.GRID_CLICKED]: {
|
||||||
|
column: string;
|
||||||
|
row: number;
|
||||||
|
};
|
||||||
|
[CoreEvents.CELL_SELECTED]: {
|
||||||
|
cell: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event management
|
||||||
|
[CoreEvents.EVENT_CREATED]: {
|
||||||
|
event: CalendarEvent;
|
||||||
|
};
|
||||||
|
[CoreEvents.EVENT_UPDATED]: {
|
||||||
|
event: CalendarEvent;
|
||||||
|
previousData?: Partial<CalendarEvent>;
|
||||||
|
};
|
||||||
|
[CoreEvents.EVENT_DELETED]: {
|
||||||
|
eventId: string;
|
||||||
|
};
|
||||||
|
[CoreEvents.EVENT_SELECTED]: {
|
||||||
|
eventId: string;
|
||||||
|
event?: CalendarEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// System events
|
||||||
|
[CoreEvents.ERROR]: {
|
||||||
|
error: Error;
|
||||||
|
context?: string;
|
||||||
|
};
|
||||||
|
[CoreEvents.REFRESH_REQUESTED]: {
|
||||||
|
view?: CalendarView;
|
||||||
|
date?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter events
|
||||||
|
[CoreEvents.FILTER_CHANGED]: {
|
||||||
|
activeFilters: string[];
|
||||||
|
visibleEvents: CalendarEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rendering events
|
||||||
|
[CoreEvents.EVENTS_RENDERED]: {
|
||||||
|
eventCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag events
|
||||||
|
'drag:start': DragStartEventPayload;
|
||||||
|
'drag:move': DragMoveEventPayload;
|
||||||
|
'drag:end': DragEndEventPayload;
|
||||||
|
'drag:mouseenter-header': DragMouseEnterHeaderEventPayload;
|
||||||
|
'drag:mouseleave-header': DragMouseLeaveHeaderEventPayload;
|
||||||
|
'drag:cancelled': {
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Header events
|
||||||
|
'header:ready': HeaderReadyEventPayload;
|
||||||
|
'header:height-changed': {
|
||||||
|
height: number;
|
||||||
|
rowCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// All-day events
|
||||||
|
'allday:checkHeight': undefined;
|
||||||
|
'allday:convert-to-allday': {
|
||||||
|
eventId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
'allday:convert-from-allday': {
|
||||||
|
eventId: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scroll events
|
||||||
|
'scroll:sync': {
|
||||||
|
scrollTop: number;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
'scroll:to-hour': {
|
||||||
|
hour: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter events
|
||||||
|
'filter:updated': {
|
||||||
|
activeFilters: string[];
|
||||||
|
visibleEvents: CalendarEvent[];
|
||||||
|
};
|
||||||
|
'filter:search': {
|
||||||
|
query: string;
|
||||||
|
results: CalendarEvent[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper type to get payload type for a specific event
|
||||||
|
export type EventPayload<T extends keyof CalendarEventPayloadMap> = CalendarEventPayloadMap[T];
|
||||||
|
|
||||||
|
// Type guard to check if an event has a payload
|
||||||
|
export function hasPayload<T extends keyof CalendarEventPayloadMap>(
|
||||||
|
eventType: T,
|
||||||
|
payload: unknown
|
||||||
|
): payload is CalendarEventPayloadMap[T] {
|
||||||
|
return payload !== undefined;
|
||||||
|
}
|
||||||
92
src/types/ManagerTypes.ts
Normal file
92
src/types/ManagerTypes.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { IEventBus, CalendarEvent, CalendarView } from './CalendarTypes';
|
||||||
|
import { IManager } from '../interfaces/IManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete type definition for all managers returned by ManagerFactory
|
||||||
|
*/
|
||||||
|
export interface CalendarManagers {
|
||||||
|
eventManager: EventManager;
|
||||||
|
eventRenderer: EventRenderingService;
|
||||||
|
gridManager: GridManager;
|
||||||
|
scrollManager: ScrollManager;
|
||||||
|
navigationManager: unknown; // Avoid interface conflicts
|
||||||
|
viewManager: ViewManager;
|
||||||
|
calendarManager: CalendarManager;
|
||||||
|
dragDropManager: unknown; // Avoid interface conflicts
|
||||||
|
allDayManager: unknown; // Avoid interface conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventManager extends IManager {
|
||||||
|
loadData(): Promise<void>;
|
||||||
|
getEvents(): CalendarEvent[];
|
||||||
|
getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[];
|
||||||
|
getResourceData(): ResourceData | null;
|
||||||
|
navigateToEvent(eventId: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventRenderingService extends IManager {
|
||||||
|
// EventRenderingService doesn't have a render method in current implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridManager extends IManager {
|
||||||
|
render(): Promise<void>;
|
||||||
|
getDisplayDates(): Date[];
|
||||||
|
setResourceData(resourceData: import('./CalendarTypes').ResourceCalendarData | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrollManager extends IManager {
|
||||||
|
scrollTo(scrollTop: number): void;
|
||||||
|
scrollToHour(hour: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a more flexible interface that matches actual implementation
|
||||||
|
export interface NavigationManager extends IManager {
|
||||||
|
[key: string]: unknown; // Allow any properties from actual implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewManager extends IManager {
|
||||||
|
// ViewManager doesn't have setView in current implementation
|
||||||
|
getCurrentView?(): CalendarView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarManager extends IManager {
|
||||||
|
setView(view: CalendarView): void;
|
||||||
|
setCurrentDate(date: Date): void;
|
||||||
|
getInitializationReport(): InitializationReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DragDropManager extends IManager {
|
||||||
|
// DragDropManager has different interface in current implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllDayManager extends IManager {
|
||||||
|
[key: string]: unknown; // Allow any properties from actual implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceData {
|
||||||
|
resources: Resource[];
|
||||||
|
assignments?: ResourceAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceAssignment {
|
||||||
|
resourceId: string;
|
||||||
|
eventId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializationReport {
|
||||||
|
initialized: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
managers: {
|
||||||
|
[key: string]: {
|
||||||
|
initialized: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { calendarConfig } from '../core/CalendarConfig.js';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { DateCalculator } from './DateCalculator.js';
|
import { DateCalculator } from './DateCalculator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PositionUtils - Static positioning utilities using singleton calendarConfig
|
* PositionUtils - Static positioning utilities using singleton calendarConfig
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue