your personal website on atproto - mirror
blento.app
1import { type LayoutItem, type Layout } from 'react-grid-layout/core';
2import {
3 collides,
4 moveElement,
5 correctBounds,
6 getFirstCollision,
7 verticalCompactor
8} from 'react-grid-layout/core';
9import type { Item } from '../types';
10import { COLUMNS } from '$lib';
11import { clamp } from '../helper';
12
13function toLayoutItem(item: Item, mobile: boolean): LayoutItem {
14 if (mobile) {
15 return {
16 x: item.mobileX,
17 y: item.mobileY,
18 w: item.mobileW,
19 h: item.mobileH,
20 i: item.id
21 };
22 }
23 return {
24 x: item.x,
25 y: item.y,
26 w: item.w,
27 h: item.h,
28 i: item.id
29 };
30}
31
32function toLayout(items: Item[], mobile: boolean): LayoutItem[] {
33 return items.map((i) => toLayoutItem(i, mobile));
34}
35
36function applyLayout(items: Item[], layout: LayoutItem[], mobile: boolean): void {
37 const itemsMap: Map<string, Item> = new Map();
38
39 for (const item of items) {
40 itemsMap.set(item.id, item);
41 }
42 for (const l of layout) {
43 const item = itemsMap.get(l.i);
44
45 if (!item) {
46 console.error('item not found in layout!! this should never happen!');
47 continue;
48 }
49
50 if (mobile) {
51 item.mobileX = l.x;
52 item.mobileY = l.y;
53 } else {
54 item.x = l.x;
55 item.y = l.y;
56 }
57 }
58}
59
60export function overlaps(a: Item, b: Item, mobile: boolean) {
61 if (a === b) return false;
62 return collides(toLayoutItem(a, mobile), toLayoutItem(b, mobile));
63}
64
65/** Returns true if any two items overlap in the given layout. */
66export function hasOverlaps(items: Item[], mobile: boolean): boolean {
67 for (let i = 0; i < items.length; i++) {
68 for (let j = i + 1; j < items.length; j++) {
69 if (overlaps(items[i], items[j], mobile)) return true;
70 }
71 }
72 return false;
73}
74
75export function fixCollisions(
76 items: Item[],
77 item: Item,
78 mobile: boolean = false,
79 skipCompact: boolean = false,
80 originalPos?: { x: number; y: number }
81) {
82 if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW);
83 else item.x = clamp(item.x, 0, COLUMNS - item.w);
84
85 const targetX = mobile ? item.mobileX : item.x;
86 const targetY = mobile ? item.mobileY : item.y;
87
88 let layout = toLayout(items, mobile);
89
90 const movedLayoutItem = layout.find((i) => i.i === item.id);
91
92 if (!movedLayoutItem) {
93 console.error('item not found in layout! this should never happen!');
94 return;
95 }
96
97 // If we know the original position, set it on the layout item so
98 // moveElement can detect direction and push items properly.
99 if (originalPos) {
100 movedLayoutItem.x = originalPos.x;
101 movedLayoutItem.y = originalPos.y;
102 }
103
104 layout = moveElement(layout, movedLayoutItem, targetX, targetY, true, false, 'vertical', COLUMNS);
105
106 if (!skipCompact) layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
107
108 applyLayout(items, layout, mobile);
109}
110
111export function fixAllCollisions(items: Item[], mobile: boolean) {
112 let layout = toLayout(items, mobile);
113 correctBounds(layout as any, { cols: COLUMNS });
114 layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
115 applyLayout(items, layout, mobile);
116}
117
118/**
119 * Only fix items that are out of grid bounds, without compacting or resolving overlaps.
120 * This is safe to call on load — it won't shift already-valid layouts.
121 */
122export function sanitizeBounds(items: Item[], mobile: boolean) {
123 const layout = toLayout(items, mobile);
124 correctBounds(layout as any, { cols: COLUMNS });
125 applyLayout(items, layout, mobile);
126}
127
128export function compactItems(items: Item[], mobile: boolean) {
129 const layout = toLayout(items, mobile);
130 const compacted = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
131 applyLayout(items, compacted, mobile);
132}
133
134export function setPositionOfNewItem(
135 newItem: Item,
136 items: Item[],
137 viewportCenter?: { gridY: number; isMobile: boolean }
138) {
139 const desktopLayout = toLayout(items, false);
140 const mobileLayout = toLayout(items, true);
141
142 function hasCollision(mobile: boolean): boolean {
143 const layout = mobile ? mobileLayout : desktopLayout;
144 return getFirstCollision(layout, toLayoutItem(newItem, mobile)) !== undefined;
145 }
146
147 if (viewportCenter) {
148 const { gridY, isMobile } = viewportCenter;
149
150 if (isMobile) {
151 // Place at viewport center Y
152 newItem.mobileY = Math.max(0, Math.round(gridY - newItem.mobileH / 2));
153 newItem.mobileY = Math.floor(newItem.mobileY / 2) * 2;
154
155 // Try to find a free X at this Y
156 let found = false;
157 for (
158 newItem.mobileX = 0;
159 newItem.mobileX <= COLUMNS - newItem.mobileW;
160 newItem.mobileX += 2
161 ) {
162 if (!hasCollision(true)) {
163 found = true;
164 break;
165 }
166 }
167 if (!found) {
168 newItem.mobileX = 0;
169 }
170
171 // Desktop: derive from mobile
172 newItem.y = Math.max(0, Math.round(newItem.mobileY / 2));
173 found = false;
174 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
175 if (!hasCollision(false)) {
176 found = true;
177 break;
178 }
179 }
180 if (!found) {
181 newItem.x = 0;
182 }
183 } else {
184 // Place at viewport center Y
185 newItem.y = Math.max(0, Math.round(gridY - newItem.h / 2));
186
187 // Try to find a free X at this Y
188 let found = false;
189 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
190 if (!hasCollision(false)) {
191 found = true;
192 break;
193 }
194 }
195 if (!found) {
196 newItem.x = 0;
197 }
198
199 // Mobile: derive from desktop
200 newItem.mobileY = Math.max(0, Math.round(newItem.y * 2));
201 found = false;
202 for (
203 newItem.mobileX = 0;
204 newItem.mobileX <= COLUMNS - newItem.mobileW;
205 newItem.mobileX += 2
206 ) {
207 if (!hasCollision(true)) {
208 found = true;
209 break;
210 }
211 }
212 if (!found) {
213 newItem.mobileX = 0;
214 }
215 }
216 return;
217 }
218
219 let foundPosition = false;
220 while (!foundPosition) {
221 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
222 if (!hasCollision(false)) {
223 foundPosition = true;
224 break;
225 }
226 }
227 if (!foundPosition) newItem.y += 1;
228 }
229
230 let foundMobilePosition = false;
231 while (!foundMobilePosition) {
232 for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX += 1) {
233 if (!hasCollision(true)) {
234 foundMobilePosition = true;
235 break;
236 }
237 }
238 if (!foundMobilePosition) newItem.mobileY! += 1;
239 }
240}
241
242/**
243 * Find a valid position for a new item in a single mode (desktop or mobile).
244 * This modifies the item's position properties in-place.
245 */
246export function findValidPosition(newItem: Item, items: Item[], mobile: boolean) {
247 const layout = toLayout(items, mobile);
248
249 if (mobile) {
250 let foundPosition = false;
251 newItem.mobileY = 0;
252 while (!foundPosition) {
253 for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX++) {
254 if (!getFirstCollision(layout, toLayoutItem(newItem, true))) {
255 foundPosition = true;
256 break;
257 }
258 }
259 if (!foundPosition) newItem.mobileY! += 1;
260 }
261 } else {
262 let foundPosition = false;
263 newItem.y = 0;
264 while (!foundPosition) {
265 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
266 if (!getFirstCollision(layout, toLayoutItem(newItem, false))) {
267 foundPosition = true;
268 break;
269 }
270 }
271 if (!foundPosition) newItem.y += 1;
272 }
273 }
274}