Refactor calendar V2 core with DI and new features
Introduces dependency injection container and composition root Adds core services like DateService and NavigationAnimator Simplifies CalendarOrchestrator with improved store handling Implements mock stores and demo application for V2 calendar
This commit is contained in:
parent
1ad7d10266
commit
a0c0ef9e8d
17 changed files with 331 additions and 134 deletions
16
build.js
16
build.js
|
|
@ -61,6 +61,22 @@ async function build() {
|
|||
|
||||
console.log('V2 bundle created: wwwroot/js/calendar-v2.js');
|
||||
|
||||
// V2 demo bundle (with DI transformer for autowiring)
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/v2/demo/index.ts'],
|
||||
bundle: true,
|
||||
outfile: 'wwwroot/js/v2-demo.js',
|
||||
format: 'esm',
|
||||
sourcemap: 'inline',
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
keepNames: true,
|
||||
platform: 'browser',
|
||||
plugins: [NovadiUnplugin.esbuild({ debug: false, enableAutowiring: true })]
|
||||
});
|
||||
|
||||
console.log('V2 demo bundle created: wwwroot/js/v2-demo.js');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
|
|
|
|||
53
src/v2/V2CompositionRoot.ts
Normal file
53
src/v2/V2CompositionRoot.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Container } from '@novadi/core';
|
||||
import { IGroupingRenderer } from './core/IGroupingRenderer';
|
||||
import { IGroupingStore } from './core/IGroupingStore';
|
||||
import { DateRenderer } from './features/date/DateRenderer';
|
||||
import { DateService } from './core/DateService';
|
||||
import { ITimeFormatConfig } from './core/ITimeFormatConfig';
|
||||
import { ResourceRenderer } from './features/resource/ResourceRenderer';
|
||||
import { TeamRenderer } from './features/team/TeamRenderer';
|
||||
import { RendererRegistry } from './core/RendererRegistry';
|
||||
import { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||
import { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||
import { MockTeamStore, MockResourceStore } from './demo/MockStores';
|
||||
import { DemoApp } from './demo/DemoApp';
|
||||
|
||||
const defaultTimeFormatConfig: ITimeFormatConfig = {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
use24HourFormat: true,
|
||||
locale: 'da-DK',
|
||||
dateFormat: 'locale',
|
||||
showSeconds: false
|
||||
};
|
||||
|
||||
export function createV2Container(): Container {
|
||||
const container = new Container();
|
||||
const builder = container.builder();
|
||||
|
||||
// Config
|
||||
builder.registerInstance(defaultTimeFormatConfig).as<ITimeFormatConfig>();
|
||||
|
||||
// Services
|
||||
builder.registerType(DateService).as<DateService>();
|
||||
|
||||
// Renderers - registreres som IGroupingRenderer
|
||||
builder.registerType(DateRenderer).as<IGroupingRenderer>();
|
||||
builder.registerType(ResourceRenderer).as<IGroupingRenderer>();
|
||||
builder.registerType(TeamRenderer).as<IGroupingRenderer>();
|
||||
|
||||
// RendererRegistry modtager IGroupingRenderer[] automatisk (array injection)
|
||||
builder.registerType(RendererRegistry).as<RendererRegistry>();
|
||||
|
||||
// Stores - registreres som IGroupingStore
|
||||
builder.registerType(MockTeamStore).as<IGroupingStore>();
|
||||
builder.registerType(MockResourceStore).as<IGroupingStore>();
|
||||
|
||||
// CalendarOrchestrator modtager IGroupingStore[] automatisk (array injection)
|
||||
builder.registerType(CalendarOrchestrator).as<CalendarOrchestrator>();
|
||||
builder.registerType(TimeAxisRenderer).as<TimeAxisRenderer>();
|
||||
|
||||
// Demo app
|
||||
builder.registerType(DemoApp).as<DemoApp>();
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { ViewConfig, GroupingConfig } from './ViewConfig';
|
||||
import { RenderContext } from './RenderContext';
|
||||
import { RendererRegistry } from './RendererRegistry';
|
||||
import { IStoreRegistry } from './IGroupingStore';
|
||||
import { IGroupingStore } from './IGroupingStore';
|
||||
|
||||
interface HierarchyNode {
|
||||
type: string;
|
||||
|
|
@ -19,9 +19,13 @@ interface GroupingData {
|
|||
export class CalendarOrchestrator {
|
||||
constructor(
|
||||
private rendererRegistry: RendererRegistry,
|
||||
private storeRegistry: IStoreRegistry
|
||||
private stores: IGroupingStore[]
|
||||
) {}
|
||||
|
||||
private getStore(type: string): IGroupingStore | undefined {
|
||||
return this.stores.find(s => s.type === type);
|
||||
}
|
||||
|
||||
async render(viewConfig: ViewConfig, container: HTMLElement): Promise<void> {
|
||||
const headerContainer = container.querySelector('swp-calendar-header') as HTMLElement;
|
||||
const columnContainer = container.querySelector('swp-day-columns') as HTMLElement;
|
||||
|
|
@ -66,7 +70,9 @@ export class CalendarOrchestrator {
|
|||
continue;
|
||||
}
|
||||
|
||||
const rawItems = this.storeRegistry.get(g.type).getByIds(g.values);
|
||||
const store = this.getStore(g.type);
|
||||
if (!store) continue;
|
||||
const rawItems = store.getByIds(g.values);
|
||||
const items = rawItems.map((item: any) => ({ id: item.id, data: item }));
|
||||
const byParent = g.parentKey
|
||||
? this.groupBy(items, item => (item.data as any)[g.parentKey!])
|
||||
|
|
|
|||
21
src/v2/core/DateService.ts
Normal file
21
src/v2/core/DateService.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { ITimeFormatConfig } from './ITimeFormatConfig';
|
||||
|
||||
export class DateService {
|
||||
constructor(private config: ITimeFormatConfig) {}
|
||||
|
||||
parseISO(isoString: string): Date {
|
||||
return dayjs(isoString).toDate();
|
||||
}
|
||||
|
||||
getDayName(date: Date, format: 'short' | 'long' = 'short'): string {
|
||||
return new Intl.DateTimeFormat(this.config.locale, { weekday: format }).format(date);
|
||||
}
|
||||
|
||||
getWeekDates(offset = 0): string[] {
|
||||
const monday = dayjs().startOf('week').add(1, 'day').add(offset, 'week');
|
||||
return Array.from({ length: 5 }, (_, i) =>
|
||||
monday.add(i, 'day').format('YYYY-MM-DD')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
export interface IGroupingStore<T = unknown> {
|
||||
readonly type: string;
|
||||
getByIds(ids: string[]): T[];
|
||||
}
|
||||
|
||||
export interface IStoreRegistry {
|
||||
get(type: string): IGroupingStore;
|
||||
}
|
||||
|
|
|
|||
7
src/v2/core/ITimeFormatConfig.ts
Normal file
7
src/v2/core/ITimeFormatConfig.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface ITimeFormatConfig {
|
||||
timezone: string;
|
||||
use24HourFormat: boolean;
|
||||
locale: string;
|
||||
dateFormat: 'locale' | 'technical';
|
||||
showSeconds: boolean;
|
||||
}
|
||||
41
src/v2/core/NavigationAnimator.ts
Normal file
41
src/v2/core/NavigationAnimator.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export class NavigationAnimator {
|
||||
constructor(
|
||||
private headerTrack: HTMLElement,
|
||||
private contentTrack: HTMLElement
|
||||
) {}
|
||||
|
||||
async slide(direction: 'left' | 'right', renderFn: () => Promise<void>): Promise<void> {
|
||||
const out = direction === 'left' ? '-100%' : '100%';
|
||||
const into = direction === 'left' ? '100%' : '-100%';
|
||||
|
||||
await this.animateOut(out);
|
||||
await renderFn();
|
||||
await this.animateIn(into);
|
||||
}
|
||||
|
||||
private async animateOut(translate: string): Promise<void> {
|
||||
await Promise.all([
|
||||
this.headerTrack.animate(
|
||||
[{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
|
||||
{ duration: 200, easing: 'ease-in' }
|
||||
).finished,
|
||||
this.contentTrack.animate(
|
||||
[{ transform: 'translateX(0)' }, { transform: `translateX(${translate})` }],
|
||||
{ duration: 200, easing: 'ease-in' }
|
||||
).finished
|
||||
]);
|
||||
}
|
||||
|
||||
private async animateIn(translate: string): Promise<void> {
|
||||
await Promise.all([
|
||||
this.headerTrack.animate(
|
||||
[{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
|
||||
{ duration: 200, easing: 'ease-out' }
|
||||
).finished,
|
||||
this.contentTrack.animate(
|
||||
[{ transform: `translateX(${translate})` }, { transform: 'translateX(0)' }],
|
||||
{ duration: 200, easing: 'ease-out' }
|
||||
).finished
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { IGroupingStore, IStoreRegistry } from './IGroupingStore';
|
||||
|
||||
export class StoreRegistry implements IStoreRegistry {
|
||||
private stores = new Map<string, IGroupingStore>();
|
||||
|
||||
register(type: string, store: IGroupingStore): void {
|
||||
this.stores.set(type, store);
|
||||
}
|
||||
|
||||
get(type: string): IGroupingStore {
|
||||
const store = this.stores.get(type);
|
||||
if (!store) throw new Error(`No store for type: ${type}`);
|
||||
return store;
|
||||
}
|
||||
}
|
||||
84
src/v2/demo/DemoApp.ts
Normal file
84
src/v2/demo/DemoApp.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { CalendarOrchestrator } from '../core/CalendarOrchestrator';
|
||||
import { TimeAxisRenderer } from '../features/timeaxis/TimeAxisRenderer';
|
||||
import { NavigationAnimator } from '../core/NavigationAnimator';
|
||||
import { DateService } from '../core/DateService';
|
||||
import { ViewConfig } from '../core/ViewConfig';
|
||||
|
||||
export class DemoApp {
|
||||
private animator!: NavigationAnimator;
|
||||
private container!: HTMLElement;
|
||||
private weekOffset = 0;
|
||||
private views!: Record<string, ViewConfig>;
|
||||
|
||||
constructor(
|
||||
private orchestrator: CalendarOrchestrator,
|
||||
private timeAxisRenderer: TimeAxisRenderer,
|
||||
private dateService: DateService
|
||||
) {}
|
||||
|
||||
init(): void {
|
||||
this.container = document.querySelector('swp-calendar-container') as HTMLElement;
|
||||
|
||||
// NavigationAnimator har DOM-dependencies - tilladt med new
|
||||
this.animator = new NavigationAnimator(
|
||||
document.querySelector('swp-header-track') as HTMLElement,
|
||||
document.querySelector('swp-content-track') as HTMLElement
|
||||
);
|
||||
|
||||
// View configs
|
||||
const dates = this.dateService.getWeekDates();
|
||||
this.views = {
|
||||
simple: { templateId: 'simple', groupings: [{ type: 'date', values: dates }] },
|
||||
resource: {
|
||||
templateId: 'resource',
|
||||
groupings: [
|
||||
{ type: 'resource', values: ['alice', 'bob', 'carol'] },
|
||||
{ type: 'date', values: dates.slice(0, 3) }
|
||||
]
|
||||
},
|
||||
team: {
|
||||
templateId: 'team',
|
||||
groupings: [
|
||||
{ type: 'team', values: ['alpha', 'beta'] },
|
||||
{ type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' },
|
||||
{ type: 'date', values: dates.slice(0, 3) }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Render time axis
|
||||
this.timeAxisRenderer.render(document.getElementById('time-axis') as HTMLElement);
|
||||
|
||||
// Setup event handlers
|
||||
this.setupNavigation();
|
||||
this.setupViewSwitchers();
|
||||
|
||||
// Initial render
|
||||
this.orchestrator.render(this.views.simple, this.container);
|
||||
}
|
||||
|
||||
private setupNavigation(): void {
|
||||
document.getElementById('btn-prev')!.onclick = () => {
|
||||
this.weekOffset--;
|
||||
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
||||
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
||||
};
|
||||
|
||||
document.getElementById('btn-next')!.onclick = () => {
|
||||
this.weekOffset++;
|
||||
this.views.simple.groupings[0].values = this.dateService.getWeekDates(this.weekOffset);
|
||||
this.animator.slide('left', () => this.orchestrator.render(this.views.simple, this.container));
|
||||
};
|
||||
}
|
||||
|
||||
private setupViewSwitchers(): void {
|
||||
document.getElementById('btn-simple')!.onclick = () =>
|
||||
this.animator.slide('right', () => this.orchestrator.render(this.views.simple, this.container));
|
||||
|
||||
document.getElementById('btn-resource')!.onclick = () =>
|
||||
this.animator.slide('left', () => this.orchestrator.render(this.views.resource, this.container));
|
||||
|
||||
document.getElementById('btn-team')!.onclick = () =>
|
||||
this.animator.slide('left', () => this.orchestrator.render(this.views.team, this.container));
|
||||
}
|
||||
}
|
||||
40
src/v2/demo/MockStores.ts
Normal file
40
src/v2/demo/MockStores.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { IGroupingStore } from '../core/IGroupingStore';
|
||||
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
id: string;
|
||||
name: string;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
export class MockTeamStore implements IGroupingStore<Team> {
|
||||
readonly type = 'team';
|
||||
|
||||
private teams: Team[] = [
|
||||
{ id: 'alpha', name: 'Team Alpha' },
|
||||
{ id: 'beta', name: 'Team Beta' }
|
||||
];
|
||||
|
||||
getByIds(ids: string[]): Team[] {
|
||||
return this.teams.filter(t => ids.includes(t.id));
|
||||
}
|
||||
}
|
||||
|
||||
export class MockResourceStore implements IGroupingStore<Resource> {
|
||||
readonly type = 'resource';
|
||||
|
||||
private resources: Resource[] = [
|
||||
{ id: 'alice', name: 'Alice', teamId: 'alpha' },
|
||||
{ id: 'bob', name: 'Bob', teamId: 'alpha' },
|
||||
{ id: 'carol', name: 'Carol', teamId: 'beta' },
|
||||
{ id: 'dave', name: 'Dave', teamId: 'beta' }
|
||||
];
|
||||
|
||||
getByIds(ids: string[]): Resource[] {
|
||||
return this.resources.filter(r => ids.includes(r.id));
|
||||
}
|
||||
}
|
||||
5
src/v2/demo/index.ts
Normal file
5
src/v2/demo/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createV2Container } from '../V2CompositionRoot';
|
||||
import { DemoApp } from './DemoApp';
|
||||
|
||||
const app = createV2Container();
|
||||
app.resolveType<DemoApp>().init();
|
||||
|
|
@ -1,20 +1,11 @@
|
|||
import { IGroupingRenderer } from '../../core/IGroupingRenderer';
|
||||
import { RenderContext } from '../../core/RenderContext';
|
||||
|
||||
export interface IDateService {
|
||||
parseISO(dateStr: string): Date;
|
||||
getDayName(date: Date, format: 'short' | 'long'): string;
|
||||
}
|
||||
|
||||
export const defaultDateService: IDateService = {
|
||||
parseISO: (str) => new Date(str),
|
||||
getDayName: (date, format) => date.toLocaleDateString('da-DK', { weekday: format })
|
||||
};
|
||||
import { DateService } from '../../core/DateService';
|
||||
|
||||
export class DateRenderer implements IGroupingRenderer {
|
||||
readonly type = 'date';
|
||||
|
||||
constructor(private dateService: IDateService = defaultDateService) {}
|
||||
constructor(private dateService: DateService) {}
|
||||
|
||||
render(context: RenderContext): void {
|
||||
for (const dateStr of context.values) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { DateRenderer, IDateService, defaultDateService } from './DateRenderer';
|
||||
export { DateRenderer } from './DateRenderer';
|
||||
|
|
|
|||
10
src/v2/features/timeaxis/TimeAxisRenderer.ts
Normal file
10
src/v2/features/timeaxis/TimeAxisRenderer.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export class TimeAxisRenderer {
|
||||
render(container: HTMLElement, startHour = 6, endHour = 20): void {
|
||||
container.innerHTML = '';
|
||||
for (let hour = startHour; hour <= endHour; hour++) {
|
||||
const marker = document.createElement('swp-hour-marker');
|
||||
marker.textContent = `${hour.toString().padStart(2, '0')}:00`;
|
||||
container.appendChild(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,17 @@
|
|||
export { ViewTemplate, ViewConfig, GroupingConfig } from './core/ViewConfig';
|
||||
export { RenderContext } from './core/RenderContext';
|
||||
export { IGroupingRenderer } from './core/IGroupingRenderer';
|
||||
export { IGroupingStore, IStoreRegistry } from './core/IGroupingStore';
|
||||
export { IGroupingStore } from './core/IGroupingStore';
|
||||
export { RendererRegistry } from './core/RendererRegistry';
|
||||
export { StoreRegistry } from './core/StoreRegistry';
|
||||
export { CalendarOrchestrator } from './core/CalendarOrchestrator';
|
||||
export { NavigationAnimator } from './core/NavigationAnimator';
|
||||
|
||||
// Feature exports
|
||||
export { DateRenderer, IDateService, defaultDateService } from './features/date';
|
||||
export { DateRenderer } from './features/date';
|
||||
export { DateService } from './core/DateService';
|
||||
export { ITimeFormatConfig } from './core/ITimeFormatConfig';
|
||||
export { EventRenderer, IEventData, IEventStore } from './features/event';
|
||||
export { ResourceRenderer } from './features/resource';
|
||||
export { TeamRenderer } from './features/team';
|
||||
export { TimeAxisRenderer } from './features/timeaxis/TimeAxisRenderer';
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,22 @@ swp-grid-container {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Viewport/Track for slide animation */
|
||||
swp-header-viewport,
|
||||
swp-content-viewport {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
swp-header-track,
|
||||
swp-content-track {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
swp-header-track > swp-calendar-header,
|
||||
swp-content-track > swp-scrollable-content {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
swp-calendar-header {
|
||||
display: grid;
|
||||
|
|
|
|||
101
wwwroot/v2.html
101
wwwroot/v2.html
|
|
@ -17,6 +17,8 @@
|
|||
<swp-week-number>V2</swp-week-number>
|
||||
<swp-date-range id="view-info"></swp-date-range>
|
||||
</swp-week-info>
|
||||
<swp-nav-button id="btn-prev">←</swp-nav-button>
|
||||
<swp-nav-button id="btn-next">→</swp-nav-button>
|
||||
</swp-calendar-nav>
|
||||
|
||||
<swp-calendar-container>
|
||||
|
|
@ -25,107 +27,26 @@
|
|||
<swp-time-axis-content id="time-axis"></swp-time-axis-content>
|
||||
</swp-time-axis>
|
||||
<swp-grid-container>
|
||||
<swp-header-viewport>
|
||||
<swp-header-track>
|
||||
<swp-calendar-header></swp-calendar-header>
|
||||
</swp-header-track>
|
||||
</swp-header-viewport>
|
||||
<swp-content-viewport>
|
||||
<swp-content-track>
|
||||
<swp-scrollable-content>
|
||||
<swp-time-grid>
|
||||
<swp-grid-lines></swp-grid-lines>
|
||||
<swp-day-columns></swp-day-columns>
|
||||
</swp-time-grid>
|
||||
</swp-scrollable-content>
|
||||
</swp-content-track>
|
||||
</swp-content-viewport>
|
||||
</swp-grid-container>
|
||||
</swp-calendar-container>
|
||||
</swp-calendar>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
CalendarOrchestrator,
|
||||
RendererRegistry,
|
||||
StoreRegistry,
|
||||
DateRenderer,
|
||||
ResourceRenderer,
|
||||
TeamRenderer
|
||||
} from './js/calendar-v2.js';
|
||||
|
||||
const rendererRegistry = new RendererRegistry([
|
||||
new DateRenderer(),
|
||||
new ResourceRenderer(),
|
||||
new TeamRenderer()
|
||||
]);
|
||||
|
||||
const mockTeams = [
|
||||
{ id: 'alpha', name: 'Team Alpha' },
|
||||
{ id: 'beta', name: 'Team Beta' }
|
||||
];
|
||||
|
||||
const mockResources = [
|
||||
{ id: 'alice', name: 'Alice', teamId: 'alpha' },
|
||||
{ id: 'bob', name: 'Bob', teamId: 'alpha' },
|
||||
{ id: 'carol', name: 'Carol', teamId: 'beta' },
|
||||
{ id: 'dave', name: 'Dave', teamId: 'beta' }
|
||||
];
|
||||
|
||||
const storeRegistry = new StoreRegistry();
|
||||
storeRegistry.register('team', { getByIds: ids => mockTeams.filter(t => ids.includes(t.id)) });
|
||||
storeRegistry.register('resource', { getByIds: ids => mockResources.filter(r => ids.includes(r.id)) });
|
||||
|
||||
const orchestrator = new CalendarOrchestrator(rendererRegistry, storeRegistry);
|
||||
const container = document.querySelector('swp-calendar-container');
|
||||
const viewInfo = document.getElementById('view-info');
|
||||
|
||||
function getWeekDates() {
|
||||
const today = new Date();
|
||||
const mon = new Date(today);
|
||||
mon.setDate(today.getDate() - today.getDay() + 1);
|
||||
return Array.from({ length: 5 }, (_, i) => {
|
||||
const d = new Date(mon);
|
||||
d.setDate(mon.getDate() + i);
|
||||
return d.toISOString().split('T')[0];
|
||||
});
|
||||
}
|
||||
|
||||
const dates = getWeekDates();
|
||||
|
||||
const views = {
|
||||
simple: {
|
||||
templateId: 'simple',
|
||||
groupings: [{ type: 'date', values: dates }]
|
||||
},
|
||||
resource: {
|
||||
templateId: 'resource',
|
||||
groupings: [
|
||||
{ type: 'resource', values: ['alice', 'bob', 'carol'] },
|
||||
{ type: 'date', values: dates.slice(0, 3) }
|
||||
]
|
||||
},
|
||||
team: {
|
||||
templateId: 'team',
|
||||
groupings: [
|
||||
{ type: 'team', values: ['alpha', 'beta'] },
|
||||
{ type: 'resource', values: ['alice', 'bob', 'carol', 'dave'], parentKey: 'teamId' },
|
||||
{ type: 'date', values: dates.slice(0, 3) }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function generateTimeAxis() {
|
||||
const el = document.getElementById('time-axis');
|
||||
el.innerHTML = Array.from({ length: 15 }, (_, i) =>
|
||||
`<swp-hour-marker>${(6 + i).toString().padStart(2, '0')}:00</swp-hour-marker>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function render(view, label) {
|
||||
viewInfo.textContent = label;
|
||||
await orchestrator.render(view, container);
|
||||
}
|
||||
|
||||
document.getElementById('btn-simple').onclick = () => render(views.simple, '5 datoer');
|
||||
document.getElementById('btn-resource').onclick = () => render(views.resource, '3 resources × 3 datoer');
|
||||
document.getElementById('btn-team').onclick = () => render(views.team, '2 teams × 2 resources × 3 datoer');
|
||||
|
||||
generateTimeAxis();
|
||||
render(views.simple, '5 datoer');
|
||||
</script>
|
||||
<script type="module" src="js/v2-demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue