@forcecalendar/interface 1.0.18 → 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.
@@ -1,621 +0,0 @@
1
- /**
2
- * MonthView - Month grid view component
3
- *
4
- * Displays a traditional month calendar grid with events
5
- */
6
-
7
- import { BaseComponent } from '../../core/BaseComponent.js';
8
- import { DOMUtils } from '../../utils/DOMUtils.js';
9
- import { DateUtils } from '../../utils/DateUtils.js';
10
- import { StyleUtils } from '../../utils/StyleUtils.js';
11
-
12
- export class MonthView extends BaseComponent {
13
- constructor() {
14
- super();
15
- this._stateManager = null;
16
- this.viewData = null;
17
- this.config = {
18
- maxEventsToShow: 3,
19
- };
20
- this._registryCheckInterval = null;
21
- }
22
-
23
- connectedCallback() {
24
- super.connectedCallback();
25
- console.log('[MonthView] connectedCallback - starting registry polling');
26
- // Poll for registry since attributeChangedCallback doesn't work in Locker Service
27
- this._startRegistryPolling();
28
- }
29
-
30
- disconnectedCallback() {
31
- super.disconnectedCallback();
32
- this._stopRegistryPolling();
33
- }
34
-
35
- _startRegistryPolling() {
36
- // Check immediately
37
- this._checkRegistry();
38
-
39
- // Then poll every 100ms until we find it (max 5 seconds)
40
- let attempts = 0;
41
- this._registryCheckInterval = setInterval(() => {
42
- attempts++;
43
- if (this._stateManager || attempts > 50) {
44
- this._stopRegistryPolling();
45
- return;
46
- }
47
- this._checkRegistry();
48
- }, 100);
49
- }
50
-
51
- _stopRegistryPolling() {
52
- if (this._registryCheckInterval) {
53
- clearInterval(this._registryCheckInterval);
54
- this._registryCheckInterval = null;
55
- }
56
- }
57
-
58
- _checkRegistry() {
59
- const registryId = this.getAttribute('data-state-registry');
60
- console.log('[MonthView] Checking registry for ID:', registryId);
61
- if (registryId && window.__forceCalendarRegistry && window.__forceCalendarRegistry[registryId]) {
62
- const manager = window.__forceCalendarRegistry[registryId];
63
- console.log('[MonthView] Found stateManager in registry');
64
- this._stopRegistryPolling();
65
- this.setStateManager(manager);
66
- }
67
- }
68
-
69
- set stateManager(manager) {
70
- console.log('[MonthView] stateManager setter called with:', !!manager);
71
- this.setStateManager(manager);
72
- }
73
-
74
- // Method alternative for Salesforce Locker Service compatibility
75
- setStateManager(manager) {
76
- console.log('[MonthView] setStateManager method called with:', !!manager);
77
- // Prevent re-initialization if same manager
78
- if (this._stateManager === manager) {
79
- console.log('[MonthView] stateManager already set, skipping');
80
- return;
81
- }
82
- this._stateManager = manager;
83
- if (manager) {
84
- console.log('[MonthView] Subscribing to state changes and loading view data');
85
- // Subscribe to state changes
86
- this.unsubscribe = manager.subscribe(this.handleStateUpdate.bind(this));
87
- this.loadViewData();
88
- }
89
- }
90
-
91
- get stateManager() {
92
- return this._stateManager;
93
- }
94
-
95
- handleStateUpdate(newState, oldState) {
96
- if (newState.currentDate !== oldState.currentDate) {
97
- this.loadViewData(); // Full reload if the month/year changes
98
- return;
99
- }
100
-
101
- if (newState.events !== oldState.events) {
102
- this.updateEvents();
103
- }
104
-
105
- if (newState.selectedDate !== oldState.selectedDate) {
106
- this.updateSelection(newState.selectedDate, oldState.selectedDate);
107
- }
108
- }
109
-
110
- updateEvents() {
111
- this.loadViewData(); // For now, we still do a full reload. A more granular update would be more complex.
112
- }
113
-
114
- updateSelection(newDate, oldDate) {
115
- if (oldDate) {
116
- const oldDateEl = this.shadowRoot.querySelector(`[data-date^="${oldDate.toISOString().split('T')[0]}"]`);
117
- if (oldDateEl) {
118
- oldDateEl.classList.remove('selected');
119
- }
120
- }
121
- if (newDate) {
122
- const newDateEl = this.shadowRoot.querySelector(`[data-date^="${newDate.toISOString().split('T')[0]}"]`);
123
- if (newDateEl) {
124
- newDateEl.classList.add('selected');
125
- }
126
- }
127
- }
128
-
129
- loadViewData() {
130
- console.log('[MonthView] loadViewData called, stateManager:', !!this.stateManager);
131
- if (!this.stateManager) return;
132
-
133
- const viewData = this.stateManager.getViewData();
134
- console.log('[MonthView] viewData from stateManager:', viewData);
135
- this.viewData = this.processViewData(viewData);
136
- console.log('[MonthView] processed viewData:', this.viewData);
137
- this.render();
138
- console.log('[MonthView] render completed');
139
- }
140
-
141
- processViewData(viewData) {
142
- if (!viewData || !viewData.weeks) return null;
143
-
144
- const selectedDate = this.stateManager?.getState()?.selectedDate;
145
-
146
- const weeks = viewData.weeks.map(week => {
147
- return week.days.map(day => {
148
- const dayDate = new Date(day.date);
149
- const isSelected = selectedDate && dayDate.toDateString() === selectedDate.toDateString();
150
-
151
- const processedEvents = day.events.map(event => ({
152
- ...event,
153
- textColor: this.getContrastingTextColor(event.backgroundColor)
154
- }));
155
-
156
- return {
157
- ...day,
158
- date: dayDate,
159
- isOtherMonth: !day.isCurrentMonth,
160
- isSelected,
161
- events: processedEvents,
162
- };
163
- });
164
- });
165
-
166
- return {
167
- ...viewData,
168
- weeks,
169
- month: viewData.month,
170
- year: viewData.year
171
- };
172
- }
173
-
174
- getContrastingTextColor(bgColor) {
175
- if (!bgColor || typeof bgColor !== 'string') return 'white';
176
-
177
- // Extract hex color, removing # if present
178
- const color = (bgColor.charAt(0) === '#') ? bgColor.substring(1) : bgColor;
179
-
180
- // Validate hex format (3 or 6 characters)
181
- if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(color)) {
182
- return 'white'; // Fallback for invalid format
183
- }
184
-
185
- // Expand 3-char hex to 6-char
186
- const fullColor = color.length === 3
187
- ? color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
188
- : color;
189
-
190
- const r = parseInt(fullColor.substring(0, 2), 16);
191
- const g = parseInt(fullColor.substring(2, 4), 16);
192
- const b = parseInt(fullColor.substring(4, 6), 16);
193
-
194
- // Check for NaN (shouldn't happen with validation, but just in case)
195
- if (isNaN(r) || isNaN(g) || isNaN(b)) {
196
- return 'white';
197
- }
198
-
199
- const uicolors = [r / 255, g / 255, b / 255];
200
- const c = uicolors.map((col) => {
201
- if (col <= 0.03928) {
202
- return col / 12.92;
203
- }
204
- return Math.pow((col + 0.055) / 1.055, 2.4);
205
- });
206
- const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
207
- return (L > 0.179) ? 'black' : 'white';
208
- }
209
-
210
- isSelectedDate(date) {
211
- const selectedDate = this.stateManager?.getState()?.selectedDate;
212
- return selectedDate && date.toDateString() === selectedDate.toDateString();
213
- }
214
-
215
- getStyles() {
216
- return `
217
- :host {
218
- display: block;
219
- height: 100%;
220
- }
221
-
222
- .month-view {
223
- display: flex;
224
- flex-direction: column;
225
- height: 100%;
226
- background: var(--fc-background);
227
- }
228
-
229
- .month-header {
230
- display: grid;
231
- grid-template-columns: repeat(7, 1fr);
232
- background: var(--fc-background);
233
- border-bottom: 1px solid var(--fc-border-color);
234
- z-index: 5;
235
- }
236
-
237
- .month-header-cell {
238
- padding: var(--fc-spacing-sm);
239
- text-align: left; /* Align with dates */
240
- font-weight: var(--fc-font-weight-bold);
241
- font-size: 10px;
242
- color: var(--fc-text-light);
243
- text-transform: uppercase;
244
- letter-spacing: 0.1em;
245
- border-left: 1px solid transparent; /* Alignment hack */
246
- padding-left: 8px;
247
- }
248
-
249
- .month-body {
250
- flex: 1;
251
- display: flex;
252
- flex-direction: column;
253
- overflow: hidden;
254
- }
255
-
256
- .month-week {
257
- flex: 1;
258
- display: grid;
259
- grid-template-columns: repeat(7, 1fr);
260
- border-bottom: 1px solid var(--fc-border-color);
261
- }
262
-
263
- .month-week:last-child {
264
- border-bottom: none;
265
- }
266
-
267
- .month-day {
268
- background: var(--fc-background);
269
- padding: 4px;
270
- position: relative;
271
- cursor: default;
272
- overflow: hidden;
273
- min-height: 80px;
274
- border-right: 1px solid var(--fc-border-color);
275
- display: flex;
276
- flex-direction: column;
277
- min-width: 0; /* Critical for Grid Item shrinking */
278
- }
279
-
280
- .month-day:last-child {
281
- border-right: none;
282
- }
283
-
284
- .month-day:hover {
285
- background: var(--fc-background-alt);
286
- }
287
-
288
- .month-day.other-month {
289
- background: var(--fc-background-alt);
290
- background-image: linear-gradient(45deg, #f9fafb 25%, transparent 25%, transparent 50%, #f9fafb 50%, #f9fafb 75%, transparent 75%, transparent);
291
- background-size: 10px 10px;
292
- }
293
-
294
- .month-day.other-month .day-number {
295
- color: var(--fc-text-light);
296
- opacity: 0.5;
297
- }
298
-
299
- .month-day.selected {
300
- background: var(--fc-background-hover);
301
- }
302
-
303
- .day-header {
304
- display: flex;
305
- align-items: center;
306
- justify-content: space-between;
307
- padding: 4px;
308
- margin-bottom: 2px;
309
- }
310
-
311
- .day-number {
312
- font-size: 12px;
313
- font-family: var(--fc-font-family); /* Ensure monospaced feel if available */
314
- font-weight: var(--fc-font-weight-medium);
315
- color: var(--fc-text-color);
316
- line-height: 1;
317
- }
318
-
319
- .month-day.today .day-number {
320
- color: white;
321
- background: var(--fc-danger-color); /* Red for Today (Calendar standard) */
322
- width: 20px;
323
- height: 20px;
324
- display: flex;
325
- align-items: center;
326
- justify-content: center;
327
- border-radius: 50%;
328
- margin-left: -4px; /* Optical adjustment */
329
- }
330
-
331
- .day-events {
332
- display: flex;
333
- flex-direction: column;
334
- gap: 2px;
335
- flex: 1;
336
- overflow: hidden;
337
- }
338
-
339
- /* Precision Event Style */
340
- .event-item {
341
- font-size: 11px;
342
- padding: 2px 6px;
343
- border-radius: 2px; /* Micro rounding */
344
-
345
- /* High Contrast */
346
- background: var(--fc-primary-color);
347
- color: white;
348
-
349
- overflow: hidden;
350
- text-overflow: ellipsis;
351
- white-space: nowrap;
352
- cursor: pointer;
353
- line-height: 1.3;
354
- font-weight: var(--fc-font-weight-medium);
355
- margin: 0 1px;
356
- border: 1px solid rgba(0,0,0,0.05); /* Subtle border for definition */
357
- }
358
-
359
- .event-item:hover {
360
- opacity: 0.9;
361
- }
362
-
363
- .event-time {
364
- font-weight: var(--fc-font-weight-bold);
365
- margin-right: 4px;
366
- opacity: 0.9;
367
- font-size: 10px;
368
- }
369
-
370
- .more-events {
371
- font-size: 10px;
372
- color: var(--fc-text-secondary);
373
- cursor: pointer;
374
- padding: 1px 4px;
375
- font-weight: var(--fc-font-weight-medium);
376
- text-align: right;
377
- }
378
-
379
- .more-events:hover {
380
- color: var(--fc-text-color);
381
- text-decoration: underline;
382
- }
383
-
384
- /* Responsive adjustments */
385
- @media (max-width: 768px) {
386
- .month-day {
387
- min-height: 60px;
388
- padding: 2px;
389
- }
390
-
391
- .day-number {
392
- font-size: 11px;
393
- }
394
-
395
- .event-item {
396
- font-size: 10px;
397
- padding: 1px 3px;
398
- }
399
-
400
- .month-header-cell {
401
- font-size: 9px;
402
- padding: 4px;
403
- }
404
- }
405
-
406
- /* Loading state */
407
- .month-loading {
408
- display: flex;
409
- align-items: center;
410
- justify-content: center;
411
- height: 100%;
412
- color: var(--fc-text-secondary);
413
- font-weight: var(--fc-font-weight-medium);
414
- }
415
-
416
- /* Empty state */
417
- .month-empty {
418
- display: flex;
419
- flex-direction: column;
420
- align-items: center;
421
- justify-content: center;
422
- height: 100%;
423
- color: var(--fc-text-secondary);
424
- gap: var(--fc-spacing-md);
425
- }
426
-
427
- .empty-icon {
428
- width: 48px;
429
- height: 48px;
430
- opacity: 0.3;
431
- }
432
- `;
433
- }
434
-
435
- template() {
436
- if (!this.viewData) {
437
- return `
438
- <div class="month-view">
439
- <div class="month-loading">Loading calendar...</div>
440
- </div>
441
- `;
442
- }
443
-
444
- return `
445
- <div class="month-view">
446
- ${this.renderHeader()}
447
- ${this.renderBody()}
448
- </div>
449
- `;
450
- }
451
-
452
- renderHeader() {
453
- const { config } = this.stateManager.getState();
454
- const days = [];
455
- const weekStartsOn = config.weekStartsOn || 0;
456
-
457
- for (let i = 0; i < 7; i++) {
458
- const dayIndex = (weekStartsOn + i) % 7;
459
- const dayName = DateUtils.getDayAbbreviation(dayIndex, config.locale);
460
- days.push(`<div class="month-header-cell">${dayName}</div>`);
461
- }
462
-
463
- return `
464
- <div class="month-header">
465
- ${days.join('')}
466
- </div>
467
- `;
468
- }
469
-
470
- renderBody() {
471
- if (!this.viewData.weeks || this.viewData.weeks.length === 0) {
472
- return `
473
- <div class="month-body">
474
- <div class="month-empty">
475
- <svg class="empty-icon" viewBox="0 0 24 24">
476
- <path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z"/>
477
- </svg>
478
- <p>No calendar data available</p>
479
- </div>
480
- </div>
481
- `;
482
- }
483
-
484
- const weeks = this.viewData.weeks.map(week => this.renderWeek(week));
485
-
486
- return `
487
- <div class="month-body">
488
- ${weeks.join('')}
489
- </div>
490
- `;
491
- }
492
-
493
- renderWeek(weekDays) {
494
- const days = weekDays.map(day => this.renderDay(day));
495
-
496
- return `
497
- <div class="month-week">
498
- ${days.join('')}
499
- </div>
500
- `;
501
- }
502
-
503
- renderDay(dayData) {
504
- const { date, dayOfMonth, isOtherMonth, isToday, isSelected, isWeekend, events = [] } = dayData;
505
- const dayNumber = dayOfMonth;
506
-
507
- // Build classes
508
- const classes = ['month-day'];
509
- if (isOtherMonth) classes.push('other-month');
510
- if (isToday) classes.push('today');
511
- if (isSelected) classes.push('selected');
512
- if (isWeekend) classes.push('weekend');
513
-
514
- // Render events
515
- const visibleEvents = events.slice(0, this.config.maxEventsToShow);
516
- const remainingCount = events.length - this.config.maxEventsToShow;
517
-
518
- const eventsHtml = visibleEvents.map(event => this.renderEvent(event)).join('');
519
- const moreHtml = remainingCount > 0 ?
520
- `<div class="more-events">+${remainingCount} more</div>` : '';
521
-
522
- return `
523
- <div class="${classes.join(' ')}"
524
- data-date="${date.toISOString()}"
525
- data-day="${dayNumber}">
526
- <div class="day-header">
527
- <span class="day-number">${dayNumber}</span>
528
- </div>
529
- <div class="day-events">
530
- ${eventsHtml}
531
- ${moreHtml}
532
- </div>
533
- </div>
534
- `;
535
- }
536
-
537
- renderEvent(event) {
538
- const { title, start, allDay, backgroundColor, textColor } = event;
539
-
540
- let style = '';
541
- if (backgroundColor) {
542
- const safeColor = StyleUtils.sanitizeColor(backgroundColor);
543
- const safeTextColor = StyleUtils.sanitizeColor(textColor, 'white');
544
- style += `background-color: ${safeColor}; color: ${safeTextColor};`;
545
- }
546
-
547
- let timeStr = '';
548
- if (!allDay && start) {
549
- timeStr = DateUtils.formatTime(new Date(start), false, false);
550
- }
551
-
552
- const classes = ['event-item'];
553
- if (allDay) classes.push('all-day');
554
-
555
- return `
556
- <div class="${classes.join(' ')}"
557
- style="${style}"
558
- data-event-id="${event.id}"
559
- title="${DOMUtils.escapeHTML(title)}">
560
- ${timeStr ? `<span class="event-time">${timeStr}</span>` : ''}
561
- <span class="event-title">${DOMUtils.escapeHTML(title)}</span>
562
- </div>
563
- `;
564
- }
565
-
566
- afterRender() {
567
- // Add click handlers for days
568
- this.$$('.month-day').forEach(dayEl => {
569
- this.addListener(dayEl, 'click', this.handleDayClick);
570
- });
571
-
572
- // Add click handlers for events
573
- this.$$('.event-item').forEach(eventEl => {
574
- this.addListener(eventEl, 'click', this.handleEventClick);
575
- });
576
-
577
- // Add click handlers for "more events"
578
- this.$$('.more-events').forEach(moreEl => {
579
- this.addListener(moreEl, 'click', this.handleMoreClick);
580
- });
581
- }
582
-
583
- handleDayClick(event) {
584
- event.stopPropagation();
585
- const dayEl = event.currentTarget;
586
- const date = new Date(dayEl.dataset.date);
587
-
588
- this.stateManager.selectDate(date);
589
- this.emit('day-click', { date });
590
- }
591
-
592
- handleEventClick(event) {
593
- event.stopPropagation();
594
- const eventEl = event.currentTarget;
595
- const eventId = eventEl.dataset.eventId;
596
- const calendarEvent = this.stateManager.getEvents().find(e => e.id === eventId);
597
-
598
- if (calendarEvent) {
599
- this.stateManager.selectEvent(calendarEvent);
600
- this.emit('event-click', { event: calendarEvent });
601
- }
602
- }
603
-
604
- handleMoreClick(event) {
605
- event.stopPropagation();
606
- const dayEl = event.currentTarget.closest('.month-day');
607
- const date = new Date(dayEl.dataset.date);
608
- const events = this.stateManager.getEventsForDate(date);
609
-
610
- this.emit('more-events-click', { date, events });
611
- }
612
-
613
- unmount() {
614
- if (this.unsubscribe) {
615
- this.unsubscribe();
616
- }
617
- }
618
- }
619
-
620
- // Export both the class and as default
621
- export default MonthView;