@forcecalendar/interface 1.0.55 → 1.0.57
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 +84 -46
- package/dist/force-calendar-interface.esm.js.map +1 -1
- package/dist/force-calendar-interface.umd.js +37 -41
- package/dist/force-calendar-interface.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ForceCalendar.js +1 -1
- package/src/core/BaseComponent.js +94 -8
- package/src/renderers/DayViewRenderer.js +2 -2
- package/src/renderers/MonthViewRenderer.js +3 -3
- package/src/renderers/WeekViewRenderer.js +2 -2
package/package.json
CHANGED
|
@@ -676,7 +676,7 @@ export class ForceCalendar extends BaseComponent {
|
|
|
676
676
|
<button class="fc-nav-arrow" data-action="previous" title="Previous">
|
|
677
677
|
${this.getIcon('chevron-left')}
|
|
678
678
|
</button>
|
|
679
|
-
<h2 class="fc-title">${title}</h2>
|
|
679
|
+
<h2 class="fc-title">${DOMUtils.escapeHTML(title)}</h2>
|
|
680
680
|
<button class="fc-nav-arrow" data-action="next" title="Next">
|
|
681
681
|
${this.getIcon('chevron-right')}
|
|
682
682
|
</button>
|
|
@@ -31,6 +31,8 @@ export class BaseComponent extends HTMLElement {
|
|
|
31
31
|
disconnectedCallback() {
|
|
32
32
|
this.unmount();
|
|
33
33
|
this.cleanup();
|
|
34
|
+
this._styleEl = null;
|
|
35
|
+
this._contentWrapper = null;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// To be overridden by child classes
|
|
@@ -131,18 +133,102 @@ export class BaseComponent extends HTMLElement {
|
|
|
131
133
|
// Clean up existing listeners before replacing DOM
|
|
132
134
|
this.cleanup();
|
|
133
135
|
|
|
134
|
-
const styles = `
|
|
135
|
-
<style>
|
|
136
|
-
${this.getBaseStyles()}
|
|
137
|
-
${this.getStyles()}
|
|
138
|
-
</style>
|
|
139
|
-
`;
|
|
140
|
-
|
|
141
136
|
const template = this.template();
|
|
142
|
-
|
|
137
|
+
|
|
138
|
+
// First render: create style element and content wrapper
|
|
139
|
+
if (!this._styleEl) {
|
|
140
|
+
this._styleEl = document.createElement('style');
|
|
141
|
+
this.shadowRoot.appendChild(this._styleEl);
|
|
142
|
+
this._contentWrapper = document.createElement('div');
|
|
143
|
+
this._contentWrapper.setAttribute('id', 'fc-root');
|
|
144
|
+
this._contentWrapper.style.display = 'contents';
|
|
145
|
+
this.shadowRoot.appendChild(this._contentWrapper);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update styles on every render in case getStyles() depends on state
|
|
149
|
+
this._styleEl.textContent = this.getBaseStyles() + '\n' + this.getStyles();
|
|
150
|
+
|
|
151
|
+
// Save scroll positions and focused element before DOM replacement
|
|
152
|
+
const scrollPositions = this._saveScrollPositions();
|
|
153
|
+
const activeSelector = this._getActiveElementSelector();
|
|
154
|
+
|
|
155
|
+
this._contentWrapper.innerHTML = template;
|
|
156
|
+
|
|
157
|
+
// Restore scroll positions and focus
|
|
158
|
+
this._restoreScrollPositions(scrollPositions);
|
|
159
|
+
this._restoreFocus(activeSelector);
|
|
160
|
+
|
|
143
161
|
this.afterRender();
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Save scroll positions of all scrollable containers within shadow DOM
|
|
166
|
+
* @returns {Map<string, {top: number, left: number}>}
|
|
167
|
+
*/
|
|
168
|
+
_saveScrollPositions() {
|
|
169
|
+
const positions = new Map();
|
|
170
|
+
if (!this._contentWrapper) return positions;
|
|
171
|
+
const scrollables = this._contentWrapper.querySelectorAll('[id]');
|
|
172
|
+
scrollables.forEach(el => {
|
|
173
|
+
if (el.scrollTop !== 0 || el.scrollLeft !== 0) {
|
|
174
|
+
positions.set(el.id, { top: el.scrollTop, left: el.scrollLeft });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return positions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Restore previously saved scroll positions
|
|
182
|
+
* @param {Map<string, {top: number, left: number}>} positions
|
|
183
|
+
*/
|
|
184
|
+
_restoreScrollPositions(positions) {
|
|
185
|
+
if (!this._contentWrapper || positions.size === 0) return;
|
|
186
|
+
positions.forEach((pos, id) => {
|
|
187
|
+
const el = this._contentWrapper.querySelector(`[id="${CSS.escape(id)}"]`);
|
|
188
|
+
if (el) {
|
|
189
|
+
el.scrollTop = pos.top;
|
|
190
|
+
el.scrollLeft = pos.left;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get a CSS selector for the currently focused element within shadow DOM
|
|
197
|
+
* @returns {string|null}
|
|
198
|
+
*/
|
|
199
|
+
_getActiveElementSelector() {
|
|
200
|
+
const active = this.shadowRoot.activeElement;
|
|
201
|
+
if (!active || active === this._contentWrapper) return null;
|
|
202
|
+
if (active.id) return `[id="${CSS.escape(active.id)}"]`;
|
|
203
|
+
// Prefer data attributes over className to avoid invalid selectors
|
|
204
|
+
if (active.dataset && active.dataset.action) {
|
|
205
|
+
return `[data-action="${CSS.escape(active.dataset.action)}"]`;
|
|
206
|
+
}
|
|
207
|
+
if (active.dataset && active.dataset.view) {
|
|
208
|
+
return `[data-view="${CSS.escape(active.dataset.view)}"]`;
|
|
209
|
+
}
|
|
210
|
+
if (active.tagName) {
|
|
211
|
+
return active.tagName.toLowerCase();
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore focus to a previously focused element
|
|
218
|
+
* @param {string|null} selector
|
|
219
|
+
*/
|
|
220
|
+
_restoreFocus(selector) {
|
|
221
|
+
if (!selector || !this._contentWrapper) return;
|
|
222
|
+
try {
|
|
223
|
+
const el = this._contentWrapper.querySelector(selector);
|
|
224
|
+
if (el && typeof el.focus === 'function') {
|
|
225
|
+
el.focus();
|
|
226
|
+
}
|
|
227
|
+
} catch (_) {
|
|
228
|
+
// Invalid selector, ignore
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
146
232
|
template() {
|
|
147
233
|
// Override in child classes to provide component template
|
|
148
234
|
return '';
|
|
@@ -102,10 +102,10 @@ export class DayViewRenderer extends BaseViewRenderer {
|
|
|
102
102
|
<div style="border-right: 1px solid var(--fc-border-color);"></div>
|
|
103
103
|
<div style="padding: 16px 24px;">
|
|
104
104
|
<div style="font-size: 12px; font-weight: 700; color: var(--fc-text-light); text-transform: uppercase; letter-spacing: 0.1em;">
|
|
105
|
-
${dayName}
|
|
105
|
+
${this.escapeHTML(dayName)}
|
|
106
106
|
</div>
|
|
107
107
|
<div style="font-size: 24px; font-weight: 600; margin-top: 4px; ${isToday ? 'color: var(--fc-danger-color);' : 'color: var(--fc-text-color);'}">
|
|
108
|
-
${dayDate.getDate()}
|
|
108
|
+
${this.escapeHTML(String(dayDate.getDate()))}
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
@@ -37,7 +37,7 @@ export class MonthViewRenderer extends BaseViewRenderer {
|
|
|
37
37
|
let html = `
|
|
38
38
|
<div class="fc-month-view" style="display: flex; flex-direction: column; height: 100%; min-height: 400px; background: var(--fc-background); border: 1px solid var(--fc-border-color);">
|
|
39
39
|
<div class="fc-month-header" style="display: grid; grid-template-columns: repeat(7, 1fr); border-bottom: 1px solid var(--fc-border-color); background: var(--fc-background-alt);">
|
|
40
|
-
${dayNames.map(d => `<div class="fc-month-header-cell" style="padding: 12px 8px; text-align: center; font-size: 11px; font-weight: 600; color: var(--fc-text-light); text-transform: uppercase;">${d}</div>`).join('')}
|
|
40
|
+
${dayNames.map(d => `<div class="fc-month-header-cell" style="padding: 12px 8px; text-align: center; font-size: 11px; font-weight: 600; color: var(--fc-text-light); text-transform: uppercase;">${this.escapeHTML(d)}</div>`).join('')}
|
|
41
41
|
</div>
|
|
42
42
|
<div class="fc-month-body" style="display: flex; flex-direction: column; flex: 1;">
|
|
43
43
|
`;
|
|
@@ -87,10 +87,10 @@ export class MonthViewRenderer extends BaseViewRenderer {
|
|
|
87
87
|
const moreCount = events.length - this.maxEventsToShow;
|
|
88
88
|
|
|
89
89
|
return `
|
|
90
|
-
<div class="fc-month-day" data-date="${day.date}"
|
|
90
|
+
<div class="fc-month-day" data-date="${this.escapeHTML(day.date)}"
|
|
91
91
|
style="background: ${dayBg}; border-right: 1px solid var(--fc-border-color); border-bottom: 1px solid var(--fc-border-color); padding: 4px; min-height: 80px; cursor: pointer; display: flex; flex-direction: column;">
|
|
92
92
|
<div class="fc-day-number" style="font-size: 13px; font-weight: 500; color: ${dayNumColor}; padding: 2px 4px; margin-bottom: 4px; ${todayStyle}">
|
|
93
|
-
${day.dayOfMonth}
|
|
93
|
+
${this.escapeHTML(String(day.dayOfMonth))}
|
|
94
94
|
</div>
|
|
95
95
|
<div class="fc-day-events" style="display: flex; flex-direction: column; gap: 2px; flex: 1; overflow: hidden;">
|
|
96
96
|
${visibleEvents.map(evt => this._renderEvent(evt)).join('')}
|
|
@@ -72,10 +72,10 @@ export class WeekViewRenderer extends BaseViewRenderer {
|
|
|
72
72
|
day => `
|
|
73
73
|
<div style="padding: 12px 8px; text-align: center; border-right: 1px solid var(--fc-border-color);">
|
|
74
74
|
<div style="font-size: 10px; font-weight: 700; color: var(--fc-text-light); text-transform: uppercase; letter-spacing: 0.1em;">
|
|
75
|
-
${day.dayName}
|
|
75
|
+
${this.escapeHTML(day.dayName)}
|
|
76
76
|
</div>
|
|
77
77
|
<div style="font-size: 16px; font-weight: 500; margin-top: 4px; ${day.isToday ? 'background: var(--fc-danger-color); color: white; border-radius: 50%; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center;' : 'color: var(--fc-text-color);'}">
|
|
78
|
-
${day.dayOfMonth}
|
|
78
|
+
${this.escapeHTML(String(day.dayOfMonth))}
|
|
79
79
|
</div>
|
|
80
80
|
</div>
|
|
81
81
|
`
|