182 lines
6.0 KiB
JavaScript
182 lines
6.0 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.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 };
|
|
}
|