@yeger/vue-masonry-wall
Version:
Responsive masonry layout with SSR support and zero dependencies for Vue 3.
134 lines (133 loc) • 5.15 kB
JavaScript
import { Fragment, createElementBlock, createTextVNode, defineComponent, nextTick, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, useTemplateRef, watch } from "vue";
import { debounce } from "@yeger/debounce";
//#region src/masonry-wall.vue?vue&type=script&setup=true&lang.ts
const _hoisted_1 = ["data-index"];
//#endregion
//#region src/masonry-wall.vue
var masonry_wall_default = /* @__PURE__ */ defineComponent({
__name: "masonry-wall",
props: {
columnWidth: { default: 400 },
items: {},
gap: { default: 0 },
rtl: {
type: Boolean,
default: false
},
ssrColumns: { default: 0 },
scrollContainer: { default: null },
minColumns: { default: 1 },
maxColumns: {},
keyMapper: { type: Function }
},
emits: ["redraw", "redrawSkip"],
setup(__props, { emit: __emit }) {
const emit = __emit;
const columns = ref([]);
const wall = useTemplateRef("wall");
function createColumns(count) {
return Array.from({ length: count }).map(() => []);
}
function countIteratively(containerWidth, gap, count, consumed) {
const nextWidth = getColumnWidthTarget(count);
if (consumed + gap + nextWidth <= containerWidth) return countIteratively(containerWidth, gap, count + 1, consumed + gap + nextWidth);
return count;
}
function getColumnWidthTarget(columnIndex) {
const widths = Array.isArray(__props.columnWidth) ? __props.columnWidth : [__props.columnWidth];
return widths[columnIndex % widths.length];
}
function columnCount() {
const boundedCount = aboveMin(belowMax(countIteratively(wall.value.getBoundingClientRect().width, __props.gap, 0, -__props.gap)));
return boundedCount > 0 ? boundedCount : 1;
}
function belowMax(count) {
if (!__props.maxColumns) return count;
return Math.min(count, __props.maxColumns);
}
function aboveMin(count) {
return Math.max(count, __props.minColumns);
}
if (__props.ssrColumns > 0) {
const newColumns = createColumns(__props.ssrColumns);
for (let i = 0; i < __props.items.length; i++) newColumns[i % __props.ssrColumns].push(i);
columns.value = newColumns;
}
let currentRedrawId = 0;
async function fillColumns(itemIndex, assignedRedrawId) {
if (itemIndex >= __props.items.length) return;
await nextTick();
if (currentRedrawId !== assignedRedrawId) return;
const target = [...wall.value.children].reduce((prev, curr) => curr.getBoundingClientRect().height < prev.getBoundingClientRect().height ? curr : prev);
columns.value[+target.dataset.index].push(itemIndex);
await fillColumns(itemIndex + 1, assignedRedrawId);
}
async function redraw(force = false) {
const newColumnCount = columnCount();
if (columns.value.length === newColumnCount && !force) {
emit("redrawSkip");
return;
}
columns.value = createColumns(newColumnCount);
const scrollY = __props.scrollContainer ? __props.scrollContainer.scrollTop : window.scrollY;
await fillColumns(0, ++currentRedrawId);
if (__props.scrollContainer) __props.scrollContainer.scrollBy({ top: scrollY - __props.scrollContainer.scrollTop });
else window.scrollTo({ top: scrollY });
emit("redraw");
}
const resizeObserver = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(debounce(() => redraw()));
onMounted(async () => {
await redraw();
resizeObserver?.observe(wall.value);
});
onBeforeUnmount(() => resizeObserver?.unobserve(wall.value));
watch(() => __props.items, () => redraw(true));
watch([
() => __props.columnWidth,
() => __props.gap,
() => __props.minColumns,
() => __props.maxColumns
], () => redraw());
return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", {
ref_key: "wall",
ref: wall,
class: "masonry-wall",
style: normalizeStyle({
display: "flex",
gap: `${__props.gap}px`,
flexDirection: __props.rtl ? "row-reverse" : void 0
})
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column, columnIndex) => {
return openBlock(), createElementBlock("div", {
key: columnIndex,
class: "masonry-column",
"data-index": columnIndex,
style: normalizeStyle({
display: "flex",
"flex-basis": `${getColumnWidthTarget(columnIndex)}px`,
"flex-direction": "column",
"flex-grow": 1,
gap: `${__props.gap}px`,
height: "max-content",
"min-width": 0
})
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(column, (itemIndex, row) => {
return openBlock(), createElementBlock("div", {
key: __props.keyMapper?.(__props.items[itemIndex], columnIndex, row, itemIndex) ?? itemIndex,
class: "masonry-item"
}, [renderSlot(_ctx.$slots, "default", {
item: __props.items[itemIndex],
column: columnIndex,
columnCount: columns.value.length,
row,
index: itemIndex
}, () => [createTextVNode(toDisplayString(__props.items[itemIndex]), 1)])]);
}), 128))], 12, _hoisted_1);
}), 128))], 4);
};
}
});
//#endregion
export { masonry_wall_default as MasonryWall };
//# sourceMappingURL=index.mjs.map