perf(calendar): re-render only the changed day cell, not the whole grid (#10)
Extracts the day-cell construction into a self-contained makeDayCell(d). Slot changes and holiday/closure toggles now replace just that one cell (cell.replaceWith) plus update export-button visibility, instead of rebuilding the entire month grid and re-attaching every listener on each interaction. Structural changes (month/year/slot-count) still do a full render. Faithful extraction; verified headless that the page loads without JS errors and tests pass. Interactive render coverage to follow in the test-layer issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c076b5e1ce
commit
8cd8908f38
1 file changed
+49
-51
+49
-51
@@ -159,44 +159,13 @@ function renderGrid(wrapper, exportBtn) {
|
||||
|
||||
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
||||
const empNameById = Object.fromEntries(state.employees.map(e => [e.id, e.name]));
|
||||
const entriesByDate = {};
|
||||
for (const a of currentPlan) {
|
||||
if (!entriesByDate[a.date]) entriesByDate[a.date] = [];
|
||||
entriesByDate[a.date].push(a);
|
||||
}
|
||||
// Sort each date's entries alphabetically by employee name (stable slot order)
|
||||
for (const date of Object.keys(entriesByDate)) {
|
||||
entriesByDate[date].sort((x, y) =>
|
||||
(empNameById[x.employeeId] ?? '').localeCompare(empNameById[y.employeeId] ?? '', 'de')
|
||||
);
|
||||
}
|
||||
const updateExportVisibility = () => {
|
||||
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
|
||||
};
|
||||
|
||||
// Day-of-week header row
|
||||
const headerGrid = document.createElement('div');
|
||||
headerGrid.className = 'calendar-header-grid';
|
||||
WD_LABELS.forEach(lbl => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cal-header-cell';
|
||||
cell.textContent = lbl;
|
||||
headerGrid.appendChild(cell);
|
||||
});
|
||||
wrapper.appendChild(headerGrid);
|
||||
|
||||
// Body grid
|
||||
const bodyGrid = document.createElement('div');
|
||||
bodyGrid.className = 'calendar-body-grid';
|
||||
|
||||
// Offset empty cells before first day
|
||||
const firstISO = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-01';
|
||||
const firstWD = (() => { const d = new Date(firstISO + 'T12:00:00Z').getUTCDay(); return d === 0 ? 7 : d; })();
|
||||
for (let i = 1; i < firstWD; i++) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'cal-day empty-cell';
|
||||
bodyGrid.appendChild(empty);
|
||||
}
|
||||
|
||||
// Day cells
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
// Build one day cell. Self-contained so a change to a single day re-renders
|
||||
// just that cell (cell.replaceWith) instead of rebuilding the whole grid.
|
||||
function makeDayCell(d) {
|
||||
const iso = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-' + String(d).padStart(2,'0');
|
||||
const wdNum = (() => { const x = new Date(iso + 'T12:00:00Z').getUTCDay(); return x === 0 ? 7 : x; })();
|
||||
const isWeekend = wdNum >= 6;
|
||||
@@ -209,7 +178,6 @@ function renderGrid(wrapper, exportBtn) {
|
||||
else if (isHoliday) cell.classList.add('holiday');
|
||||
else if (isClosure) cell.classList.add('closure');
|
||||
|
||||
// Day number + weekday label
|
||||
const dayNum = document.createElement('div');
|
||||
dayNum.className = 'day-num';
|
||||
dayNum.textContent = d + ' ';
|
||||
@@ -219,9 +187,11 @@ function renderGrid(wrapper, exportBtn) {
|
||||
dayNum.appendChild(wdSpan);
|
||||
cell.appendChild(dayNum);
|
||||
|
||||
// Re-render only this cell after a change affecting this day
|
||||
const refresh = () => { cell.replaceWith(makeDayCell(d)); updateExportVisibility(); };
|
||||
|
||||
if (!isWeekend) {
|
||||
if (isHoliday || isClosure) {
|
||||
// Show label + remove button
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'cal-day-tag-label';
|
||||
tag.textContent = isHoliday ? 'Feiertag' : 'Betriebsurlaub';
|
||||
@@ -232,18 +202,18 @@ function renderGrid(wrapper, exportBtn) {
|
||||
if (isHoliday) toggleHoliday(state, iso);
|
||||
else toggleClosure(state, iso);
|
||||
saveState();
|
||||
renderGrid(wrapper, exportBtn);
|
||||
refresh();
|
||||
});
|
||||
tag.appendChild(removeBtn);
|
||||
cell.appendChild(tag);
|
||||
} else {
|
||||
// N slots per day
|
||||
const slotCount = state.settings.moderatorsPerDay;
|
||||
const dayEntries = entriesByDate[iso] ?? []; // already alphabetically sorted
|
||||
const dayEntries = currentPlan
|
||||
.filter(a => a.date === iso)
|
||||
.sort((x, y) =>
|
||||
(empNameById[x.employeeId] ?? '').localeCompare(empNameById[y.employeeId] ?? '', 'de'));
|
||||
const slotValues = []; // employeeId per slot index, '' if empty
|
||||
for (let s = 0; s < slotCount; s++) {
|
||||
slotValues.push(dayEntries[s]?.employeeId ?? '');
|
||||
}
|
||||
for (let s = 0; s < slotCount; s++) slotValues.push(dayEntries[s]?.employeeId ?? '');
|
||||
|
||||
for (let slotIdx = 0; slotIdx < slotCount; slotIdx++) {
|
||||
const sel = document.createElement('select');
|
||||
@@ -278,36 +248,64 @@ function renderGrid(wrapper, exportBtn) {
|
||||
currentPlan.push(entry);
|
||||
}
|
||||
saveState();
|
||||
renderGrid(wrapper, exportBtn);
|
||||
refresh();
|
||||
});
|
||||
|
||||
cell.appendChild(sel);
|
||||
}
|
||||
|
||||
// FT / BU mark buttons
|
||||
const btnRow = document.createElement('div');
|
||||
btnRow.className = 'cal-btn-row';
|
||||
|
||||
const ftBtn = document.createElement('button');
|
||||
ftBtn.type = 'button';
|
||||
ftBtn.textContent = 'Feiertag';
|
||||
ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); renderGrid(wrapper, exportBtn); });
|
||||
ftBtn.addEventListener('click', () => { toggleHoliday(state, iso); saveState(); refresh(); });
|
||||
|
||||
const buBtn = document.createElement('button');
|
||||
buBtn.type = 'button';
|
||||
buBtn.textContent = 'Betriebsurl.';
|
||||
buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); renderGrid(wrapper, exportBtn); });
|
||||
buBtn.addEventListener('click', () => { toggleClosure(state, iso); saveState(); refresh(); });
|
||||
|
||||
btnRow.append(ftBtn, buBtn);
|
||||
cell.appendChild(btnRow);
|
||||
}
|
||||
}
|
||||
|
||||
bodyGrid.appendChild(cell);
|
||||
return cell;
|
||||
}
|
||||
|
||||
// Day-of-week header row
|
||||
const headerGrid = document.createElement('div');
|
||||
headerGrid.className = 'calendar-header-grid';
|
||||
WD_LABELS.forEach(lbl => {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'cal-header-cell';
|
||||
cell.textContent = lbl;
|
||||
headerGrid.appendChild(cell);
|
||||
});
|
||||
wrapper.appendChild(headerGrid);
|
||||
|
||||
// Body grid
|
||||
const bodyGrid = document.createElement('div');
|
||||
bodyGrid.className = 'calendar-body-grid';
|
||||
|
||||
// Offset empty cells before first day
|
||||
const firstISO = currentYear + '-' + String(currentMonth).padStart(2,'0') + '-01';
|
||||
const firstWD = (() => { const d = new Date(firstISO + 'T12:00:00Z').getUTCDay(); return d === 0 ? 7 : d; })();
|
||||
for (let i = 1; i < firstWD; i++) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'cal-day empty-cell';
|
||||
bodyGrid.appendChild(empty);
|
||||
}
|
||||
|
||||
// Day cells — each cell can re-render itself; no full-grid rebuild per click
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
bodyGrid.appendChild(makeDayCell(d));
|
||||
}
|
||||
|
||||
wrapper.appendChild(bodyGrid);
|
||||
exportBtn.style.display = currentPlan.length > 0 ? 'inline-block' : 'none';
|
||||
updateExportVisibility();
|
||||
}
|
||||
|
||||
function renderSkippedWarning(box, skippedDates) {
|
||||
|
||||
Reference in new issue
Block a user