1import { serverReq, block, unblock, ICON_ARROW_HTML } from './helpers.js'; 2 3const STORAGE_KEY = 'photohuman_favorites'; 4const FAVORITES_ANIM_MS = 350; 5 6const readServerFavorites = () => { 7 const bootstrap = document.getElementById('photohuman-favorites-bootstrap'); 8 9 if (!bootstrap) { 10 return null; 11 } 12 13 try { 14 const parsed = JSON.parse(bootstrap.textContent); 15 16 return Array.isArray(parsed) ? parsed.map((id) => Number(id)).filter(Boolean) : []; 17 } catch (error) { 18 return null; 19 } 20}; 21 22const readLocalFavorites = () => { 23 try { 24 const raw = localStorage.getItem(STORAGE_KEY); 25 const parsed = raw ? JSON.parse(raw) : []; 26 27 return Array.isArray(parsed) ? parsed.map((id) => Number(id)).filter(Boolean) : []; 28 } catch (error) { 29 return []; 30 } 31}; 32 33const writeLocalFavorites = (favorites, { syncCookie = true } = {}) => { 34 localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites)); 35 36 // Do not write the cookie before AJAX: PHP toggle would see the ID and remove it for guests. 37 if (syncCookie) { 38 document.cookie = `${STORAGE_KEY}=${encodeURIComponent(JSON.stringify(favorites))}; path=/; max-age=31536000; SameSite=Lax`; 39 } 40}; 41 42const updateFavoritesBootstrap = (favorites) => { 43 const bootstrap = document.getElementById('photohuman-favorites-bootstrap'); 44 45 if (bootstrap && Array.isArray(favorites)) { 46 bootstrap.textContent = JSON.stringify(favorites); 47 } 48}; 49 50const updateFavoritesCount = (count) => { 51 const safeCount = Math.max(0, Number(count) || 0); 52 53 document.querySelectorAll('.photohuman-favorites-count').forEach((element) => { 54 element.textContent = String(safeCount); 55 }); 56 57 document.querySelectorAll('[data-favorites-page-count]').forEach((element) => { 58 element.textContent = `[${safeCount}]`; 59 }); 60}; 61 62const syncFavoriteCheckboxes = (favoriteIds) => { 63 const favorites = Array.isArray(favoriteIds) 64 ? favoriteIds.map((id) => Number(id)).filter(Boolean) 65 : readLocalFavorites(); 66 67 document.querySelectorAll('[data-change="favorite"]').forEach((input) => { 68 const productId = Number(input.dataset.productId); 69 input.checked = Boolean(productId && favorites.includes(productId)); 70 }); 71}; 72 73const isFavoritesPage = () => Boolean(document.querySelector('[data-favorites-page]')); 74 75const getFavoritesPageId = () => { 76 const page = document.querySelector('[data-favorites-page]'); 77 78 return page ? Number(page.dataset.favoritesPageId || 0) : 0; 79}; 80 81const getFavoritesContent = () => document.querySelector('.favorites-content'); 82 83const getFavoritesGrid = () => { 84 const content = getFavoritesContent(); 85 86 return content ? content.querySelector('.operation-body') : null; 87}; 88 89const getFavoriteCardInGrid = (productId) => { 90 const content = getFavoritesContent(); 91 92 if (!content || !productId) { 93 return null; 94 } 95 96 return content.querySelector(`.operation-card[data-product-id="${productId}"]`); 97}; 98 99const getFavoritesLoadParams = () => { 100 const page = document.querySelector('[data-favorites-page]'); 101 102 return page?.dataset.favoritesLoadParams || ''; 103}; 104 105const getFavoritesPerPage = () => { 106 const page = document.querySelector('[data-favorites-page]'); 107 108 return Math.max(1, Number(page?.dataset.favoritesPerPage || 10)); 109}; 110 111const getHiddenFavoriteIds = (favorites) => { 112 const shown = new Set(getShownFavoriteIds()); 113 114 return favorites.filter((id) => !shown.has(id)); 115}; 116 117const favoritesHasMore = (favorites) => getHiddenFavoriteIds(favorites).length > 0; 118 119const getShownFavoriteIds = () => { 120 const grid = getFavoritesGrid(); 121 122 if (!grid) { 123 return []; 124 } 125 126 return [...grid.querySelectorAll('.operation-card')] 127 .map((card) => Number(card.dataset.productId)) 128 .filter(Boolean); 129}; 130 131const decodeLoadParams = (encoded) => { 132 if (!encoded) { 133 return null; 134 } 135 136 try { 137 return JSON.parse(atob(encoded)); 138 } catch (error) { 139 return null; 140 } 141}; 142 143const buildLoadParamsEncoded = () => { 144 const params = decodeLoadParams(getFavoritesLoadParams()) || { 145 args: { context: 'favorites' }, 146 template: 'template-parts/products/card-product', 147 target: '#photohuman-favorites-grid', 148 }; 149 150 params.shown_ids = getShownFavoriteIds(); 151 delete params.paged; 152 153 return btoa(JSON.stringify(params)); 154}; 155 156const updateFavoritesLoadMore = (favorites) => { 157 const favoritesContent = getFavoritesContent(); 158 const grid = getFavoritesGrid(); 159 160 if (!favoritesContent || !grid) { 161 return; 162 } 163 164 const list = Array.isArray(favorites) ? favorites : readLocalFavorites(); 165 const hasMore = favoritesHasMore(list); 166 let footer = favoritesContent.querySelector('[data-favorites-load-more]'); 167 168 if (!hasMore) { 169 footer?.remove(); 170 return; 171 } 172 173 const params = buildLoadParamsEncoded(); 174 175 if (!params) { 176 return; 177 } 178 179 if (!footer) { 180 favoritesContent.insertAdjacentHTML( 181 'beforeend', 182 `<div class="catalog-more" data-favorites-load-more><button type="button" class="button _black" data-click="loadPosts" data-params="${params}">ÐÐÐÐÐÐТЬ ÐЩÐ${ICON_ARROW_HTML}</button></div>` 183 ); 184 return; 185 } 186 187 const button = footer.querySelector('[data-click="loadPosts"]'); 188 189 if (button) { 190 button.dataset.params = params; 191 } 192}; 193
194const updateFavoritesHeadIntro = (html) => { 195 const container = document.querySelector('[data-favorites-head-intro]'); 196 197 if (!container) { 198 return; 199 } 200 201 if (html && html.trim()) { 202 container.innerHTML = html; 203 container.hidden = false; 204 return; 205 } 206 207 container.innerHTML = ''; 208 container.hidden = true; 209}; 210 211const waitForTransition = (element, propertyName = 'opacity') => new Promise((resolve) => { 212 if (!element) { 213 resolve(); 214 return; 215 } 216 217 let settled = false; 218 219 const finish = () => { 220 if (settled) { 221 return; 222 } 223 224 settled = true; 225 element.removeEventListener('transitionend', onEnd); 226 resolve(); 227 }; 228 229 const onEnd = (event) => { 230 if (event.target !== element || event.propertyName !== propertyName) { 231 return; 232 } 233 234 finish(); 235 }; 236 237 element.addEventListener('transitionend', onEnd); 238 window.setTimeout(finish, FAVORITES_ANIM_MS + 50); 239}); 240 241const startRemoveFavoriteCard = (card) => { 242 if (!card) { 243 return Promise.resolve(null); 244 } 245 246 card.classList.add('_favorites-removing'); 247 248 return waitForTransition(card); 249}; 250 251const cancelRemoveFavoriteCard = (card) => { 252 if (!card) { 253 return; 254 } 255 256 card.classList.remove('_favorites-removing'); 257}; 258 259const animateFavoriteCard = (card) => { 260 if (!card) { 261 return; 262 } 263 264 card.classList.add('_favorites-adding'); 265 266 requestAnimationFrame(() => { 267 requestAnimationFrame(() => { 268 card.classList.add('_favorites-added'); 269 card.classList.remove('_favorites-adding'); 270 }); 271 }); 272}; 273 274const ensureFavoritesGrid = () => { 275 const favoritesContent = getFavoritesContent(); 276 277 if (!favoritesContent) { 278 return null; 279 } 280 281 let grid = getFavoritesGrid(); 282 283 if (!grid) { 284 favoritesContent.innerHTML = '<div class="operation-body" id="photohuman-favorites-grid"></div>'; 285 grid = getFavoritesGrid(); 286 } 287 288 return grid; 289}; 290 291const insertFavoriteCardsHtml = (cardsHtml, { prepend = false, animate = true } = {}) => { 292 if (!cardsHtml || !cardsHtml.trim()) { 293 return; 294 } 295 296 const grid = ensureFavoritesGrid(); 297 298 if (!grid) { 299 return; 300 } 301 302 const template = document.createElement('template'); 303 template.innerHTML = cardsHtml.trim(); 304 const cards = [...template.content.children]; 305 306 cards.forEach((card) => { 307 if (prepend) { 308 grid.prepend(card); 309 } else { 310 grid.appendChild(card); 311 } 312 313 if (animate) { 314 animateFavoriteCard(card); 315 } 316 }); 317}; 318 319const showFavoritesEmptyState = (emptyHtml) => { 320 const favoritesContent = getFavoritesContent(); 321 322 if (!favoritesContent || typeof emptyHtml !== 'string') { 323 return; 324 } 325 326 favoritesContent.innerHTML = emptyHtml; 327}; 328 329const applyFavoritesPageAdd = (res, favorites) => { 330 const syncedFavorites = Array.isArray(favorites) ? favorites : readLocalFavorites(); 331 const existingCard = getFavoriteCardInGrid(Number(res.product_id)); 332 333 if (!existingCard && res.card_html) { 334 insertFavoriteCardsHtml(res.card_html, { prepend: true }); 335 } 336 337 if (typeof res.head_intro_html === 'string') { 338 updateFavoritesHeadIntro(res.head_intro_html); 339 } 340 341 updateFavoritesLoadMore(syncedFavorites); 342 window.photohumanMatchOperationCardHeights?.(); 343}; 344 345const applyFavoritesPageRemove = async (res, gridCard, removeAnimation) => { 346 if (gridCard) { 347 await removeAnimation; 348 349 if (gridCard.parentNode) { 350 gridCard.remove(); 351 } 352 } 353 354 if (Number(res.count) === 0 && typeof res.empty_html === 'string') { 355 showFavoritesEmptyState(res.empty_html); 356 } else if ( 357 typeof res.refill_html === 'string' 358 && res.refill_html.trim() 359 && getShownFavoriteIds().length === 0 360 ) { 361 insertFavoriteCardsHtml(res.refill_html, { prepend: false }); 362 } 363 364 if (typeof res.head_intro_html === 'string') { 365 updateFavoritesHeadIntro(res.head_intro_html); 366 } 367 368 updateFavoritesLoadMore(readLocalFavorites()); 369 window.photohumanMatchOperationCardHeights?.(); 370}; 371 372const revertFavoriteToggle = (input, shouldAdd, favorites, gridCard = null) => { 373 input.checked = !shouldAdd; 374 writeLocalFavorites(favorites); 375 updateFavoritesCount(favorites.length); 376 syncFavoriteCheckboxes(favorites); 377 cancelRemoveFavoriteCard(gridCard); 378}; 379 380const readCookieFavorites = () => { 381 try { 382 const match = document.cookie.match(/(?:^|; )photohuman_favorites=([^;]*)/); 383 384 if (!match) { 385 return []; 386 } 387 388 const parsed = JSON.parse(decodeURIComponent(match[1])); 389 390 return Array.isArray(parsed) ? parsed.map((id) =>
390 Number(id)).filter(Boolean) : []; 391 } catch (error) { 392 return []; 393 } 394}; 395 396const readClientFavorites = () => { 397 const local = readLocalFavorites(); 398 399 if (local.length) { 400 return local; 401 } 402 403 return readCookieFavorites(); 404}; 405 406const isLoggedInUser = () => document.body.classList.contains('logged-in'); 407 408export const favorite = (input) => { 409 const productId = Number(input.dataset.productId); 410 411 if (!productId) { 412 return; 413 } 414 415 const favorites = readLocalFavorites(); 416 const shouldAdd = input.checked; 417 const onFavoritesPage = isFavoritesPage(); 418 const favoritesContent = onFavoritesPage ? getFavoritesContent() : null; 419 const gridCard = onFavoritesPage && !shouldAdd ? getFavoriteCardInGrid(productId) : null; 420 let nextFavorites = favorites; 421 let removeAnimation = Promise.resolve(); 422 423 if (shouldAdd && !favorites.includes(productId)) { 424 nextFavorites = [productId, ...favorites.filter((id) => id !== productId)]; 425 } 426 427 if (!shouldAdd) { 428 nextFavorites = favorites.filter((id) => id !== productId); 429 } 430 431 writeLocalFavorites(nextFavorites, { syncCookie: false }); 432 updateFavoritesCount(nextFavorites.length); 433 434 if (onFavoritesPage && gridCard) { 435 removeAnimation = startRemoveFavoriteCard(gridCard); 436 } 437 438 if (onFavoritesPage && favoritesContent) { 439 block(favoritesContent); 440 } 441 442 const body = new FormData(); 443 body.append('product_id', String(productId)); 444 body.append('active', shouldAdd ? '1' : '0'); 445 446 if (onFavoritesPage) { 447 body.append('favorites_page', '1'); 448 body.append('shown_ids', JSON.stringify(getShownFavoriteIds())); 449 450 const pageId = getFavoritesPageId(); 451 452 if (pageId) { 453 body.append('page_id', String(pageId)); 454 } 455 } 456 457 serverReq(body, 'toggleFavorite') 458 .then(async (res) => { 459 if (!res?.success) { 460 await removeAnimation; 461 revertFavoriteToggle(input, shouldAdd, favorites, gridCard); 462 return; 463 } 464 465 let syncedFavorites = nextFavorites; 466 467 if (Array.isArray(res.favorites)) { 468 syncedFavorites = res.favorites.map((id) => Number(id)).filter(Boolean); 469 470 writeLocalFavorites(syncedFavorites); 471 updateFavoritesBootstrap(syncedFavorites); 472 updateFavoritesCount(typeof res.count === 'number' ? res.count : syncedFavorites.length); 473 input.checked = Boolean(res.is_active); 474 syncFavoriteCheckboxes(syncedFavorites); 475 } 476 477 if (!onFavoritesPage) { 478 return; 479 } 480 481 if (res.is_active) { 482 applyFavoritesPageAdd(res, syncedFavorites); 483 return; 484 } 485 486 await applyFavoritesPageRemove(res, gridCard, removeAnimation); 487 }) 488 .catch(async () => { 489 await removeAnimation; 490 revertFavoriteToggle(input, shouldAdd, favorites, gridCard); 491 }) 492 .finally(() => { 493 if (favoritesContent) { 494 unblock(favoritesContent); 495 } 496 }); 497}; 498 499document.addEventListener('DOMContentLoaded', () => { 500 const serverFavorites = readServerFavorites(); 501 const clientFavorites = readClientFavorites(); 502 let favorites; 503 504 if (isLoggedInUser() && serverFavorites !== null) { 505 // Logged-in pages are not shared-cached: server meta is source of truth. 506 favorites = serverFavorites; 507 writeLocalFavorites(serverFavorites); 508 } else { 509 // Guests: page HTML/bootstrap may come from shared page cache (often []). 510 // Never overwrite real client favorites with a stale empty bootstrap. 511 favorites = clientFavorites.length 512 ? clientFavorites 513 : (serverFavorites ?? []); 514 writeLocalFavorites(favorites); 515 } 516 517 syncFavoriteCheckboxes(favorites); 518 updateFavoritesCount(favorites.length); 519});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.