;try { let widget = '.widget-type_widget_v4_product_info_2_5f6f3f54bc3450ac4a98b2a8fb5a2a3b'; let $widget = $(widget); $(function() { // --------------------------------------------------- // Tabs (как было) // --------------------------------------------------- $widget.find("[data-tabs-item]").on("click", function() { let this_tab_head = $(this); let tabs = $(this).closest(".tabs"); let tab_item_id = $(this).data("tabsItem"); let open_tab = tabs.find('#' + tab_item_id); if (open_tab.length) { if (this_tab_head.parents(".tabs__content").length && this_tab_head.is(".is-active")) { this_tab_head.removeClass("is-active"); tabs.find('#' + tab_item_id).addClass("is-hide-mobile"); } else { tabs.find(".tabs__item.is-active, .tabs__head-item.is-active").removeClass("is-active"); tabs.find('#' + tab_item_id).addClass("is-active").removeClass("is-hide-mobile"); this_tab_head.addClass("is-active"); tabs.find('[data-tabs-item="' + tab_item_id + '"]').addClass("is-active"); $('html, body').animate({ scrollTop: this_tab_head.offset().top - 5 }, 'slow'); } if (open_tab.find(".masonry-reviews-list").length) { resizeAllMassonryGridItems(); } } }); // --------------------------------------------------- // ✅ SEO STRUCTURED DESCRIPTION (FIXED DISTRIBUTION) // --------------------------------------------------- (function() { const desc = document.querySelector(widget + " .product-description.static-text"); if (!desc) return; const rawOriginal = (desc.innerText || "").replace(/\u00A0/g, " ").trim(); if (!rawOriginal || rawOriginal.length < 10) return; if (desc.querySelector(".seo-structured-desc")) return; const norm = (s) => String(s || "").replace(/\s+/g, " ").trim(); const lower = (s) => norm(s).toLowerCase(); const uniq = (arr) => Array.from(new Set((arr || []).filter(Boolean))); // ------------------------------- // 1) FIX "one word per line" // ------------------------------- function normalizeBrokenLines(text) { let t = String(text || ""); t = t.replace(/\r/g, "\n"); const joinPairs = [ ["Utility\nnumber", "Utility numbers"], ["Utility\nnumbers", "Utility numbers"], ["OE\nnumbers", "OE numbers"], ["Parts\nDescription", "Parts Description"], ["Dealer\npart\nnumber", "Dealer part number"], ["Part\nNumber", "Part Number"], ["Parts\nstate", "Parts state"], ["Packing\nunit", "Packing unit"], ["Quantity\nper\nPU", "Quantity per PU"], ["Fitting\nPosition", "Fitting Position"], ["Properties\nvehicle", "Properties vehicle"], ["Constr.\nyear", "Constr.year"], ["Engine\nCode", "Engine Code"], ["KBA\nKeynumber", "KBA Keynumber"], ["Brake\nSystem", "Brake System"], ["Supplementary\nArticle", "Supplementary Article"], ["Observe\nvehicle\nspecifications", "Observe the vehicle manufacturer specifications"], ["Replacement\nnumbers", "Replacement numbers"], ["General\nInformation", "General Information"], ["Additional\ninformation", "Additional information"], ]; joinPairs.forEach(([a,b]) => { t = t.replace(new RegExp(a, "gi"), b); }); t = t.replace(/\n{3,}/g, "\n\n"); return t.trim(); } const raw = normalizeBrokenLines(rawOriginal); let lines = raw.split(/\n/).map(s => norm(s)).filter(Boolean); // ------------------------------- // 2) SRB/HR -> EN keys (как раньше) // ------------------------------- const mapLine = (ln) => { let l = ln; l = l.replace(/\bTezina\b/gi, "Weight"); l = l.replace(/\bVisina\b/gi, "Height"); l = l.replace(/\bDuzina\b/gi, "Length"); l = l.replace(/\bSirina\b/gi, "Width"); l = l.replace(/\bMaterijal\b/gi, "Material"); l = l.replace(/\bStrana\s+ugradnje\b/gi, "Fitting Position"); l = l.replace(/\bPrednja\s+osovina\b/gi, "Front Axle"); l = l.replace(/\bZadnja\s+osovina\b/gi, "Rear Axle"); l = l.replace(/\bPrednja\b/gi, "Front"); l = l.replace(/\bZadnja\b/gi, "Rear"); return norm(l); }; lines = lines.map(mapLine); // ------------------------------- // 3) SECTION FINDERS // ------------------------------- const HEADER_RE = /^(description|dealer part number|part number|parts description|packing unit|quantity per pu|parts state|ean|vehicle|constr\.year|power|capacity|engine code|kba keynumber|properties vehicle|characteristics|utility numbers|oe numbers|replacement numbers|additional information|general information|images|page\b|from\b)/i; function findLineIndexStartsWith(label) { const l = label.toLowerCase(); for (let i = 0; i < lines.length; i++) { if (lower(lines[i]).startsWith(l)) return i; } return -1; } function collectUntilNextHeader(startIdx) { if (startIdx < 0) return []; let out = []; for (let i = startIdx + 1; i < lines.length; i++) { if (HEADER_RE.test(lines[i])) break; out.push(lines[i]); } return out; } function getValueAfterPrefix(prefix) { const p = prefix.toLowerCase(); for (const ln of lines) { const ll = ln.toLowerCase(); if (ll.startsWith(p)) return norm(ln.slice(prefix.length)); } return ""; } // ------------------------------- // 4) BASE FIELDS // ------------------------------- // EAN: берем первый 13-значный, остальные как "other numbers" const all13 = uniq(raw.match(/\b\d{13}\b/g) || []); const ean = all13[0] || ""; const other13 = all13.slice(1); const dealerPartNumber = getValueAfterPrefix("Dealer part number"); const partNumber = getValueAfterPrefix("Part Number"); const partsDesc = getValueAfterPrefix("Parts Description"); const packingUnit = getValueAfterPrefix("Packing unit"); const qtyPerPU = getValueAfterPrefix("Quantity per PU"); const partsState = getValueAfterPrefix("Parts state"); // Vehicle section: лучше собирать как секцию const idxVehicle = findLineIndexStartsWith("Vehicle"); const vehicleLines = idxVehicle >= 0 ? [lines[idxVehicle], ...collectUntilNextHeader(idxVehicle)] : []; const vehicleLine = getValueAfterPrefix("Vehicle"); const constrYear = getValueAfterPrefix("Constr.year"); const power = getValueAfterPrefix("Power"); const capacity = getValueAfterPrefix("Capacity"); const engineCode = getValueAfterPrefix("Engine Code"); const kba = getValueAfterPrefix("KBA Keynumber"); // ------------------------------- // 5) OE NUMBERS (FIX: только OE-форматы) // ------------------------------- const idxOE = findLineIndexStartsWith("OE numbers"); const oeLines = idxOE >= 0 ? collectUntilNextHeader(idxOE) : []; // Mercedes style: A651 140 00 60 / 651 140 00 60 const reMercedesOE = /\bA?\d{3}\s?\d{3}\s?\d{2}\s?\d{2}\b/g; // VAG/PSA style: 1K0 698 451 D / JZW 698 451 D / 8P0 098 601 M const reGroupOE = /\b[A-Z0-9]{1,4}\s?\d{3}\s?\d{3}\s?\d{3}\s?[A-Z]?\b/g; // Compact OE: A6511400060 / 1610489680 const reCompactOE = /\bA?\d{9,11}\b/g; const oeText = oeLines.join(" "); const oeFound = uniq([ ...(oeText.match(reMercedesOE) || []), ...(oeText.match(reGroupOE) || []), ...(oeText.match(reCompactOE) || []), ].map(norm)); // Brand prefix line like "Mercedes-Benz: A651 140 00 60" // вытаскиваем такие тоже, но чистим до номера const brandPref = uniq( oeLines .map(l => { const m = l.match(/^[A-Za-z\- ]+:\s*(.+)$/); return m ? m[1] : ""; }) .filter(Boolean) .flatMap(v => (v.match(reMercedesOE) || []).concat(v.match(reGroupOE) || []).concat(v.match(reCompactOE) || [])) .map(norm) ); const oeNumbers = uniq([...oeFound, ...brandPref]) .filter(x => x && x !== ean); // ------------------------------- // 6) Utility numbers (чистим мусор) // ------------------------------- const idxUtil = findLineIndexStartsWith("Utility numbers"); const utilLines = idxUtil >= 0 ? collectUntilNextHeader(idxUtil) : []; let utilTokens = utilLines.join(", ").split(",").map(norm).filter(Boolean); const isGarbage = (t) => { const tl = lower(t); if (!t) return true; if (tl === "-" || tl === "—") return true; if (tl.startsWith("page")) return true; if (tl === "from") return true; if (tl.startsWith("images")) return true; if (/\b\d{2}\.\d{2}\.\d{4}\b/.test(tl)) return true; if (/^\d+$/.test(tl) && tl.length <= 2) return true; // длинные "CIAK - ... - ..." if (t.includes(" - ") && /^.{2,}\s-\s.{2,}\s-\s/.test(t)) return true; return false; }; const utilityNumbers = uniq(utilTokens.filter(t => !isGarbage(t))); // ------------------------------- // 7) Replacement numbers (Новый блок) // ------------------------------- const idxRepl = findLineIndexStartsWith("Replacement numbers"); const replLines = idxRepl >= 0 ? collectUntilNextHeader(idxRepl) : []; const replTokens = uniq( replLines .join(" ") .split(/[,\s]+/) .map(norm) .filter(Boolean) .filter(t => !isGarbage(t)) ); // ------------------------------- // 8) Characteristics section (строками) // ------------------------------- const idxChars = findLineIndexStartsWith("Characteristics"); const charsLines = idxChars >= 0 ? collectUntilNextHeader(idxChars) : []; const charsClean = charsLines.filter(l => !isGarbage(l)); // ------------------------------- // 9) Additional information / General information (как отдельный блок) // ------------------------------- const idxAdd = findLineIndexStartsWith("Additional information"); const addLines = idxAdd >= 0 ? collectUntilNextHeader(idxAdd) : []; const idxGen = findLineIndexStartsWith("General Information"); const genLines = idxGen >= 0 ? collectUntilNextHeader(idxGen) : []; const infoLines = uniq([...addLines, ...genLines].map(norm)).filter(Boolean); // ------------------------------- // 10) Sizes / Tech (mm, kg, kW, HP, dB(A), inch…) // ------------------------------- const tech = uniq(raw.match(/\b\d+(?:[.,]\d+)?\s?(mm|cm|m|bar|v|volt|kg|g|nm|a|w|kw|hp|db\(a\)|inch|")\b/gi) || []) .map(norm); // ------------------------------- // 11) Fitment tokens (Front/Rear/Left/Right/Inner/Outer...) // ------------------------------- let placeTokens = []; const rawL = raw.toLowerCase(); if (rawL.includes("front axle")) placeTokens.push("Front Axle"); if (rawL.includes("rear axle")) placeTokens.push("Rear Axle"); const posMap = [ ["fitting position front", "Front"], ["fitting position rear", "Rear"], ["front", "Front"], ["rear", "Rear"], ["left", "Left"], ["right", "Right"], ["upper", "Upper"], ["lower", "Lower"], ["inner", "Inner"], ["outer", "Outer"] ]; posMap.forEach(([k,v]) => { if (rawL.includes(k)) placeTokens.push(v); }); placeTokens = uniq(placeTokens); // ------------------------------- // 12) ✅ ANALOGS/CROSSES (FIXED) // Берем ТОЛЬКО коды/номера, без слов // ------------------------------- const STOP_WORDS = new Set([ "dealer","part","number","numbers","packing","quantity","parts","state","manufacturer", "vehicle","constr.year","power","capacity","engine","code","keynumber","characteristics", "operating","electric","supplementary","article","without","gasket","recommended","accessories", "diameter","observe","specifications","control","software","trained","updated","always","compare", "replacement","replaced","additional","information","general","components","electrostatic","discharge", "contacts","valve","page","from","images" ]); // "код" = содержит цифру И (есть дефис/точка/или буквы+цифры), длина >= 5 function isCodeToken(t) { const s = norm(t).replace(/[.,;:]+$/g, ""); if (!s) return false; const sl = s.toLowerCase(); if (STOP_WORDS.has(sl)) return false; if (isGarbage(s)) return false; // не единицы if (/^(mm|cm|m|kg|g|kw|hp|v|w|nm|db\(a\)|inch|")$/i.test(s)) return false; // исключаем OE и основной EAN if (ean && s === ean) return false; if (oeNumbers.includes(s)) return false; // исключаем чисто "120kW" "163HP" — это тех данные if (/^\d+(?:[.,]\d+)?\s?(kw|hp|mm|cm|kg|g)\b/i.test(s)) return false; // основные паттерны кодов: // A2C59514268-VDO / E110059-MLS / LAK37-MAH / 83.1714A2 / 0 986 461 769 (последнее не поймаем как один, но ок) const compact = s.replace(/\s+/g, ""); if (/^[A-Z0-9]{2,}[A-Z0-9.\-]{3,}$/i.test(compact) && /\d/.test(compact)) return true; // 5+ цифр (utility) if (/^\d{5,}$/.test(compact)) return true; return false; } // собираем: // - dealerPartNumber / partNumber // - utilityNumbers // - replacement tokens // - другие коды из текста const tokensFromRaw = (raw.match(/[A-Z0-9][A-Z0-9.\-]{2,}/gi) || []).map(norm); const analogs = uniq([ dealerPartNumber, partNumber, ...utilityNumbers, ...replTokens, ...other13, // остальные 13-значные (как “другие номера”) ...tokensFromRaw ].filter(isCodeToken)); // отдельный блок “Замены” const replacementOut = uniq([...replTokens, ...other13].filter(isCodeToken)); // ------------------------------- // 13) OUTPUT HTML // ------------------------------- const esc = (s) => String(s || "") .replace(/&/g, "&").replace(//g, ">"); const listBlock = (arr) => ""; let html = `
`; html += `
Структурированное описание
`; if (oeNumbers.length) { html += `

OE номера / Utility / WVA

`; html += listBlock(oeNumbers); } if (ean) { html += `

EAN

`; html += listBlock([ean]); } // ✅ Аналоги только коды if (analogs.length) { html += `

Аналоги / Кроссы

`; html += listBlock(analogs); } // ✅ Замены отдельно if (replacementOut.length) { html += `

Замены / Replacement numbers

`; html += listBlock(replacementOut); } if (placeTokens.length) { html += `

Место установки

`; html += listBlock(placeTokens); } if (tech.length) { html += `

Размеры / Технические данные

`; html += listBlock(tech); } const metaRows = []; if (dealerPartNumber) metaRows.push("Dealer part number: " + dealerPartNumber); if (partNumber) metaRows.push("Part Number: " + partNumber); if (partsDesc) metaRows.push("Parts Description: " + partsDesc); if (packingUnit) metaRows.push("Packing unit: " + packingUnit); if (qtyPerPU) metaRows.push("Quantity per PU: " + qtyPerPU); if (partsState) metaRows.push("Parts state: " + partsState); if (metaRows.length) { html += `

Артикул / Упаковка

`; html += "
" + metaRows.map(esc).join("
") + "
"; } const vehicleInfo = []; if (vehicleLine) vehicleInfo.push("Vehicle: " + vehicleLine); if (constrYear) vehicleInfo.push("Constr.year: " + constrYear); if (power) vehicleInfo.push("Power: " + power); if (capacity) vehicleInfo.push("Capacity: " + capacity); if (engineCode) vehicleInfo.push("Engine Code: " + engineCode); if (kba) vehicleInfo.push("KBA Keynumber: " + kba); // если Vehicle секция есть, но строки “разорваны” — добавим их аккуратно // (чтобы не попадали в Аналоги) if (!vehicleInfo.length && vehicleLines.length) { vehicleInfo.push(norm(vehicleLines.join(" "))); } if (vehicleInfo.length) { html += `

Применимость / Автомобиль

`; html += "
" + vehicleInfo.map(esc).join("
") + "
"; } if (charsClean.length) { html += `

Характеристики

`; html += "
" + charsClean.map(esc).join("
") + "
"; } if (infoLines.length) { html += `

Дополнительная информация

`; html += "
" + infoLines.map(esc).join("
") + "
"; } html += `
`; // insert desc.insertAdjacentHTML("afterbegin", html); // ✅ hide raw repeat Array.from(desc.childNodes).forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.classList && node.classList.contains("seo-structured-desc")) return; if (node.nodeType === Node.ELEMENT_NODE) node.style.display = "none"; if (node.nodeType === Node.TEXT_NODE) node.textContent = ""; }); })(); // --------------------------------------------------- // More items (как было) // --------------------------------------------------- $widget.find('.js-more-items').on("click", function() { const block_with_more_items = $(this).parents(".block-with-more-items:first"); block_with_more_items.find('.hidden-item').removeClass('hidden-item'); $(this).hide().parents(".more-items").hide(); if (block_with_more_items.find(".masonry-reviews-list").length) { resizeAllMassonryGridItems(); } }); // --------------------------------------------------- // Reviews masonry (как было) // --------------------------------------------------- function resizeMassonryGridItem(item) { let grid = document.getElementsByClassName("masonry-reviews-list")[0]; if (!grid) return; let rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows')); let rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap')); if (rowGap == 0) rowGap = 1; let rowSpan = Math.ceil((item.querySelector('.masonry-reviews-item__content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap)); item.style.gridRowEnd = "span " + rowSpan; } function resizeAllMassonryGridItems() { const allItems = document.getElementsByClassName("masonry-reviews-item"); for (let x = 0; x < allItems.length; x++) { resizeMassonryGridItem(allItems[x]); } } window.onload = function() { resizeAllMassonryGridItems(); } window.addEventListener("resize", resizeAllMassonryGridItems); $(function() { if (window.EventBus && EventBus.subscribe) { EventBus.subscribe(['widget:input-setting:insales:system:editor', 'widget:change-setting:insales:system:editor'], (data) => { const masonryReviewsList = document.querySelector('[data-widget-id="' + data.widget_id + '"] .masonry-reviews-list'); if (masonryReviewsList) resizeAllMassonryGridItems(); }); } }); }); } catch(error) { console.error('Widget "widget-type_widget_v4_product_info_2_5f6f3f54bc3450ac4a98b2a8fb5a2a3b"', error) }