your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

at fix/layout-stuff 274 lines 7.1 kB view raw
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}