PageSourceSearch

https://photohuman.space/wp-content/themes/photohuman/source/js/modules/favorite.js?v=1.6.622

js photohuman.space collected 2026-09-28 08:54:30 UTC 13,212 bytes, 519 lines download raw bytes

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.