export function initMiniProjectsMasonry() { const lists = document.querySelectorAll("[data-mini-projects-list]"); if (!lists.length) return null; if (lists.length === 1) return initMiniProjectsMasonryList(lists[0]); return Array.from(lists, initMiniProjectsMasonryList); } function initMiniProjectsMasonryList(list) { if (!list) return null; const allItems = Array.from(list.querySelectorAll(".mini-projects-grid__item")); const extraItems = allItems.filter((el) => el.hasAttribute("data-mp-extra")); const baseItems = allItems.filter((el) => !el.hasAttribute("data-mp-extra")); const gap = 16; let rafId = 0; let expanded = false; let fullHeight = 0; let collapsedHeight = 0; extraItems.forEach((el) => el.removeAttribute("hidden")); function rowCellsForWidth(w) { if (w >= 1180) return 6; if (w >= 880) return 4; if (w >= 520) return 2; return 1; } function readDeclaredCells(el, rowCells) { const full = Number.parseInt(String(el.getAttribute("data-mp-cells")), 10) || 1; if (rowCells <= 2) { const xs = el.getAttribute("data-mp-cells-xs"); if (xs != null) { const n = Number.parseInt(String(xs), 10); if (Number.isFinite(n)) return Math.min(Math.max(n, 1), rowCells); } } if (rowCells <= 4) { const sm = el.getAttribute("data-mp-cells-sm"); if (sm != null) { const n = Number.parseInt(String(sm), 10); if (Number.isFinite(n)) return Math.min(Math.max(n, 1), rowCells); } } return Math.min(Math.max(full, 1), rowCells); } function pickStartColumn(heights, rowCells, wCells) { let bestC = 0; let bestTop = Infinity; for (let c = 0; c <= rowCells - wCells; c += 1) { let top = 0; for (let k = 0; k < wCells; k += 1) { top = Math.max(top, heights[c + k]); } if (top < bestTop || (top === bestTop && c < bestC)) { bestTop = top; bestC = c; } } return bestC; } /** Целые ширины клеток в сумме дают внутреннюю ширину ряда (без промежутков между колонками). */ function integerCellWidths(innerPx, rowCells) { if (rowCells <= 0) return []; const base = Math.floor(innerPx / rowCells); const remainder = innerPx % rowCells; const widths = []; for (let i = 0; i < rowCells; i += 1) { widths.push(base + (i < remainder ? 1 : 0)); } return widths; } function spanInnerWidth(cellWidths, startCol, wCells) { let sum = 0; for (let i = 0; i < wCells; i += 1) { sum += cellWidths[startCol + i]; } return sum; } function xAtColumn(cellWidths, startCol) { let x = 0; for (let i = 0; i < startCol; i += 1) { x += cellWidths[i] + gap; } return x; } function performLayout() { if (!allItems.length) { list.style.height = "0px"; return; } const listWidth = list.clientWidth; const rowCells = rowCellsForWidth(listWidth); const innerPx = Math.max(0, listWidth - gap * (rowCells - 1)); const cellWidths = integerCellWidths(innerPx, rowCells); const heights = Array(rowCells).fill(0); const positions = new Map(); allItems.filter((item) => !item.hidden).forEach((item) => { const want = readDeclaredCells(item, rowCells); let hVal = 0; if (rowCells <= 2) { const xs = item.getAttribute("data-mp-height-xs"); if (xs != null) hVal = Number.parseInt(xs, 10) || 0; } if (!hVal && rowCells <= 4) { const sm = item.getAttribute("data-mp-height-sm"); if (sm != null) hVal = Number.parseInt(sm, 10) || 0; } if (!hVal) { const hAttr = item.getAttribute("data-mp-height"); if (hAttr) hVal = Number.parseInt(hAttr, 10) || 0; } item.style.height = hVal > 0 ? `${hVal}px` : ""; const wCells = Math.min(want, rowCells); const c = pickStartColumn(heights, rowCells, wCells); let y = 0; for (let k = 0; k < wCells; k += 1) { y = Math.max(y, heights[c + k]); } y = Math.round(y); const widthPx = spanInnerWidth(cellWidths, c, wCells) + (wCells - 1) * gap; const x = xAtColumn(cellWidths, c); item.style.width = `${widthPx}px`; item.style.transform = `translate(${x}px, ${y}px)`; const h = item.offsetHeight; const next = y + h + gap; for (let k = 0; k < wCells; k += 1) { heights[c + k] = next; } positions.set(item, { y, h }); }); fullHeight = Math.max(0, Math.round(Math.max(...heights, 0) - gap) + 1); collapsedHeight = 0; baseItems.filter((item) => !item.hidden).forEach((item) => { const p = positions.get(item); if (!p) return; collapsedHeight = Math.max(collapsedHeight, Math.round(p.y + p.h) + 1); }); list.style.height = `${expanded ? fullHeight : collapsedHeight}px`; } function relayout() { if (rafId) window.cancelAnimationFrame(rafId); rafId = window.requestAnimationFrame(performLayout); } function relayoutAfterThemeChange() { relayout(); if (document.fonts?.ready) { document.fonts.ready.then(relayout); } window.requestAnimationFrame(() => { relayout(); window.requestAnimationFrame(relayout); }); } window.addEventListener("site-theme-applied", relayoutAfterThemeChange, { passive: true }); window.addEventListener("resize", relayout, { passive: true }); window.addEventListener("load", relayout, { once: true }); list.querySelectorAll("img").forEach((img) => { if (img.complete) return; img.addEventListener("load", () => relayout(), { passive: true }); }); relayout(); function setExpanded(next) { expanded = next; list.style.height = `${Math.round(expanded ? fullHeight : collapsedHeight)}px`; } return { list, relayout, setExpanded, extraItems }; }