@forcecalendar/interface 0.9.0 → 0.10.0

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": "0.9.0",
3
+ "version": "0.10.0",
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,7 +300,10 @@ export class EventForm extends BaseComponent {
300
300
  if (e.target === this) this.close();
301
301
  });
302
302
 
303
- // Close on Escape key
303
+ // Close on Escape key - remove old listener before adding new one
304
+ if (this._handleKeyDown) {
305
+ window.removeEventListener('keydown', this._handleKeyDown);
306
+ }
304
307
  this._handleKeyDown = (e) => {
305
308
  if (e.key === 'Escape' && this.hasAttribute('open')) {
306
309
  this.close();
@@ -436,13 +436,13 @@ export class ForceCalendar extends BaseComponent {
436
436
  viewElement.stateManager = this.stateManager;
437
437
  }
438
438
 
439
- // Add event listeners for buttons
439
+ // Add event listeners for buttons using tracked addListener
440
440
  this.$$('[data-action]').forEach(button => {
441
- button.addEventListener('click', this.handleNavigation.bind(this));
441
+ this.addListener(button, 'click', this.handleNavigation);
442
442
  });
443
443
 
444
444
  this.$$('[data-view]').forEach(button => {
445
- button.addEventListener('click', this.handleViewChange.bind(this));
445
+ this.addListener(button, 'click', this.handleViewChange);
446
446
  });
447
447
 
448
448
  // Event Modal Handling
@@ -450,13 +450,13 @@ export class ForceCalendar extends BaseComponent {
450
450
  const createBtn = this.$('#create-event-btn');
451
451
 
452
452
  if (createBtn && modal) {
453
- createBtn.addEventListener('click', () => {
453
+ this.addListener(createBtn, 'click', () => {
454
454
  modal.open(new Date());
455
455
  });
456
456
  }
457
457
 
458
458
  // Listen for day clicks from the view
459
- this.shadowRoot.addEventListener('day-click', (e) => {
459
+ this.addListener(this.shadowRoot, 'day-click', (e) => {
460
460
  if (modal) {
461
461
  modal.open(e.detail.date);
462
462
  }
@@ -464,13 +464,13 @@ export class ForceCalendar extends BaseComponent {
464
464
 
465
465
  // Handle event saving
466
466
  if (modal) {
467
- modal.addEventListener('save', (e) => {
467
+ this.addListener(modal, 'save', (e) => {
468
468
  const eventData = e.detail;
469
469
  // Robust Safari support check for randomUUID
470
- const id = (window.crypto && typeof window.crypto.randomUUID === 'function')
471
- ? window.crypto.randomUUID()
470
+ const id = (window.crypto && typeof window.crypto.randomUUID === 'function')
471
+ ? window.crypto.randomUUID()
472
472
  : Math.random().toString(36).substring(2, 15);
473
-
473
+
474
474
  this.stateManager.addEvent({
475
475
  id,
476
476
  ...eventData
@@ -318,18 +318,18 @@ export class DayView extends BaseComponent {
318
318
  renderTimedEvent(event) {
319
319
  const start = new Date(event.start);
320
320
  const end = new Date(event.end);
321
-
321
+
322
322
  const startMinutes = start.getHours() * 60 + start.getMinutes();
323
323
  const durationMinutes = (end - start) / (1000 * 60);
324
-
324
+
325
325
  const top = startMinutes;
326
326
  const height = Math.max(durationMinutes, 30);
327
-
328
- const color = event.backgroundColor || 'var(--fc-primary-color)';
329
- const textColor = StyleUtils.getContrastColor(color);
327
+
328
+ const color = StyleUtils.sanitizeColor(event.backgroundColor);
329
+ const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
330
330
 
331
331
  return `
332
- <div class="event-container"
332
+ <div class="event-container"
333
333
  style="top: ${top}px; height: ${height}px; background-color: ${color}; color: ${textColor};"
334
334
  data-event-id="${event.id}">
335
335
  <span class="event-title">${DOMUtils.escapeHTML(event.title)}</span>
@@ -339,11 +339,11 @@ export class DayView extends BaseComponent {
339
339
  }
340
340
 
341
341
  renderAllDayEvent(event) {
342
- const color = event.backgroundColor || 'var(--fc-primary-color)';
343
- const textColor = StyleUtils.getContrastColor(color);
344
-
342
+ const color = StyleUtils.sanitizeColor(event.backgroundColor);
343
+ const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
344
+
345
345
  return `
346
- <div class="event-item"
346
+ <div class="event-item"
347
347
  style="background-color: ${color}; color: ${textColor}; font-size: 12px; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 500; margin-bottom: 2px;"
348
348
  data-event-id="${event.id}">
349
349
  ${DOMUtils.escapeHTML(event.title)}
@@ -7,6 +7,7 @@
7
7
  import { BaseComponent } from '../../core/BaseComponent.js';
8
8
  import { DOMUtils } from '../../utils/DOMUtils.js';
9
9
  import { DateUtils } from '../../utils/DateUtils.js';
10
+ import { StyleUtils } from '../../utils/StyleUtils.js';
10
11
 
11
12
  export class MonthView extends BaseComponent {
12
13
  constructor() {
@@ -455,7 +456,9 @@ export class MonthView extends BaseComponent {
455
456
 
456
457
  let style = '';
457
458
  if (backgroundColor) {
458
- style += `background-color: ${backgroundColor}; color: ${textColor};`;
459
+ const safeColor = StyleUtils.sanitizeColor(backgroundColor);
460
+ const safeTextColor = StyleUtils.sanitizeColor(textColor, 'white');
461
+ style += `background-color: ${safeColor}; color: ${safeTextColor};`;
459
462
  }
460
463
 
461
464
  let timeStr = '';
@@ -326,18 +326,18 @@ export class WeekView extends BaseComponent {
326
326
  renderTimedEvent(event) {
327
327
  const start = new Date(event.start);
328
328
  const end = new Date(event.end);
329
-
329
+
330
330
  const startMinutes = start.getHours() * 60 + start.getMinutes();
331
331
  const durationMinutes = (end - start) / (1000 * 60);
332
-
333
- const top = startMinutes;
332
+
333
+ const top = startMinutes;
334
334
  const height = Math.max(durationMinutes, 20);
335
-
336
- const color = event.backgroundColor || 'var(--fc-primary-color)';
337
- const textColor = StyleUtils.getContrastColor(color);
335
+
336
+ const color = StyleUtils.sanitizeColor(event.backgroundColor);
337
+ const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
338
338
 
339
339
  return `
340
- <div class="event-container"
340
+ <div class="event-container"
341
341
  style="top: ${top}px; height: ${height}px; background-color: ${color}; color: ${textColor};"
342
342
  data-event-id="${event.id}">
343
343
  <span class="event-title">${DOMUtils.escapeHTML(event.title)}</span>
@@ -347,11 +347,11 @@ export class WeekView extends BaseComponent {
347
347
  }
348
348
 
349
349
  renderAllDayEvent(event) {
350
- const color = event.backgroundColor || 'var(--fc-primary-color)';
351
- const textColor = StyleUtils.getContrastColor(color);
352
-
350
+ const color = StyleUtils.sanitizeColor(event.backgroundColor);
351
+ const textColor = StyleUtils.sanitizeColor(StyleUtils.getContrastColor(color), 'white');
352
+
353
353
  return `
354
- <div class="event-item"
354
+ <div class="event-item"
355
355
  style="background-color: ${color}; color: ${textColor}; font-size: 10px; padding: 2px 4px; border-radius: 2px; cursor: pointer; margin-bottom: 2px;"
356
356
  data-event-id="${event.id}">
357
357
  ${DOMUtils.escapeHTML(event.title)}
@@ -126,6 +126,9 @@ export class BaseComponent extends HTMLElement {
126
126
 
127
127
  // Template rendering
128
128
  render() {
129
+ // Clean up existing listeners before replacing DOM
130
+ this.cleanup();
131
+
129
132
  const styles = `
130
133
  <style>
131
134
  ${this.getBaseStyles()}
@@ -312,6 +312,50 @@ export class StyleUtils {
312
312
  return yiq >= 128 ? '#000000' : '#FFFFFF';
313
313
  }
314
314
 
315
+ /**
316
+ * Sanitize color value to prevent CSS injection
317
+ * Returns the color if valid, or a fallback color if invalid
318
+ */
319
+ static sanitizeColor(color, fallback = 'var(--fc-primary-color)') {
320
+ if (!color || typeof color !== 'string') {
321
+ return fallback;
322
+ }
323
+
324
+ // Trim and check for dangerous characters that could break out of CSS
325
+ const trimmed = color.trim();
326
+ if (/[;{}()<>\"\'\\]/.test(trimmed)) {
327
+ return fallback;
328
+ }
329
+
330
+ // Allow hex colors (#RGB, #RRGGBB, #RRGGBBAA)
331
+ if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(trimmed)) {
332
+ return trimmed;
333
+ }
334
+
335
+ // Allow CSS variables
336
+ if (/^var\(--[a-zA-Z0-9-]+\)$/.test(trimmed)) {
337
+ return trimmed;
338
+ }
339
+
340
+ // Allow rgb/rgba with numbers only
341
+ if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*(0|1|0?\.\d+))?\s*\)$/.test(trimmed)) {
342
+ return trimmed;
343
+ }
344
+
345
+ // Allow safe CSS color keywords
346
+ const safeKeywords = [
347
+ 'transparent', 'currentColor', 'inherit',
348
+ 'black', 'white', 'red', 'green', 'blue', 'yellow', 'orange', 'purple',
349
+ 'pink', 'brown', 'gray', 'grey', 'cyan', 'magenta', 'lime', 'navy',
350
+ 'teal', 'aqua', 'maroon', 'olive', 'silver', 'fuchsia'
351
+ ];
352
+ if (safeKeywords.includes(trimmed.toLowerCase())) {
353
+ return trimmed;
354
+ }
355
+
356
+ return fallback;
357
+ }
358
+
315
359
  /**
316
360
  * Convert hex to rgba
317
361
  */