From 70e505526fcfb8147fc70258427b0e2289b9e304 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sun, 7 Dec 2025 15:08:23 +0100 Subject: [PATCH] Adds scroll synchronization for calendar view Introduces ScrollManager to synchronize time axis and header scrolling Improves user experience by keeping time axis and header aligned during scrolling Enables dynamic vertical and horizontal scroll tracking for calendar components --- src/v2/V2CompositionRoot.ts | 2 ++ src/v2/core/ScrollManager.ts | 23 ++++++++++++++++++++ src/v2/demo/DemoApp.ts | 7 +++++- wwwroot/css/calendar-v2.css | 42 +++++++++++++++++++++++++++++------- 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 src/v2/core/ScrollManager.ts diff --git a/src/v2/V2CompositionRoot.ts b/src/v2/V2CompositionRoot.ts index 7fc9102..24f2518 100644 --- a/src/v2/V2CompositionRoot.ts +++ b/src/v2/V2CompositionRoot.ts @@ -9,6 +9,7 @@ import { TeamRenderer } from './features/team/TeamRenderer'; import { RendererRegistry } from './core/RendererRegistry'; import { CalendarOrchestrator } from './core/CalendarOrchestrator'; import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer'; +import { ScrollManager } from './core/ScrollManager'; import { MockTeamStore, MockResourceStore } from './demo/MockStores'; import { DemoApp } from './demo/DemoApp'; @@ -45,6 +46,7 @@ export function createV2Container(): Container { // CalendarOrchestrator modtager IGroupingStore[] automatisk (array injection) builder.registerType(CalendarOrchestrator).as(); builder.registerType(TimeAxisRenderer).as(); + builder.registerType(ScrollManager).as(); // Demo app builder.registerType(DemoApp).as(); diff --git a/src/v2/core/ScrollManager.ts b/src/v2/core/ScrollManager.ts new file mode 100644 index 0000000..3e4fbd5 --- /dev/null +++ b/src/v2/core/ScrollManager.ts @@ -0,0 +1,23 @@ +export class ScrollManager { + private scrollableContent!: HTMLElement; + private timeAxisContent!: HTMLElement; + private calendarHeader!: HTMLElement; + + init(container: HTMLElement): void { + this.scrollableContent = container.querySelector('swp-scrollable-content')!; + this.timeAxisContent = container.querySelector('swp-time-axis-content')!; + this.calendarHeader = container.querySelector('swp-calendar-header')!; + + this.scrollableContent.addEventListener('scroll', () => this.onScroll()); + } + + private onScroll(): void { + const { scrollTop, scrollLeft } = this.scrollableContent; + + // Synkroniser time-axis vertikalt + this.timeAxisContent.style.transform = `translateY(-${scrollTop}px)`; + + // Synkroniser header horisontalt + this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`; + } +} diff --git a/src/v2/demo/DemoApp.ts b/src/v2/demo/DemoApp.ts index 5c85f1a..708e0d5 100644 --- a/src/v2/demo/DemoApp.ts +++ b/src/v2/demo/DemoApp.ts @@ -2,6 +2,7 @@ import { CalendarOrchestrator } from '../core/CalendarOrchestrator'; import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer'; import { NavigationAnimator } from '../core/NavigationAnimator'; import { DateService } from '../core/DateService'; +import { ScrollManager } from '../core/ScrollManager'; import { ViewConfig } from '../core/ViewConfig'; export class DemoApp { @@ -13,7 +14,8 @@ export class DemoApp { constructor( private orchestrator: CalendarOrchestrator, private timeAxisRenderer: TimeAxisRenderer, - private dateService: DateService + private dateService: DateService, + private scrollManager: ScrollManager ) {} init(): void { @@ -49,6 +51,9 @@ export class DemoApp { // Render time axis this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement); + // Init scroll synkronisering + this.scrollManager.init(this.container); + // Setup event handlers this.setupNavigation(); this.setupViewSwitchers(); diff --git a/wwwroot/css/calendar-v2.css b/wwwroot/css/calendar-v2.css index b68a0ab..99aa182 100644 --- a/wwwroot/css/calendar-v2.css +++ b/wwwroot/css/calendar-v2.css @@ -2,6 +2,8 @@ --hour-height: 60px; --time-axis-width: 60px; --grid-columns: 5; + --day-start-hour: 0; + --day-end-hour: 24; --color-border: #e0e0e0; --color-surface: #fff; --color-text-secondary: #666; @@ -68,6 +70,7 @@ swp-calendar-container { grid-template-columns: var(--time-axis-width) 1fr; grid-template-rows: auto 1fr; overflow: hidden; + height: 100%; } /* Time axis */ @@ -78,6 +81,7 @@ swp-time-axis { grid-template-rows: subgrid; border-right: 1px solid var(--color-border); background: var(--color-surface); + overflow: hidden; } swp-header-spacer { @@ -85,8 +89,8 @@ swp-header-spacer { } swp-time-axis-content { - display: flex; - flex-direction: column; + display: grid; + grid-auto-rows: var(--hour-height); overflow: hidden; } @@ -108,19 +112,31 @@ swp-grid-container { } /* Viewport/Track for slide animation */ -swp-header-viewport, -swp-content-viewport { +swp-header-viewport { overflow: hidden; } -swp-header-track, -swp-content-track { +swp-content-viewport { + overflow: hidden; + min-height: 0; /* Tillader at krympe i grid */ +} + +swp-header-track { display: flex; } -swp-header-track > swp-calendar-header, +swp-content-track { + display: flex; + height: 100%; +} + +swp-header-track > swp-calendar-header { + flex: 0 0 100%; +} + swp-content-track > swp-scrollable-content { flex: 0 0 100%; + height: 100%; } /* Header */ @@ -129,6 +145,16 @@ swp-calendar-header { grid-template-columns: repeat(var(--grid-columns), 1fr); grid-auto-rows: auto; background: var(--color-surface); + overflow-y: scroll; + overflow-x: hidden; +} + +swp-calendar-header::-webkit-scrollbar { + background: transparent; +} + +swp-calendar-header::-webkit-scrollbar-thumb { + background: transparent; } swp-calendar-header[data-levels="date"] > swp-day-header { grid-row: 1; } @@ -182,7 +208,7 @@ swp-scrollable-content { swp-time-grid { display: block; position: relative; - min-height: calc(15 * var(--hour-height)); + min-height: calc((var(--day-end-hour) - var(--day-start-hour)) * var(--hour-height)); } swp-grid-lines {