@forcecalendar/interface 1.0.58 → 1.0.60
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 +85 -54
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +17 -17
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/components/EventForm.js +6 -6
- package/src/utils/DOMUtils.js +59 -10
- package/src/utils/StyleUtils.js +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forcecalendar/interface",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.60",
|
|
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",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@babel/core": "^7.28.5",
|
|
56
56
|
"@babel/preset-env": "^7.28.5",
|
|
57
|
-
"@forcecalendar/core": "^2.1.
|
|
57
|
+
"@forcecalendar/core": "^2.1.68",
|
|
58
58
|
"babel-jest": "^30.2.0",
|
|
59
59
|
"eslint": "^8.57.1",
|
|
60
60
|
"jest": "^30.2.0",
|
|
@@ -253,12 +253,12 @@ export class EventForm extends BaseComponent {
|
|
|
253
253
|
${this.config.colors
|
|
254
254
|
.map(
|
|
255
255
|
c => `
|
|
256
|
-
<button type="button"
|
|
257
|
-
class="color-btn ${c.color === this._formData.color ? 'selected' : ''}"
|
|
258
|
-
style="background-color: ${c.color}"
|
|
259
|
-
data-color="${c.color}"
|
|
260
|
-
title="${c.label}"
|
|
261
|
-
aria-label="${c.label}"
|
|
256
|
+
<button type="button"
|
|
257
|
+
class="color-btn ${c.color === this._formData.color ? 'selected' : ''}"
|
|
258
|
+
style="background-color: ${StyleUtils.sanitizeColor(c.color)}"
|
|
259
|
+
data-color="${DOMUtils.escapeHTML(c.color)}"
|
|
260
|
+
title="${DOMUtils.escapeHTML(c.label)}"
|
|
261
|
+
aria-label="${DOMUtils.escapeHTML(c.label)}"
|
|
262
262
|
aria-checked="${c.color === this._formData.color ? 'true' : 'false'}"
|
|
263
263
|
role="radio"></button>
|
|
264
264
|
`
|
package/src/utils/DOMUtils.js
CHANGED
|
@@ -129,14 +129,24 @@ export class DOMUtils {
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Wait for animation/transition to complete
|
|
132
|
+
* Resolves after `timeout` ms even if the event never fires (animation
|
|
133
|
+
* cancelled, element removed from DOM, etc.) so awaiting callers can't hang.
|
|
134
|
+
* @param {Element} element - Element to listen on
|
|
135
|
+
* @param {string} [eventType='animationend'] - Event to wait for
|
|
136
|
+
* @param {number} [timeout=3000] - Max wait in ms; 0 disables the timeout
|
|
132
137
|
*/
|
|
133
|
-
static waitForAnimation(element, eventType = 'animationend') {
|
|
138
|
+
static waitForAnimation(element, eventType = 'animationend', timeout = 3000) {
|
|
134
139
|
return new Promise(resolve => {
|
|
140
|
+
let timeoutId = null;
|
|
135
141
|
const handler = () => {
|
|
142
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
136
143
|
element.removeEventListener(eventType, handler);
|
|
137
144
|
resolve();
|
|
138
145
|
};
|
|
139
146
|
element.addEventListener(eventType, handler);
|
|
147
|
+
if (timeout > 0) {
|
|
148
|
+
timeoutId = setTimeout(handler, timeout);
|
|
149
|
+
}
|
|
140
150
|
});
|
|
141
151
|
}
|
|
142
152
|
|
|
@@ -149,13 +159,48 @@ export class DOMUtils {
|
|
|
149
159
|
|
|
150
160
|
/**
|
|
151
161
|
* Parse HTML string safely
|
|
162
|
+
* Sanitizes by default: strips script-capable elements, inline event
|
|
163
|
+
* handlers and javascript: URLs so the returned node is inert even if the
|
|
164
|
+
* input contains an XSS payload. Pass { sanitize: false } only for trusted,
|
|
165
|
+
* non-user-controlled markup.
|
|
152
166
|
*/
|
|
153
|
-
static parseHTML(htmlString) {
|
|
167
|
+
static parseHTML(htmlString, { sanitize = true } = {}) {
|
|
154
168
|
const template = document.createElement('template');
|
|
155
169
|
template.innerHTML = htmlString.trim();
|
|
170
|
+
if (sanitize) {
|
|
171
|
+
this._sanitizeNode(template.content);
|
|
172
|
+
}
|
|
156
173
|
return template.content.firstChild;
|
|
157
174
|
}
|
|
158
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Remove script-capable elements, on* handlers and javascript: URLs
|
|
178
|
+
* from a parsed DOM fragment (in place)
|
|
179
|
+
*/
|
|
180
|
+
static _sanitizeNode(root) {
|
|
181
|
+
const DANGEROUS_TAGS = 'script, iframe, object, embed, link, meta, base';
|
|
182
|
+
root.querySelectorAll(DANGEROUS_TAGS).forEach(el => el.remove());
|
|
183
|
+
|
|
184
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
185
|
+
let node = walker.nextNode();
|
|
186
|
+
while (node) {
|
|
187
|
+
for (const attr of Array.from(node.attributes)) {
|
|
188
|
+
const name = attr.name.toLowerCase();
|
|
189
|
+
// Browsers ignore control chars/whitespace inside URL schemes,
|
|
190
|
+
// so strip them before checking for javascript:/data: payloads
|
|
191
|
+
const value = attr.value.replace(/[\u0000-\u0020]/g, '').toLowerCase();
|
|
192
|
+
if (
|
|
193
|
+
name.startsWith('on') ||
|
|
194
|
+
value.startsWith('javascript:') ||
|
|
195
|
+
value.startsWith('data:text/html')
|
|
196
|
+
) {
|
|
197
|
+
node.removeAttribute(attr.name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
node = walker.nextNode();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
159
204
|
/**
|
|
160
205
|
* Escape HTML to prevent XSS
|
|
161
206
|
*/
|
|
@@ -234,14 +279,13 @@ export class DOMUtils {
|
|
|
234
279
|
}
|
|
235
280
|
|
|
236
281
|
/**
|
|
237
|
-
* Clone element
|
|
282
|
+
* Clone an element. Event listeners are NOT copied — the Web Platform
|
|
283
|
+
* provides no way to enumerate listeners, so callers must re-attach their
|
|
284
|
+
* own handlers to the clone.
|
|
285
|
+
* @deprecated Use element.cloneNode(deep) directly and re-bind listeners.
|
|
238
286
|
*/
|
|
239
287
|
static cloneWithEvents(element, deep = true) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Copy event listeners (Note: This is a simplified version)
|
|
243
|
-
// In production, you'd need a more robust event copying mechanism
|
|
244
|
-
return clone;
|
|
288
|
+
return element.cloneNode(deep);
|
|
245
289
|
}
|
|
246
290
|
|
|
247
291
|
/**
|
|
@@ -266,13 +310,18 @@ export class DOMUtils {
|
|
|
266
310
|
const handleKeyDown = e => {
|
|
267
311
|
if (e.key !== 'Tab') return;
|
|
268
312
|
|
|
313
|
+
// Inside Shadow DOM, document.activeElement reports the host element;
|
|
314
|
+
// the shadow root's own activeElement gives the truly focused node.
|
|
315
|
+
const root = container.getRootNode();
|
|
316
|
+
const activeElement = root.activeElement ?? document.activeElement;
|
|
317
|
+
|
|
269
318
|
if (e.shiftKey) {
|
|
270
|
-
if (
|
|
319
|
+
if (activeElement === firstFocusable) {
|
|
271
320
|
lastFocusable?.focus();
|
|
272
321
|
e.preventDefault();
|
|
273
322
|
}
|
|
274
323
|
} else {
|
|
275
|
-
if (
|
|
324
|
+
if (activeElement === lastFocusable) {
|
|
276
325
|
firstFocusable?.focus();
|
|
277
326
|
e.preventDefault();
|
|
278
327
|
}
|
package/src/utils/StyleUtils.js
CHANGED
|
@@ -321,9 +321,9 @@ export class StyleUtils {
|
|
|
321
321
|
*/
|
|
322
322
|
static getContrastColor(bgColor) {
|
|
323
323
|
const color = bgColor.replace('#', '');
|
|
324
|
-
const r = parseInt(color.
|
|
325
|
-
const g = parseInt(color.
|
|
326
|
-
const b = parseInt(color.
|
|
324
|
+
const r = parseInt(color.substring(0, 2), 16);
|
|
325
|
+
const g = parseInt(color.substring(2, 4), 16);
|
|
326
|
+
const b = parseInt(color.substring(4, 6), 16);
|
|
327
327
|
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
328
328
|
return yiq >= 128 ? '#000000' : '#FFFFFF';
|
|
329
329
|
}
|
|
@@ -398,9 +398,9 @@ export class StyleUtils {
|
|
|
398
398
|
*/
|
|
399
399
|
static hexToRgba(hex, alpha = 1) {
|
|
400
400
|
const color = hex.replace('#', '');
|
|
401
|
-
const r = parseInt(color.
|
|
402
|
-
const g = parseInt(color.
|
|
403
|
-
const b = parseInt(color.
|
|
401
|
+
const r = parseInt(color.substring(0, 2), 16);
|
|
402
|
+
const g = parseInt(color.substring(2, 4), 16);
|
|
403
|
+
const b = parseInt(color.substring(4, 6), 16);
|
|
404
404
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
405
405
|
}
|
|
406
406
|
|