PageSourceSearch

https://xn--80apeb5b.xn--p1ai/js/addons/my_changes/rf_footer.js?v=20260925-3

js xn--80apeb5b.xn--p1ai collected 2026-10-09 21:28:17 UTC 32,599 bytes, 621 lines download raw bytes

1/* rf_footer.js — вынесено из footer.post.tpl 22.09.2026 (П4 разбора
2   docs/brand_page_load_analysis_20260922.md): два блока приезжали инлайном
3   в каждом HTML (~28 КБ) и не кэшировались. Токен window.rfTk остаётся в HTML.
4   Версия в ?v= своя и не зависит от cache_id, файл переживает пересборку бандлов.
5   1) Недавно просмотренные (localStorage + ES)  2) Избранное без перезагрузки */
6/* Hybrid "Недавно просмотренные": instant localStorage render + background ES refresh.
7   Runs on all product pages (localStorage + ES).
8   Скрыт в личном кабинете (profiles/orders/auth/...): rfRvOff ставится Smarty выше;
9   на странице «Отложенные товары» (wishlist.view) блок остаётся. */
10(function () {
11  if (window.rfRvOff) return;
12
13  var API = '/index.php?dispatch=rf_recently_viewed';
14  var LS_UID = 'rf_uid', LS_ITEMS = 'rf_rv_items', MAX = 20;
15
16  function is404Page() {
17    var code = document.querySelector('.ty-exception__code');
18    if (code && /\b404\b/.test(code.textContent || '')) return true;
19    var ogTitle = document.querySelector('meta[property="og:title"], meta[name="twitter:title"]');
20    var titleText = ((ogTitle && ogTitle.getAttribute('content')) || '') + ' ' + (document.title || '');
21    return !!(document.querySelector('.ty-exception') && /Страница не найдена|Page not found|404/i.test(titleText));
22  }
23  if (is404Page()) return;
24
25  // Suppress the slow legacy block_manager.render AJAX (~880ms) for the
26  // recently-viewed widget on test URLs — the hybrid replaces it.
27  try {
28    var $$ = (window.Tygh && Tygh.$) || window.jQuery || window.$;
29    if ($$ && $$(document).ajaxSend) {
30      $$(document).ajaxSend(function (e, xhr, settings) {
31        if (settings && settings.url && settings.url.indexOf('block_manager.render') > -1) {
32          try { xhr.abort(); } catch (er) {}
33        }
34      });
35    }
36  } catch (er) {}
37
38  function priceText(sel) {
39    var e = document.querySelector(sel);
40    if (!e) return '';
41    var c = e.cloneNode(true);
42    [].forEach.call(c.querySelectorAll('sup, .ty-price__decimals, [class*="decimal"], [class*="cents"], .ty-price-num__sup'), function (x) { x.remove(); });
43    return (c.textContent || '').replace(/\s+/g, ' ').trim();
44  }
45
46  function uid() {
47    var u = null;
48    try { u = localStorage.getItem(LS_UID); } catch (e) {}
49    if (!u) {
50      u = 'rv_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
51      try { localStorage.setItem(LS_UID, u); } catch (e) {}
52    }
53    return u;
54  }
55  function getItems() { try { return JSON.parse(localStorage.getItem(LS_ITEMS) || '[]'); } catch (e) { return []; } }
56  function compactItems(a) {
57    var out = [];
58    (a || []).forEach(function (it) {
59      if (!it) return;
60      var duplicate = out.some(function (x) { return sameItem(x, it); });
61      if (!duplicate) out.push(it);
62    });
63    return out;
64  }
65  function setItems(a) { try { localStorage.setItem(LS_ITEMS, JSON.stringify(compactItems(a).slice(0, MAX))); } catch (e) {} }
66  function txt(sel, root) { var e = (root || document).querySelector(sel); return e ? (e.textContent || '').replace(/\s+/g, ' ').trim() : ''; }
67  function cleanPath(u) {
68    try { return new URL(u || location.pathname, location.origin).pathname.replace(/\/+$/, '/') || '/'; }
69    catch (e) { return String(u || '').split('?')[0].replace(/\/+$/, '/') || '/'; }
70  }
71  function readImg(el) {
72    if (!el) return '';
73    var vals = [el.currentSrc, el.getAttribute('src'), el.getAttribute('data-src'), el.getAttribute('data-lazy-src')];
74    for (var i = 0; i < vals.length; i++) {
75      var u = vals[i] || '';
76      if (u && !/^(data:|about:)/.test(u) && !/spacer/.test(u)) return u;
77    }
78    return '';
79  }
80  function okThumb(u) { return u && /\/thumbnails\/\d+\/\d+\//.test(u) && !/spacer|data:/.test(u); }
81  function rfRvStripWebp(u){ return (u||'').replace('/images/ab__webp/thumbnails/','/images/thumbnails/').replace(/_(jpe?g|png)\.webp(\?.*)?$/i,function(_,e,q){return '.'+e.toLowerCase()+(q||'');}); }
82  function normalizeThumb340(u) { return rfRvStripWebp(u).replace(/\/thumbn
82ails\/\d+\/\d+\//,'/thumbnails/340/340/'); }
83  function collectThumbs(sel, limit) {
84    var out = [];
85    [].forEach.call(document.querySelectorAll(sel), function (g) {
86      var raw = readImg(g);
87      if (okThumb(raw) && out.indexOf(raw) < 0) out.push(raw);
88    });
89    return out.slice(0, limit || 6);
90  }
91  function metaImage() {
92    var m = document.querySelector('meta[property="og:image"], meta[name="twitter:image"]');
93    var u = m ? (m.getAttribute('content') || '') : '';
94    return okThumb(u) ? normalizeThumb340(u) : '';
95  }
96  function categoryThumbMap() {
97    var map = {};
98    [].forEach.call(document.querySelectorAll('.ty-subcategories__item a, .subcategories a'), function (a) {
99      var img = a.querySelector('img');
100      var u = readImg(img);
101      if (okThumb(u)) map[cleanPath(a.getAttribute('href') || '')] = normalizeThumb340(u);
102    });
103    return map;
104  }
105  function categoryImageForPath(path) {
106    var own = metaImage();
107    if (own) return own;
108    var hit = categoryThumbMap()[cleanPath(path)];
109    if (hit) return hit;
110    return '';
111  }
112
113  function isProductDetailPage() {
114    return !!document.querySelector('.ut2-pb__img-wrapper, .ty-product-img');
115  }
116  function productIdFromReturnUrl(v) {
117    var m = String(v || '').match(/[?&]product_id=(\d+)/);
118    return m ? m[1] : '';
119  }
120  function currentProductId() {
121    if (!isProductDetailPage()) return '';
122    var f = document.querySelector('form[name^="product_form_"]');
123    if (f) { var m2 = f.getAttribute('name').match(/product_form_(\d+)/); if (m2) return m2[1]; }
124    var ret = document.querySelector('input[name="redirect_url"][value*="dispatch=products.view"], input[name="return_url"][value*="dispatch=products.view"]');
125    return productIdFromReturnUrl(ret ? ret.getAttribute('value') : '');
126  }
127  function currentCategoryId() {
128    var el = document.querySelector('#pagination_contents[data-rf-fast-category-id]') || document.querySelector('[data-rf-fast-category-id]');
129    var id = el ? (el.getAttribute('data-rf-fast-category-id') || '') : '';
130    return /^\d+$/.test(id) ? id : '';
131  }
132  function currentItem() {
133    var pid = currentProductId();
134    if (pid) return { item_type: 'product', item_id: pid, product_id: pid, category_id: '' };
135    var cid = currentCategoryId();
136    if (cid) return { item_type: 'category', item_id: cid, product_id: '', category_id: cid };
137    return null;
138  }
139  function itemType(it) {
140    if (!it) return '';
141    var t = String(it.item_type || it.type || '').toLowerCase();
142    if (t === 'product' || t === 'category') return t;
143    if (it.category_id && !it.product_id) return 'category';
144    if (it.product_id) return 'product';
145    return '';
146  }
147  function itemId(it) {
148    if (!it) return '';
149    var t = itemType(it);
150    if (t === 'category') return String(it.item_id || it.category_id || '');
151    if (t === 'product') return String(it.item_id || it.product_id || '');
152    return String(it.item_id || it.product_id || it.category_id || '');
153  }
154  function itemKey(it) {
155    var id = itemId(it);
156    return id ? itemType(it) + ':' + id : '';
157  }
158  function sameItem(a, b) {
159    var ak = itemKey(a), bk = itemKey(b);
160    if (ak && bk && ak === bk) return true;
161    if (a && b && a.url && b.url && cleanPath(a.url) === cleanPath(b.url)) return true;
162    return false;
163  }
164  function normalizeItemCard(it, map) {
165    if (!it) return it;
166    if (itemType(it) !== 'category') return it;
167    var one = it.img || (it.imgs && it.imgs.length ? it.imgs[0] : '') || '';
168    if (!one && it.url && map) one = map[cleanPath(it.url)] || '';
169    one = one ? normalizeThumb340(one) : '';
170    it.item_type = it.type = 'category';
171    it.product_id = it.product_id || 0;
172    it.category_id = it.category_id || it.item_id || 0;
173    it.img = one;
174    it.imgs = one ? [one] : [];
175    it.price = '';
176    it.old_price = '';
177    it.sold = '';
178    return it;
179  }
180  function repairStoredImages(items) {
181    var map = categoryThumbMap(), changed = false;
182    items.forEach(function (it) {
183      if (!it) return;
184      var beforeType = it.item_type || it.type || '';
185      var beforeImg = it.img || '';
186      var beforeImgs = it.imgs ? it.imgs.join('|') : '';
187      var beforePrice = (it.price || '') + '|' + (it.old_price || '') + '|' + (it.sold || '');
188      if (!itemType(it) && !it.product_id && it.url && map[cleanPath(it.url)]) it.item_type = it.type = 'category';
189      normalizeItemCard(it, map);
190      var afterImgs = it.imgs ? it.imgs.join('|') : '';
191      if (beforeType !== (it.item_type || it.type || '') || beforeImg !== (it.img || '') || beforeImgs !== afterImgs || beforePrice !== ((it.price || '') + '|' + (it.old_price || '') + '|' + (it.sold || ''))) changed = true;
192    });
193    if (changed) setItems(items);
194    return items;
195  }
196
197  function hasRvImage(it) {
198    return !!(it && (it.img || (it.imgs && it.imgs.length)));
199  }
200  function isServiceRvUrl(u) {
201    var p = cleanPath(u || '');
202    return /^\/(?:login|cart|checkout|profiles|orders|auth|wishlist|compare|index\.php)(?:\/|$)/i.test(p);
203  }
204  function isRenderableRvItem(it) {
205    var t = itemType(it), id = itemId(it);
206    if (!t || !id || !it || !it.url || !it.name || !hasRvImage(it) || isServiceRvUrl(it.url)) return false;
207    if (t === 'product' && !(it.product_id || it.item_id)) return false;
208    if (t === 'category' && !(it.category_id || it.item_id)) return false;
209    return true;
210  }
211
212  function scrapeCurrent() {
213    var item = currentItem();
214    if (!item || !item.item_id) return null;
215    var isCategory = item.item_type === 'category';
216    var imgs = isCategory
217      ? [categoryImageForPath(location.pathname)].filter(Boolean)
218      : collectThumbs('.ut2-pb__img-wrapper img, .ty-product-img img', 6);
219    if (!imgs.length) imgs = [metaImage()].filter(Boolean);
220    imgs = imgs.map(function (u) { return isCategory ? normalizeThumb340(u) : u; }).filter(function (u, i, a) { return u && a.indexOf(u) === i; }).slice(0, isCategory ? 1 : 6);
221    var imgUrl = imgs[0] || '';
222    var soldM = (document.body.textContent || '').match(/Продано\s+[^\s,<]+(\s*(тыс|млн|тысяч)?\.?)?/);
223    return normalizeItemCard({
224      item_type: item.item_type,
225      type: item.item_type,
226      item_id: item.item_id,
227      product_id: item.product_id || 0,
228      category_id: item.category_id || 0,
229      url: location.pathname,
230      name: txt('h1'),
231      price: isCategory ? '' : priceText('.ut2-pb__price-actual .ty-price-num, .ut2-pb__price-wrap .ty-price-num, .ty-price-num'),
232      old_price: isCategory ? '' : priceText('.ut2-pb__price-old .ty-price-num, del .ty-price-num'),
233      img: imgUrl,
234      imgs: imgs,
235      sold: isCategory ? '' : (soldM ? soldM[0].trim() : ''),
236      ts: Date.now()
237    });
238  }
239
240  function sid() {
241    var s = null, now = Date.now();
242    try { var raw = localStorage.getItem('rf_sid'); if (raw) { var o = JSON.parse(raw); if (o.t && now - o.t < 1800000) s = o.id; } } catch (e) {}
243    if (!s) s = 's_' + now.toString(36) + Math.random().toString(36).slice(2, 8);
244    try { localStorage.setItem('rf_sid', JSON.stringify({ id: s, t: now })); } catch (e) {}
245    return s;
246  }
247  function capture() {
248    var pd = scrapeCurrent();
249    if (!pd || !pd.item_id) return;
250    var items = repairStoredImages(getItems()).filter(function (x) { return !sameItem(x, pd); });
251    var referrer_id = items.length ? (itemId(items[0]) || '') : '';
252    var referrer_type = items.length ? itemType(items[0]) : '';
253    var session_id = sid();
254    items.unshift(pd);
255    setItems(items);
256    var body = 'uid=' + encodeURIComponent(uid()) + '&item_type=' + encodeURIComponent(pd.item_type) + '&item_id=' + encodeURIComponent(pd.item_id) + '&product_id=' + encodeURIComponent(pd.product_id || '') + '&category_id=' + encodeURIComponent(pd.category_id || '') + '&session_id=' + encodeURIComponent(session_id) + '&referrer_id=' + encodeURIComponent(referrer_id) + '&referrer_type=' + encodeURIComponent(referrer_type) + '&card=' + encodeURIComponent(JSON.stringify(pd)) + '&tk=' + encodeURIComponent(window.rfTk || '');
257    try {
258      if (navigator.sendBeacon) {
259        navigator.sendBeacon(API + '.track', new Blob([body], { type: 'application/x-www-form-urlencoded' }));
260      } else {
261        fetch(API + '.track', { method: 'POST', body: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, keepalive: true });
262      }
263    } catch (e) {}
264  }
265
266  function esc(s) { return String(s || '').replace(/"/g, '&quot;').replace(/</g, '&lt;'); }
267  // rf_rv-карточки могут хранить webp-URL или старый размер миниатюры. Приводим
268  // к стабильному 340 JPG URL; nginx сам отдаст WebP по Accept, если файл есть.
269  function card340(u){ return normalizeThumb340(u); }
270  function card480(u){ return card340(u); }
271  // If a rare 340 thumbnail is missing, fall back to the original stored JPG URL.
272  function attachFallback(root) {
273    [].forEach.call(root.querySelectorAll('img[data-rvfull]'), function (im) {
274      if (im._rvfb) return; im._rvfb = 1;
275      im.addEventListener('error', function () {
276        var f = im.getAttribute('data-rvfull');
277        if (f && im.src.indexOf(f) < 0) { im.removeAttribute('data-rvfull'); im.src = f; }
278      });
279    });
280  }
281  function cardHtml(it) {
282    if (!it || !it.name) return '';
283    var isCategory = itemType(it) === 'category';
284    if (isCategory) normalizeItemCard(it);
285    var imgs = (it.imgs && it.imgs.length) ? it.imgs : (it.img ? [it.img] : []);
286    if (isCategory) imgs = imgs.slice(0, 1);
287    var mainFull = imgs[0] || it.img || '';
288    var main = card480(mainFull);
289    // hover-gallery: invisible hover strips + indicator dots (theme's delegated JS binds by class)
290    var gallery = '';
291    if (!isCategory && imgs.length > 1) {
292      var strips = '<div class="item"></div>';
293      var dots = '<span class="active"></span>';
294      for (var i = 1; i < imgs.length; i++) {
295        strips += '<div class="item" data-ca-product-additional-image-src="' + esc(imgs[i]) + '"></div>';
296        dots += '<span></span>';
297      }
298      gallery = '<div class="cm-ab-hover-gallery abt__ut2_hover_gallery lines">' + strips +
299        '<div class="abt__ut2_hover_gallery_indicators">' + dots + '</div></div>';
300    }
301    // ⛔ Одна цена «от N ₽» (решение владельца 25.09.2026) — так же, как в общем сборщике
302    // cards_render.js (layout recently_viewed); старую цену в этом блоке не выводим.
303    var sold = (!isCategory && it.sold) ? '<span class="rf-rv__sold">' + esc(it.sold) + '</span>' : '';
304    var image = main ? '<img class="ty-pict img-ab-hover-gallery" src="' + esc(main) + '" data-rvfull="' + esc(rfRvStripWebp(mainFull)) + '" alt="' + esc(it.name) + '">' : '<span class="rf-rv__image-placeholder"></span>';
305    // ⛔ Блок цены — всегда (кроме категорий): нет цены ни у одного варианта → «нет цены»,
306    // «Продано» при этом остаётся. Цена в снимке истории — строка, поэтому приводим к числу.
307    var pnum = parseFloat(String(it.price == null ? '' : it.price).replace(/[^\d.,-]/g, '').replace(',', '.')) || 0;
308    var price = isCategory ? '' : '<div class="ut2-gl__price-wrap">' + (pnum > 0
309      ? '<span class="ut2-gl__price"><span class="ty-price"><span class="rf-rv__from">от</span>&nbsp;<span class="ty-price-num">' + esc(it.price) + '</span>&nbsp;₽</span></span>'
310      : '<span class="rf-rv__noprice">нет цены</span>') + sold + '</div>';
311    var itemClass = isCategory ? 'category' : 'product';
312    return '' +
313      '<div class="ut2-gl__item rf-rv__item rf-rv__item--' + itemClass + '">' +
314        '<div class="ut2-gl__body">' +
315          '<div class="ut2-gl__image">' +
316            '<a href="' + esc(it.url) + '">' +
317              image +
318              gallery +
319            '</a>' +
320          '</div>' +
321          '<div class="ut2-gl__content">' +
322            '<div class="ut2-gl__name"><a class="product-title" href="' + esc(it.url) + '">' + esc(it.name) + '</a></div>' +
323            price +
324          '</div>' +
325        '</div>' +
326      '</div>';
327  }
328
329  // Свежие карточки товаров с сервера (общий ProductCard) и контекст компактного профиля.
330  // ⛔ История (localStorage и ES) даёт только список и порядок: лежащие в ней поля карточки —
331  // снимок на момент просмотра и устаревают. Пока сервер их не прислал, работает прежний путь.
332  var rvCards = null, rvCtx = null;
333
334  function rvUseCommon() {
335    return !!(rvCards && rvCards.length && window.rfCards && typeof window.rfCards.render === 'function');
336  }
337
338  function renderItems(mount, items) {
339    var cats = [], products = [];
340    items.forEach(function (x) {
341      if (itemType(x) === 'category') cats.push(normalizeItemCard(x));
342      else if (itemType(x) === 'product') products.push(x);
343    });
344    cats = cats.filter(isRenderableRvItem).slice(0, 12);
345    products = products.filter(isRenderableRvItem).slice(0, 12);
346    var catGrid = mount.querySelector('.rf-rv__grid--categories');
347    var productGrid = mount.querySelector('.rf-rv__grid--products');
348    var catSection = mount.querySelector('.rf-rv__section--categories');
349    var productSection = mount.querySelector('.rf-rv__section--products');
350    if (catGrid) {
351      catGrid.innerHTML = cats.map(cardHtml).join('');
352      if (catSection) catSection.style.display = cats.length ? '' : 'none';
353      attachFallback(catGrid);
354    }
355    if (productGrid) {
356      if (rvUseCommon()) {
357        // общий сборщик витрины, компактный профиль: своей вёрстки у блока больше нет
358        productGrid.innerHTML = window.rfCards.render(rvCards, rvCtx || {}, {});
359        if (productSection) productSection.style.display = rvCards.length ? '' : 'none';
360        products = rvCards;
361      } else {
362        productGrid.innerHTML = products.map(cardHtml).join('');
363        if (productSection) productSection.style.display = products.length ? '' : 'none';
364        attachFallback(productGrid);
365      }
366    }
367    mount.style.display = (cats.length || products.length) ? '' : 'none';
368  }
369
370  function findContainer() {
371    // old grid_tab widget wrapper (contains the "Недавно просмотренные" tab)
372    var tab = document.querySelector('[id^="abt__ut2_grid_tab_"]');
373    if (tab) {
374      var box = tab.closest('.ty-tabs, [class*="tabs"], [class*="block"]') || tab.parentElement;
375      var h = box ? box.parentElement || box : null;
376      if (h) { h.setAttribute('data-rv-legacy', '1'); return h; }
377    }
378    var main = document.querySelector('#product_detail_page');
379    if (main) {
380      var mainGrid = main.closest('.main-content-grid') || main.parentElement;
381      if (mainGrid) { mainGrid.setAttribute('data-rv-append', '1'); return mainGrid; }
382    }
383    // legacy block disabled -> anchor before footer (do not hide it)
384    return document.querySelector('.ty-footer-grid, .ty-footer, #tygh_footer') || null;
385  }
386
387  function render() {
388    var host = findContainer();
389    if (!host) return;
390    var current = currentItem();
391    var items = repairStoredImages(getItems()).filter(function (x) { return !sameItem(x, current); });
392    if (!items.length) return; // nothing yet — leave old block as-is
393    // build hybrid block, hide old widget
394    var mount = document.getElementById('rf_rv_hybrid');
395    if (!mount) {
396      mount = document.createElement('div');
397      mount.id = 'rf_rv_hybrid';
398      mount.className = 'rf-rv';
399      if (host.getAttribute && host.getAttribute('data-rv-append')) host.appendChild(mount);
400      else host.parentNode.insertBefore(mount, host);
401    }
402    if (!mount.querySelector('.rf-rv__grid--products')) {
403      mount.innerHTML = '<section class="rf-rv__section rf-rv__section--categories"><h2 class="rf-rv__title ty-block__title">Просмотренные категории</h2><div class="grid-list ut2-gl rf-rv__grid rf-rv__grid--categories"></div></section><section class="rf-rv__section rf-rv__section--products"><h2 class="rf-rv__title ty-block__title">Просмотренные товары</h2><div class="grid-list ut2-gl rf-rv__grid rf-rv__grid--products"></div></section>';
404    }
405    if (host.getAttribute && host.getAttribute('data-rv-legacy')) host.style.display = 'none';
406    renderItems(mount, items);
407    // background refresh from ES (fresh price/img)
408    var t0 = performance.now();
409    var rvWant = '';
410    if (window.rfCards && typeof window.rfCards.render === 'function') {
411      // свой локальный список отдаём как порядок и состав; карточки придут свежими с сервера
412      var rvIds = [];
413      try {
414        getItems().forEach(function (x) {
415          var pid = parseInt(x.product_id || x.item_id || 0, 10);
416          if (itemType(x) === 'product' && pid && rvIds.indexOf(pid) === -1) { rvIds.push(pid); }
417        });
418      } catch (e) {}
419      rvWant = '&rf_cards=1' + (rvIds.length ? '&ids=' + encodeURIComponent(rvIds.slice(0, 24).join(',')) : '');
420    }
421    fetch(API + '.list' + rvWant + '&uid=' + encodeURIComponent(uid()) + '&limit=12&exclude_type=' + encodeURIComponent(current ? current.item_type : '') + '&exclude_item_id=' + encodeURIComponent(current ? current.item_id : ''), { credentials: 'same-origin' })
422      .then(function (r) { return r.json(); })
423      .then(function (res) {
424        if (window.console) console.log('[rf_rv] ES refresh ' + Math.round(performance.now() - t0) + 'ms, items=' + (res.items ? res.items.length : 0));
425        if (res && res.cards && res.cards.length && res.render_context) {
426          rvCards = res.cards; rvCtx = res.render_context;
427        }
428        if (res && res.ok && res.items && res.items.length) {
429          var fresh = res.items.map(function (h) { var c = h.card || {}; c.item_type = c.item_type || h.item_type || ''; c.type = c.type || c.item_type; c.item_id = c.item_id || h.item_id || h.product_id || h.category_id; c.product_id = c.product_id || h.product_id || 0; c.category_id = c.category_id || h.category_id || 0; return normalizeItemCard(c); }).filter(isRenderableRvItem);
430          if (fresh.length) {
431            // MERGE (do not clobber): refresh LS items with ES data, keep LS order, append ES-only
432            var esById = {}; fresh.forEach(function (c) { esById[itemKey(c)] = c; });
433            var ls = repairStoredImages(getItems());
434            var merged = ls.map(function (x) {
435              var e = esById[itemKey(x)] || fresh.filter(function (c) { return sameItem(c, x); })[0];
436              if (!e) return x;
437              if ((!e.imgs || !e.imgs.length) && x.imgs && x.imgs.length) e.imgs = x.imgs;
438              if (!e.img && x.img) e.img = x.img;
439              if (!e.item_type && x.item_type) e.item_type = e.type = x.item_type;
440              if (!e.item_id && x.item_id) e.item_id = x.item_id;
441              if (!e.category_id && x.category_id) e.category_id = x.category_id;
442              return e;
443            });
444            fresh.forEach(function (c) {
445              var seen = merged.some(function (x) { return sameItem(x, c); });
446              if (!seen) merged.push(c);
447            });
448            merged = repairStoredImages(merged);
449            setItems(merged);
450            var vis = merged.filter(function (x) { return !sameItem(x, current); });
451            renderItems(mount, vis);
452          }
453        }
454      }).catch(function () {});
455  }
456
457  function run() { try { capture(); render(); } catch (e) { if (window.console) console.warn('[rf_rv]', e); } }
458  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run();
459
460  // dwell: active foreground time on the product/category page, sent once on exit
461  (function () {
462    var cur = currentItem(); if (!cur || !cur.item_id) return;
463    var start = Date.now(), acc = 0, active = true, sent = false;
464    function flush() { if (active) { acc += Date.now() - start; start = Date.now(); } }
465    function sendDwell() {
466      flush(); if (sent || acc < 500) return; sent = true;
467      var body = 'uid=' + encodeURIComponent(uid()) + '&item_type=' + encodeURIComponent(cur.item_type) + '&item_id=' + encodeURIComponent(cur.item_id) + '&product_id=' + encodeURIComponent(cur.product_id || '') + '&category_id=' + encodeURIComponent(cur.category_id || '') + '&tk=' + encodeURIComponent(window.rfTk || '') + '&dwell_ms=' + acc;
468      try { navigator.sendBeacon(API + '.track', new Blob([body], { type: 'application/x-www-form-urlencoded' })); } catch (e) {}
469    }
470    document.addEventListener('visibilitychange', function () {
471      if (document.hidden) { flush(); active = false; sendDwell(); } else { start = Date.now(); active = true; }
472    });
473    window.addEventListener('pagehide', sendDwell);
474  })();
475})();
476
477(function () {
478  if (window.__kitWishAjax) { return; }
479  window.__kitWishAjax = 1;
480
481  var enc = encodeURIComponent;
482
483  function getHash() {
484    var el = document.querySelector('input[name="security_hash"]');
485    if (el && el.value) { return Promise.resolve(el.value); }
486    return fetch('/index.php?dispatch=my_changes_csrf.refresh', { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
487      .then(function (r) { return r.json(); })
488      .then(function (j) { return j.security_hash || ''; })
489      .catch(function () { return ''; });
490  }
491
492  /* Счётчик в шапке: сервер не рендерит .count при нуле — создаём/удаляем сами. */
493  function bump(d) {
494    [].forEach.call(document.querySelectorAll('[id="abt__ut2_wishlist_count"]'), function (box) {
495      var wc = box.querySelector('.count'), link = box.querySelector('a');
496      var n = Math.max(0, (wc ? parseInt(wc.textContent, 10) || 0 : 0) + d);
497      var sticky = !!(link && (link.className || '').indexOf('sticky-panel') > -1);
498      if (n > 0) {
499        if (!wc) {
500          var icon = box.querySelector('i');
501          wc = document.createElement(sticky ? 'em' : 'span');
502          wc.className = 'count';
503          if (sticky && icon) { icon.appendChild(wc); }
504          else if (icon && icon.parentNode) { icon.parentNode.insertBefore(wc, icon.nextSibling); }
505          else if (link) { link.appendChild(wc); }
506          else { return; }
507        }
508        wc.textContent = String(n);
509      } else if (wc) {
510        wc.parentNode.removeChild(wc);
511      }
512    });
513  }
514
515  function toast(msg, kind) { if (window.kitToastCo) { window.kitToastCo(msg, kind); } }
516
517  /* Сердечки витрины: каталог/лента — `a.ut2-add-to-wish` с i
517d `button_wishlist_<id>`,
518     карточка товара — `button.rf-fav` (id у неё нет, товар берём из dispatch-имени).
519     Обе ведут себя одинаково: клик переключает избранное, класс `active` — залитое сердце. */
520  var WISH_SEL = '.ut2-add-to-wish, .rf-fav';
521
522  function pidOf(el) {
523    var m = String(el.id || '').match(/button_wishlist_(\d+)/);
524    if (m) { return m[1]; }
525    m = String(el.getAttribute('data-ca-dispatch') || el.getAttribute('name') || '').match(/wishlist\.add\.\.(\d+)/);
526    return m ? m[1] : '';
527  }
528
529  /* ===== подсветка уже добавленных (23.08.2026) =====
530     Карточки ленты рисует HomeFeed, её HTML кэшируется общим кэшем на всех — отметку
531     «в избранном» сервер туда положить не может. Забираем список id и красим сами;
532     первую страницу листинга сервер и так отдаёт с классом active, ей это не мешает. */
533  var wishIds = null;
534
535  function paint(root) {
536    if (!wishIds) { return; }
537    [].forEach.call((root || document).querySelectorAll(WISH_SEL), function (el) {
538      var pid = pidOf(el);
539      /* wish_ids — полный сессионный список, поэтому красим В ОБЕ стороны:
540         карточка товара приходит из общего кэша блока и своего состояния не знает */
541      if (pid) { el.classList.toggle('active', !!wishIds[pid]); }
542    });
543  }
544
545  function loadWishIds() {
546    return fetch('/index.php?dispatch=my_changes.wish_ids', { credentials: 'same-origin', cache: 'no-store', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
547      .then(function (r) { return r.json(); })
548      .then(function (j) {
549        wishIds = {};
550        (j && j.ids || []).forEach(function (id) { wishIds[String(id)] = 1; });
551        paint();
552      })
553      .catch(function () {});
554  }
555
556  loadWishIds();
557
558  /* карточки, догруженные лентой/пагинацией, красим по мере появления */
559  new MutationObserver(function (muts) {
560    for (var i = 0; i < muts.length; i++) {
561      var added = muts[i].addedNodes;
562      for (var j = 0; j < added.length; j++) {
563        var n = added[j];
564        if (n.nodeType === 1 && n.querySelector && n.querySelector(WISH_SEL)) { paint(n); }
565      }
566    }
567  }).observe(document.body, { childList: true, subtree: true });
568
569  document.addEventListener('click', function (e) {
570    var b = e.target && e.target.closest ? e.target.closest(WISH_SEL) : null;
571    if (!b) { return; }
572    var pid = pidOf(b);
573    if (!pid) { return; }                       // не наша кнопка — отдаём штатному обработчику
574
575    e.preventDefault();
576    e.stopPropagation();
577
578    if (b.getAttribute('data-kit-busy')) { return; }
579    b.setAttribute('data-kit-busy', '1');
580    var free = function () { b.removeAttribute('data-kit-busy'); };
581
582    var wasOn = b.classList.contains('active');
583    // оптимистично: сердечко и счётчик реагируют сразу, сеть догоняет
584    b.classList.toggle('active', !wasOn);
585    bump(wasOn ? -1 : 1);
586    toast(wasOn ? 'Товар удалён из избранного' : 'Товар добавлен в избранное', wasOn ? 'remove' : '');
587
588    getHash().then(function (h) {
589      var body;
590      if (wasOn) {
591        /* удаление — штатным dispatch по product_id с force: ключ списка (cart_id)
592           клиенту неизвестен, а force-вариант принимает именно product_id */
593        body = 'security_hash=' + enc(h) + '&redirect_url=%2F&' + enc('dispatch[wishlist.delete.force.' + pid + ']') + '=1';
594        return fetch('/index.php', {
595          method: 'POST', credentials: 'same-origin',
596          headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' },
597          body: body
598        });
599      }
600      body = 'security_hash=' + enc(h) + '&product_data[' + pid + '][product_id]=' + pid + '&product_data[' + pid + '][amount]=1';
601      return fetch('/index.php?dispatch=wishlist.add', {
602        method: 'POST', credentials: 'same-origin',
603        headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' },
604        body: body
605      });
606    }).then(function () {
607      // все кнопки этого товара на странице (карточка может встречаться дважды)
608      [].forEach.call(document.querySelectorAll(WISH_SEL), function (el) {
609        if (pidOf(el) === pid) { el.classList.toggle('active', !wasOn); }
610      });
611      if (wishIds) { if (wasOn) { delete wishIds[pid]; } else { wishIds[pid] = 1; } }
612      free();
613    }, function () {
614      // сеть не ответила — откатываем оптимистичное состояние
615      b.classList.toggle('active', wasOn);
616      bump(wasOn ? 1 : -1);
617      toast('Не удалось изменить избранное', 'error');
618      free();
619    });
620  }, true);
621})();

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.