Skip to content

implemented autoAdjustItemWidth for FlexGrid & ResponsiveGrid #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
interval: "monthly"
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,14 @@ A lower value results in more frequent updates, offering smoother visual updates
<td>Defines the threshold for triggering <code>onVerticalEndReached</code>. Represented as a fraction of the total height of the scrollable grid, indicating how far from the end the vertical scroll must be to trigger the event.</td>
</tr>

<tr>
<td><code>autoAdjustItemWidth</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td><code>false</code></td>
<td> Prevents width overflow by adjusting items with width ratios that exceed available columns in their row & width overlap by adjusting items that would overlap with items extending from previous rows</td>
</tr>

<tr>
<td><code>HeaderComponent</code></td>
<td><code>React.ComponentType&lt;any&gt; | React.ReactElement | null | undefined</code></td>
Expand Down Expand Up @@ -446,6 +454,14 @@ A lower value results in more frequent updates, offering smoother visual updates
<td> Defines the distance from the end of the content at which <code>onEndReached</code> should be triggered, expressed as a proportion of the total content length. For example, a value of <code>0.1</code> triggers the callback when the user has scrolled to within 10% of the end of the content. </td>
</tr>

<tr>
<td><code>autoAdjustItemWidth</code></td>
<td><code>boolean</code></td>
<td><code>true</code></td>
<td><code>false</code></td>
<td> Prevents width overflow by adjusting items with width ratios that exceed available columns in their row & width overlap by adjusting items that would overlap with items extending from previous rows</td>
</tr>

<tr>
<td><code>HeaderComponent</code></td>
<td><code>React.ComponentType&lt;any&gt; | React.ReactElement | null | undefined</code></td>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@
"directories": {
"example": "example"
}
}
}
133 changes: 115 additions & 18 deletions src/flex-grid/calc-flex-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,145 @@ import type { FlexGridItem, FlexGridTile } from './types';
export const calcFlexGrid = (
data: FlexGridTile[],
maxColumnRatioUnits: number,
itemSizeUnit: number
itemSizeUnit: number,
autoAdjustItemWidth: boolean = true
): {
gridItems: FlexGridItem[];
totalHeight: number;
totalWidth: number;
} => {
const gridItems: FlexGridItem[] = [];
let columnHeights = new Array(maxColumnRatioUnits).fill(0); // Track the height of each column.
let columnHeights = new Array(maxColumnRatioUnits).fill(0);

const findAvailableWidth = (
startColumn: number,
currentTop: number
): number => {
let availableWidth = 0;
let column = startColumn;

while (column < maxColumnRatioUnits) {
// Check for protruding items at this column
const hasProtruding = gridItems.some((item) => {
const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
const itemLeft = Math.floor(item.left / itemSizeUnit);
const itemRight = itemLeft + (item.widthRatio || 1);

return (
item.top < currentTop &&
itemBottom > currentTop &&
column >= itemLeft &&
column < itemRight
);
});

if (hasProtruding) {
break;
}

availableWidth++;
column++;
}

return availableWidth;
};

const findEndOfProtrudingItem = (
column: number,
currentTop: number
): number => {
const protrudingItem = gridItems.find((item) => {
const itemBottom = item.top + (item.heightRatio || 1) * itemSizeUnit;
const itemLeft = Math.floor(item.left / itemSizeUnit);
const itemRight = itemLeft + (item.widthRatio || 1);

return (
item.top < currentTop &&
itemBottom > currentTop &&
column >= itemLeft &&
column < itemRight
);
});

if (protrudingItem) {
return (
Math.floor(protrudingItem.left / itemSizeUnit) +
(protrudingItem.widthRatio || 1)
);
}

return column;
};

const findNextColumnIndex = (currentTop: number): number => {
let nextColumn = 0;
let maxColumn = -1;

// Find the right most occupied column at this height
gridItems.forEach((item) => {
if (Math.abs(item.top - currentTop) < 0.1) {
maxColumn = Math.max(
maxColumn,
Math.floor(item.left / itemSizeUnit) + (item.widthRatio || 1)
);
}
});

// If we found items in this row, start after the last one
if (maxColumn !== -1) {
nextColumn = maxColumn;
}

// Check if there's a protruding item at the next position
const protrudingEnd = findEndOfProtrudingItem(nextColumn, currentTop);
if (protrudingEnd > nextColumn) {
nextColumn = protrudingEnd;
}

return nextColumn;
};

data.forEach((item) => {
const widthRatio = item.widthRatio || 1;
let widthRatio = item.widthRatio || 1;
const heightRatio = item.heightRatio || 1;

// Find the column with the minimum height to start placing the current item.
// Find shortest column for current row
let columnIndex = columnHeights.indexOf(Math.min(...columnHeights));
// If the item doesn't fit in the remaining columns, reset the row.
if (widthRatio + columnIndex > maxColumnRatioUnits) {
columnIndex = 0;
const maxHeight = Math.max(...columnHeights);
columnHeights.fill(maxHeight); // Align all columns to the height of the tallest column.
const currentTop = columnHeights[columnIndex];

// Find where this item should be placed in the current row
columnIndex = findNextColumnIndex(currentTop);

if (autoAdjustItemWidth) {
// Get available width considering both row end and protruding items
const availableWidth = findAvailableWidth(columnIndex, currentTop);
const remainingWidth = maxColumnRatioUnits - columnIndex;

// Use the smaller of the two constraints
const maxWidth = Math.min(availableWidth, remainingWidth);

if (widthRatio > maxWidth) {
widthRatio = Math.max(1, maxWidth);
}
}

// Push the item with calculated position into the gridItems array.
gridItems.push({
...item,
top: columnHeights[columnIndex],
top: currentTop,
left: columnIndex * itemSizeUnit,
widthRatio,
heightRatio,
});

// Update the heights of the columns spanned by this item.
// Update column heights
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
columnHeights[i] += heightRatio * itemSizeUnit;
columnHeights[i] = currentTop + heightRatio * itemSizeUnit;
}
});

// After positioning all data, calculate the total height of the grid.
const totalHeight = Math.max(...columnHeights);

// Return the positioned data and the total height of the grid.
return {
gridItems,
totalHeight,
totalHeight: Math.max(...columnHeights),
totalWidth: maxColumnRatioUnits * itemSizeUnit,
};
};
10 changes: 8 additions & 2 deletions src/flex-grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const FlexGrid: React.FC<FlexGridProps> = ({
virtualizedBufferFactor = 2,
showScrollIndicator = true,
renderItem = () => null,
autoAdjustItemWidth = true,
style = {},
itemContainerStyle = {},
keyExtractor = (_, index) => String(index), // default to item index if no keyExtractor is provided
Expand All @@ -43,8 +44,13 @@ export const FlexGrid: React.FC<FlexGridProps> = ({
});

const { totalHeight, totalWidth, gridItems } = useMemo(() => {
return calcFlexGrid(data, maxColumnRatioUnits, itemSizeUnit);
}, [data, maxColumnRatioUnits, itemSizeUnit]);
return calcFlexGrid(
data,
maxColumnRatioUnits,
itemSizeUnit,
autoAdjustItemWidth
);
}, [data, maxColumnRatioUnits, itemSizeUnit, autoAdjustItemWidth]);

const renderedList = virtualization ? visibleItems : gridItems;

Expand Down
8 changes: 8 additions & 0 deletions src/flex-grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export interface FlexGridProps {
/** Defines the base unit size for grid items. Actual item size is calculated by multiplying this with width and height ratios. */
itemSizeUnit: number;

/**
* Prevents width overflow by adjusting items with width ratios that exceed
* available columns in their row & width overlap by adjusting items that would overlap with items
* extending from previous rows
* @default true
*/
autoAdjustItemWidth?: boolean;

/** Function to render each item in the grid. Receives the item and its index as parameters. */
renderItem: ({ item, index }: RenderItemProps) => ReactNode;

Expand Down
76 changes: 56 additions & 20 deletions src/responsive-grid/calc-responsive-grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,77 @@ export const calcResponsiveGrid = (
data: TileItem[],
maxItemsPerColumn: number,
containerWidth: number,
itemUnitHeight?: number
itemUnitHeight?: number,
autoAdjustItemWidth: boolean = true
): {
gridItems: GridItem[];
gridViewHeight: number;
} => {
const gridItems: GridItem[] = [];
const itemSizeUnit = containerWidth / maxItemsPerColumn; // Determine TileSize based on container width and max number of columns
let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0); // Track the height of each column end.
const itemSizeUnit = containerWidth / maxItemsPerColumn;
let columnHeights: number[] = new Array(maxItemsPerColumn).fill(0);

data.forEach((item) => {
const widthRatio = item.widthRatio || 1;
const heightRatio = item.heightRatio || 1;
const findAvailableWidth = (
startColumn: number,
currentTop: number
): number => {
// Check each column from the start position
let availableWidth = 0;

const itemWidth = widthRatio * itemSizeUnit;
for (let i = startColumn; i < maxItemsPerColumn; i++) {
// Check if there's any item from above rows protruding into this space
const hasProtrudingItem = gridItems.some((item) => {
const itemBottom = item.top + item.height;
const itemRight = item.left + item.width;
return (
item.top < currentTop && // Item starts above current row
itemBottom > currentTop && // Item extends into current row
item.left <= i * itemSizeUnit && // Item starts at or before this column
itemRight > i * itemSizeUnit // Item extends into this column
);
});

const itemHeight = itemUnitHeight
? itemUnitHeight * heightRatio
: heightRatio * itemSizeUnit; // Use itemUnitHeight if provided, else fallback to itemSizeUnit
if (hasProtrudingItem) {
break; // Stop counting available width when we hit a protruding item
}

availableWidth++;
}

return availableWidth;
};

data.forEach((item) => {
let widthRatio = item.widthRatio || 1;
const heightRatio = item.heightRatio || 1;

// Find the column where the item should be placed.
let columnIndex = findColumnForItem(
columnHeights,
widthRatio,
maxItemsPerColumn
);

// Calculate item's top and left positions.
if (autoAdjustItemWidth) {
// Get current row's height at the column index
const currentTop = columnHeights[columnIndex];

// Calculate available width considering both row end and protruding items
const availableWidth = findAvailableWidth(columnIndex, currentTop!);

// If widthRatio exceeds available space, adjust it
if (widthRatio > availableWidth) {
widthRatio = Math.max(1, availableWidth);
}
}

const itemWidth = widthRatio * itemSizeUnit;
const itemHeight = itemUnitHeight
? itemUnitHeight * heightRatio
: heightRatio * itemSizeUnit;

const top = columnHeights[columnIndex]!;
const left = columnIndex * itemSizeUnit;

// Place the item.
gridItems.push({
...item,
top,
Expand All @@ -43,19 +83,15 @@ export const calcResponsiveGrid = (
height: itemHeight,
});

// Update the column heights for the columns that the item spans.
// This needs to accommodate the actual height used (itemHeight).
// Update the column heights
for (let i = columnIndex; i < columnIndex + widthRatio; i++) {
columnHeights[i] = top + itemHeight; // Update to use itemHeight
columnHeights[i] = top + itemHeight;
}
});

// Calculate the total height of the grid.
const gridViewHeight = Math.max(...columnHeights);

return {
gridItems,
gridViewHeight,
gridViewHeight: Math.max(...columnHeights),
};
};

Expand Down
6 changes: 4 additions & 2 deletions src/responsive-grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
maxItemsPerColumn = 3,
virtualizedBufferFactor = 5,
renderItem,
autoAdjustItemWidth = true,
scrollEventInterval = 200, // milliseconds
virtualization = true,
showScrollIndicator = true,
Expand Down Expand Up @@ -44,9 +45,10 @@ export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
data,
maxItemsPerColumn,
containerSize.width,
itemUnitHeight
itemUnitHeight,
autoAdjustItemWidth
),
[data, maxItemsPerColumn, containerSize]
[data, maxItemsPerColumn, containerSize, autoAdjustItemWidth]
);

const renderedItems = virtualization ? visibleItems : gridItems;
Expand Down
8 changes: 8 additions & 0 deletions src/responsive-grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export interface ResponsiveGridProps {
/** Defines the maximum number of items that can be displayed within a single column of the grid. */
maxItemsPerColumn: number;

/**
* Prevents width overflow by adjusting items with width ratios that exceed
* available columns in their row & width overlap by adjusting items that would overlap with items
* extending from previous rows
* @default true
*/
autoAdjustItemWidth?: boolean;

/** Interval in milliseconds at which scroll events are processed for virtualization. Default is 200ms. */
scrollEventInterval?: number;

Expand Down
Loading