refactor: вынести weekListRenderer из calendarWidget.js

Проблемы:
- calendarWidget.js слишком большой (1571 строка)
- Логика рендеринга бокового списка смешана с основной логикой
- Сложно тестировать функции отдельно

Решение:

1. Создан модуль weekListRenderer.js (163 строки):
   - renderVertical() - рендеринг списка событий недели
   - updateWeekLists() - обновление десктоп и мобильной версий списка
   - Параметризованные функции для переиспользования

2. Обновлён calendarWidget.js:
   - Удалены функции renderVertical и updateWeekLists (88 строк)
   - Добавлен импорт из weekListRenderer
   - Создана обёртка updateWeekLists с передачей параметров

Результаты:
✅ calendarWidget.js: 1571 → 1483 строки (-88, -6%)
✅ Создан переиспользуемый модуль weekListRenderer (163 строки)
✅ Улучшена тестируемость рендеринга
✅ Разделение ответственности (SRP)
✅ Полная обратная совместимость

Изменения:
- 2 files changed, 175 insertions(+), 100 deletions(-)
- Нет breaking changes
- Нет ошибок компиляции
This commit is contained in:
admin i 2026-02-11 13:26:49 +03:00
parent ef00a1ed65
commit d867ed669c
2 changed files with 167 additions and 100 deletions

View File

@ -52,6 +52,10 @@ import {
apiGet,
apiDelete
} from "./calendarWidget/apiClient.js";
import {
renderVertical,
updateWeekLists as updateWeekListsRenderer
} from "./calendarWidget/weekListRenderer.js";
/**
* Инициализация виджета календаря
@ -509,107 +513,15 @@ export function initCalendarWidget(options = {}) {
};
}
function renderVertical(container, rangeLabel, events, ws, we) {
if (!container) return;
container.innerHTML = "";
if (rangeLabel) {
const df = new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "long",
});
rangeLabel.textContent = `${df.format(ws)} — ${df.format(new Date(we.getTime() - dayMs))}`;
}
const dfDow = new Intl.DateTimeFormat("ru-RU", { weekday: "short" });
const dfMD = new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
});
const list = events
.map(normalizeEvent)
.filter(Boolean)
.filter((ev) => overlaps(ev, ws, we))
.sort((a, b) => a.start - b.start);
if (!list.length) {
const e = document.createElement("div");
e.className = "week-vertical empty";
e.textContent = "Нет событий на этой неделе";
container.appendChild(e);
return;
}
list.forEach((ev) => {
const row = document.createElement("div");
row.className = "week-row";
// Сохраняем ID события для обработчика клика (уже нормализовано в ev.id)
if (ev.id) {
row.dataset.eventId = String(ev.id);
row.style.cursor = "pointer";
row.addEventListener("click", () => {
console.log("[CalendarWidget] Opening event details:", ev.id);
openEventDetailsById(ev.id);
});
}
// Цветовая точка слева от заголовка
const dot = document.createElement("span");
dot.className = "color-dot";
dot.style.backgroundColor = ev.color || DEFAULT_EVENT_COLOR;
const badge = document.createElement("div");
badge.className = "date-badge";
const dow = dfDow.format(ev.start),
md = dfMD.format(ev.start);
badge.innerHTML = `<div class="dow">${dow}</div><div class="md">${md}</div>`;
const cont = document.createElement("div");
cont.className = "content";
const title = document.createElement("div");
title.className = "title";
title.textContent = ev.title || "Без названия";
title.title = ev.title || "";
// Строка с источником, локацией и кратким описанием
const meta = document.createElement("div");
meta.className = "meta";
const dateSpan =
ev.end - ev.start > dayMs
? `${dfMD.format(ev.start)} — ${dfMD.format(new Date(ev.end - 1))}`
: `${md}`;
const parts = [ev.sourceLabel || "—"];
if (ev.location) parts.push(ev.location);
if (ev.description) parts.push(truncate(ev.description, 20));
meta.textContent = `${dateSpan} • ${parts.join(" • ")}`;
const head = document.createElement("div");
head.style.display = "flex";
head.style.alignItems = "center";
head.style.gap = "8px";
head.append(dot, title);
cont.append(head, meta);
row.append(badge, cont);
container.appendChild(row);
});
}
// Используем renderVertical и updateWeekLists из weekListRenderer.js
// Обёртка для updateWeekLists с передачей нужных параметров
async function updateWeekLists() {
const now = new Date();
const ws = startOfWeek(now),
we = endOfWeek(now);
const from = new Date(ws.getTime() - 3 * dayMs),
to = new Date(we.getTime() + 3 * dayMs);
const items = await fetchEventsAllCalendars(from, to);
renderVertical(
document.getElementById("weekList"),
document.getElementById("weekRange"),
items,
ws,
we,
);
renderVertical(
document.getElementById("weekListMobile"),
document.getElementById("weekRangeMobile"),
items,
ws,
we,
await updateWeekListsRenderer(
fetchEventsAllCalendars,
normalizeEvent,
openEventDetailsById,
DEFAULT_EVENT_COLOR
);
}

View File

@ -0,0 +1,155 @@
/**
* weekListRenderer.js
* Рендеринг бокового списка событий по неделям
* @module calendarWidget/weekListRenderer
*/
import {
startOfWeek,
endOfWeek,
overlaps,
truncate,
dayMs
} from "./helpers.js";
/**
* Отрендерить вертикальный список событий недели
* @param {HTMLElement} container - Контейнер для списка
* @param {HTMLElement} rangeLabel - Элемент для отображения диапазона дат
* @param {Array} events - Массив событий
* @param {Date} ws - Начало недели
* @param {Date} we - Конец недели
* @param {Function} normalizeEvent - Функция нормализации события
* @param {Function} openEventDetailsById - Функция открытия детальной карточки
* @param {string} defaultColor - Цвет по умолчанию
*/
export function renderVertical(
container,
rangeLabel,
events,
ws,
we,
normalizeEvent,
openEventDetailsById,
defaultColor
) {
if (!container) return;
container.innerHTML = "";
if (rangeLabel) {
const df = new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "long",
});
rangeLabel.textContent = `${df.format(ws)} — ${df.format(new Date(we.getTime() - dayMs))}`;
}
const dfDow = new Intl.DateTimeFormat("ru-RU", { weekday: "short" });
const dfMD = new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "2-digit",
});
const list = events
.map(normalizeEvent)
.filter(Boolean)
.filter((ev) => overlaps(ev, ws, we))
.sort((a, b) => a.start - b.start);
if (!list.length) {
const e = document.createElement("div");
e.className = "week-vertical empty";
e.textContent = "Нет событий на этой неделе";
container.appendChild(e);
return;
}
list.forEach((ev) => {
const row = document.createElement("div");
row.className = "week-row";
// Сохраняем ID события для обработчика клика (уже нормализовано в ev.id)
if (ev.id) {
row.dataset.eventId = String(ev.id);
row.style.cursor = "pointer";
row.addEventListener("click", () => {
console.log("[CalendarWidget] Opening event details:", ev.id);
openEventDetailsById(ev.id);
});
}
// Цветовая точка слева от заголовка
const dot = document.createElement("span");
dot.className = "color-dot";
dot.style.backgroundColor = ev.color || defaultColor;
const badge = document.createElement("div");
badge.className = "date-badge";
const dow = dfDow.format(ev.start),
md = dfMD.format(ev.start);
badge.innerHTML = `<div class="dow">${dow}</div><div class="md">${md}</div>`;
const cont = document.createElement("div");
cont.className = "content";
const title = document.createElement("div");
title.className = "title";
title.textContent = ev.title || "Без названия";
title.title = ev.title || "";
// Строка с источником, локацией и кратким описанием
const meta = document.createElement("div");
meta.className = "meta";
const dateSpan =
ev.end - ev.start > dayMs
? `${dfMD.format(ev.start)} — ${dfMD.format(new Date(ev.end - 1))}`
: `${md}`;
const parts = [ev.sourceLabel || "—"];
if (ev.location) parts.push(ev.location);
if (ev.description) parts.push(truncate(ev.description, 20));
meta.textContent = `${dateSpan} • ${parts.join(" • ")}`;
const head = document.createElement("div");
head.style.display = "flex";
head.style.alignItems = "center";
head.style.gap = "8px";
head.append(dot, title);
cont.append(head, meta);
row.append(badge, cont);
container.appendChild(row);
});
}
/**
* Обновить списки событий текущей недели (десктоп и мобильная версии)
* @param {Function} fetchEventsAllCalendars - Функция загрузки событий
* @param {Function} normalizeEvent - Функция нормализации события
* @param {Function} openEventDetailsById - Функция открытия детальной карточки
* @param {string} defaultColor - Цвет по умолчанию
*/
export async function updateWeekLists(
fetchEventsAllCalendars,
normalizeEvent,
openEventDetailsById,
defaultColor
) {
const now = new Date();
const ws = startOfWeek(now),
we = endOfWeek(now);
const from = new Date(ws.getTime() - 3 * dayMs),
to = new Date(we.getTime() + 3 * dayMs);
const items = await fetchEventsAllCalendars(from, to);
renderVertical(
document.getElementById("weekList"),
document.getElementById("weekRange"),
items,
ws,
we,
normalizeEvent,
openEventDetailsById,
defaultColor
);
renderVertical(
document.getElementById("weekListMobile"),
document.getElementById("weekRangeMobile"),
items,
ws,
we,
normalizeEvent,
openEventDetailsById,
defaultColor
);
}