@forcecalendar/interface 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
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",
@@ -300,16 +300,16 @@ export class EventForm extends BaseComponent {
300
300
  if (e.target === this) this.close();
301
301
  });
302
302
 
303
- // Close on Escape key - remove old listener before adding new one
304
- if (this._handleKeyDown) {
305
- window.removeEventListener('keydown', this._handleKeyDown);
303
+ // Close on Escape key - only add once to prevent memory leaks
304
+ if (!this._keydownListenerAdded) {
305
+ this._handleKeyDown = (e) => {
306
+ if (e.key === 'Escape' && this.hasAttribute('open')) {
307
+ this.close();
308
+ }
309
+ };
310
+ window.addEventListener('keydown', this._handleKeyDown);
311
+ this._keydownListenerAdded = true;
306
312
  }
307
- this._handleKeyDown = (e) => {
308
- if (e.key === 'Escape' && this.hasAttribute('open')) {
309
- this.close();
310
- }
311
- };
312
- window.addEventListener('keydown', this._handleKeyDown);
313
313
  }
314
314
 
315
315
  updateColorSelection() {
@@ -410,7 +410,12 @@ export class EventForm extends BaseComponent {
410
410
  if (this._cleanupFocusTrap) {
411
411
  this._cleanupFocusTrap();
412
412
  }
413
- window.removeEventListener('keydown', this._handleKeyDown);
413
+ // Clean up window listener
414
+ if (this._handleKeyDown) {
415
+ window.removeEventListener('keydown', this._handleKeyDown);
416
+ this._handleKeyDown = null;
417
+ this._keydownListenerAdded = false;
418
+ }
414
419
  }
415
420
  }
416
421
 
@@ -11,11 +11,16 @@ import { StyleUtils } from '../utils/StyleUtils.js';
11
11
  import { DateUtils } from '../utils/DateUtils.js';
12
12
  import { DOMUtils } from '../utils/DOMUtils.js';
13
13
 
14
- // Import view components
14
+ // Import view renderers (pure JS classes, Locker Service compatible)
15
+ import { MonthViewRenderer } from '../renderers/MonthViewRenderer.js';
16
+ import { WeekViewRenderer } from '../renderers/WeekViewRenderer.js';
17
+ import { DayViewRenderer } from '../renderers/DayViewRenderer.js';
18
+
19
+ // Import view components (Web Components, for non-Salesforce usage)
15
20
  import { MonthView } from './views/MonthView.js';
16
21
  import { WeekView } from './views/WeekView.js';
17
22
  import { DayView } from './views/DayView.js';
18
- import { EventForm } from './EventForm.js'; // Import EventForm
23
+ import { EventForm } from './EventForm.js';
19
24
 
20
25
  // Register view components
21
26
  if (!customElements.get('forcecal-month')) {
@@ -161,17 +166,20 @@ export class ForceCalendar extends BaseComponent {
161
166
  }
162
167
  }
163
168
 
164
- // Create new view
169
+ // Create new view using renderer classes
165
170
  try {
166
- const viewRenderer = this._createViewRenderer(this.currentView);
167
- if (viewRenderer) {
168
- viewRenderer._viewType = this.currentView;
169
- this._currentViewInstance = viewRenderer;
170
- viewRenderer.stateManager = this.stateManager;
171
- viewRenderer.container = container;
172
- viewRenderer.render();
173
- // Note: No subscription - handleStateChange manages all view updates
174
- }
171
+ const renderers = {
172
+ month: MonthViewRenderer,
173
+ week: WeekViewRenderer,
174
+ day: DayViewRenderer
175
+ };
176
+
177
+ const RendererClass = renderers[this.currentView] || MonthViewRenderer;
178
+ const viewRenderer = new RendererClass(container, this.stateManager);
179
+ viewRenderer._viewType = this.currentView;
180
+ this._currentViewInstance = viewRenderer;
181
+ viewRenderer.render();
182
+ // Note: No subscription - handleStateChange manages all view updates
175
183
  } catch (err) {
176
184
  console.error('[ForceCalendar] Error switching view:', err);
177
185
  }
@@ -663,7 +671,7 @@ export class ForceCalendar extends BaseComponent {
663
671
  }
664
672
 
665
673
  afterRender() {
666
- // Manually instantiate and mount view component (bypasses Locker Service)
674
+ // Manually instantiate and mount view renderer (bypasses Locker Service)
667
675
  const container = this.$('#calendar-view-container');
668
676
 
669
677
  // Only create view once per view type change
@@ -684,18 +692,21 @@ export class ForceCalendar extends BaseComponent {
684
692
  }
685
693
  }
686
694
 
687
- // Create a simple view renderer that doesn't use custom elements
695
+ // Create view renderer using the appropriate renderer class
688
696
  try {
689
- const viewRenderer = this._createViewRenderer(this.currentView);
690
- if (viewRenderer) {
691
- viewRenderer._viewType = this.currentView;
692
- this._currentViewInstance = viewRenderer;
693
- viewRenderer.stateManager = this.stateManager;
694
- viewRenderer.container = container;
695
- viewRenderer.render();
696
- // Note: No subscription here - handleStateChange manages all view updates
697
- // via _updateViewContent(), _switchView(), or full re-render
698
- }
697
+ const renderers = {
698
+ month: MonthViewRenderer,
699
+ week: WeekViewRenderer,
700
+ day: DayViewRenderer
701
+ };
702
+
703
+ const RendererClass = renderers[this.currentView] || MonthViewRenderer;
704
+ const viewRenderer = new RendererClass(container, this.stateManager);
705
+ viewRenderer._viewType = this.currentView;
706
+ this._currentViewInstance = viewRenderer;
707
+ viewRenderer.render();
708
+ // Note: No subscription here - handleStateChange manages all view updates
709
+ // via _updateViewContent(), _switchView(), or full re-render
699
710
  } catch (err) {
700
711
  console.error('[ForceCalendar] Error creating/rendering view:', err);
701
712
  }
@@ -747,435 +758,21 @@ export class ForceCalendar extends BaseComponent {
747
758
  this._hasRendered = true;
748
759
  }
749
760
 
761
+ /**
762
+ * Create a view renderer instance for the given view type
763
+ * Uses pure JavaScript renderer classes for Salesforce Locker Service compatibility
764
+ * @param {string} viewName - 'month', 'week', or 'day'
765
+ * @returns {BaseViewRenderer} Renderer instance
766
+ */
750
767
  _createViewRenderer(viewName) {
751
- // Create a simple view renderer that bypasses custom elements
752
- // This is necessary for Salesforce Locker Service compatibility
753
- const self = this;
754
- const currentViewName = viewName;
755
-
756
- return {
757
- stateManager: null,
758
- container: null,
759
- _listeners: [],
760
- _scrolled: false,
761
-
762
- _escapeHTML(str) {
763
- if (str == null) return '';
764
- return DOMUtils.escapeHTML(String(str));
765
- },
766
-
767
- cleanup() {
768
- this._listeners.forEach(({ element, event, handler }) => {
769
- element.removeEventListener(event, handler);
770
- });
771
- this._listeners = [];
772
- },
773
-
774
- addListener(element, event, handler) {
775
- element.addEventListener(event, handler);
776
- this._listeners.push({ element, event, handler });
777
- },
778
-
779
- render() {
780
- if (!this.container || !this.stateManager) return;
781
-
782
- const viewData = this.stateManager.getViewData();
783
- if (!viewData) {
784
- this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Loading...</div>';
785
- return;
786
- }
787
-
788
- this.cleanup();
789
- const config = this.stateManager.getState().config;
790
- let html = '';
791
-
792
- switch (currentViewName) {
793
- case 'week':
794
- html = this._renderWeekView(viewData, config);
795
- break;
796
- case 'day':
797
- html = this._renderDayView(viewData, config);
798
- break;
799
- case 'month':
800
- default:
801
- if (!viewData.weeks) {
802
- this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No data available for month view.</div>';
803
- return;
804
- }
805
- html = this._renderMonthView(viewData, config);
806
- break;
807
- }
808
-
809
- this.container.innerHTML = html;
810
- this._attachEventHandlers(currentViewName);
811
- },
812
-
813
- _renderMonthView(viewData, config) {
814
- const weekStartsOn = config.weekStartsOn || 0;
815
- const dayNames = [];
816
- for (let i = 0; i < 7; i++) {
817
- const dayIndex = (weekStartsOn + i) % 7;
818
- dayNames.push(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][dayIndex]);
819
- }
820
-
821
- let html = `
822
- <div class="fc-month-view" style="display: flex; flex-direction: column; height: 100%; min-height: 400px; background: #fff; border: 1px solid #e5e7eb;">
823
- <div class="fc-month-header" style="display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #f9fafb;">
824
- ${dayNames.map(d => `<div class="fc-month-header-cell" style="padding: 12px 8px; text-align: center; font-size: 11px; font-weight: 600; color: #6b7280; text-transform: uppercase;">${d}</div>`).join('')}
825
- </div>
826
- <div class="fc-month-body" style="display: flex; flex-direction: column; flex: 1;">
827
- `;
828
-
829
- viewData.weeks.forEach(week => {
830
- html += '<div class="fc-month-week" style="display: grid; grid-template-columns: repeat(7, 1fr); flex: 1; min-height: 80px;">';
831
- week.days.forEach(day => {
832
- const isOtherMonth = !day.isCurrentMonth;
833
- const isToday = day.isToday;
834
-
835
- const dayBg = isOtherMonth ? '#f3f4f6' : '#fff';
836
- const dayNumColor = isOtherMonth ? '#9ca3af' : '#111827';
837
- const todayStyle = isToday ? 'background: #2563eb; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;' : '';
838
-
839
- const events = day.events || [];
840
- const visibleEvents = events.slice(0, 3);
841
- const moreCount = events.length - 3;
842
-
843
- html += `
844
- <div class="fc-month-day" data-date="${day.date}" style="background: ${dayBg}; border-right: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb; padding: 4px; min-height: 80px; cursor: pointer;">
845
- <div class="fc-day-number" style="font-size: 13px; font-weight: 500; color: ${dayNumColor}; padding: 2px 4px; margin-bottom: 4px; ${todayStyle}">${day.dayOfMonth}</div>
846
- <div class="fc-day-events" style="display: flex; flex-direction: column; gap: 2px;">
847
- ${visibleEvents.map(evt => `
848
- <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 11px; padding: 2px 6px; border-radius: 3px; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
849
- ${this._escapeHTML(evt.title)}
850
- </div>
851
- `).join('')}
852
- ${moreCount > 0 ? `<div class="fc-more-events" style="font-size: 10px; color: #6b7280; padding: 2px 4px; font-weight: 500;">+${moreCount} more</div>` : ''}
853
- </div>
854
- </div>
855
- `;
856
- });
857
- html += '</div>';
858
- });
859
-
860
- html += '</div></div>';
861
- return html;
862
- },
863
-
864
- _renderWeekView(viewData, config) {
865
- const days = viewData.days || [];
866
- if (days.length === 0) {
867
- return '<div style="padding: 20px; text-align: center; color: #666;">No data available for week view.</div>';
868
- }
869
-
870
- const weekStartsOn = config.weekStartsOn || 0;
871
- const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
872
- const hours = Array.from({ length: 24 }, (_, i) => i);
873
-
874
- // Process days to add events
875
- const processedDays = days.map(day => {
876
- const dayDate = new Date(day.date);
877
- const events = day.events || [];
878
- return {
879
- ...day,
880
- date: dayDate,
881
- dayName: dayNames[dayDate.getDay()],
882
- dayOfMonth: dayDate.getDate(),
883
- isToday: this._isToday(dayDate),
884
- timedEvents: events.filter(e => !e.allDay),
885
- allDayEvents: events.filter(e => e.allDay)
886
- };
887
- });
888
-
889
- let html = `
890
- <div class="fc-week-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
891
- <!-- Header -->
892
- <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
893
- <div style="border-right: 1px solid #e5e7eb;"></div>
894
- ${processedDays.map(day => `
895
- <div style="padding: 12px 8px; text-align: center; border-right: 1px solid #e5e7eb;">
896
- <div style="font-size: 10px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">${day.dayName}</div>
897
- <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;'}">${day.dayOfMonth}</div>
898
- </div>
899
- `).join('')}
900
- </div>
901
-
902
- <!-- All Day Row -->
903
- <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 32px; flex-shrink: 0;">
904
- <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;">All day</div>
905
- ${processedDays.map(day => `
906
- <div style="border-right: 1px solid #e5e7eb; padding: 4px; display: flex; flex-direction: column; gap: 2px;">
907
- ${day.allDayEvents.map(evt => `
908
- <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" 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;">
909
- ${this._escapeHTML(evt.title)}
910
- </div>
911
- `).join('')}
912
- </div>
913
- `).join('')}
914
- </div>
915
-
916
- <!-- Time Grid Body -->
917
- <div id="week-scroll-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
918
- <div style="display: grid; grid-template-columns: 60px repeat(7, 1fr); position: relative; height: 1440px;">
919
- <!-- Time Gutter -->
920
- <div style="border-right: 1px solid #e5e7eb; background: #fafafa;">
921
- ${hours.map(h => `
922
- <div style="height: 60px; font-size: 10px; color: #6b7280; text-align: right; padding-right: 8px; font-weight: 500;">
923
- ${h === 0 ? '' : this._formatHour(h)}
924
- </div>
925
- `).join('')}
926
- </div>
927
-
928
- <!-- Day Columns -->
929
- ${processedDays.map(day => `
930
- <div class="fc-week-day-column" data-date="${day.date.toISOString()}" style="border-right: 1px solid #e5e7eb; position: relative; cursor: pointer;">
931
- <!-- Hour grid lines -->
932
- ${hours.map(() => `<div style="height: 60px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
933
-
934
- <!-- Now indicator for today -->
935
- ${day.isToday ? this._renderNowIndicator() : ''}
936
-
937
- <!-- Timed events -->
938
- ${day.timedEvents.map(evt => this._renderTimedEvent(evt)).join('')}
939
- </div>
940
- `).join('')}
941
- </div>
942
- </div>
943
- </div>
944
- `;
945
-
946
- return html;
947
- },
948
-
949
- _renderDayView(viewData, config) {
950
- // Day view from core has: type, date, dayName, isToday, allDayEvents, hours
951
- // We need to handle both the core structure and enriched structure
952
- const currentDate = this.stateManager?.getState()?.currentDate || new Date();
953
-
954
- let dayDate, dayName, isToday, allDayEvents, timedEvents;
955
-
956
- if (viewData.type === 'day' && viewData.date) {
957
- // Core day view structure
958
- dayDate = new Date(viewData.date);
959
- dayName = viewData.dayName || ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayDate.getDay()];
960
- isToday = viewData.isToday !== undefined ? viewData.isToday : this._isToday(dayDate);
961
- allDayEvents = viewData.allDayEvents || [];
962
-
963
- // Extract timed events from hours array or get from stateManager
964
- if (viewData.hours && Array.isArray(viewData.hours)) {
965
- // Collect unique events from hours (events can span multiple hours)
966
- const eventMap = new Map();
967
- viewData.hours.forEach(hour => {
968
- (hour.events || []).forEach(evt => {
969
- if (!eventMap.has(evt.id)) {
970
- eventMap.set(evt.id, evt);
971
- }
972
- });
973
- });
974
- timedEvents = Array.from(eventMap.values());
975
- } else {
976
- timedEvents = [];
977
- }
978
- } else if (viewData.days && viewData.days.length > 0) {
979
- // Enriched structure with days array
980
- const dayData = viewData.days.find(d => this._isSameDay(new Date(d.date), currentDate)) || viewData.days[0];
981
- dayDate = new Date(dayData.date);
982
- dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dayDate.getDay()];
983
- isToday = this._isToday(dayDate);
984
- const events = dayData.events || [];
985
- allDayEvents = events.filter(e => e.allDay);
986
- timedEvents = events.filter(e => !e.allDay);
987
- } else {
988
- return '<div style="padding: 20px; text-align: center; color: #666;">No data available for day view.</div>';
989
- }
990
-
991
- const hours = Array.from({ length: 24 }, (_, i) => i);
992
-
993
- let html = `
994
- <div class="fc-day-view" style="display: flex; flex-direction: column; height: 100%; background: #fff; overflow: hidden;">
995
- <!-- Header -->
996
- <div style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #f9fafb; flex-shrink: 0;">
997
- <div style="border-right: 1px solid #e5e7eb;"></div>
998
- <div style="padding: 16px 24px;">
999
- <div style="font-size: 12px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.1em;">${dayName}</div>
1000
- <div style="font-size: 24px; font-weight: 600; margin-top: 4px; ${isToday ? 'color: #dc2626;' : 'color: #111827;'}">${dayDate.getDate()}</div>
1001
- </div>
1002
- </div>
1003
-
1004
- <!-- All Day Row -->
1005
- <div style="display: grid; grid-template-columns: 60px 1fr; border-bottom: 1px solid #e5e7eb; background: #fafafa; min-height: 36px; flex-shrink: 0;">
1006
- <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;">All day</div>
1007
- <div style="padding: 6px 12px; display: flex; flex-wrap: wrap; gap: 4px;">
1008
- ${allDayEvents.map(evt => `
1009
- <div class="fc-event" data-event-id="${this._escapeHTML(evt.id)}" style="background-color: ${evt.backgroundColor || '#2563eb'}; font-size: 12px; padding: 4px 8px; border-radius: 4px; color: white; cursor: pointer; font-weight: 500;">
1010
- ${this._escapeHTML(evt.title)}
1011
- </div>
1012
- `).join('')}
1013
- </div>
1014
- </div>
1015
-
1016
- <!-- Time Grid Body -->
1017
- <div id="day-scroll-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; position: relative;">
1018
- <div style="display: grid; grid-template-columns: 60px 1fr; position: relative; height: 1440px;">
1019
- <!-- Time Gutter -->
1020
- <div style="border-right: 1px solid #e5e7eb; background: #fafafa;">
1021
- ${hours.map(h => `
1022
- <div style="height: 60px; font-size: 11px; color: #6b7280; text-align: right; padding-right: 12px; font-weight: 500;">
1023
- ${h === 0 ? '' : this._formatHour(h)}
1024
- </div>
1025
- `).join('')}
1026
- </div>
1027
-
1028
- <!-- Day Column -->
1029
- <div class="fc-day-column" data-date="${dayDate.toISOString()}" style="position: relative; cursor: pointer;">
1030
- <!-- Hour grid lines -->
1031
- ${hours.map(() => `<div style="height: 60px; border-bottom: 1px solid #f3f4f6;"></div>`).join('')}
1032
-
1033
- <!-- Now indicator for today -->
1034
- ${isToday ? this._renderNowIndicator() : ''}
1035
-
1036
- <!-- Timed events -->
1037
- ${timedEvents.map(evt => this._renderTimedEventDay(evt)).join('')}
1038
- </div>
1039
- </div>
1040
- </div>
1041
- </div>
1042
- `;
1043
-
1044
- return html;
1045
- },
1046
-
1047
- _renderTimedEvent(event) {
1048
- const start = new Date(event.start);
1049
- const end = new Date(event.end);
1050
- const startMinutes = start.getHours() * 60 + start.getMinutes();
1051
- const durationMinutes = Math.max((end - start) / (1000 * 60), 20);
1052
- const color = event.backgroundColor || '#2563eb';
1053
-
1054
- return `
1055
- <div class="fc-event" data-event-id="${this._escapeHTML(event.id)}"
1056
- style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 2px; right: 2px;
1057
- background-color: ${color}; border-radius: 4px; padding: 4px 8px; font-size: 11px;
1058
- font-weight: 500; color: white; overflow: hidden; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
1059
- cursor: pointer; z-index: 5;">
1060
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${this._escapeHTML(event.title)}</div>
1061
- <div style="font-size: 10px; opacity: 0.9;">${this._formatTime(start)}</div>
1062
- </div>
1063
- `;
1064
- },
1065
-
1066
- _renderTimedEventDay(event) {
1067
- const start = new Date(event.start);
1068
- const end = new Date(event.end);
1069
- const startMinutes = start.getHours() * 60 + start.getMinutes();
1070
- const durationMinutes = Math.max((end - start) / (1000 * 60), 30);
1071
- const color = event.backgroundColor || '#2563eb';
1072
-
1073
- return `
1074
- <div class="fc-event" data-event-id="${this._escapeHTML(event.id)}"
1075
- style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px; left: 12px; right: 24px;
1076
- background-color: ${color}; border-radius: 6px; padding: 8px 12px; font-size: 13px;
1077
- font-weight: 500; color: white; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1078
- cursor: pointer; z-index: 5;">
1079
- <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${this._escapeHTML(event.title)}</div>
1080
- <div style="font-size: 11px; opacity: 0.9;">${this._formatTime(start)} - ${this._formatTime(end)}</div>
1081
- </div>
1082
- `;
1083
- },
1084
-
1085
- _renderNowIndicator() {
1086
- const now = new Date();
1087
- const minutes = now.getHours() * 60 + now.getMinutes();
1088
- return `<div style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
1089
- },
1090
-
1091
- _formatHour(hour) {
1092
- const period = hour >= 12 ? 'PM' : 'AM';
1093
- const displayHour = hour % 12 || 12;
1094
- return `${displayHour} ${period}`;
1095
- },
1096
-
1097
- _formatTime(date) {
1098
- const hours = date.getHours();
1099
- const minutes = date.getMinutes();
1100
- const period = hours >= 12 ? 'PM' : 'AM';
1101
- const displayHour = hours % 12 || 12;
1102
- return minutes === 0 ? `${displayHour} ${period}` : `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
1103
- },
1104
-
1105
- _isToday(date) {
1106
- const today = new Date();
1107
- return date.getDate() === today.getDate() &&
1108
- date.getMonth() === today.getMonth() &&
1109
- date.getFullYear() === today.getFullYear();
1110
- },
1111
-
1112
- _isSameDay(date1, date2) {
1113
- return date1.getDate() === date2.getDate() &&
1114
- date1.getMonth() === date2.getMonth() &&
1115
- date1.getFullYear() === date2.getFullYear();
1116
- },
1117
-
1118
- _attachEventHandlers(viewType) {
1119
- const stateManager = this.stateManager;
1120
- const self = this;
1121
-
1122
- // Day click handlers (for month view)
1123
- this.container.querySelectorAll('.fc-month-day').forEach(dayEl => {
1124
- this.addListener(dayEl, 'click', (e) => {
1125
- const date = new Date(dayEl.dataset.date);
1126
- stateManager.selectDate(date);
1127
- });
1128
- });
1129
-
1130
- // Week view day column click handlers
1131
- this.container.querySelectorAll('.fc-week-day-column').forEach(dayEl => {
1132
- this.addListener(dayEl, 'click', (e) => {
1133
- if (e.target.closest('.fc-event')) return;
1134
- const date = new Date(dayEl.dataset.date);
1135
- const rect = dayEl.getBoundingClientRect();
1136
- const scrollContainer = this.container.querySelector('#week-scroll-container');
1137
- const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
1138
- date.setHours(Math.floor(y / 60), Math.floor(y % 60), 0, 0);
1139
- stateManager.selectDate(date);
1140
- });
1141
- });
1142
-
1143
- // Day view column click handlers
1144
- this.container.querySelectorAll('.fc-day-column').forEach(dayEl => {
1145
- this.addListener(dayEl, 'click', (e) => {
1146
- if (e.target.closest('.fc-event')) return;
1147
- const date = new Date(dayEl.dataset.date);
1148
- const rect = dayEl.getBoundingClientRect();
1149
- const scrollContainer = this.container.querySelector('#day-scroll-container');
1150
- const y = e.clientY - rect.top + (scrollContainer ? scrollContainer.scrollTop : 0);
1151
- date.setHours(Math.floor(y / 60), Math.floor(y % 60), 0, 0);
1152
- stateManager.selectDate(date);
1153
- });
1154
- });
1155
-
1156
- // Event click handlers
1157
- this.container.querySelectorAll('.fc-event').forEach(eventEl => {
1158
- this.addListener(eventEl, 'click', (e) => {
1159
- e.stopPropagation();
1160
- const eventId = eventEl.dataset.eventId;
1161
- const event = stateManager.getEvents().find(ev => ev.id === eventId);
1162
- if (event) {
1163
- stateManager.selectEvent(event);
1164
- }
1165
- });
1166
- });
1167
-
1168
- // Scroll to 8 AM for week and day views
1169
- if (viewType === 'week' || viewType === 'day') {
1170
- const scrollContainerId = viewType === 'week' ? '#week-scroll-container' : '#day-scroll-container';
1171
- const scrollContainer = this.container.querySelector(scrollContainerId);
1172
- if (scrollContainer && !this._scrolled) {
1173
- scrollContainer.scrollTop = 8 * 60 - 50;
1174
- this._scrolled = true;
1175
- }
1176
- }
1177
- }
768
+ const renderers = {
769
+ month: MonthViewRenderer,
770
+ week: WeekViewRenderer,
771
+ day: DayViewRenderer
1178
772
  };
773
+
774
+ const RendererClass = renderers[viewName] || MonthViewRenderer;
775
+ return new RendererClass(null, null); // Container and stateManager set after creation
1179
776
  }
1180
777
 
1181
778
  handleNavigation(event) {