@forcecalendar/interface 1.0.27 → 1.0.29

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