@forcecalendar/interface 1.0.17 → 1.0.19

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * WeekViewRenderer - Renders week calendar view
3
+ *
4
+ * Pure JavaScript renderer for week view, compatible with Salesforce Locker Service.
5
+ */
6
+
7
+ import { BaseViewRenderer } from './BaseViewRenderer.js';
8
+
9
+ export class WeekViewRenderer extends BaseViewRenderer {
10
+ constructor(container, stateManager) {
11
+ super(container, stateManager);
12
+ this.hourHeight = 60; // pixels per hour
13
+ this.totalHeight = 24 * this.hourHeight; // 1440px for 24 hours
14
+ }
15
+
16
+ render() {
17
+ if (!this.container || !this.stateManager) return;
18
+
19
+ const viewData = this.stateManager.getViewData();
20
+ if (!viewData || !viewData.days || viewData.days.length === 0) {
21
+ this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No data available for week view.</div>';
22
+ return;
23
+ }
24
+
25
+ this.cleanup();
26
+ const config = this.stateManager.getState().config;
27
+ const html = this._renderWeekView(viewData, config);
28
+ this.container.innerHTML = html;
29
+ this._attachEventHandlers();
30
+ this._scrollToCurrentTime();
31
+ }
32
+
33
+ _renderWeekView(viewData, config) {
34
+ const days = viewData.days;
35
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
36
+ const hours = Array.from({ length: 24 }, (_, i) => i);
37
+
38
+ // Process days to categorize events
39
+ const processedDays = days.map(day => {
40
+ const dayDate = new Date(day.date);
41
+ const events = day.events || [];
42
+ return {
43
+ ...day,
44
+ date: dayDate,
45
+ dayName: dayNames[dayDate.getDay()],
46
+ dayOfMonth: dayDate.getDate(),
47
+ isToday: this.isToday(dayDate),
48
+ timedEvents: events.filter(e => !e.allDay),
49
+ allDayEvents: events.filter(e => e.allDay)
50
+ };
51
+ });
52
+
53
+ return `
54
+ <div class="fc-week-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
55
+ ${this._renderHeader(processedDays)}
56
+ ${this._renderAllDayRow(processedDays)}
57
+ ${this._renderTimeGrid(processedDays, hours)}
58
+ </div>
59
+ `;
60
+ }
61
+
62
+ _renderHeader(days) {
63
+ return `
64
+ <div class="fc-week-header" style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
65
+ <div style="border-right: 1px solid #e5e7eb;"></div>
66
+ ${days.map(day => `
67
+ <div style="padding: 12px 8px; text-align: center; border-right: 1px solid #e5e7eb;">
68
+ <div style="font-size: 10px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">
69
+ ${day.dayName}
70
+ </div>
71
+ <div style="font-size: 16px; font-weight: 500; margin-top: 4px; ${day.isToday ? 'background: #dc2626; color: white; border-radius: 50%; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center;' : 'color: #111827;'}">
72
+ ${day.dayOfMonth}
73
+ </div>
74
+ </div>
75
+ `).join('')}
76
+ </div>
77
+ `;
78
+ }
79
+
80
+ _renderAllDayRow(days) {
81
+ return `
82
+ <div class="fc-all-day-row" style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 32px; flex-shrink: 0;">
83
+ <div style="font-size: 9px; color: #6b7280; display: flex; align-items: center; justify-content: center; border-right: 1px solid #e5e7eb; text-transform: uppercase; font-weight: 700;">
84
+ All day
85
+ </div>
86
+ ${days.map(day => `
87
+ <div class="fc-all-day-cell" data-date="${day.date.toISOString()}" style="border-right: 1px solid #e5e7eb; padding: 4px; display: flex; flex-direction: column; gap: 2px;">
88
+ ${day.allDayEvents.map(evt => `
89
+ <div class="fc-event fc-all-day-event" data-event-id="${this.escapeHTML(evt.id)}"
90
+ style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 10px; padding: 2px 4px; border-radius: 2px; color: white; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
91
+ ${this.escapeHTML(evt.title)}
92
+ </div>
93
+ `).join('')}
94
+ </div>
95
+ `).join('')}
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ _renderTimeGrid(days, hours) {
101
+ return `
102
+ <div id="week-scroll-container" class="fc-time-grid-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
103
+ <div class="fc-time-grid" style="display: grid; grid-template-columns: 60px repeat(7, 1fr); position: relative; height: ${this.totalHeight}px;">
104
+ ${this._renderTimeGutter(hours)}
105
+ ${days.map(day => this._renderDayColumn(day, hours)).join('')}
106
+ </div>
107
+ </div>
108
+ `;
109
+ }
110
+
111
+ _renderTimeGutter(hours) {
112
+ return `
113
+ <div class="fc-time-gutter" style="border-right: 1px solid #e5e7eb; background: #fafafa;">
114
+ ${hours.map(h => `
115
+ <div style="height: ${this.hourHeight}px; font-size: 10px; color: #6b7280; text-align: right; padding-right: 8px; font-weight: 500;">
116
+ ${h === 0 ? '' : this.formatHour(h)}
117
+ </div>
118
+ `).join('')}
119
+ </div>
120
+ `;
121
+ }
122
+
123
+ _renderDayColumn(day, hours) {
124
+ return `
125
+ <div class="fc-week-day-column" data-date="${day.date.toISOString()}" style="border-right: 1px solid #e5e7eb; position: relative; cursor: pointer;">
126
+ <!-- Hour grid lines -->
127
+ ${hours.map(() => `<div style="height: ${this.hourHeight}px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
128
+
129
+ <!-- Now indicator for today -->
130
+ ${day.isToday ? this.renderNowIndicator() : ''}
131
+
132
+ <!-- Timed events -->
133
+ ${day.timedEvents.map(evt => this.renderTimedEvent(evt, { compact: true })).join('')}
134
+ </div>
135
+ `;
136
+ }
137
+
138
+ _attachEventHandlers() {
139
+ // Day column click handlers
140
+ this.container.querySelectorAll('.fc-week-day-column').forEach(dayEl => {
141
+ this.addListener(dayEl, 'click', (e) => {
142
+ if (e.target.closest('.fc-event')) return;
143
+
144
+ const date = new Date(dayEl.dataset.date);
145
+ const rect = dayEl.getBoundingClientRect();
146
+ const scrollContainer = this.container.querySelector('#week-scroll-container');
147
+ const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
148
+
149
+ // Calculate time from click position
150
+ date.setHours(Math.floor(y / this.hourHeight), Math.floor((y % this.hourHeight) / (this.hourHeight / 60)), 0, 0);
151
+ this.stateManager.selectDate(date);
152
+ });
153
+ });
154
+
155
+ // Common event handlers (event clicks)
156
+ this.attachCommonEventHandlers();
157
+ }
158
+
159
+ _scrollToCurrentTime() {
160
+ if (this._scrolled) return;
161
+
162
+ const scrollContainer = this.container.querySelector('#week-scroll-container');
163
+ if (scrollContainer) {
164
+ // Scroll to 8 AM, minus some offset for visibility
165
+ scrollContainer.scrollTop = 8 * this.hourHeight - 50;
166
+ this._scrolled = true;
167
+ }
168
+ }
169
+ }
170
+
171
+ export default WeekViewRenderer;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * View Renderers
3
+ *
4
+ * Pure JavaScript renderers for calendar views.
5
+ * Compatible with Salesforce Locker Service (no custom elements).
6
+ */
7
+
8
+ export { BaseViewRenderer } from './BaseViewRenderer.js';
9
+ export { MonthViewRenderer } from './MonthViewRenderer.js';
10
+ export { WeekViewRenderer } from './WeekViewRenderer.js';
11
+ export { DayViewRenderer } from './DayViewRenderer.js';
@@ -1,437 +0,0 @@
1
- /**
2
- * DayView - Professional Time-Grid Day View
3
- *
4
- * Displays a single day schedule with a time axis and event positioning.
5
- */
6
-
7
- import { BaseComponent } from '../../core/BaseComponent.js';
8
- import { DateUtils } from '../../utils/DateUtils.js';
9
- import { StyleUtils } from '../../utils/StyleUtils.js';
10
- import { DOMUtils } from '../../utils/DOMUtils.js';
11
-
12
- export class DayView extends BaseComponent {
13
- constructor() {
14
- super();
15
- this._stateManager = null;
16
- this.viewData = null;
17
- this.hours = Array.from({ length: 24 }, (_, i) => i);
18
- this._registryCheckInterval = null;
19
- }
20
-
21
- connectedCallback() {
22
- super.connectedCallback();
23
- this._startRegistryPolling();
24
- }
25
-
26
- disconnectedCallback() {
27
- super.disconnectedCallback();
28
- if (this._registryCheckInterval) {
29
- clearInterval(this._registryCheckInterval);
30
- }
31
- }
32
-
33
- _startRegistryPolling() {
34
- this._checkRegistry();
35
- let attempts = 0;
36
- this._registryCheckInterval = setInterval(() => {
37
- attempts++;
38
- if (this._stateManager || attempts > 50) {
39
- clearInterval(this._registryCheckInterval);
40
- return;
41
- }
42
- this._checkRegistry();
43
- }, 100);
44
- }
45
-
46
- _checkRegistry() {
47
- const registryId = this.getAttribute('data-state-registry');
48
- if (registryId && window.__forceCalendarRegistry && window.__forceCalendarRegistry[registryId]) {
49
- clearInterval(this._registryCheckInterval);
50
- this.setStateManager(window.__forceCalendarRegistry[registryId]);
51
- }
52
- }
53
-
54
- set stateManager(manager) {
55
- this.setStateManager(manager);
56
- }
57
-
58
- setStateManager(manager) {
59
- if (this._stateManager === manager) return;
60
- this._stateManager = manager;
61
- if (manager) {
62
- this.unsubscribe = manager.subscribe(this.handleStateUpdate.bind(this));
63
- this.loadViewData();
64
- }
65
- }
66
-
67
- get stateManager() {
68
- return this._stateManager;
69
- }
70
-
71
- handleStateUpdate(newState, oldState) {
72
- // Granular updates
73
- if (newState.currentDate !== oldState?.currentDate || newState.view !== oldState?.view) {
74
- this.loadViewData();
75
- return;
76
- }
77
-
78
- if (newState.events !== oldState?.events) {
79
- this.loadViewData(); // Simple reload for now
80
- }
81
-
82
- if (newState.selectedDate !== oldState?.selectedDate) {
83
- this.updateSelection(newState.selectedDate, oldState?.selectedDate);
84
- }
85
- }
86
-
87
- updateSelection(newDate, oldDate) {
88
- const dayCol = this.shadowRoot.querySelector('.day-column');
89
- if (!dayCol) return;
90
-
91
- const isMatch = (date) => date && DateUtils.isSameDay(date, new Date(dayCol.dataset.date));
92
-
93
- if (isMatch(newDate)) {
94
- dayCol.classList.add('selected');
95
- } else {
96
- dayCol.classList.remove('selected');
97
- }
98
- }
99
-
100
- loadViewData() {
101
- if (!this.stateManager) return;
102
- const viewData = this.stateManager.getViewData();
103
- this.viewData = this.processViewData(viewData);
104
- this.render();
105
- }
106
-
107
- processViewData(viewData) {
108
- if (!viewData) return null;
109
-
110
- let dayData = null;
111
- const currentState = this.stateManager?.getState();
112
- const currentDate = currentState?.currentDate || new Date();
113
-
114
- if (viewData.days && Array.isArray(viewData.days) && viewData.days.length > 0) {
115
- dayData = viewData.days.find(d => DateUtils.isSameDay(new Date(d.date), currentDate)) || viewData.days[0];
116
- }
117
- else if (viewData.weeks && Array.isArray(viewData.weeks) && viewData.weeks.length > 0) {
118
- const allDays = viewData.weeks.flatMap(w => w.days || []);
119
- dayData = allDays.find(d => DateUtils.isSameDay(new Date(d.date), currentDate)) || allDays[0];
120
- }
121
- else if (viewData.date) {
122
- dayData = viewData;
123
- }
124
-
125
- if (!dayData) return null;
126
-
127
- const dayDate = new Date(dayData.date);
128
- return {
129
- ...viewData,
130
- day: {
131
- ...dayData,
132
- date: dayDate,
133
- isToday: DateUtils.isToday(dayDate),
134
- timedEvents: (dayData.events || []).filter(e => !e.allDay),
135
- allDayEvents: (dayData.events || []).filter(e => e.allDay)
136
- }
137
- };
138
- }
139
-
140
- getStyles() {
141
- return `
142
- :host {
143
- display: flex;
144
- flex-direction: column;
145
- height: 100%;
146
- min-height: 0;
147
- }
148
-
149
- .day-view {
150
- display: flex;
151
- flex-direction: column;
152
- height: 100%;
153
- background: var(--fc-background);
154
- min-height: 0;
155
- overflow: hidden;
156
- }
157
-
158
- /* Header */
159
- .day-header {
160
- display: grid;
161
- grid-template-columns: 60px 1fr;
162
- border-bottom: 1px solid var(--fc-border-color);
163
- background: var(--fc-background);
164
- z-index: 20;
165
- flex-shrink: 0;
166
- }
167
-
168
- .day-column-header {
169
- padding: 16px 24px;
170
- text-align: left;
171
- display: flex;
172
- flex-direction: column;
173
- gap: 4px;
174
- }
175
-
176
- .day-name {
177
- font-size: 12px;
178
- font-weight: 700;
179
- color: var(--fc-text-light);
180
- text-transform: uppercase;
181
- letter-spacing: 0.1em;
182
- }
183
-
184
- .day-number {
185
- font-size: 24px;
186
- font-weight: 600;
187
- color: var(--fc-text-color);
188
- }
189
-
190
- .is-today .day-number {
191
- color: var(--fc-danger-color);
192
- }
193
-
194
- /* All Day Events */
195
- .all-day-row {
196
- display: grid;
197
- grid-template-columns: 60px 1fr;
198
- border-bottom: 1px solid var(--fc-border-color);
199
- background: var(--fc-background-alt);
200
- min-height: 36px;
201
- flex-shrink: 0;
202
- }
203
-
204
- .all-day-label {
205
- font-size: 9px;
206
- color: var(--fc-text-light);
207
- display: flex;
208
- align-items: center;
209
- justify-content: center;
210
- border-right: 1px solid var(--fc-border-color);
211
- text-transform: uppercase;
212
- font-weight: 700;
213
- }
214
-
215
- .all-day-cell {
216
- padding: 6px 12px;
217
- display: flex;
218
- flex-wrap: wrap;
219
- gap: 4px;
220
- }
221
-
222
- /* Body */
223
- .day-body {
224
- flex: 1;
225
- overflow-y: auto;
226
- overflow-x: hidden;
227
- position: relative;
228
- display: grid;
229
- grid-template-columns: 60px 1fr;
230
- background: var(--fc-background);
231
- }
232
-
233
- .time-gutter {
234
- border-right: 1px solid var(--fc-border-color);
235
- background: var(--fc-background-alt);
236
- height: 1440px;
237
- }
238
-
239
- .time-slot-label {
240
- height: 60px;
241
- font-size: 11px;
242
- color: var(--fc-text-light);
243
- text-align: right;
244
- padding-right: 12px;
245
- font-weight: 500;
246
- }
247
-
248
- .day-column {
249
- position: relative;
250
- height: 1440px;
251
- }
252
-
253
- .day-column.selected {
254
- background: var(--fc-background-hover);
255
- }
256
-
257
- /* Grid Lines */
258
- .grid-lines {
259
- position: absolute;
260
- top: 0;
261
- left: 60px;
262
- right: 0;
263
- bottom: 0;
264
- pointer-events: none;
265
- }
266
-
267
- .grid-line {
268
- height: 60px;
269
- border-bottom: 1px solid var(--fc-border-color);
270
- width: 100%;
271
- }
272
-
273
- /* Event Style */
274
- .event-container {
275
- position: absolute;
276
- left: 12px;
277
- right: 24px;
278
- border-radius: 6px;
279
- padding: 8px 12px;
280
- font-size: 13px;
281
- font-weight: 500;
282
- color: white;
283
- background: var(--fc-primary-color);
284
- border: 1px solid rgba(0,0,0,0.1);
285
- overflow: hidden;
286
- box-shadow: var(--fc-shadow);
287
- cursor: pointer;
288
- transition: all 0.15s ease;
289
- z-index: 5;
290
- }
291
-
292
- .event-container:hover {
293
- z-index: 10;
294
- transform: translateX(4px);
295
- }
296
-
297
- .now-indicator {
298
- position: absolute;
299
- left: 0;
300
- right: 0;
301
- height: 2px;
302
- background: var(--fc-danger-color);
303
- z-index: 15;
304
- pointer-events: none;
305
- }
306
- `;
307
- }
308
-
309
- template() {
310
- if (!this.viewData || !this.viewData.day) {
311
- return '<div class="day-view" style="padding: 20px; color: var(--fc-text-light);">No data available.</div>';
312
- }
313
-
314
- const { day } = this.viewData;
315
- const locale = this.stateManager?.state?.config?.locale || 'en-US';
316
- const dayName = DateUtils.formatDate(day.date, 'day', locale).split(' ')[0];
317
-
318
- return `
319
- <div class="day-view">
320
- <div class="day-header">
321
- <div class="time-gutter-header"></div>
322
- <div class="day-column-header ${day.isToday ? 'is-today' : ''}">
323
- <span class="day-name">${dayName}</span>
324
- <span class="day-number">${day.date.getDate()}</span>
325
- </div>
326
- </div>
327
-
328
- <div class="all-day-row">
329
- <div class="all-day-label">All day</div>
330
- <div class="all-day-cell">
331
- ${day.allDayEvents.map(e => this.renderAllDayEvent(e)).join('')}
332
- </div>
333
- </div>
334
-
335
- <div class="day-body" id="scroll-container">
336
- <div class="grid-lines">
337
- ${this.hours.map(() => `<div class="grid-line"></div>`).join('')}
338
- </div>
339
-
340
- <div class="time-gutter">
341
- ${this.hours.map(h => `
342
- <div class="time-slot-label">
343
- ${h === 0 ? '' : DateUtils.formatTime(new Date().setHours(h, 0), false)}
344
- </div>
345
- `).join('')}
346
- </div>
347
-
348
- <div class="day-column" data-date="${day.date.toISOString()}">
349
- ${day.isToday ? this.renderNowIndicator() : ''}
350
- ${day.timedEvents.map(e => this.renderTimedEvent(e)).join('')}
351
- </div>
352
- </div>
353
- </div>
354
- `;
355
- }
356
-
357
- renderTimedEvent(event) {
358
- const start = new Date(event.start);
359
- const end = new Date(event.end);
360
-
361
- const startMinutes = start.getHours() * 60 + start.getMinutes();
362
- const durationMinutes = (end - start) / (1000 * 60);
363
-
364
- const top = startMinutes;
365
- const height = Math.max(durationMinutes, 30);
366
-
367
- const color = StyleUtils.sanitizeColor(event.backgroundColor);
368
- const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
369
-
370
- return `
371
- <div class="event-container"
372
- style="top: ${top}px; height: ${height}px; background-color: ${color}; color: ${textColor};"
373
- data-event-id="${event.id}">
374
- <span class="event-title">${DOMUtils.escapeHTML(event.title)}</span>
375
- <span class="event-time">${DateUtils.formatTime(start)} - ${DateUtils.formatTime(end)}</span>
376
- </div>
377
- `;
378
- }
379
-
380
- renderAllDayEvent(event) {
381
- const color = StyleUtils.sanitizeColor(event.backgroundColor);
382
- const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
383
-
384
- return `
385
- <div class="event-item"
386
- style="background-color: ${color}; color: ${textColor}; font-size: 12px; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 500; margin-bottom: 2px;"
387
- data-event-id="${event.id}">
388
- ${DOMUtils.escapeHTML(event.title)}
389
- </div>
390
- `;
391
- }
392
-
393
- renderNowIndicator() {
394
- const now = new Date();
395
- const minutes = now.getHours() * 60 + now.getMinutes();
396
- return `<div class="now-indicator" style="top: ${minutes}px"></div>`;
397
- }
398
-
399
- afterRender() {
400
- const container = this.$('#scroll-container');
401
- if (container && !this._scrolled) {
402
- container.scrollTop = 8 * 60 - 50;
403
- this._scrolled = true;
404
- }
405
-
406
- this.$$('[data-event-id]').forEach(el => {
407
- this.addListener(el, 'click', (e) => {
408
- e.stopPropagation();
409
- const eventId = e.currentTarget.dataset.eventId;
410
- const event = this.stateManager.getEvents().find(ev => ev.id === eventId);
411
- if (event) this.emit('event-click', { event });
412
- });
413
- });
414
-
415
- const dayCol = this.$('.day-column');
416
- if (dayCol) {
417
- this.addListener(dayCol, 'click', (e) => {
418
- const col = e.currentTarget;
419
- const container = this.$('#scroll-container');
420
- const rect = col.getBoundingClientRect();
421
- const y = e.clientY - rect.top + (container ? container.scrollTop : 0);
422
-
423
- const date = new Date(col.dataset.date);
424
- date.setHours(Math.floor(y / 60), Math.floor(y % 60), 0, 0);
425
-
426
- this.stateManager.selectDate(date);
427
- this.emit('day-click', { date });
428
- });
429
- }
430
- }
431
-
432
- unmount() {
433
- if (this.unsubscribe) this.unsubscribe();
434
- }
435
- }
436
-
437
- export default DayView;