Adds navigation buttons management and refactors navigation

Introduces NavigationButtonsManager to handle navigation button interactions
Renames NavigationRenderer to WeekInfoRenderer for clarity
Adds new NAV_BUTTON_CLICKED event for better separation of concerns

Improves event-driven navigation workflow
This commit is contained in:
Janus C. H. Knudsen 2025-11-07 23:23:19 +01:00
parent 29ba0bfa37
commit bd8f5ae6c6
5 changed files with 102 additions and 30 deletions

View file

@ -13,7 +13,8 @@ export const CoreEvents = {
VIEW_RENDERED: 'view:rendered', VIEW_RENDERED: 'view:rendered',
WORKWEEK_CHANGED: 'workweek:changed', WORKWEEK_CHANGED: 'workweek:changed',
// Navigation events (4) // Navigation events (5)
NAV_BUTTON_CLICKED: 'nav:button-clicked',
DATE_CHANGED: 'nav:date-changed', DATE_CHANGED: 'nav:date-changed',
NAVIGATION_COMPLETED: 'nav:navigation-completed', NAVIGATION_COMPLETED: 'nav:navigation-completed',
PERIOD_INFO_UPDATE: 'nav:period-info-update', PERIOD_INFO_UPDATE: 'nav:period-info-update',

View file

@ -12,6 +12,7 @@ import { EventRenderingService } from './renderers/EventRendererManager';
import { GridManager } from './managers/GridManager'; import { GridManager } from './managers/GridManager';
import { ScrollManager } from './managers/ScrollManager'; import { ScrollManager } from './managers/ScrollManager';
import { NavigationManager } from './managers/NavigationManager'; import { NavigationManager } from './managers/NavigationManager';
import { NavigationButtonsManager } from './managers/NavigationButtonsManager';
import { ViewSelectorManager } from './managers/ViewSelectorManager'; import { ViewSelectorManager } from './managers/ViewSelectorManager';
import { CalendarManager } from './managers/CalendarManager'; import { CalendarManager } from './managers/CalendarManager';
import { DragDropManager } from './managers/DragDropManager'; import { DragDropManager } from './managers/DragDropManager';
@ -38,7 +39,7 @@ import { DateColumnRenderer, type IColumnRenderer } from './renderers/ColumnRend
import { DateEventRenderer, type IEventRenderer } from './renderers/EventRenderer'; import { DateEventRenderer, type IEventRenderer } from './renderers/EventRenderer';
import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { AllDayEventRenderer } from './renderers/AllDayEventRenderer';
import { GridRenderer } from './renderers/GridRenderer'; import { GridRenderer } from './renderers/GridRenderer';
import { NavigationRenderer } from './renderers/NavigationRenderer'; import { WeekInfoRenderer } from './renderers/WeekInfoRenderer';
// Import utilities and services // Import utilities and services
import { DateService } from './utils/DateService'; import { DateService } from './utils/DateService';
@ -116,7 +117,7 @@ async function initializeCalendar(): Promise<void> {
builder.registerType(TimeFormatter).as<TimeFormatter>(); builder.registerType(TimeFormatter).as<TimeFormatter>();
builder.registerType(PositionUtils).as<PositionUtils>(); builder.registerType(PositionUtils).as<PositionUtils>();
// Note: AllDayLayoutEngine is instantiated per-operation with specific dates, not a singleton // Note: AllDayLayoutEngine is instantiated per-operation with specific dates, not a singleton
builder.registerType(NavigationRenderer).as<NavigationRenderer>(); builder.registerType(WeekInfoRenderer).as<WeekInfoRenderer>();
builder.registerType(AllDayEventRenderer).as<AllDayEventRenderer>(); builder.registerType(AllDayEventRenderer).as<AllDayEventRenderer>();
builder.registerType(EventRenderingService).as<EventRenderingService>(); builder.registerType(EventRenderingService).as<EventRenderingService>();
@ -124,6 +125,7 @@ async function initializeCalendar(): Promise<void> {
builder.registerType(GridManager).as<GridManager>(); builder.registerType(GridManager).as<GridManager>();
builder.registerType(ScrollManager).as<ScrollManager>(); builder.registerType(ScrollManager).as<ScrollManager>();
builder.registerType(NavigationManager).as<NavigationManager>(); builder.registerType(NavigationManager).as<NavigationManager>();
builder.registerType(NavigationButtonsManager).as<NavigationButtonsManager>();
builder.registerType(ViewSelectorManager).as<ViewSelectorManager>(); builder.registerType(ViewSelectorManager).as<ViewSelectorManager>();
builder.registerType(DragDropManager).as<DragDropManager>(); builder.registerType(DragDropManager).as<DragDropManager>();
builder.registerType(AllDayManager).as<AllDayManager>(); builder.registerType(AllDayManager).as<AllDayManager>();
@ -148,6 +150,7 @@ async function initializeCalendar(): Promise<void> {
const dragDropManager = app.resolveType<DragDropManager>(); const dragDropManager = app.resolveType<DragDropManager>();
const viewSelectorManager = app.resolveType<ViewSelectorManager>(); const viewSelectorManager = app.resolveType<ViewSelectorManager>();
const navigationManager = app.resolveType<NavigationManager>(); const navigationManager = app.resolveType<NavigationManager>();
const navigationButtonsManager = app.resolveType<NavigationButtonsManager>();
const edgeScrollManager = app.resolveType<EdgeScrollManager>(); const edgeScrollManager = app.resolveType<EdgeScrollManager>();
const allDayManager = app.resolveType<AllDayManager>(); const allDayManager = app.resolveType<AllDayManager>();
const urlManager = app.resolveType<URLManager>(); const urlManager = app.resolveType<URLManager>();

View file

@ -0,0 +1,71 @@
import { IEventBus } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
/**
* NavigationButtonsManager - Manages navigation button UI and state
*
* RESPONSIBILITY:
* ===============
* This manager owns all logic related to the <swp-nav-group> UI element.
* It follows the principle that each functional UI element has its own manager.
*
* RESPONSIBILITIES:
* - Handles button clicks on swp-nav-button elements
* - Validates navigation actions (prev, next, today)
* - Emits NAV_BUTTON_CLICKED events
* - Manages button UI listeners
*
* EVENT FLOW:
* ===========
* User clicks button validateAction() emit event NavigationManager handles navigation
*
* SUBSCRIBERS:
* ============
* - NavigationManager: Performs actual navigation logic (animations, grid updates, week calculations)
*/
export class NavigationButtonsManager {
private eventBus: IEventBus;
private buttonListeners: Map<Element, EventListener> = new Map();
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
this.setupButtonListeners();
}
/**
* Setup click listeners on all navigation buttons
*/
private setupButtonListeners(): void {
const buttons = document.querySelectorAll('swp-nav-button[data-action]');
buttons.forEach(button => {
const clickHandler = (event: Event) => {
event.preventDefault();
const action = button.getAttribute('data-action');
if (action && this.isValidAction(action)) {
this.handleNavigation(action);
}
};
button.addEventListener('click', clickHandler);
this.buttonListeners.set(button, clickHandler);
});
}
/**
* Handle navigation action
*/
private handleNavigation(action: string): void {
// Emit navigation button clicked event
this.eventBus.emit(CoreEvents.NAV_BUTTON_CLICKED, {
action: action
});
}
/**
* Validate if string is a valid navigation action
*/
private isValidAction(action: string): boolean {
return ['prev', 'next', 'today'].includes(action);
}
}

View file

@ -2,12 +2,12 @@ import { IEventBus } from '../types/CalendarTypes';
import { EventRenderingService } from '../renderers/EventRendererManager'; import { EventRenderingService } from '../renderers/EventRendererManager';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { NavigationRenderer } from '../renderers/NavigationRenderer'; import { WeekInfoRenderer } from '../renderers/WeekInfoRenderer';
import { GridRenderer } from '../renderers/GridRenderer'; import { GridRenderer } from '../renderers/GridRenderer';
export class NavigationManager { export class NavigationManager {
private eventBus: IEventBus; private eventBus: IEventBus;
private navigationRenderer: NavigationRenderer; private weekInfoRenderer: WeekInfoRenderer;
private gridRenderer: GridRenderer; private gridRenderer: GridRenderer;
private dateService: DateService; private dateService: DateService;
private currentWeek: Date; private currentWeek: Date;
@ -19,11 +19,11 @@ export class NavigationManager {
eventRenderer: EventRenderingService, eventRenderer: EventRenderingService,
gridRenderer: GridRenderer, gridRenderer: GridRenderer,
dateService: DateService, dateService: DateService,
navigationRenderer: NavigationRenderer weekInfoRenderer: WeekInfoRenderer
) { ) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.dateService = dateService; this.dateService = dateService;
this.navigationRenderer = navigationRenderer; this.weekInfoRenderer = weekInfoRenderer;
this.gridRenderer = gridRenderer; this.gridRenderer = gridRenderer;
this.currentWeek = this.getISOWeekStart(new Date()); this.currentWeek = this.getISOWeekStart(new Date());
this.targetWeek = new Date(this.currentWeek); this.targetWeek = new Date(this.currentWeek);
@ -54,17 +54,12 @@ export class NavigationManager {
// Listen for filter changes and apply to pre-rendered grids // Listen for filter changes and apply to pre-rendered grids
this.eventBus.on(CoreEvents.FILTER_CHANGED, (e: Event) => { this.eventBus.on(CoreEvents.FILTER_CHANGED, (e: Event) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
this.navigationRenderer.applyFilterToPreRenderedGrids(detail); this.weekInfoRenderer.applyFilterToPreRenderedGrids(detail);
}); });
// Listen for navigation button clicks // Listen for navigation button clicks from NavigationButtonsManager
document.addEventListener('click', (e) => { this.eventBus.on(CoreEvents.NAV_BUTTON_CLICKED, (event: Event) => {
const target = e.target as HTMLElement; const { action } = (event as CustomEvent).detail;
const navButton = target.closest('[data-action]') as HTMLElement;
if (!navButton) return;
const action = navButton.dataset.action;
switch (action) { switch (action) {
case 'prev': case 'prev':

View file

@ -3,20 +3,22 @@ import { CoreEvents } from '../constants/CoreEvents';
import { EventRenderingService } from './EventRendererManager'; import { EventRenderingService } from './EventRendererManager';
/** /**
* NavigationRenderer - Handles DOM rendering for navigation containers * WeekInfoRenderer - Handles DOM rendering for week info display
* Separated from NavigationManager to follow Single Responsibility Principle * Updates swp-week-number and swp-date-range elements
*
* Renamed from NavigationRenderer to better reflect its actual responsibility
*/ */
export class NavigationRenderer { export class WeekInfoRenderer {
private eventBus: IEventBus; private eventBus: IEventBus;
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.setupEventListeners(); this.setupEventListeners();
} }
/** /**
* Setup event listeners for DOM updates * Setup event listeners for DOM updates
*/ */
@ -28,36 +30,36 @@ export class NavigationRenderer {
}); });
} }
private updateWeekInfoInDOM(weekNumber: number, dateRange: string): void { private updateWeekInfoInDOM(weekNumber: number, dateRange: string): void {
const weekNumberElement = document.querySelector('swp-week-number'); const weekNumberElement = document.querySelector('swp-week-number');
const dateRangeElement = document.querySelector('swp-date-range'); const dateRangeElement = document.querySelector('swp-date-range');
if (weekNumberElement) { if (weekNumberElement) {
weekNumberElement.textContent = `Week ${weekNumber}`; weekNumberElement.textContent = `Week ${weekNumber}`;
} }
if (dateRangeElement) { if (dateRangeElement) {
dateRangeElement.textContent = dateRange; dateRangeElement.textContent = dateRange;
} }
} }
/** /**
* Apply filter state to pre-rendered grids * Apply filter state to pre-rendered grids
*/ */
public applyFilterToPreRenderedGrids(filterState: { active: boolean; matchingIds: string[] }): void { public applyFilterToPreRenderedGrids(filterState: { active: boolean; matchingIds: string[] }): void {
// Find all grid containers (including pre-rendered ones) // Find all grid containers (including pre-rendered ones)
const allGridContainers = document.querySelectorAll('swp-grid-container'); const allGridContainers = document.querySelectorAll('swp-grid-container');
allGridContainers.forEach(container => { allGridContainers.forEach(container => {
const eventsLayers = container.querySelectorAll('swp-events-layer'); const eventsLayers = container.querySelectorAll('swp-events-layer');
eventsLayers.forEach(layer => { eventsLayers.forEach(layer => {
if (filterState.active) { if (filterState.active) {
// Apply filter active state // Apply filter active state
layer.setAttribute('data-filter-active', 'true'); layer.setAttribute('data-filter-active', 'true');
// Mark matching events in this layer // Mark matching events in this layer
const events = layer.querySelectorAll('swp-event'); const events = layer.querySelectorAll('swp-event');
events.forEach(event => { events.forEach(event => {
@ -71,7 +73,7 @@ export class NavigationRenderer {
} else { } else {
// Remove filter state // Remove filter state
layer.removeAttribute('data-filter-active'); layer.removeAttribute('data-filter-active');
// Remove all match attributes // Remove all match attributes
const events = layer.querySelectorAll('swp-event'); const events = layer.querySelectorAll('swp-event');
events.forEach(event => { events.forEach(event => {
@ -82,4 +84,4 @@ export class NavigationRenderer {
}); });
} }
} }