@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.
- package/dist/force-calendar-interface.esm.js +614 -1542
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +150 -847
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ForceCalendar.js +48 -466
- package/src/index.js +6 -5
- package/src/renderers/BaseViewRenderer.js +219 -0
- package/src/renderers/DayViewRenderer.js +199 -0
- package/src/renderers/MonthViewRenderer.js +125 -0
- package/src/renderers/WeekViewRenderer.js +171 -0
- package/src/renderers/index.js +11 -0
- package/src/components/views/DayView.js +0 -437
- package/src/components/views/MonthView.js +0 -621
- package/src/components/views/WeekView.js +0 -444
package/package.json
CHANGED
|
@@ -11,23 +11,13 @@ 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
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!customElements.get('forcecal-month')) {
|
|
22
|
-
customElements.define('forcecal-month', MonthView);
|
|
23
|
-
}
|
|
24
|
-
if (!customElements.get('forcecal-week')) {
|
|
25
|
-
customElements.define('forcecal-week', WeekView);
|
|
26
|
-
}
|
|
27
|
-
if (!customElements.get('forcecal-day')) {
|
|
28
|
-
customElements.define('forcecal-day', DayView);
|
|
29
|
-
}
|
|
30
|
-
// EventForm is self-registering in its file
|
|
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 EventForm component
|
|
20
|
+
import { EventForm } from './EventForm.js';
|
|
31
21
|
|
|
32
22
|
|
|
33
23
|
export class ForceCalendar extends BaseComponent {
|
|
@@ -161,17 +151,20 @@ export class ForceCalendar extends BaseComponent {
|
|
|
161
151
|
}
|
|
162
152
|
}
|
|
163
153
|
|
|
164
|
-
// Create new view
|
|
154
|
+
// Create new view using renderer classes
|
|
165
155
|
try {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
156
|
+
const renderers = {
|
|
157
|
+
month: MonthViewRenderer,
|
|
158
|
+
week: WeekViewRenderer,
|
|
159
|
+
day: DayViewRenderer
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
163
|
+
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
164
|
+
viewRenderer._viewType = this.currentView;
|
|
165
|
+
this._currentViewInstance = viewRenderer;
|
|
166
|
+
viewRenderer.render();
|
|
167
|
+
// Note: No subscription - handleStateChange manages all view updates
|
|
175
168
|
} catch (err) {
|
|
176
169
|
console.error('[ForceCalendar] Error switching view:', err);
|
|
177
170
|
}
|
|
@@ -663,7 +656,7 @@ export class ForceCalendar extends BaseComponent {
|
|
|
663
656
|
}
|
|
664
657
|
|
|
665
658
|
afterRender() {
|
|
666
|
-
// Manually instantiate and mount view
|
|
659
|
+
// Manually instantiate and mount view renderer (bypasses Locker Service)
|
|
667
660
|
const container = this.$('#calendar-view-container');
|
|
668
661
|
|
|
669
662
|
// Only create view once per view type change
|
|
@@ -684,18 +677,21 @@ export class ForceCalendar extends BaseComponent {
|
|
|
684
677
|
}
|
|
685
678
|
}
|
|
686
679
|
|
|
687
|
-
// Create
|
|
680
|
+
// Create view renderer using the appropriate renderer class
|
|
688
681
|
try {
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
682
|
+
const renderers = {
|
|
683
|
+
month: MonthViewRenderer,
|
|
684
|
+
week: WeekViewRenderer,
|
|
685
|
+
day: DayViewRenderer
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const RendererClass = renderers[this.currentView] || MonthViewRenderer;
|
|
689
|
+
const viewRenderer = new RendererClass(container, this.stateManager);
|
|
690
|
+
viewRenderer._viewType = this.currentView;
|
|
691
|
+
this._currentViewInstance = viewRenderer;
|
|
692
|
+
viewRenderer.render();
|
|
693
|
+
// Note: No subscription here - handleStateChange manages all view updates
|
|
694
|
+
// via _updateViewContent(), _switchView(), or full re-render
|
|
699
695
|
} catch (err) {
|
|
700
696
|
console.error('[ForceCalendar] Error creating/rendering view:', err);
|
|
701
697
|
}
|
|
@@ -747,435 +743,21 @@ export class ForceCalendar extends BaseComponent {
|
|
|
747
743
|
this._hasRendered = true;
|
|
748
744
|
}
|
|
749
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Create a view renderer instance for the given view type
|
|
748
|
+
* Uses pure JavaScript renderer classes for Salesforce Locker Service compatibility
|
|
749
|
+
* @param {string} viewName - 'month', 'week', or 'day'
|
|
750
|
+
* @returns {BaseViewRenderer} Renderer instance
|
|
751
|
+
*/
|
|
750
752
|
_createViewRenderer(viewName) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
}
|
|
753
|
+
const renderers = {
|
|
754
|
+
month: MonthViewRenderer,
|
|
755
|
+
week: WeekViewRenderer,
|
|
756
|
+
day: DayViewRenderer
|
|
1178
757
|
};
|
|
758
|
+
|
|
759
|
+
const RendererClass = renderers[viewName] || MonthViewRenderer;
|
|
760
|
+
return new RendererClass(null, null); // Container and stateManager set after creation
|
|
1179
761
|
}
|
|
1180
762
|
|
|
1181
763
|
handleNavigation(event) {
|
package/src/index.js
CHANGED
|
@@ -15,15 +15,16 @@ export { DateUtils } from './utils/DateUtils.js';
|
|
|
15
15
|
export { DOMUtils } from './utils/DOMUtils.js';
|
|
16
16
|
export { StyleUtils } from './utils/StyleUtils.js';
|
|
17
17
|
|
|
18
|
+
// View Renderers (pure JS classes, Locker Service compatible)
|
|
19
|
+
export { BaseViewRenderer } from './renderers/BaseViewRenderer.js';
|
|
20
|
+
export { MonthViewRenderer } from './renderers/MonthViewRenderer.js';
|
|
21
|
+
export { WeekViewRenderer } from './renderers/WeekViewRenderer.js';
|
|
22
|
+
export { DayViewRenderer } from './renderers/DayViewRenderer.js';
|
|
23
|
+
|
|
18
24
|
// Components
|
|
19
25
|
import './components/ForceCalendar.js';
|
|
20
26
|
export { ForceCalendar } from './components/ForceCalendar.js';
|
|
21
27
|
|
|
22
|
-
// Views
|
|
23
|
-
export { MonthView } from './components/views/MonthView.js';
|
|
24
|
-
export { WeekView } from './components/views/WeekView.js';
|
|
25
|
-
export { DayView } from './components/views/DayView.js';
|
|
26
|
-
|
|
27
28
|
// Auto-register main component if in browser environment
|
|
28
29
|
if (typeof window !== 'undefined' && typeof customElements !== 'undefined') {
|
|
29
30
|
// The ForceCalendar component self-registers
|