Enhances event layout engine with advanced rendering logic
Introduces sophisticated event layout algorithm for handling complex scheduling scenarios Adds support for: - Grid and stacked event rendering - Automatic column allocation - Nested event stacking - Threshold-based event grouping Improves visual representation of overlapping and concurrent events
This commit is contained in:
parent
4e22fbc948
commit
70172e8f10
26 changed files with 2108 additions and 44 deletions
279
src/v2/features/event/EventLayoutEngine.ts
Normal file
279
src/v2/features/event/EventLayoutEngine.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* EventLayoutEngine - Simplified stacking/grouping algorithm for V2
|
||||
*
|
||||
* Supports two layout modes:
|
||||
* - GRID: Events starting at same time rendered side-by-side
|
||||
* - STACKING: Overlapping events with margin-left offset (15px per level)
|
||||
*
|
||||
* Simplified from V1: No prev/next chains, single-pass greedy algorithm
|
||||
*/
|
||||
|
||||
import { ICalendarEvent } from '../../types/CalendarTypes';
|
||||
import { IGridConfig } from '../../core/IGridConfig';
|
||||
import { calculateEventPosition } from '../../utils/PositionUtils';
|
||||
import { IColumnLayout, IGridGroupLayout, IStackedEventLayout } from './EventLayoutTypes';
|
||||
|
||||
/**
|
||||
* Check if two events overlap (strict - touching at boundary = NOT overlapping)
|
||||
* This matches Scenario 8: end===start is NOT overlap
|
||||
*/
|
||||
export function eventsOverlap(a: ICalendarEvent, b: ICalendarEvent): boolean {
|
||||
return a.start < b.end && a.end > b.start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two events are within threshold for grid grouping.
|
||||
* This includes:
|
||||
* 1. Start-to-start: Events start within threshold of each other
|
||||
* 2. End-to-start: One event starts within threshold before another ends
|
||||
*/
|
||||
function eventsWithinThreshold(a: ICalendarEvent, b: ICalendarEvent, thresholdMinutes: number): boolean {
|
||||
const thresholdMs = thresholdMinutes * 60 * 1000;
|
||||
|
||||
// Start-to-start: both events start within threshold
|
||||
const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime());
|
||||
if (startToStartDiff <= thresholdMs) return true;
|
||||
|
||||
// End-to-start: one event starts within threshold before the other ends
|
||||
// B starts within threshold before A ends
|
||||
const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime();
|
||||
if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs) return true;
|
||||
|
||||
// A starts within threshold before B ends
|
||||
const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime();
|
||||
if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all events in a group start within threshold of each other
|
||||
*/
|
||||
function allStartWithinThreshold(events: ICalendarEvent[], thresholdMinutes: number): boolean {
|
||||
if (events.length <= 1) return true;
|
||||
|
||||
// Find earliest and latest start times
|
||||
let earliest = events[0].start.getTime();
|
||||
let latest = events[0].start.getTime();
|
||||
|
||||
for (const event of events) {
|
||||
const time = event.start.getTime();
|
||||
if (time < earliest) earliest = time;
|
||||
if (time > latest) latest = time;
|
||||
}
|
||||
|
||||
const diffMinutes = (latest - earliest) / (1000 * 60);
|
||||
return diffMinutes <= thresholdMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find groups of overlapping events (connected by overlap chain)
|
||||
* Events are grouped if they overlap with any event in the group
|
||||
*/
|
||||
function findOverlapGroups(events: ICalendarEvent[]): ICalendarEvent[][] {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const used = new Set<string>();
|
||||
const groups: ICalendarEvent[][] = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
if (used.has(event.id)) continue;
|
||||
|
||||
// Start a new group with this event
|
||||
const group: ICalendarEvent[] = [event];
|
||||
used.add(event.id);
|
||||
|
||||
// Expand group by finding all connected events (via overlap)
|
||||
let expanded = true;
|
||||
while (expanded) {
|
||||
expanded = false;
|
||||
for (const candidate of sorted) {
|
||||
if (used.has(candidate.id)) continue;
|
||||
|
||||
// Check if candidate overlaps with any event in group
|
||||
const connects = group.some(member => eventsOverlap(member, candidate));
|
||||
|
||||
if (connects) {
|
||||
group.push(candidate);
|
||||
used.add(candidate.id);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find grid candidates within a group - events connected via threshold chain
|
||||
* Uses V1 logic: events are connected if within threshold (no overlap requirement)
|
||||
*/
|
||||
function findGridCandidates(
|
||||
events: ICalendarEvent[],
|
||||
thresholdMinutes: number
|
||||
): ICalendarEvent[][] {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const used = new Set<string>();
|
||||
const groups: ICalendarEvent[][] = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
if (used.has(event.id)) continue;
|
||||
|
||||
const group: ICalendarEvent[] = [event];
|
||||
used.add(event.id);
|
||||
|
||||
// Expand by threshold chain (V1 logic: no overlap requirement, just threshold)
|
||||
let expanded = true;
|
||||
while (expanded) {
|
||||
expanded = false;
|
||||
for (const candidate of sorted) {
|
||||
if (used.has(candidate.id)) continue;
|
||||
|
||||
const connects = group.some(member =>
|
||||
eventsWithinThreshold(member, candidate, thresholdMinutes)
|
||||
);
|
||||
|
||||
if (connects) {
|
||||
group.push(candidate);
|
||||
used.add(candidate.id);
|
||||
expanded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(group);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate stack levels for overlapping events using greedy algorithm
|
||||
* For each event: level = max(overlapping already-processed events) + 1
|
||||
*/
|
||||
function calculateStackLevels(events: ICalendarEvent[]): Map<string, number> {
|
||||
const levels = new Map<string, number>();
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
for (const event of sorted) {
|
||||
let maxOverlappingLevel = -1;
|
||||
|
||||
// Find max level among overlapping events already processed
|
||||
for (const [id, level] of levels) {
|
||||
const other = events.find(e => e.id === id);
|
||||
if (other && eventsOverlap(event, other)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, level);
|
||||
}
|
||||
}
|
||||
|
||||
levels.set(event.id, maxOverlappingLevel + 1);
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate events to columns for GRID layout using greedy algorithm
|
||||
* Non-overlapping events can share a column to minimize total columns
|
||||
*/
|
||||
function allocateColumns(events: ICalendarEvent[]): ICalendarEvent[][] {
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
const columns: ICalendarEvent[][] = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
// Find first column where event doesn't overlap with existing events
|
||||
let placed = false;
|
||||
for (const column of columns) {
|
||||
const canFit = !column.some(e => eventsOverlap(event, e));
|
||||
if (canFit) {
|
||||
column.push(event);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No suitable column found, create new one
|
||||
if (!placed) {
|
||||
columns.push([event]);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point: Calculate complete layout for a column's events
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Find overlap groups (events connected by overlap chain)
|
||||
* 2. For each overlap group, find grid candidates (events within threshold chain)
|
||||
* 3. If all events in overlap group form a single grid candidate → GRID mode
|
||||
* 4. Otherwise → STACKING mode with calculated levels
|
||||
*/
|
||||
export function calculateColumnLayout(
|
||||
events: ICalendarEvent[],
|
||||
config: IGridConfig
|
||||
): IColumnLayout {
|
||||
const thresholdMinutes = config.gridStartThresholdMinutes ?? 10;
|
||||
|
||||
const result: IColumnLayout = {
|
||||
grids: [],
|
||||
stacked: []
|
||||
};
|
||||
|
||||
if (events.length === 0) return result;
|
||||
|
||||
// Find all overlapping event groups
|
||||
const overlapGroups = findOverlapGroups(events);
|
||||
|
||||
for (const overlapGroup of overlapGroups) {
|
||||
if (overlapGroup.length === 1) {
|
||||
// Single event - no grouping needed
|
||||
result.stacked.push({
|
||||
event: overlapGroup[0],
|
||||
stackLevel: 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Within this overlap group, find grid candidates (threshold-connected subgroups)
|
||||
const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes);
|
||||
|
||||
// Check if the ENTIRE overlap group forms a single grid candidate
|
||||
// This happens when all events are connected via threshold chain
|
||||
const largestGridCandidate = gridSubgroups.reduce((max, g) =>
|
||||
g.length > max.length ? g : max, gridSubgroups[0]);
|
||||
|
||||
if (largestGridCandidate.length === overlapGroup.length) {
|
||||
// All events in overlap group are connected via threshold chain → GRID mode
|
||||
const columns = allocateColumns(overlapGroup);
|
||||
const earliest = overlapGroup.reduce((min, e) =>
|
||||
e.start < min.start ? e : min, overlapGroup[0]);
|
||||
const position = calculateEventPosition(earliest.start, earliest.end, config);
|
||||
|
||||
result.grids.push({
|
||||
events: overlapGroup,
|
||||
columns,
|
||||
stackLevel: 0,
|
||||
position: { top: position.top }
|
||||
});
|
||||
} else {
|
||||
// Not all events connected via threshold → STACKING mode
|
||||
const levels = calculateStackLevels(overlapGroup);
|
||||
for (const event of overlapGroup) {
|
||||
result.stacked.push({
|
||||
event,
|
||||
stackLevel: levels.get(event.id) ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
35
src/v2/features/event/EventLayoutTypes.ts
Normal file
35
src/v2/features/event/EventLayoutTypes.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { ICalendarEvent } from '../../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* Stack link metadata stored on event elements
|
||||
* Simplified from V1: No prev/next chains - only stackLevel needed for rendering
|
||||
*/
|
||||
export interface IStackLink {
|
||||
stackLevel: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout result for a stacked event (overlapping events with margin offset)
|
||||
*/
|
||||
export interface IStackedEventLayout {
|
||||
event: ICalendarEvent;
|
||||
stackLevel: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout result for a grid group (simultaneous events side-by-side)
|
||||
*/
|
||||
export interface IGridGroupLayout {
|
||||
events: ICalendarEvent[];
|
||||
columns: ICalendarEvent[][]; // Events grouped by column (non-overlapping within column)
|
||||
stackLevel: number; // Stack level for entire group (if nested in another event)
|
||||
position: { top: number }; // Top position of earliest event in pixels
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete layout result for a column's events
|
||||
*/
|
||||
export interface IColumnLayout {
|
||||
grids: IGridGroupLayout[];
|
||||
stacked: IStackedEventLayout[];
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { ICalendarEvent, IEventBus } from '../../types/CalendarTypes';
|
||||
import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../../types/CalendarTypes';
|
||||
import { EventService } from '../../storage/events/EventService';
|
||||
import { DateService } from '../../core/DateService';
|
||||
import { IGridConfig } from '../../core/IGridConfig';
|
||||
import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
|
||||
import { CoreEvents } from '../../constants/CoreEvents';
|
||||
import { IDragColumnChangePayload, IDragMovePayload } from '../../types/DragTypes';
|
||||
import { calculateColumnLayout } from './EventLayoutEngine';
|
||||
import { IGridGroupLayout } from './EventLayoutTypes';
|
||||
|
||||
/**
|
||||
* EventRenderer - Renders calendar events to the DOM
|
||||
|
|
@ -15,19 +17,21 @@ import { IDragColumnChangePayload, IDragMovePayload } from '../../types/DragType
|
|||
* - Event data retrieved via EventService when needed
|
||||
*/
|
||||
export class EventRenderer {
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private dateService: DateService,
|
||||
private gridConfig: IGridConfig,
|
||||
private eventBus: IEventBus
|
||||
) {
|
||||
this.setupDragListeners();
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for drag-drop events
|
||||
* Setup listeners for drag-drop and update events
|
||||
*/
|
||||
private setupDragListeners(): void {
|
||||
private setupListeners(): void {
|
||||
this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => {
|
||||
const payload = (e as CustomEvent<IDragColumnChangePayload>).detail;
|
||||
this.handleColumnChange(payload);
|
||||
|
|
@ -37,6 +41,95 @@ export class EventRenderer {
|
|||
const payload = (e as CustomEvent<IDragMovePayload>).detail;
|
||||
this.updateDragTimestamp(payload);
|
||||
});
|
||||
|
||||
this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => {
|
||||
const payload = (e as CustomEvent<IEventUpdatedPayload>).detail;
|
||||
this.handleEventUpdated(payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle EVENT_UPDATED - re-render affected columns
|
||||
*/
|
||||
private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
|
||||
// Re-render source column (if different from target)
|
||||
if (payload.sourceDateKey !== payload.targetDateKey ||
|
||||
payload.sourceResourceId !== payload.targetResourceId) {
|
||||
await this.rerenderColumn(payload.sourceDateKey, payload.sourceResourceId);
|
||||
}
|
||||
|
||||
// Re-render target column
|
||||
await this.rerenderColumn(payload.targetDateKey, payload.targetResourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render a single column with fresh data from IndexedDB
|
||||
*/
|
||||
private async rerenderColumn(dateKey: string, resourceId?: string): Promise<void> {
|
||||
const column = this.findColumn(dateKey, resourceId);
|
||||
if (!column) return;
|
||||
|
||||
// Get date range for this day
|
||||
const startDate = new Date(dateKey);
|
||||
const endDate = new Date(dateKey);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
|
||||
// Fetch events from IndexedDB
|
||||
const events = resourceId
|
||||
? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate)
|
||||
: await this.eventService.getByDateRange(startDate, endDate);
|
||||
|
||||
// Filter to timed events and match dateKey exactly
|
||||
const timedEvents = events.filter(event =>
|
||||
!event.allDay && this.dateService.getDateKey(event.start) === dateKey
|
||||
);
|
||||
|
||||
// Get or create events layer
|
||||
let eventsLayer = column.querySelector('swp-events-layer');
|
||||
if (!eventsLayer) {
|
||||
eventsLayer = document.createElement('swp-events-layer');
|
||||
column.appendChild(eventsLayer);
|
||||
}
|
||||
|
||||
// Clear existing events
|
||||
eventsLayer.innerHTML = '';
|
||||
|
||||
// Calculate layout with stacking/grouping
|
||||
const layout = calculateColumnLayout(timedEvents, this.gridConfig);
|
||||
|
||||
// Render GRID groups
|
||||
layout.grids.forEach(grid => {
|
||||
const groupEl = this.renderGridGroup(grid);
|
||||
eventsLayer!.appendChild(groupEl);
|
||||
});
|
||||
|
||||
// Render STACKED events
|
||||
layout.stacked.forEach(item => {
|
||||
const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
|
||||
eventsLayer!.appendChild(eventEl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a column element by dateKey and optional resourceId
|
||||
*/
|
||||
private findColumn(dateKey: string, resourceId?: string): HTMLElement | null {
|
||||
if (!this.container) return null;
|
||||
|
||||
const columns = this.container.querySelectorAll('swp-day-column');
|
||||
for (const col of columns) {
|
||||
const colEl = col as HTMLElement;
|
||||
if (colEl.dataset.date !== dateKey) continue;
|
||||
|
||||
// If resourceId specified, must match
|
||||
if (resourceId && colEl.dataset.resourceId !== resourceId) continue;
|
||||
|
||||
// If no resourceId specified but column has one, skip (simple view case)
|
||||
if (!resourceId && colEl.dataset.resourceId) continue;
|
||||
|
||||
return colEl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,6 +186,9 @@ export class EventRenderer {
|
|||
* @param filter - Filter with 'date' and optionally 'resource' arrays
|
||||
*/
|
||||
async render(container: HTMLElement, filter: Record<string, string[]>): Promise<void> {
|
||||
// Store container reference for later re-renders
|
||||
this.container = container;
|
||||
|
||||
const visibleDates = filter['date'] || [];
|
||||
|
||||
if (visibleDates.length === 0) return;
|
||||
|
|
@ -142,12 +238,22 @@ export class EventRenderer {
|
|||
// Clear existing events
|
||||
eventsLayer.innerHTML = '';
|
||||
|
||||
// Render each timed event
|
||||
columnEvents.forEach(event => {
|
||||
if (!event.allDay) {
|
||||
const eventElement = this.createEventElement(event);
|
||||
eventsLayer!.appendChild(eventElement);
|
||||
}
|
||||
// Filter to timed events only
|
||||
const timedEvents = columnEvents.filter(event => !event.allDay);
|
||||
|
||||
// Calculate layout with stacking/grouping
|
||||
const layout = calculateColumnLayout(timedEvents, this.gridConfig);
|
||||
|
||||
// Render GRID groups (simultaneous events side-by-side)
|
||||
layout.grids.forEach(grid => {
|
||||
const groupEl = this.renderGridGroup(grid);
|
||||
eventsLayer!.appendChild(groupEl);
|
||||
});
|
||||
|
||||
// Render STACKED events (overlapping with margin offset)
|
||||
layout.stacked.forEach(item => {
|
||||
const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
|
||||
eventsLayer!.appendChild(eventEl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -162,8 +268,8 @@ export class EventRenderer {
|
|||
private createEventElement(event: ICalendarEvent): HTMLElement {
|
||||
const element = document.createElement('swp-event');
|
||||
|
||||
// Only essential data attribute
|
||||
element.dataset.id = event.id;
|
||||
// Only essential data attribute (eventId for DragDropManager compatibility)
|
||||
element.dataset.eventId = event.id;
|
||||
|
||||
// Calculate position
|
||||
const position = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
|
|
@ -187,9 +293,15 @@ export class EventRenderer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get color class based on event type
|
||||
* Get color class based on metadata.color or event type
|
||||
*/
|
||||
private getColorClass(event: ICalendarEvent): string {
|
||||
// Check metadata.color first
|
||||
if (event.metadata?.color) {
|
||||
return `is-${event.metadata.color}`;
|
||||
}
|
||||
|
||||
// Fallback to type-based color
|
||||
const typeColors: Record<string, string> = {
|
||||
'customer': 'is-blue',
|
||||
'vacation': 'is-green',
|
||||
|
|
@ -208,4 +320,70 @@ export class EventRenderer {
|
|||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a GRID group with side-by-side columns
|
||||
* Used when multiple events start at the same time
|
||||
*/
|
||||
private renderGridGroup(layout: IGridGroupLayout): HTMLElement {
|
||||
const group = document.createElement('swp-event-group');
|
||||
group.classList.add(`cols-${layout.columns.length}`);
|
||||
group.style.top = `${layout.position.top}px`;
|
||||
|
||||
// Stack level styling for entire group (if nested in another event)
|
||||
if (layout.stackLevel > 0) {
|
||||
group.style.marginLeft = `${layout.stackLevel * 15}px`;
|
||||
group.style.zIndex = `${100 + layout.stackLevel}`;
|
||||
}
|
||||
|
||||
// Calculate the height needed for the group (tallest event)
|
||||
let maxBottom = 0;
|
||||
for (const event of layout.events) {
|
||||
const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
const eventBottom = pos.top + pos.height;
|
||||
if (eventBottom > maxBottom) maxBottom = eventBottom;
|
||||
}
|
||||
const groupHeight = maxBottom - layout.position.top;
|
||||
group.style.height = `${groupHeight}px`;
|
||||
|
||||
// Create wrapper div for each column
|
||||
layout.columns.forEach(columnEvents => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
columnEvents.forEach(event => {
|
||||
const eventEl = this.createEventElement(event);
|
||||
// Position relative to group top
|
||||
const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
eventEl.style.top = `${pos.top - layout.position.top}px`;
|
||||
eventEl.style.position = 'absolute';
|
||||
eventEl.style.left = '0';
|
||||
eventEl.style.right = '0';
|
||||
wrapper.appendChild(eventEl);
|
||||
});
|
||||
|
||||
group.appendChild(wrapper);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a STACKED event with margin-left offset
|
||||
* Used for overlapping events that don't start at the same time
|
||||
*/
|
||||
private renderStackedEvent(event: ICalendarEvent, stackLevel: number): HTMLElement {
|
||||
const element = this.createEventElement(event);
|
||||
|
||||
// Add stack metadata for drag-drop and other features
|
||||
element.dataset.stackLink = JSON.stringify({ stackLevel });
|
||||
|
||||
// Visual styling based on stack level
|
||||
if (stackLevel > 0) {
|
||||
element.style.marginLeft = `${stackLevel * 15}px`;
|
||||
element.style.zIndex = `${100 + stackLevel}`;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class HeaderDrawerRenderer {
|
|||
|
||||
// Create header item
|
||||
const item = document.createElement('swp-header-item');
|
||||
item.dataset.id = payload.eventId;
|
||||
item.dataset.eventId = payload.eventId;
|
||||
item.dataset.itemType = payload.itemType;
|
||||
item.dataset.date = payload.sourceDate;
|
||||
item.dataset.duration = String(payload.duration);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue