@forcecalendar/interface 1.0.27 → 1.0.28

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/src/index.js CHANGED
@@ -27,6 +27,6 @@ export { ForceCalendar } from './components/ForceCalendar.js';
27
27
 
28
28
  // Auto-register main component if in browser environment
29
29
  if (typeof window !== 'undefined' && typeof customElements !== 'undefined') {
30
- // The ForceCalendar component self-registers
31
- console.log('Force Calendar Interface loading...');
32
- }
30
+ // The ForceCalendar component self-registers
31
+ console.log('Force Calendar Interface loading...');
32
+ }
@@ -10,176 +10,179 @@ import { DateUtils } from '../utils/DateUtils.js';
10
10
  import { StyleUtils } from '../utils/StyleUtils.js';
11
11
 
12
12
  export class BaseViewRenderer {
13
- /**
14
- * @param {HTMLElement} container - The DOM element to render into
15
- * @param {StateManager} stateManager - The state manager instance
16
- */
17
- constructor(container, stateManager) {
18
- this.container = container;
19
- this.stateManager = stateManager;
20
- this._listeners = [];
21
- this._scrolled = false;
13
+ /**
14
+ * @param {HTMLElement} container - The DOM element to render into
15
+ * @param {StateManager} stateManager - The state manager instance
16
+ */
17
+ constructor(container, stateManager) {
18
+ this.container = container;
19
+ this.stateManager = stateManager;
20
+ this._listeners = [];
21
+ this._scrolled = false;
22
+ }
23
+
24
+ /**
25
+ * Render the view into the container
26
+ * Must be implemented by subclasses
27
+ */
28
+ render() {
29
+ throw new Error('render() must be implemented by subclass');
30
+ }
31
+
32
+ /**
33
+ * Clean up event listeners
34
+ */
35
+ cleanup() {
36
+ this._listeners.forEach(({ element, event, handler }) => {
37
+ element.removeEventListener(event, handler);
38
+ });
39
+ this._listeners = [];
40
+ }
41
+
42
+ /**
43
+ * Add an event listener with automatic cleanup tracking
44
+ * @param {HTMLElement} element
45
+ * @param {string} event
46
+ * @param {Function} handler
47
+ */
48
+ addListener(element, event, handler) {
49
+ const boundHandler = handler.bind(this);
50
+ element.addEventListener(event, boundHandler);
51
+ this._listeners.push({ element, event, handler: boundHandler });
52
+ }
53
+
54
+ /**
55
+ * Escape HTML to prevent XSS
56
+ * @param {string} str
57
+ * @returns {string}
58
+ */
59
+ escapeHTML(str) {
60
+ if (str == null) return '';
61
+ return DOMUtils.escapeHTML(String(str));
62
+ }
63
+
64
+ /**
65
+ * Check if a date is today
66
+ * @param {Date} date
67
+ * @returns {boolean}
68
+ */
69
+ isToday(date) {
70
+ const today = new Date();
71
+ return (
72
+ date.getDate() === today.getDate() &&
73
+ date.getMonth() === today.getMonth() &&
74
+ date.getFullYear() === today.getFullYear()
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Check if two dates are the same day
80
+ * @param {Date} date1
81
+ * @param {Date} date2
82
+ * @returns {boolean}
83
+ */
84
+ isSameDay(date1, date2) {
85
+ return (
86
+ date1.getDate() === date2.getDate() &&
87
+ date1.getMonth() === date2.getMonth() &&
88
+ date1.getFullYear() === date2.getFullYear()
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Format hour for display (e.g., "9 AM", "2 PM")
94
+ * @param {number} hour - Hour in 24-hour format (0-23)
95
+ * @returns {string}
96
+ */
97
+ formatHour(hour) {
98
+ const period = hour >= 12 ? 'PM' : 'AM';
99
+ const displayHour = hour % 12 || 12;
100
+ return `${displayHour} ${period}`;
101
+ }
102
+
103
+ /**
104
+ * Format time for display (e.g., "9 AM", "2:30 PM")
105
+ * @param {Date} date
106
+ * @returns {string}
107
+ */
108
+ formatTime(date) {
109
+ const hours = date.getHours();
110
+ const minutes = date.getMinutes();
111
+ const period = hours >= 12 ? 'PM' : 'AM';
112
+ const displayHour = hours % 12 || 12;
113
+ return minutes === 0
114
+ ? `${displayHour} ${period}`
115
+ : `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
116
+ }
117
+
118
+ /**
119
+ * Get contrasting text color for a background color
120
+ * Uses WCAG luminance formula
121
+ * @param {string} bgColor - Hex color string
122
+ * @returns {string} 'black' or 'white'
123
+ */
124
+ getContrastingTextColor(bgColor) {
125
+ if (!bgColor || typeof bgColor !== 'string') return 'white';
126
+
127
+ const color = bgColor.charAt(0) === '#' ? bgColor.substring(1) : bgColor;
128
+
129
+ if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(color)) {
130
+ return 'white';
22
131
  }
23
132
 
24
- /**
25
- * Render the view into the container
26
- * Must be implemented by subclasses
27
- */
28
- render() {
29
- throw new Error('render() must be implemented by subclass');
30
- }
31
-
32
- /**
33
- * Clean up event listeners
34
- */
35
- cleanup() {
36
- this._listeners.forEach(({ element, event, handler }) => {
37
- element.removeEventListener(event, handler);
38
- });
39
- this._listeners = [];
40
- }
133
+ const fullColor =
134
+ color.length === 3 ? color[0] + color[0] + color[1] + color[1] + color[2] + color[2] : color;
41
135
 
42
- /**
43
- * Add an event listener with automatic cleanup tracking
44
- * @param {HTMLElement} element
45
- * @param {string} event
46
- * @param {Function} handler
47
- */
48
- addListener(element, event, handler) {
49
- const boundHandler = handler.bind(this);
50
- element.addEventListener(event, boundHandler);
51
- this._listeners.push({ element, event, handler: boundHandler });
52
- }
53
-
54
- /**
55
- * Escape HTML to prevent XSS
56
- * @param {string} str
57
- * @returns {string}
58
- */
59
- escapeHTML(str) {
60
- if (str == null) return '';
61
- return DOMUtils.escapeHTML(String(str));
62
- }
136
+ const r = parseInt(fullColor.substring(0, 2), 16);
137
+ const g = parseInt(fullColor.substring(2, 4), 16);
138
+ const b = parseInt(fullColor.substring(4, 6), 16);
63
139
 
64
- /**
65
- * Check if a date is today
66
- * @param {Date} date
67
- * @returns {boolean}
68
- */
69
- isToday(date) {
70
- const today = new Date();
71
- return date.getDate() === today.getDate() &&
72
- date.getMonth() === today.getMonth() &&
73
- date.getFullYear() === today.getFullYear();
140
+ if (isNaN(r) || isNaN(g) || isNaN(b)) {
141
+ return 'white';
74
142
  }
75
143
 
76
- /**
77
- * Check if two dates are the same day
78
- * @param {Date} date1
79
- * @param {Date} date2
80
- * @returns {boolean}
81
- */
82
- isSameDay(date1, date2) {
83
- return date1.getDate() === date2.getDate() &&
84
- date1.getMonth() === date2.getMonth() &&
85
- date1.getFullYear() === date2.getFullYear();
86
- }
87
-
88
- /**
89
- * Format hour for display (e.g., "9 AM", "2 PM")
90
- * @param {number} hour - Hour in 24-hour format (0-23)
91
- * @returns {string}
92
- */
93
- formatHour(hour) {
94
- const period = hour >= 12 ? 'PM' : 'AM';
95
- const displayHour = hour % 12 || 12;
96
- return `${displayHour} ${period}`;
97
- }
98
-
99
- /**
100
- * Format time for display (e.g., "9 AM", "2:30 PM")
101
- * @param {Date} date
102
- * @returns {string}
103
- */
104
- formatTime(date) {
105
- const hours = date.getHours();
106
- const minutes = date.getMinutes();
107
- const period = hours >= 12 ? 'PM' : 'AM';
108
- const displayHour = hours % 12 || 12;
109
- return minutes === 0
110
- ? `${displayHour} ${period}`
111
- : `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
112
- }
113
-
114
- /**
115
- * Get contrasting text color for a background color
116
- * Uses WCAG luminance formula
117
- * @param {string} bgColor - Hex color string
118
- * @returns {string} 'black' or 'white'
119
- */
120
- getContrastingTextColor(bgColor) {
121
- if (!bgColor || typeof bgColor !== 'string') return 'white';
122
-
123
- const color = (bgColor.charAt(0) === '#') ? bgColor.substring(1) : bgColor;
124
-
125
- if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(color)) {
126
- return 'white';
127
- }
128
-
129
- const fullColor = color.length === 3
130
- ? color[0] + color[0] + color[1] + color[1] + color[2] + color[2]
131
- : color;
132
-
133
- const r = parseInt(fullColor.substring(0, 2), 16);
134
- const g = parseInt(fullColor.substring(2, 4), 16);
135
- const b = parseInt(fullColor.substring(4, 6), 16);
136
-
137
- if (isNaN(r) || isNaN(g) || isNaN(b)) {
138
- return 'white';
139
- }
140
-
141
- const uicolors = [r / 255, g / 255, b / 255];
142
- const c = uicolors.map((col) => {
143
- if (col <= 0.03928) {
144
- return col / 12.92;
145
- }
146
- return Math.pow((col + 0.055) / 1.055, 2.4);
147
- });
148
- const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
149
- return (L > 0.179) ? 'black' : 'white';
150
- }
151
-
152
- /**
153
- * Render the "now" indicator line for time-based views
154
- * @returns {string} HTML string
155
- */
156
- renderNowIndicator() {
157
- const now = new Date();
158
- const minutes = now.getHours() * 60 + now.getMinutes();
159
- return `<div class="fc-now-indicator" style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
160
- }
161
-
162
- /**
163
- * Render a timed event block
164
- * @param {Object} event - Event object
165
- * @param {Object} options - Rendering options
166
- * @returns {string} HTML string
167
- */
168
- renderTimedEvent(event, options = {}) {
169
- const { compact = true } = options;
170
- const start = new Date(event.start);
171
- const end = new Date(event.end);
172
- const startMinutes = start.getHours() * 60 + start.getMinutes();
173
- const durationMinutes = Math.max((end - start) / (1000 * 60), compact ? 20 : 30);
174
- const color = this.getEventColor(event);
175
-
176
- const padding = compact ? '4px 8px' : '8px 12px';
177
- const fontSize = compact ? '11px' : '13px';
178
- const margin = compact ? '2px' : '12px';
179
- const rightMargin = compact ? '2px' : '24px';
180
- const borderRadius = compact ? '4px' : '6px';
181
-
182
- return `
144
+ const uicolors = [r / 255, g / 255, b / 255];
145
+ const c = uicolors.map(col => {
146
+ if (col <= 0.03928) {
147
+ return col / 12.92;
148
+ }
149
+ return Math.pow((col + 0.055) / 1.055, 2.4);
150
+ });
151
+ const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
152
+ return L > 0.179 ? 'black' : 'white';
153
+ }
154
+
155
+ /**
156
+ * Render the "now" indicator line for time-based views
157
+ * @returns {string} HTML string
158
+ */
159
+ renderNowIndicator() {
160
+ const now = new Date();
161
+ const minutes = now.getHours() * 60 + now.getMinutes();
162
+ return `<div class="fc-now-indicator" style="position: absolute; left: 0; right: 0; top: ${minutes}px; height: 2px; background: #dc2626; z-index: 15; pointer-events: none;"></div>`;
163
+ }
164
+
165
+ /**
166
+ * Render a timed event block
167
+ * @param {Object} event - Event object
168
+ * @param {Object} options - Rendering options
169
+ * @returns {string} HTML string
170
+ */
171
+ renderTimedEvent(event, options = {}) {
172
+ const { compact = true } = options;
173
+ const start = new Date(event.start);
174
+ const end = new Date(event.end);
175
+ const startMinutes = start.getHours() * 60 + start.getMinutes();
176
+ const durationMinutes = Math.max((end - start) / (1000 * 60), compact ? 20 : 30);
177
+ const color = this.getEventColor(event);
178
+
179
+ const padding = compact ? '4px 8px' : '8px 12px';
180
+ const fontSize = compact ? '11px' : '13px';
181
+ const margin = compact ? '2px' : '12px';
182
+ const rightMargin = compact ? '2px' : '24px';
183
+ const borderRadius = compact ? '4px' : '6px';
184
+
185
+ return `
183
186
  <div class="fc-event fc-timed-event" data-event-id="${this.escapeHTML(event.id)}"
184
187
  style="position: absolute; top: ${startMinutes}px; height: ${durationMinutes}px;
185
188
  left: ${margin}; right: ${rightMargin};
@@ -196,34 +199,34 @@ export class BaseViewRenderer {
196
199
  </div>
197
200
  </div>
198
201
  `;
199
- }
200
-
201
- /**
202
- * Get a safe, sanitized event color value.
203
- * @param {Object} event
204
- * @returns {string}
205
- */
206
- getEventColor(event) {
207
- return StyleUtils.sanitizeColor(event?.backgroundColor, '#2563eb');
208
- }
209
-
210
- /**
211
- * Attach common event handlers for day/event clicks
212
- */
213
- attachCommonEventHandlers() {
214
- // Delegate event clicks at container level to avoid rebinding per event node.
215
- this.addListener(this.container, 'click', (e) => {
216
- const eventEl = e.target.closest('.fc-event');
217
- if (!eventEl || !this.container.contains(eventEl)) return;
218
-
219
- e.stopPropagation();
220
- const eventId = eventEl.dataset.eventId;
221
- const event = this.stateManager.getEvents().find(ev => ev.id === eventId);
222
- if (event) {
223
- this.stateManager.selectEvent(event);
224
- }
225
- });
226
- }
202
+ }
203
+
204
+ /**
205
+ * Get a safe, sanitized event color value.
206
+ * @param {Object} event
207
+ * @returns {string}
208
+ */
209
+ getEventColor(event) {
210
+ return StyleUtils.sanitizeColor(event?.backgroundColor, '#2563eb');
211
+ }
212
+
213
+ /**
214
+ * Attach common event handlers for day/event clicks
215
+ */
216
+ attachCommonEventHandlers() {
217
+ // Delegate event clicks at container level to avoid rebinding per event node.
218
+ this.addListener(this.container, 'click', e => {
219
+ const eventEl = e.target.closest('.fc-event');
220
+ if (!eventEl || !this.container.contains(eventEl)) return;
221
+
222
+ e.stopPropagation();
223
+ const eventId = eventEl.dataset.eventId;
224
+ const event = this.stateManager.getEvents().find(ev => ev.id === eventId);
225
+ if (event) {
226
+ this.stateManager.selectEvent(event);
227
+ }
228
+ });
229
+ }
227
230
  }
228
231
 
229
232
  export default BaseViewRenderer;