@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/dist/force-calendar-interface.esm.js +1566 -1526
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +36 -36
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EventForm.js +4 -1
- package/src/components/ForceCalendar.js +9 -9
- package/src/components/views/DayView.js +10 -10
- package/src/components/views/MonthView.js +4 -1
- package/src/components/views/WeekView.js +11 -11
- package/src/core/BaseComponent.js +3 -0
- package/src/utils/StyleUtils.js +44 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
441
|
+
this.addListener(button, 'click', this.handleNavigation);
|
|
442
442
|
});
|
|
443
443
|
|
|
444
444
|
this.$$('[data-view]').forEach(button => {
|
|
445
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)}
|
package/src/utils/StyleUtils.js
CHANGED
|
@@ -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
|
*/
|