Files
site-portfolio/public/js/modules/mini-projects/masonry.js
T

182 lines
5.9 KiB
JavaScript

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.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.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 };
}