mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 13:58:50 +08:00
feat(component): grouped masonry (#11958)
- support group for masonry - expand/collapse group - sticky group header  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced Masonry component with support for grouped items, collapsible and sticky group headers. - Added multi-view transitions enabling switching between Masonry, Grid, and List layouts. - Introduced virtual scrolling with group support for efficient handling of large datasets. - New interactive storybook demonstrations showcasing grouped and multi-view Masonry scenarios. - **Improvements** - List view styling enhancements for improved clarity and layout. - Resize panel now supports customizable offset modifications during drag interactions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import type { MasonryItem, MasonryItemXYWH } from './type';
|
||||
import type { MasonryGroup, MasonryItem, MasonryItemXYWH } from './type';
|
||||
|
||||
export const calcColumns = (
|
||||
totalWidth: number,
|
||||
itemWidth: number | 'stretch',
|
||||
itemWidthMin: number,
|
||||
gapX: number,
|
||||
paddingX: number
|
||||
paddingX: number,
|
||||
columns?: number
|
||||
) => {
|
||||
const availableWidth = totalWidth - paddingX * 2;
|
||||
|
||||
if (columns) {
|
||||
const width = (availableWidth - (columns - 1) * gapX) / columns;
|
||||
return { columns, width };
|
||||
}
|
||||
|
||||
if (itemWidth === 'stretch') {
|
||||
let columns = 1;
|
||||
while (columns * itemWidthMin + (columns - 1) * gapX < availableWidth) {
|
||||
@@ -34,33 +40,81 @@ export const calcColumns = (
|
||||
};
|
||||
|
||||
export const calcLayout = (
|
||||
items: MasonryItem[],
|
||||
groups: MasonryGroup[],
|
||||
options: {
|
||||
totalWidth: number;
|
||||
columns: number;
|
||||
width: number;
|
||||
gapX: number;
|
||||
gapY: number;
|
||||
paddingX: number;
|
||||
paddingY: number;
|
||||
groupsGap: number;
|
||||
groupHeaderGapWithItems: number;
|
||||
collapsedGroups: string[];
|
||||
}
|
||||
) => {
|
||||
const { columns, width, gapX, gapY, paddingX, paddingY } = options;
|
||||
const {
|
||||
totalWidth,
|
||||
columns,
|
||||
width,
|
||||
gapX,
|
||||
gapY,
|
||||
paddingX,
|
||||
paddingY,
|
||||
groupsGap,
|
||||
groupHeaderGapWithItems,
|
||||
collapsedGroups,
|
||||
} = options;
|
||||
|
||||
const layoutMap = new Map<MasonryItem['id'], MasonryItemXYWH>();
|
||||
const heightStack = Array.from({ length: columns }, () => paddingY);
|
||||
const layout = new Map<string, MasonryItemXYWH>();
|
||||
let finalHeight = paddingY;
|
||||
|
||||
items.forEach(item => {
|
||||
const minHeight = Math.min(...heightStack);
|
||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||
const y = minHeight + gapY;
|
||||
heightStack[minHeightIndex] = y + item.height;
|
||||
layoutMap.set(item.id, { x, y, w: width, h: item.height });
|
||||
groups.forEach((group, index) => {
|
||||
const heightStack = Array.from({ length: columns }, () => 0);
|
||||
if (index !== 0) {
|
||||
finalHeight += groupsGap;
|
||||
}
|
||||
|
||||
// calculate group header
|
||||
const groupHeaderLayout: MasonryItemXYWH = {
|
||||
type: 'group',
|
||||
x: paddingX,
|
||||
y: finalHeight,
|
||||
w: totalWidth - paddingX * 2,
|
||||
h: group.height,
|
||||
};
|
||||
layout.set(group.id, groupHeaderLayout);
|
||||
|
||||
if (collapsedGroups.includes(group.id)) {
|
||||
finalHeight += groupHeaderLayout.h;
|
||||
return;
|
||||
}
|
||||
|
||||
finalHeight += groupHeaderLayout.h + groupHeaderGapWithItems;
|
||||
// calculate group items
|
||||
group.items.forEach(item => {
|
||||
const itemId = group.id ? `${group.id}:${item.id}` : item.id;
|
||||
const minHeight = Math.min(...heightStack);
|
||||
const minHeightIndex = heightStack.indexOf(minHeight);
|
||||
const x = minHeightIndex * (width + gapX) + paddingX;
|
||||
const y = minHeight + finalHeight;
|
||||
|
||||
heightStack[minHeightIndex] += item.height + gapY;
|
||||
layout.set(itemId, {
|
||||
type: 'item',
|
||||
x,
|
||||
y,
|
||||
w: width,
|
||||
h: item.height,
|
||||
});
|
||||
});
|
||||
|
||||
const groupHeight = Math.max(...heightStack) + paddingY;
|
||||
finalHeight += groupHeight;
|
||||
});
|
||||
|
||||
const finalHeight = Math.max(...heightStack) + paddingY;
|
||||
|
||||
return { layout: layoutMap, height: finalHeight };
|
||||
return { layout, height: finalHeight };
|
||||
};
|
||||
|
||||
export const calcSleep = (options: {
|
||||
@@ -85,3 +139,24 @@ export const calcSleep = (options: {
|
||||
|
||||
return sleepMap;
|
||||
};
|
||||
|
||||
export const calcSticky = (options: {
|
||||
scrollY: number;
|
||||
layoutMap: Map<MasonryItem['id'], MasonryItemXYWH>;
|
||||
}) => {
|
||||
const { scrollY, layoutMap } = options;
|
||||
// find sticky group header
|
||||
const entries = Array.from(layoutMap.entries());
|
||||
const groupEntries = entries.filter(([_, layout]) => layout.type === 'group');
|
||||
|
||||
const stickyGroupEntry = groupEntries.find(([_, xywh], index) => {
|
||||
const next = groupEntries[index + 1];
|
||||
return xywh.y < scrollY && (!next || next[1].y > scrollY);
|
||||
});
|
||||
|
||||
return stickyGroupEntry
|
||||
? stickyGroupEntry[0]
|
||||
: groupEntries.length > 0
|
||||
? groupEntries[0][0]
|
||||
: '';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user