@forcecalendar/interface 1.0.39 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/interface",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "type": "module",
5
5
  "description": "Official interface layer for forceCalendar Core - Enterprise calendar components",
6
6
  "main": "dist/force-calendar-interface.umd.js",
@@ -161,14 +161,86 @@ export class BaseViewRenderer {
161
161
  return `<div class="fc-now-indicator" style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
162
162
  }
163
163
 
164
+ /**
165
+ * Compute overlap layout columns for a list of timed events.
166
+ * Returns a Map of event.id -> { column, totalColumns }.
167
+ * Uses a greedy left-to-right column packing algorithm.
168
+ * @param {Array} events - Array of event objects with start/end dates
169
+ * @returns {Map<string, {column: number, totalColumns: number}>}
170
+ */
171
+ computeOverlapLayout(events) {
172
+ if (!events || events.length === 0) return new Map();
173
+
174
+ // Convert to sortable entries with minute ranges
175
+ const entries = events.map(evt => {
176
+ const start = new Date(evt.start);
177
+ const end = new Date(evt.end);
178
+ const startMin = start.getHours() * 60 + start.getMinutes();
179
+ const endMin = Math.max(startMin + 1, end.getHours() * 60 + end.getMinutes());
180
+ return { id: evt.id, startMin, endMin };
181
+ });
182
+
183
+ // Sort by start time, then by longer duration first
184
+ entries.sort((a, b) => a.startMin - b.startMin || (b.endMin - b.startMin) - (a.endMin - a.startMin));
185
+
186
+ // Assign columns greedily
187
+ const columns = []; // each column tracks the end time of its last event
188
+ const layout = new Map();
189
+
190
+ for (const entry of entries) {
191
+ let placed = false;
192
+ for (let col = 0; col < columns.length; col++) {
193
+ if (columns[col] <= entry.startMin) {
194
+ columns[col] = entry.endMin;
195
+ layout.set(entry.id, { column: col, totalColumns: 0 });
196
+ placed = true;
197
+ break;
198
+ }
199
+ }
200
+ if (!placed) {
201
+ layout.set(entry.id, { column: columns.length, totalColumns: 0 });
202
+ columns.push(entry.endMin);
203
+ }
204
+ }
205
+
206
+ // Determine the max overlapping columns for each cluster of overlapping events
207
+ // Walk through entries and find connected groups
208
+ const groups = [];
209
+ let currentGroup = [];
210
+ let groupEnd = 0;
211
+
212
+ for (const entry of entries) {
213
+ if (currentGroup.length === 0 || entry.startMin < groupEnd) {
214
+ currentGroup.push(entry);
215
+ groupEnd = Math.max(groupEnd, entry.endMin);
216
+ } else {
217
+ groups.push(currentGroup);
218
+ currentGroup = [entry];
219
+ groupEnd = entry.endMin;
220
+ }
221
+ }
222
+ if (currentGroup.length > 0) groups.push(currentGroup);
223
+
224
+ for (const group of groups) {
225
+ const maxCol = Math.max(...group.map(e => layout.get(e.id).column)) + 1;
226
+ for (const entry of group) {
227
+ layout.get(entry.id).totalColumns = maxCol;
228
+ }
229
+ }
230
+
231
+ return layout;
232
+ }
233
+
164
234
  /**
165
235
  * Render a timed event block
166
236
  * @param {Object} event - Event object
167
237
  * @param {Object} options - Rendering options
238
+ * @param {Object} options.compact - Use compact layout
239
+ * @param {Object} options.overlapLayout - Map from computeOverlapLayout()
168
240
  * @returns {string} HTML string
169
241
  */
170
242
  renderTimedEvent(event, options = {}) {
171
- const { compact = true } = options;
243
+ const { compact = true, overlapLayout = null } = options;
172
244
  const start = new Date(event.start);
173
245
  const end = new Date(event.end);
174
246
  const startMinutes = start.getHours() * 60 + start.getMinutes();
@@ -177,14 +249,26 @@ export class BaseViewRenderer {
177
249
 
178
250
  const padding = compact ? '4px 8px' : '8px 12px';
179
251
  const fontSize = compact ? '11px' : '13px';
180
- const margin = compact ? '2px' : '12px';
181
- const rightMargin = compact ? '2px' : '24px';
252
+ const baseMargin = compact ? 2 : 12;
253
+ const rightPad = compact ? 2 : 24;
182
254
  const borderRadius = compact ? '4px' : '6px';
183
255
 
256
+ // Compute left/width based on overlap columns
257
+ let leftPx, widthCalc;
258
+ if (overlapLayout && overlapLayout.has(event.id)) {
259
+ const { column, totalColumns } = overlapLayout.get(event.id);
260
+ const colWidth = `(100% - ${baseMargin + rightPad}px)`;
261
+ leftPx = `calc(${baseMargin}px + ${column} * ${colWidth} / ${totalColumns})`;
262
+ widthCalc = `calc(${colWidth} / ${totalColumns})`;
263
+ } else {
264
+ leftPx = `${baseMargin}px`;
265
+ widthCalc = `calc(100% - ${baseMargin + rightPad}px)`;
266
+ }
267
+
184
268
  return `
185
269
  <div class="fc-event fc-timed-event" data-event-id="${this.escapeHTML(event.id)}"
186
270
  style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px;
187
- left: ${margin}; right: ${rightMargin};
271
+ left: ${leftPx}; width: ${widthCalc};
188
272
  background-color: ${color}; border-radius: ${borderRadius};
189
273
  padding: ${padding}; font-size: ${fontSize};
190
274
  font-weight: 500; color: white; overflow: hidden;
@@ -24,6 +24,7 @@ export class DayViewRenderer extends BaseViewRenderer {
24
24
  }
25
25
 
26
26
  this.cleanup();
27
+ this._scrolled = false;
27
28
  const config = this.stateManager.getState().config;
28
29
  const html = this._renderDayView(viewData, config);
29
30
  this.container.innerHTML = html;
@@ -168,7 +169,10 @@ export class DayViewRenderer extends BaseViewRenderer {
168
169
  ${isToday ? this.renderNowIndicator() : ''}
169
170
 
170
171
  <!-- Timed events -->
171
- ${timedEvents.map(evt => this.renderTimedEvent(evt, { compact: false })).join('')}
172
+ ${(() => {
173
+ const layout = this.computeOverlapLayout(timedEvents);
174
+ return timedEvents.map(evt => this.renderTimedEvent(evt, { compact: false, overlapLayout: layout })).join('');
175
+ })()}
172
176
  </div>
173
177
  `;
174
178
  }
@@ -24,6 +24,7 @@ export class WeekViewRenderer extends BaseViewRenderer {
24
24
  }
25
25
 
26
26
  this.cleanup();
27
+ this._scrolled = false;
27
28
  const config = this.stateManager.getState().config;
28
29
  const html = this._renderWeekView(viewData, config);
29
30
  this.container.innerHTML = html;
@@ -147,7 +148,10 @@ export class WeekViewRenderer extends BaseViewRenderer {
147
148
  ${day.isToday ? this.renderNowIndicator() : ''}
148
149
 
149
150
  <!-- Timed events -->
150
- ${day.timedEvents.map(evt => this.renderTimedEvent(evt, { compact: true })).join('')}
151
+ ${(() => {
152
+ const layout = this.computeOverlapLayout(day.timedEvents);
153
+ return day.timedEvents.map(evt => this.renderTimedEvent(evt, { compact: true, overlapLayout: layout })).join('');
154
+ })()}
151
155
  </div>
152
156
  `;
153
157
  }