163 lines
4.6 KiB
JavaScript
Raw Normal View History

2026-05-31 13:21:13 +02:00
import { defineComponent, ref, watch, onMounted, onUnmounted, h } from 'vue';
const prevW = Symbol("prevW");
const prevH = Symbol("prevH");
const FlowLayout = /* @__PURE__ */ defineComponent({
name: "FlowLayout",
props: {
cols: {
type: Number,
default: 2
},
gap: {
type: [Number, Array],
default: 4
},
duration: {
type: [Boolean, Number],
default: true
}
},
setup(props, { slots }) {
let sizeObs;
let childrenObs;
const layout = ref(null);
const layouting = ref(false);
const reLayout = () => {
if (layouting.value || !layout.value)
return;
layouting.value = true;
requestAnimationFrame(() => {
try {
const parent = layout.value;
const children = Array.from(parent.children);
const len = children.length;
const [gapX, gapY] = Array.isArray(props.gap) ? props.gap : [props.gap, props.gap];
const colWidth = (parent.offsetWidth - (props.cols - 1) * gapX) / props.cols;
for (const el of children) {
el.style.width = `${colWidth}px`;
}
const hs = Array.from({ length: len }, (_, index) => {
const el = children[index];
el[prevW] = el.offsetWidth;
el[prevH] = el.offsetHeight;
return el.offsetHeight;
});
const columnHs = Array.from({ length: props.cols }, () => 0);
for (let i = 0; i < children.length; i++) {
const el = children[i];
const minColHeight = Math.min(...columnHs);
const colIndex = columnHs.indexOf(minColHeight);
const left = colIndex * (colWidth + gapX);
const top = minColHeight;
el.style.left = `${left}px`;
el.style.top = `${top}px`;
columnHs[colIndex] += hs[i] + gapY;
}
parent.style.height = `${Math.max(...columnHs) - gapY}px`;
} finally {
if (layout.value) {
layout.value[prevW] = layout.value.offsetWidth;
layout.value[prevH] = layout.value.offsetHeight;
}
layouting.value = false;
}
});
};
const sizeListener = () => {
if (!layout.value)
return;
const parent = layout.value;
sizeObs = new ResizeObserver((els) => {
for (const { target: el } of els) {
if (el.offsetWidth !== el[prevW] || el.offsetHeight !== el[prevH]) {
reLayout();
}
}
});
sizeObs.observe(parent);
for (const el of parent.children) {
sizeObs.observe(el);
}
};
const childrenListener = () => {
if (!layout.value)
return;
const parent = layout.value;
childrenObs = new MutationObserver((ml) => {
for (const m of ml) {
for (const el of m.addedNodes) {
if (el.nodeType === 1)
sizeObs.observe(el);
}
for (const el of m.removedNodes) {
if (el.nodeType === 1)
sizeObs.unobserve(el);
}
}
reLayout();
});
childrenObs.observe(parent, { childList: true, subtree: true });
};
watch(() => [props.cols, props.gap], reLayout);
onMounted(() => {
reLayout();
sizeListener();
childrenListener();
});
onUnmounted(() => {
sizeObs.disconnect();
childrenObs.disconnect();
});
return () => {
const parentStyle = {
position: "relative",
display: "block",
boxSizing: "border-box",
overflow: "unset !important"
};
const childStyle = {
position: "absolute",
boxSizing: "border-box",
...props.duration ? {
transition: `all ${typeof props.duration === "number" ? `${props.duration}ms` : "350ms"}`
} : {}
};
const children = slots.default?.();
function setStyle(vnode, style, depath = 1) {
if (Array.isArray(vnode.children) && depath > 0) {
vnode.children.forEach((vvnode) => {
setStyle(vvnode, style, depath - 1);
});
} else {
const props2 = vnode.props = vnode.props || {};
props2.style = {
...props2.style,
...childStyle
};
}
}
children?.forEach((child) => {
setStyle(child, childStyle);
});
return h(
"div",
{
id: "vue-flow-layout",
ref: layout,
style: parentStyle
},
children
);
};
}
});
const index = {
install: (app) => {
app.component("FlowLayout", FlowLayout);
}
};
export { FlowLayout, index as default };