@forcecalendar/interface 1.0.38 → 1.0.40

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.38",
3
+ "version": "1.0.40",
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;
@@ -168,7 +168,10 @@ export class DayViewRenderer extends BaseViewRenderer {
168
168
  ${isToday ? this.renderNowIndicator() : ''}
169
169
 
170
170
  <!-- Timed events -->
171
- ${timedEvents.map(evt => this.renderTimedEvent(evt, { compact: false })).join('')}
171
+ ${(() => {
172
+ const layout = this.computeOverlapLayout(timedEvents);
173
+ return timedEvents.map(evt => this.renderTimedEvent(evt, { compact: false, overlapLayout: layout })).join('');
174
+ })()}
172
175
  </div>
173
176
  `;
174
177
  }
@@ -180,14 +183,15 @@ export class DayViewRenderer extends BaseViewRenderer {
180
183
  if (e.target.closest('.fc-event')) return;
181
184
 
182
185
  const date = new Date(dayEl.dataset.date);
183
- const rect = dayEl.getBoundingClientRect();
184
186
  const scrollContainer = this.container.querySelector('#day-scroll-container');
185
- const y = e.clientY - rect.top;
187
+ const gridTop = dayEl.offsetTop;
188
+ const y = e.clientY - dayEl.getBoundingClientRect().top + (scrollContainer ? scrollContainer.scrollTop : 0) - gridTop;
186
189
 
187
- // Calculate time from click position
190
+ // Calculate time from click position within the 1440px time grid
191
+ const clampedY = Math.max(0, Math.min(y + gridTop, this.totalHeight));
188
192
  date.setHours(
189
- Math.floor(y / this.hourHeight),
190
- Math.floor((y % this.hourHeight) / (this.hourHeight / 60)),
193
+ Math.floor(clampedY / this.hourHeight),
194
+ Math.floor((clampedY % this.hourHeight) / (this.hourHeight / 60)),
191
195
  0,
192
196
  0
193
197
  );
@@ -147,7 +147,10 @@ export class WeekViewRenderer extends BaseViewRenderer {
147
147
  ${day.isToday ? this.renderNowIndicator() : ''}
148
148
 
149
149
  <!-- Timed events -->
150
- ${day.timedEvents.map(evt => this.renderTimedEvent(evt, { compact: true })).join('')}
150
+ ${(() => {
151
+ const layout = this.computeOverlapLayout(day.timedEvents);
152
+ return day.timedEvents.map(evt => this.renderTimedEvent(evt, { compact: true, overlapLayout: layout })).join('');
153
+ })()}
151
154
  </div>
152
155
  `;
153
156
  }
@@ -159,14 +162,15 @@ export class WeekViewRenderer extends BaseViewRenderer {
159
162
  if (e.target.closest('.fc-event')) return;
160
163
 
161
164
  const date = new Date(dayEl.dataset.date);
162
- const rect = dayEl.getBoundingClientRect();
163
165
  const scrollContainer = this.container.querySelector('#week-scroll-container');
164
- const y = e.clientY - rect.top;
166
+ const gridTop = dayEl.offsetTop;
167
+ const y = e.clientY - dayEl.getBoundingClientRect().top + (scrollContainer ? scrollContainer.scrollTop : 0) - gridTop;
165
168
 
166
- // Calculate time from click position
169
+ // Calculate time from click position within the 1440px time grid
170
+ const clampedY = Math.max(0, Math.min(y + gridTop, this.totalHeight));
167
171
  date.setHours(
168
- Math.floor(y / this.hourHeight),
169
- Math.floor((y % this.hourHeight) / (this.hourHeight / 60)),
172
+ Math.floor(clampedY / this.hourHeight),
173
+ Math.floor((clampedY % this.hourHeight) / (this.hourHeight / 60)),
170
174
  0,
171
175
  0
172
176
  );