217 lines
8.2 KiB
Text
217 lines
8.2 KiB
Text
|
|
De 6 vigtigste fund (med fixes)
|
|||
|
|
|
|||
|
|
Gruppering kan “brygge” mellem to grupper uden at merge dem
|
|||
|
|
groupEventsByStartTime finder første eksisterende gruppe med konflikt og lægger eventet deri. Hvis et nyt event konflikter med flere grupper, bliver grupperne ikke merged → inkonsistente “grid”-klumper. Løs: merge alle matchende grupper eller brug union-find/sweep-line konfliktsæt.
|
|||
|
|
|
|||
|
|
EventStackManager
|
|||
|
|
|
|||
|
|
ContainerType er “GRID” for alle grupper >1 — også ved dybe overlaps
|
|||
|
|
decideContainerType returnerer altid 'GRID' når events.length > 1. Det kan være tilsigtet, men så skal du være tryg ved, at lange overlappende events, der kun næsten starter samtidigt, stadig pakkes i kolonner fremfor “stacking”. Overvej: GRID kun når samtidighed er vigtigere end varighed, ellers fald tilbage til STACKING.
|
|||
|
|
|
|||
|
|
EventStackManager
|
|||
|
|
|
|||
|
|
Stack level-algoritmen kan eskalere niveauer unødigt
|
|||
|
|
createOptimizedStackLinks sætter stackLevel = max(overlappende tidligere) + 1. Det er mere “stak-tårn” end “før-ledig-kolonne” og giver højere niveauer end nødvendigt (ikke minimal farvelægning). Løs: interval partitioning med min-heap (giver laveste ledige level).
|
|||
|
|
|
|||
|
|
EventStackManager
|
|||
|
|
|
|||
|
|
Grid-top beregnes fra ét event, men børn positioneres relativt til containerStart
|
|||
|
|
I koordinatoren bruges earliestEvent til top, og renderer bruger earliestEvent.start som containerStart. Det er ok — men sørg for, at earliestEvent garanteret er det tidligste i gruppen og sortér eksplicit inden brug (robusthed mod fremtidige ændringer).
|
|||
|
|
|
|||
|
|
EventLayoutCoordinator
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
Drag bruger rå new Date(...) i stedet for DateService
|
|||
|
|
Kan give TZ/DST-glitches. Brug samme parse/logik som resten.
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
Ingen reflow af kolonne efter drop
|
|||
|
|
handleDragEnd normaliserer DOM men recalculerer ikke layout → forkert stacking/margin efter flyt. Kald din kolonne-pipeline igen for den berørte kolonne.
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
Bonus: getEventsForColumn matcher kun start-dato === kolonnedato; events der krydser midnat forsvinder. Overvej interval-overlap mod døgnets [00:00–23:59:59.999].
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
Målrettede patches (små og sikre)
|
|||
|
|
A) Merge grupper når et event rammer flere (EventStackManager)
|
|||
|
|
|
|||
|
|
Erstat den nuværende “find første gruppe”-logik med merge af alle matchende:
|
|||
|
|
|
|||
|
|
// inde i groupEventsByStartTime
|
|||
|
|
const matches: number[] = [];
|
|||
|
|
for (let gi = 0; gi < groups.length; gi++) {
|
|||
|
|
const group = groups[gi];
|
|||
|
|
const conflict = group.events.some(ge => {
|
|||
|
|
const s2s = Math.abs(event.start.getTime() - ge.start.getTime()) / 60000;
|
|||
|
|
if (s2s <= thresholdMinutes) return true;
|
|||
|
|
const e2s = (ge.end.getTime() - event.start.getTime()) / 60000;
|
|||
|
|
if (e2s > 0 && e2s <= thresholdMinutes) return true;
|
|||
|
|
const rev = (event.end.getTime() - ge.start.getTime()) / 60000;
|
|||
|
|
if (rev > 0 && rev <= thresholdMinutes) return true;
|
|||
|
|
return false;
|
|||
|
|
});
|
|||
|
|
if (conflict) matches.push(gi);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (matches.length === 0) {
|
|||
|
|
groups.push({ events: [event], containerType: 'NONE', startTime: event.start });
|
|||
|
|
} else {
|
|||
|
|
// merge alle matchende grupper + dette event
|
|||
|
|
const base = matches[0];
|
|||
|
|
groups[base].events.push(event);
|
|||
|
|
for (let i = matches.length - 1; i >= 1; i--) {
|
|||
|
|
const idx = matches[i];
|
|||
|
|
groups[base].events.push(...groups[idx].events);
|
|||
|
|
groups.splice(idx, 1);
|
|||
|
|
}
|
|||
|
|
// opdatér startTime til min start
|
|||
|
|
groups[base].startTime = new Date(
|
|||
|
|
Math.min(...groups[base].events.map(e => e.start.getTime()))
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
Nu undgår du “brobygning” der splitter reelt sammenhængende grupper.
|
|||
|
|
|
|||
|
|
EventStackManager
|
|||
|
|
|
|||
|
|
B) Minimal stack level med min-heap (EventStackManager)
|
|||
|
|
|
|||
|
|
Udskift level-tildeling med klassisk interval partitioning:
|
|||
|
|
|
|||
|
|
public createOptimizedStackLinks(events: CalendarEvent[]): Map<string, StackLink> {
|
|||
|
|
const res = new Map<string, StackLink>();
|
|||
|
|
if (!events.length) return res;
|
|||
|
|
|
|||
|
|
const sorted = [...events].sort((a,b)=> a.start.getTime() - b.start.getTime());
|
|||
|
|
type Col = { level: number; end: number };
|
|||
|
|
const cols: Col[] = []; // min-heap på end
|
|||
|
|
|
|||
|
|
const push = (c: Col) => { cols.push(c); cols.sort((x,y)=> x.end - y.end); };
|
|||
|
|
|
|||
|
|
for (const ev of sorted) {
|
|||
|
|
const t = ev.start.getTime();
|
|||
|
|
// find første kolonne der er fri
|
|||
|
|
let placed = false;
|
|||
|
|
for (let i = 0; i < cols.length; i++) {
|
|||
|
|
if (cols[i].end <= t) { cols[i].end = ev.end.getTime(); res.set(ev.id, { stackLevel: cols[i].level }); placed = true; break; }
|
|||
|
|
}
|
|||
|
|
if (!placed) { const level = cols.length; push({ level, end: ev.end.getTime() }); res.set(ev.id, { stackLevel: level }); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// evt. byg prev/next separat hvis nødvendigt
|
|||
|
|
return res;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
Dette giver laveste ledige niveau og undgår “trappetårne”.
|
|||
|
|
|
|||
|
|
EventStackManager
|
|||
|
|
|
|||
|
|
C) Konsolidér margin/zIndex + brug DateService i drag (EventRenderer)
|
|||
|
|
|
|||
|
|
Lad StackManager styre marginLeft konsekvent (og undgå magic numbers):
|
|||
|
|
|
|||
|
|
// renderGridGroup
|
|||
|
|
groupElement.style.top = `${gridGroup.position.top}px`;
|
|||
|
|
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel); // i stedet for *15
|
|||
|
|
this.stackManager.applyStackLinkToElement(groupElement, { stackLevel: gridGroup.stackLevel });
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
Brug DateService i drag:
|
|||
|
|
|
|||
|
|
public handleDragMove(payload: DragMoveEventPayload): void {
|
|||
|
|
if (!this.draggedClone || !payload.columnBounds) return;
|
|||
|
|
const swp = this.draggedClone as SwpEventElement;
|
|||
|
|
const colDate = this.dateService.parseISODate?.(payload.columnBounds.date) ?? new Date(payload.columnBounds.date);
|
|||
|
|
swp.updatePosition(colDate, payload.snappedY);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public handleColumnChange(e: DragColumnChangeEventPayload): void {
|
|||
|
|
if (!this.draggedClone) return;
|
|||
|
|
const layer = e.newColumn.element.querySelector('swp-events-layer');
|
|||
|
|
if (layer && this.draggedClone.parentElement !== layer) {
|
|||
|
|
layer.appendChild(this.draggedClone);
|
|||
|
|
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
|||
|
|
const swp = this.draggedClone as SwpEventElement;
|
|||
|
|
const colDate = this.dateService.parseISODate?.(e.newColumn.date) ?? new Date(e.newColumn.date);
|
|||
|
|
swp.updatePosition(colDate, currentTop);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
D) Reflow efter drop (EventRenderer)
|
|||
|
|
|
|||
|
|
Genberegn layout for den berørte kolonne:
|
|||
|
|
|
|||
|
|
public handleDragEnd(id: string, original: HTMLElement, clone: HTMLElement, finalColumn: ColumnBounds): void {
|
|||
|
|
if (!clone || !original) { console.warn('Missing clone/original'); return; }
|
|||
|
|
this.fadeOutAndRemove(original);
|
|||
|
|
const cid = clone.dataset.eventId;
|
|||
|
|
if (cid && cid.startsWith('clone-')) clone.dataset.eventId = cid.replace('clone-','');
|
|||
|
|
clone.classList.remove('dragging');
|
|||
|
|
|
|||
|
|
const layer = finalColumn.element.querySelector('swp-events-layer') as HTMLElement | null;
|
|||
|
|
if (layer) {
|
|||
|
|
// 1) Hent kolonnens events fra din model/state (inkl. opdateret event)
|
|||
|
|
const columnEvents: CalendarEvent[] = /* ... */;
|
|||
|
|
// 2) Ryd
|
|||
|
|
layer.querySelectorAll('swp-event, swp-event-group').forEach(el => el.remove());
|
|||
|
|
// 3) Render igen via layout
|
|||
|
|
this.renderColumnEvents(columnEvents, layer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.draggedClone = null;
|
|||
|
|
this.originalEvent = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
E) Døgn-overlap i kolonnefilter (EventRenderer)
|
|||
|
|
|
|||
|
|
Hvis ønsket (ellers behold din nuværende):
|
|||
|
|
|
|||
|
|
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|||
|
|
const d = column.dataset.date; if (!d) return [];
|
|||
|
|
const start = this.dateService.parseISODate(`${d}T00:00:00`);
|
|||
|
|
const end = this.dateService.parseISODate(`${d}T23:59:59.999`);
|
|||
|
|
return events.filter(ev => ev.start < end && ev.end > start);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|
|||
|
|
|
|||
|
|
F) Eksplicit “earliest” i GRID (Coordinator)
|
|||
|
|
|
|||
|
|
Gør det robust i tilfælde af usorteret input:
|
|||
|
|
|
|||
|
|
const earliestEvent = [...gridCandidates].sort((a,b)=> a.start.getTime()-b.start.getTime())[0];
|
|||
|
|
const pos = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventLayoutCoordinator
|
|||
|
|
|
|||
|
|
Mini-noter
|
|||
|
|
|
|||
|
|
allocateColumns er O(n²); det er fint for typiske dagvisninger. Hvis I ser >100 events/kolonne, kan I optimere med sweep-line + min-heap.
|
|||
|
|
|
|||
|
|
EventLayoutCoordinator
|
|||
|
|
|
|||
|
|
Overvej at lade koordinatoren returnere rene layout-maps (id → {level, z, margin}) og holde DOM-påføring 100% i renderer — det gør DnD-”reflow” enklere at teste.
|
|||
|
|
|
|||
|
|
EventLayoutCoordinator
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
EventRenderer
|