;try { // ========================================================= // FULL WIDGET FILE (JS) // ========================================================= // - Tabs behavior (original, unchanged) // - Reviews masonry (original, unchanged) // - ✅ Structured description: распределение по разделам // * Артикул / Упаковка // * Применимость / Автомобиль // * Характеристики // * Utility numbers // * OE numbers // * EAN // * Аналоги / Кроссы (ТОЛЬКО коды, без "слов") // * Замены / Replacement numbers // * Место установки // * Размеры / Технические данные // - ✅ Убираем нижний дубль "сырого текста" (скрываем всё кроме SEO-блока) // ========================================================= let widget = '.widget-type_widget_v4_product_info_2_5f6f3f54bc3450ac4a98b2a8fb5a2a3b'; let $widget = $(widget); // --------------------------------------------------------- // Helpers // --------------------------------------------------------- function norm(s) { return String(s || "").replace(/\u00A0/g, " ").replace(/\s+/g, " ").trim(); } function lower(s) { return norm(s).toLowerCase(); } function uniq(arr) { return Array.from(new Set((arr || []).map(norm).filter(Boolean))); } function esc(s) { return String(s || "") .replace(/&/g, "&") .replace(//g, ">"); } function listBlock(arr) { const a = (arr || []).map(norm).filter(Boolean); if (!a.length) return ""; return ""; } // --------------------------------------------------------- // Normalize "one word per line" PDFs // --------------------------------------------------------- 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"], ["Supplementary\nArticle/Info", "Supplementary Article/Info"], ["Replacement\nnumbers", "Replacement numbers"], ["General\nInformation", "General Information"], ["Additional\ninformation", "Additional information"], ["External\nrolling\nnoise", "External rolling noise"], ["Noise\nlevel", "Noise level"], ["Fuel\nEfficiency\nClass", "Fuel Efficiency Class"], ["Wet\nGrip\nClass", "Wet Grip Class"], ["Tyre\ndimension", "Tyre dimension"], ["Tyre\nwidth", "Tyre width"], ["Width/height\nratio", "Width/height ratio"], ["Rim\ndiameter", "Rim diameter"], ["Load\nindex", "Load index"], ["Speed\nindex", "Speed index"], ["Tyre\nreinforcement", "Tyre reinforcement"], ]; joinPairs.forEach(([a, b]) => { t = t.replace(new RegExp(a, "gi"), b); }); // сгладим "Page 1 From 2" -> Page 1 From 2 t = t.replace(/\nPage\s*\n/gi, "\nPage "); t = t.replace(/\nFrom\s*\n/gi, " From "); t = t.replace(/\n{3,}/g, "\n\n"); return t.trim(); } // --------------------------------------------------------- // Parse helpers (sections by headers) // --------------------------------------------------------- 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(lines, 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(lines, 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.map(norm).filter(Boolean); } function getValueAfterPrefix(lines, prefix) { const p = prefix.toLowerCase(); for (const ln of lines) { const ll = lower(ln); if (ll.startsWith(p)) return norm(ln.slice(prefix.length)); } return ""; } function isGarbageToken(t) { const s = norm(t); const sl = lower(s); if (!s) return true; if (sl === "-" || sl === "—") return true; if (sl.startsWith("page")) return true; if (sl === "from") return true; if (sl.startsWith("images")) return true; if (/^\d+$/.test(sl) && sl.length <= 2) return true; if (/\b\d{2}\.\d{2}\.\d{4}\b/.test(sl)) return true; // длинные "CIAK - ... - ..." if (s.includes(" - ") && /^.{2,}\s-\s.{2,}\s-\s/.test(s)) return true; return false; } // ========================================================= // MAIN // ========================================================= $(function() { // ------------------------------------------------------- // Tabs (original) // ------------------------------------------------------- $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(); } } }); // ------------------------------------------------------- // ✅ Structured description (all variants from our chat) // ------------------------------------------------------- (function() { const desc = document.querySelector(widget + " .product-description.static-text"); if (!desc) return; const rawOriginal = norm(desc.innerText || ""); if (!rawOriginal || rawOriginal.length < 10) return; // already inserted if (desc.querySelector(".seo-structured-desc")) return; const raw = normalizeBrokenLines(rawOriginal); let lines = raw.split(/\n/).map(norm).filter(Boolean); // SRB/HR words -> EN (small mapping) lines = lines.map((l) => { let x = l; x = x.replace(/\bTezina\b/gi, "Weight"); x = x.replace(/\bVisina\b/gi, "Height"); x = x.replace(/\bDuzina\b/gi, "Length"); x = x.replace(/\bSirina\b/gi, "Width"); x = x.replace(/\bMaterijal\b/gi, "Material"); x = x.replace(/\bStrana\s+ugradnje\b/gi, "Fitting Position"); x = x.replace(/\bPrednja\s+osovina\b/gi, "Front Axle"); x = x.replace(/\bZadnja\s+osovina\b/gi, "Rear Axle"); x = x.replace(/\bPrednja\b/gi, "Front"); x = x.replace(/\bZadnja\b/gi, "Rear"); return norm(x); }); // --------------- EAN (first) + other 13 digits const all13 = uniq(raw.match(/\b\d{13}\b/g) || []); const ean = all13[0] || ""; const other13 = all13.slice(1); // --------------- Basic packaging/article fields const dealerPartNumber = getValueAfterPrefix(lines, "Dealer part number"); const partNumber = getValueAfterPrefix(lines, "Part Number"); const partsDesc = getValueAfterPrefix(lines, "Parts Description"); const packingUnit = getValueAfterPrefix(lines, "Packing unit"); const qtyPerPU = getValueAfterPrefix(lines, "Quantity per PU"); const partsState = getValueAfterPrefix(lines, "Parts state"); // --------------- Vehicle/applicability fields const vehicle = getValueAfterPrefix(lines, "Vehicle"); const constrYear = getValueAfterPrefix(lines, "Constr.year"); const power = getValueAfterPrefix(lines, "Power"); const capacity = getValueAfterPrefix(lines, "Capacity"); const engineCode = getValueAfterPrefix(lines, "Engine Code"); const kba = getValueAfterPrefix(lines, "KBA Keynumber"); // --------------- Utility numbers section const idxUtil = findLineIndexStartsWith(lines, "Utility numbers"); const utilLines = idxUtil >= 0 ? collectUntilNextHeader(lines, idxUtil) : []; let utilTokens = utilLines.join(", ").split(",").map(norm).filter(Boolean); utilTokens = utilTokens.filter(t => !isGarbageToken(t)); // --------------- Replacement numbers section (new) const idxRepl = findLineIndexStartsWith(lines, "Replacement numbers"); const replLines = idxRepl >= 0 ? collectUntilNextHeader(lines, idxRepl) : []; const replTokens = uniq( replLines .join(" ") .split(/[,\s]+/) .map(norm) .filter(Boolean) .filter(t => !isGarbageToken(t)) ); // --------------- Characteristics section (as lines) const idxChars = findLineIndexStartsWith(lines, "Characteristics"); const charsLines = idxChars >= 0 ? collectUntilNextHeader(lines, idxChars) : []; const charsClean = charsLines.filter(l => !isGarbageToken(l)); // --------------- Additional + General info const idxAdd = findLineIndexStartsWith(lines, "Additional information"); const addLines = idxAdd >= 0 ? collectUntilNextHeader(lines, idxAdd) : []; const idxGen = findLineIndexStartsWith(lines, "General Information"); const genLines = idxGen >= 0 ? collectUntilNextHeader(lines, idxGen) : []; const infoLines = uniq([...addLines, ...genLines].map(norm)).filter(Boolean); // --------------- OE numbers section (FIX: only OE-like patterns, no extra text) const idxOE = findLineIndexStartsWith(lines, "OE numbers"); const oeLines = idxOE >= 0 ? collectUntilNextHeader(lines, idxOE) : []; const oeText = oeLines.join(" "); // Mercedes spaced: 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 grouped: 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: 1610489680 / A6511400060 const reCompactOE = /\bA?\d{9,11}\b/g; const oeFound = uniq([ ...(oeText.match(reMercedesOE) || []), ...(oeText.match(reGroupOE) || []), ...(oeText.match(reCompactOE) || []), ].map(norm)).filter(x => x && x !== ean); // also handle "BRAND: ..." lines inside OE section 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); // --------------- Tech / sizes (mm/cm/kg/kW/HP/dB(A)/inch etc.) const tech = uniq( (raw.match(/\b\d+(?:[.,]\d+)?\s?(mm|cm|m|kg|g|bar|v|volt|w|kw|hp|nm|db\(a\)|inch|")\b/gi) || []) .map(norm) ); // --------------- Fitment (Front/Rear/Left/Right/Inner/Outer/Upper/Lower + Axle) 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); // ----------------------------------------------------- // ✅ Аналоги / Кроссы — ONLY codes (no words) // ----------------------------------------------------- const STOP_WORDS = new Set([ "dealer","part","parts","number","numbers","packing","unit","quantity","state","normal","deliverable", "manufacturer","vehicle","constr.year","power","capacity","engine","code","keynumber","properties","characteristics", "width","height","thickness","length","weight","diameter","material","observe","specifications","replacement", "replaced","additional","information","general","components","electrostatic","discharge","do","not","touch","plug","contacts", "page","from","images" ]); function isCodeToken(t) { const s0 = norm(t).replace(/[.,;:]+$/g, ""); if (!s0) return false; const sl = lower(s0); if (STOP_WORDS.has(sl)) return false; if (isGarbageToken(s0)) return false; // not pure unit if (/^(mm|cm|m|kg|g|kw|hp|v|w|nm|db\(a\)|inch|")$/i.test(s0)) return false; // exclude main EAN + OE if (ean && s0 === ean) return false; if (oeNumbers.includes(s0)) return false; // exclude pure "120kW" "163HP" (leave in tech) if (/^\d+(?:[.,]\d+)?\s?(kw|hp|mm|cm|kg|g|db\(a\)|inch|")\b/i.test(s0)) return false; // compact token without spaces const compact = s0.replace(/\s+/g, ""); // Accept: // - A2C59514268-VDO // - E110059-MLS // - 83.1714A2 // - 0 986 461 769 (won't be one token; ok) // - 05P1219 // - 2391401 if (/^[A-Z0-9]{2,}[A-Z0-9.\-]{3,}$/i.test(compact) && /\d/.test(compact)) return true; // long numeric utility if (/^\d{5,}$/.test(compact)) return true; return false; } const tokensFromRaw = (raw.match(/[A-Z0-9][A-Z0-9.\-]{2,}/gi) || []).map(norm); const utilityNumbers = uniq(utilTokens); const analogs = uniq([ dealerPartNumber, partNumber, ...utilityNumbers, ...replTokens, ...other13, ...tokensFromRaw ].filter(isCodeToken)); const replacementOut = uniq([...replTokens, ...other13].filter(isCodeToken)); // ----------------------------------------------------- // Build HTML (all sections) // ----------------------------------------------------- let html = `
`; html += `
Структурированное описание
`; // Артикул / Упаковка const packRows = []; if (dealerPartNumber) packRows.push("Dealer part number: " + dealerPartNumber); if (partNumber) packRows.push("Part Number: " + partNumber); if (partsDesc) packRows.push("Parts Description: " + partsDesc); if (packingUnit) packRows.push("Packing unit: " + packingUnit); if (qtyPerPU) packRows.push("Quantity per PU: " + qtyPerPU); if (partsState) packRows.push("Parts state: " + partsState); if (packRows.length) { html += `

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

`; html += "
" + packRows.map(esc).join("
") + "
"; } // Применимость / Автомобиль const vehRows = []; if (vehicle) vehRows.push("Vehicle: " + vehicle); if (constrYear) vehRows.push("Constr.year: " + constrYear); if (power) vehRows.push("Power: " + power); if (capacity) vehRows.push("Capacity: " + capacity); if (engineCode) vehRows.push("Engine Code: " + engineCode); if (kba) vehRows.push("KBA Keynumber: " + kba); if (vehRows.length) { html += `

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

`; html += "
" + vehRows.map(esc).join("
") + "
"; } // Характеристики if (charsClean.length) { html += `

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

`; html += "
" + charsClean.map(esc).join("
") + "
"; } // Utility numbers if (utilityNumbers.length) { html += `

Utility numbers

`; html += listBlock(utilityNumbers); } // OE numbers if (oeNumbers.length) { html += `

OE numbers

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

EAN

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

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

`; html += listBlock(analogs); } // Replacement numbers if (replacementOut.length) { html += `

Замены / Replacement numbers

`; html += listBlock(replacementOut); } // Место установки if (placeTokens.length) { html += `

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

`; html += listBlock(placeTokens); } // Размеры / Технические данные if (tech.length) { html += `

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

`; html += listBlock(tech); } // Доп. информация if (infoLines.length) { html += `

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

`; html += "
" + infoLines.map(esc).join("
") + "
"; } html += `
`; // вставляем блок наверх desc.insertAdjacentHTML("afterbegin", html); // ✅ скрываем "сырой" дубль (всё кроме seo-structured-desc) 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 (original) // ------------------------------------------------------- $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 (original) // ------------------------------------------------------- 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(); } }); } }); // ------------------------------------------------------- // Events (original) // ------------------------------------------------------- if (window.EventBus && EventBus.subscribe) { EventBus.subscribe('reviews-open:insales:site', function() { let reviews_block = $widget.find("#tab-reviews"); if (reviews_block.length) { $widget.find('[data-tabs-item="tab-reviews"]:first').click(); $('html, body').animate({ scrollTop: reviews_block.offset().top - 64 }, 500); } }); EventBus.subscribe('properties-open:insales:site', function() { let properties_block = $widget.find("#tab-characteristics"); if (properties_block.length) { $widget.find('[data-tabs-item="tab-characteristics"]:first').click(); $('html, body').animate({ scrollTop: properties_block.offset().top - 64 }, 500); } }); $widget.find('.js-show-manager').on("click", function() { $(this).parents('.masonry-reviews-item__content').find('.comments-item').toggleClass('hidden'); resizeAllMassonryGridItems(); $(this).toggleClass('hidden'); $(this).parents('.masonry-reviews-item__content').find('.js-hide-manager').toggleClass('hidden'); }); $widget.find('.js-hide-manager').on("click", function() { $(this).parents('.masonry-reviews-item__content').find('.comments-item').toggleClass('hidden'); resizeAllMassonryGridItems(); $(this).toggleClass('hidden'); $(this).parents('.masonry-reviews-item__content').find('.js-show-manager').toggleClass('hidden'); }); $widget.find('.js-show-form').on("click", function() { $widget.find('.reviews-wrapper').toggleClass('hidden'); $(this).hide(); }); $widget.find('.js-hide-form').on("click", function() { $widget.find('.reviews-wrapper').toggleClass('hidden'); $widget.find('.js-show-form').show(); }); $widget.find('.js-load-review-image').on("change", function() { let str = $(this).val(); let i = str.lastIndexOf('/') + 1; if (str.lastIndexOf('\\')) i = str.lastIndexOf('\\') + 1; let filename = str.slice(i); $widget.find('.load-review-image-name').html(filename); }); EventBus.subscribe('send-review:insales:ui_reviews', (data) => { const $thisWidget = $(data.form[0].closest('.layout')); if ($thisWidget.attr('class') !== $widget.attr('class')) { return; } const review_notice_success = $thisWidget.find("[data-reviews-form-success]"); const file_input = $thisWidget.find("[data-reviews-file-input-name]"); if (file_input.length) { file_input.text(file_input.data('reviews-file-input-name')); } if (review_notice_success.length) { $('html,body').animate({ scrollTop: review_notice_success.offset().top }, 500); } }); } }); } catch (error) { console.error('Widget "widget-type_widget_v4_product_info_2_5f6f3f54bc3450ac4a98b2a8fb5a2a3b"', error); }