UNPKG

@yeger/vue-masonry-wall

Version:

Responsive masonry layout with SSR support and zero dependencies for Vue 3.

134 lines (133 loc) 5.15 kB
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