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
65export function fixCollisions(
66 items: Item[],
67 item: Item,
68 mobile: boolean = false,
69 skipCompact: boolean = false,
70 originalPos?: { x: number; y: number }
71) {
72 if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW);
73 else item.x = clamp(item.x, 0, COLUMNS - item.w);
74
75 const targetX = mobile ? item.mobileX : item.x;
76 const targetY = mobile ? item.mobileY : item.y;
77
78 let layout = toLayout(items, mobile);
79
80 const movedLayoutItem = layout.find((i) => i.i === item.id);
81
82 if (!movedLayoutItem) {
83 console.error('item not found in layout! this should never happen!');
84 return;
85 }
86
87 // If we know the original position, set it on the layout item so
88 // moveElement can detect direction and push items properly.
89 if (originalPos) {
90 movedLayoutItem.x = originalPos.x;
91 movedLayoutItem.y = originalPos.y;
92 }
93
94 layout = moveElement(layout, movedLayoutItem, targetX, targetY, true, false, 'vertical', COLUMNS);
95
96 if (!skipCompact) layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
97
98 applyLayout(items, layout, mobile);
99}
100
101export function fixAllCollisions(items: Item[], mobile: boolean) {
102 let layout = toLayout(items, mobile);
103 correctBounds(layout as any, { cols: COLUMNS });
104 layout = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
105 applyLayout(items, layout, mobile);
106}
107
108export function compactItems(items: Item[], mobile: boolean) {
109 const layout = toLayout(items, mobile);
110 const compacted = verticalCompactor.compact(layout, COLUMNS) as LayoutItem[];
111 applyLayout(items, compacted, mobile);
112}
113
114export function setPositionOfNewItem(
115 newItem: Item,
116 items: Item[],
117 viewportCenter?: { gridY: number; isMobile: boolean }
118) {
119 const desktopLayout = toLayout(items, false);
120 const mobileLayout = toLayout(items, true);
121
122 function hasCollision(mobile: boolean): boolean {
123 const layout = mobile ? mobileLayout : desktopLayout;
124 return getFirstCollision(layout, toLayoutItem(newItem, mobile)) !== undefined;
125 }
126
127 if (viewportCenter) {
128 const { gridY, isMobile } = viewportCenter;
129
130 if (isMobile) {
131 // Place at viewport center Y
132 newItem.mobileY = Math.max(0, Math.round(gridY - newItem.mobileH / 2));
133 newItem.mobileY = Math.floor(newItem.mobileY / 2) * 2;
134
135 // Try to find a free X at this Y
136 let found = false;
137 for (
138 newItem.mobileX = 0;
139 newItem.mobileX <= COLUMNS - newItem.mobileW;
140 newItem.mobileX += 2
141 ) {
142 if (!hasCollision(true)) {
143 found = true;
144 break;
145 }
146 }
147 if (!found) {
148 newItem.mobileX = 0;
149 }
150
151 // Desktop: derive from mobile
152 newItem.y = Math.max(0, Math.round(newItem.mobileY / 2));
153 found = false;
154 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
155 if (!hasCollision(false)) {
156 found = true;
157 break;
158 }
159 }
160 if (!found) {
161 newItem.x = 0;
162 }
163 } else {
164 // Place at viewport center Y
165 newItem.y = Math.max(0, Math.round(gridY - newItem.h / 2));
166
167 // Try to find a free X at this Y
168 let found = false;
169 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) {
170 if (!hasCollision(false)) {
171 found = true;
172 break;
173 }
174 }
175 if (!found) {
176 newItem.x = 0;
177 }
178
179 // Mobile: derive from desktop
180 newItem.mobileY = Math.max(0, Math.round(newItem.y * 2));
181 found = false;
182 for (
183 newItem.mobileX = 0;
184 newItem.mobileX <= COLUMNS - newItem.mobileW;
185 newItem.mobileX += 2
186 ) {
187 if (!hasCollision(true)) {
188 found = true;
189 break;
190 }
191 }
192 if (!found) {
193 newItem.mobileX = 0;
194 }
195 }
196 return;
197 }
198
199 let foundPosition = false;
200 while (!foundPosition) {
201 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
202 if (!hasCollision(false)) {
203 foundPosition = true;
204 break;
205 }
206 }
207 if (!foundPosition) newItem.y += 1;
208 }
209
210 let foundMobilePosition = false;
211 while (!foundMobilePosition) {
212 for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX += 1) {
213 if (!hasCollision(true)) {
214 foundMobilePosition = true;
215 break;
216 }
217 }
218 if (!foundMobilePosition) newItem.mobileY! += 1;
219 }
220}
221
222/**
223 * Find a valid position for a new item in a single mode (desktop or mobile).
224 * This modifies the item's position properties in-place.
225 */
226export function findValidPosition(newItem: Item, items: Item[], mobile: boolean) {
227 const layout = toLayout(items, mobile);
228
229 if (mobile) {
230 let foundPosition = false;
231 newItem.mobileY = 0;
232 while (!foundPosition) {
233 for (newItem.mobileX = 0; newItem.mobileX <= COLUMNS - newItem.mobileW; newItem.mobileX++) {
234 if (!getFirstCollision(layout, toLayoutItem(newItem, true))) {
235 foundPosition = true;
236 break;
237 }
238 }
239 if (!foundPosition) newItem.mobileY! += 1;
240 }
241 } else {
242 let foundPosition = false;
243 newItem.y = 0;
244 while (!foundPosition) {
245 for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) {
246 if (!getFirstCollision(layout, toLayoutItem(newItem, false))) {
247 foundPosition = true;
248 break;
249 }
250 }
251 if (!foundPosition) newItem.y += 1;
252 }
253 }
254}