1/** 2 * common.js â обÑий ÑкÑÐ¸Ð¿Ñ OpenCart (коÑзина, избÑанное, ÑÑавнение, ÑоÑмÑ). 3 * ÐодклÑÑаеÑÑÑ Ð² header.twig. ЧиÑÑÑй JS, без jQuery. 4 * 5 * ÐлобалÑнÑй API (вÑзÑваеÑÑÑ Ð¸Ð· onclick-аÑÑибÑÑов Ñаблонов): 6 * cart.preview / cart.add / cart.add_single / cart.update / cart.remove / 7 * cart.remove_key / cart.clear, wishlist.*, productCompare.*, compare.add, 8 * voucher.remove, getURLVar, remove_product, showSizechart, showPreorderModal, 9 * chozeSystemSize, beautifyTableSizes, initAutocomplete, sendSelectItemEvent 10 * 11 * ÐÐÐÐÐÐÐÐÐÐ: 12 * 1. ХелпеÑÑ 13 * 2. УÑилиÑÑ (getURLVar, remove_product) 14 * 3. ТаблиÑа ÑазмеÑов (sizechart) 15 * 4. ÐÑедзаказ 16 * 5. ФоÑмÑ: бонÑÑÑ, конÑакÑÑ, ÑаÑÑÑлка, загÑÑзка Ñайлов 17 * 6. ÐаÑÑоÑки ÑоваÑа: вÑÐ±Ð¾Ñ Ð¾Ð¿Ñий, пеÑÐµÑ Ð¾Ð´Ñ 18 * 7. ÐалÑÑа, поиÑк, пеÑеклÑÑаÑÐµÐ»Ñ Ð²Ð¸Ð´Ð° ÑпиÑка 19 * 8. ÐоÑзина (cart), кÑÐ¿Ð¾Ð½Ñ (voucher) 20 * 9. ÐзбÑанное (wishlist) 21 * 10. СÑавнение (productCompare, compare) 22 * 11. Ðодалка «УÑÐ»Ð¾Ð²Ð¸Ñ ÑоглаÑениÑ» (.agree) 23 * 12. ÐвÑÐ¾ÐºÐ¾Ð¼Ð¿Ð»Ð¸Ñ (initAutocomplete) 24 * 13. ÐналиÑика GA4 (view_item_list, select_item, поиÑк) 25 * 14. ÐодпиÑка на ÑÐ¾Ð²Ð°Ñ Ð½Ðµ в налиÑии 26 * 15. ÐеиÑполÑзÑемÑй код (закомменÑиÑован) 27 */ 28 29;(() => { 30 'use strict' 31 32 /* ============================================================ 33 * 1. Ð¥ÐÐÐÐРЫ 34 * ============================================================ */ 35 36 const qs = (selector, root = document) => root.querySelector(selector) 37 const qsa = (selector, root = document) => [...root.querySelectorAll(selector)] 38 39 // ÐелегиÑование ÑобÑÑий (аналог $(document).on(event, selector, fn)) 40 const delegate = (eventName, selector, handler) => { 41 document.addEventListener(eventName, e => { 42 const target = e.target.closest(selector) 43 if (target) handler(e, target) 44 }) 45 } 46 47 // POST Ñ Ð¾ÑвеÑом-JSON (аналог $.ajax type:post dataType:json). 48 // body â ÑÑÑока "a=1&b=2", URLSearchParams или FormData 49 const postJSON = (url, body) => 50 fetch(url, { 51 method: 'POST', 52 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 53 body: typeof body === 'string' ? new URLSearchParams(body) : body 54 }).then(r => r.json()) 55 56 const getJSON = url => 57 fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then(r => r.json()) 58 59 // Ðоказ/ÑкÑÑÑие (аналог .show()/.hide()) 60 const show = (el, display = 'block') => { 61 if (el) el.style.display = display 62 } 63 const hide = el => { 64 if (el) el.style.display = 'none' 65 } 66 67 // ÐÑÑавка HTML поÑле ÑлеменÑа / Ñдаление ÑлеменÑов 68 const insertAfter = (el, html) => el?.insertAdjacentHTML('afterend', html) 69 const removeAll = selector => qsa(selector).forEach(el => el.remove()) 70 71 // ÐÑпÑавка dataLayer из оÑвеÑа ÑеÑвеÑа (повÑоÑÑеÑÑÑ Ð²Ð¾ вÑÐµÑ AJAX-обÑабоÑÑÐ¸ÐºÐ°Ñ ) 72 const pushDataLayer = json => { 73 if (!json?.dataLayer) return 74 window.dataLayer = window.dataLayer || [] 75 window.dataLayer.push({ ecommerce: null }) 76 window.dataLayer.push(json.dataLayer) 77 } 78 79 // Ðбновление ÑÑÑÑÑиков коÑÐ·Ð¸Ð½Ñ Ð² Ñапке 80 const updateCartCounters = json => { 81 setTimeout(() => { 82 qsa('.cart > .cart_counter').forEach(el => (el.innerHTML = json.total ?? '')) 83 }, 100) 84 if (json.total) {
85 qsa('.cart-fragment_header--counter').forEach(el => (el.innerHTML = json.total)) 86 } 87 if (json.cart_total) { 88 qsa('.cart-fragment_header--price').forEach(el => (el.innerHTML = json.cart_total)) 89 } 90 } 91 92 // СÑÑаниÑа ÑкÑоллиÑÑÑ Ð²Ð½ÑÑÑи <body> â пÑокÑÑÑка к ÑлеменÑÑ 93 const scrollBodyToEl = el => { 94 if (!el) return 95 const top = el.getBoundingClientRect().top + document.body.scrollTop - 100 96 document.body.scrollTo({ top, behavior: 'smooth' }) 97 } 98 99 // СеÑиализаÑÐ¸Ñ ÑоÑÐ¼Ñ Ð¾Ð¿Ñий ÑоваÑа ÑÑдом Ñ ÐºÐ½Ð¾Ð¿ÐºÐ¾Ð¹ «в коÑзинÑ». 100 // ÐаменÑÐµÑ jQuery-вÑÑажение из onclick-аÑÑибÑÑов: 101 // $(this).parent().find('form.data_origin_option').serialize() 102 const serializeOptionForm = (el, scoped = false) => { 103 if (!el?.parentElement) return '' 104 const selector = scoped ? '.render_product_data_option form.data_origin_option' : 'form.data_origin_option' 105 const form = el.parentElement.querySelector(selector) 106 return form ? new URLSearchParams(new FormData(form)).toString() : '' 107 } 108 109 /* ============================================================ 110 * 2. УТÐÐÐТЫ 111 * ============================================================ */ 112 113 // ÐнаÑение GET-паÑамеÑÑа ÑекÑÑего URL (иÑÑоÑиÑеÑкий API OpenCart) 114 function getURLVar(key) { 115 const params = new URLSearchParams(window.location.search) 116 return params.get(key) ?? '' 117 } 118 119 // Ðнопка «ÑдалиÑÑ Ð²ÑбÑанное» в коÑзине: ÑбиÑÐ°ÐµÑ Ð¾ÑмеÑеннÑе позиÑии и пеÑезагÑÑÐ¶Ð°ÐµÑ 120 function remove_product(cart_link) { 121 qsa('.cart_items ._checkbox:checked').forEach(cb => cart.remove_key(cb.value)) 122 setTimeout(() => { 123 location = cart_link 124 }, 200) 125 } 126 127 /* ============================================================ 128 * 3. ТÐÐÐÐЦРРÐÐÐÐÐ ÐÐ (SIZECHART) 129 * ============================================================ */ 130 131 // ÐÑÑÐ°Ð²Ð½Ð¸Ð²Ð°ÐµÑ Ð²ÑÑоÑÑ ÑÑеек ÑаблиÑÑ ÑазмеÑов по заголовоÑной колонке 132 function beautifyTableSizes() { 133 if (!qs('#table_sizes')) return 134 setTimeout(() => { 135 qsa('#table_sizes table tr').forEach(tr => { 136 const headcol = tr.querySelector('.headcol') 137 if (!headcol) return 138 const height = headcol.getBoundingClientRect().height 139 tr.querySelectorAll('td').forEach(td => (td.style.height = `${height}px`)) 140 }) 141 }, 20) 142 } 143 144 // ÐеÑеклÑÑение ÑиÑÑÐµÐ¼Ñ ÑазмеÑов (EUR/US/CM) â ÑÐ¾Ñ ÑанÑеÑÑÑ Ð² ÑеÑÑии 145 function chozeSystemSize(system_size, element) { 146 if (typeof system_size === 'undefined') return 147 148 // поддеÑжка вÑзова и Ñ DOM-ÑлеменÑом, и Ñо ÑÑаÑÑм jQuery-обÑекÑом 149 if (element && !(element instanceof Element) && element[0] instanceof Element) { 150 element = element[0] 151 } 152 153 const setBusy = busy => { 154 if (!(element instanceof Element)) return 155 element.style.opacity = busy ? '.5' : '1' 156 element.style.pointerEvents = busy ? 'none' : 'auto' 157 } 158 159 // lang обÑзаÑелен: без ÑзÑкового пÑеÑикÑа в URL ÑÑого AJAX-запÑоÑа 160 // startup.php Ð¿Ð°Ð´Ð°ÐµÑ Ð½Ð° деÑолÑнÑй ÑзÑк магазина (ro) вмеÑÑо ÑекÑÑего ÑзÑка ÑÑÑаниÑÑ 161 const langSize = document.documentElement.lang 162 setBusy(true) 163 postJSON(`index.php?route=product/sizechart/setSystemSize${langSize ? `&lang=${langSize}` : ''}`, `sustem_size=${encodeURIComponent(system_size)}`) 164 .then(json => console.log(json)) 165 .catch(err => console.log(err)) 166 .finally(() => setBusy(false)) 167 } 168 169 // ÐÑкÑÑÐ²Ð°ÐµÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ Ñ ÑаблиÑей ÑазмеÑов ÑоваÑа (подгÑÑжаеÑÑÑ AJAX-ом) 170 function showSizechart(product_id, sizechart_message) { 171 product_id = parseInt(product_id, 10) 172 if (isNaN(product_id)) return 173 174 const modal = qs('#table_sizes') 175 if (!modal) return 176 177 const text = modal.querySelector('.text') 178 if (text) text.innerHTML = sizechart_message ?? 'Loading...' 179 modal.querySelectorAll('.table_sizes').forEach(el => el.remove()) 180 181 show(modal) 182 183 // lang обÑзаÑелен: без ÑзÑкового пÑеÑикÑа в URL ÑÑого AJAX-запÑоÑа 184 // startup.php Ð¿Ð°Ð´Ð°ÐµÑ Ð½Ð° деÑолÑнÑй ÑзÑк магазина (ro) вмеÑÑо ÑекÑÑего ÑзÑка ÑÑÑаниÑÑ 185 const lang = document.documentElement.lang 186 postJSON(`index.php?route=product/sizechart/popup${lang ? `&lang=${lang}` : ''}`, `product_id=${product_id}`) 187 .then(json => { 188 if (json.view) { 189 // из HTML-оÑвеÑа беÑÑм ÑодеÑжимое .modal_body и его клаÑÑ
190 // (клаÑÑ Ð²Ð°Ð¶ÐµÐ½: .sizeguide-modal пеÑеклÑÑÐ°ÐµÑ Ð²ÑÑÑÑÐºÑ Ð½Ð° 191 // ÑикÑиÑованнÑÑ Ð²ÑÑоÑÑ, еÑли Ñ ÑоваÑа наÑÑÑоен калÑкÑлÑÑоÑ) 192 const tmp = document.createElement('div') 193 tmp.innerHTML = json.view 194 const body = tmp.querySelector('.modal_body') 195 const target = modal.querySelector('.modal_body') 196 if (body && target) { 197 target.className = body.className 198 target.innerHTML = body.innerHTML 199 } 200 beautifyTableSizes() 201 initSizeGuideUnitToggle(modal) 202 initSizeGuideCalculator(modal) 203 } 204 }) 205 .catch(err => console.log(err)) 206 } 207 208 // ÐакÑÑÐ²Ð°ÐµÑ Ð²Ñе оÑкÑÑÑÑе модалки (иÑполÑзовалоÑÑ ÑанÑÑе ÑолÑко кнопкой OK 209 // гида по ÑазмеÑам, ÑепеÑÑ Ñакже кнопкой "SelecteazÄ", еÑли ÑекомендованнÑй 210 // ÑÐ°Ð·Ð¼ÐµÑ Ñовпал Ñ Ñже оÑкÑÑÑÑм ÑоваÑом) 211 const closeAllModals = () => { 212 qsa('.modal_').forEach(modal => { 213 hide(modal) 214 modal.dispatchEvent(new CustomEvent('modal:hide', { bubbles: true })) 215 }) 216 qsa('.modal_overlay').forEach(hide) 217 } 218 219 // Ðнопка OK в ÑÑÑеÑе гида по ÑазмеÑам. ÐÑделÑнÑй клаÑÑ (не .close_modal), 220 // ÑÑÐ¾Ð±Ñ Ð½Ðµ пÑÑаÑÑ Ñ ÐºÑеÑÑиком закÑÑÑиÑ: Ñ .close_modal Ñвой вид 221 // (диагоналÑнÑе линии), не ÑекÑÑ. 222 // ÐÑли оÑкÑÑÑа вкладка калÑкÑлÑÑоÑа ("Ghid de mÄrimi") и она еÑÑ Ð² 223 // ÑоÑÑоÑнии ввода даннÑÑ â кнопка валидиÑÑÐµÑ Ð²Ð²ÐµÐ´ÑннÑй ÑÐ°Ð·Ð¼ÐµÑ Ð¸ 224 // пеÑеклÑÑÐ°ÐµÑ ÐºÐ¾Ð½ÑÐµÐ½Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ¸ на ÑкÑан ÑезÑлÑÑаÑа/недоÑÑÑпноÑÑи (Ñм. 225 // initSizeGuideCalculator, tryConfirm). Ðо вÑÐµÑ Ð¾ÑÑалÑнÑÑ ÑлÑÑаÑÑ 226 // (вкладка "Tabel de mÄrimi", калÑкÑлÑÑÐ¾Ñ Ð½Ðµ наÑÑÑоен, либо Ñже показан 227 // ÑкÑан ÑезÑлÑÑаÑа) â пÑоÑÑо закÑÑÐ²Ð°ÐµÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ, как ÑанÑÑе 228 delegate('click', '.sizeguide-ok', (e, btn) => { 229 const modal = btn.closest('.modal_') 230 if (modal?.__sizeGuideCalculator?.tryConfirm()) return 231 closeAllModals() 232 }) 233 234 // ÐеÑеклÑÑение вкладок "Ghid de mÄrimi"/"Tabel de mÄrimi" Ð¼Ð¾Ð¶ÐµÑ Ð²ÐµÑнÑÑÑ 235 // ÑÑÑÐµÑ Ñ ÐºÐ½Ð¾Ð¿ÐºÐ¾Ð¹ OK, еÑли он бÑл ÑкÑÑÑ ÑкÑаном ÑезÑлÑÑаÑа калÑкÑлÑÑоÑа. 236 // setTimeout â ÑÑÐ¾Ð±Ñ Ð¾ÑÑабоÑаÑÑ Ð¿Ð¾Ñле обÑего обÑабоÑÑика [data-toggle="tab"] 237 // (Ñм. ниже, п.13), коÑоÑÑй вÑÑÑавлÑÐµÑ ÐºÐ»Ð°ÑÑ .active на вкладке 238 delegate('click', '.sizeguide-tabs [data-toggle="tab"]', (e, toggle) => { 239 const modal = toggle.closest('.modal_') 240 setTimeout(() => modal?.__sizeGuideCalculator?.syncFooter?.(), 0) 241 }) 242 243 // ÐавигаÑиÑ-плиÑка ÑнивеÑÑалÑного ÑпÑавоÑника (calculator.type == 244 // 'universal') â клик по каÑегоÑии ÑкÑÐ¾Ð»Ð»Ð¸Ñ Ðº нÑÐ¶Ð½Ð¾Ð¼Ñ ÑÐ°Ð·Ð´ÐµÐ»Ñ Ð²Ð½ÑÑÑи 245 // пÑокÑÑÑиваемой облаÑÑи модалки, без пеÑÐµÑ Ð¾Ð´Ð° по ÑÑÑлке 246 delegate('click', '.sizeguide-universal-nav a[href^="#"]', (e, link) => { 247 e.preventDefault() 248 const target = document.getElementById(link.getAttribute('href').slice(1)) 249 target?.scrollIntoView({ block: 'start' }) 250 }) 251 252 // ÐеÑеклÑÑаÑÐµÐ»Ñ Ñм/дÑÐ¹Ð¼Ñ Ñ ÑÑÑок ÑаблиÑÑ, помеÑеннÑÑ Ð² админке как 253 // "знаÑÐµÐ½Ð¸Ñ Ð² Ñм" â пÑоÑÑо пÑÑÑеÑ/показÑÐ²Ð°ÐµÑ Ð·Ð°Ñанее поÑÑиÑаннÑе на 254 // бÑкенде паÑÑ <span class="unit-cm">/<span class="unit-inch"> 255 function initSizeGuideUnitToggle(modal) { 256 const select = qs('[data-table-unit-select]', modal) 257 if (!select) return 258 259 const apply = unit => { 260 // Ñ .unit-inch еÑÑÑ Ð±Ð°Ð·Ð¾Ð²Ñй display:none в CSS, поÑÑÐ¾Ð¼Ñ Ð¿Ñи показе
261 // нÑжно Ñвное 'inline', а не '' (пÑÑÑÐ°Ñ ÑÑÑока пÑоÑÑо ÑÐ½Ð¸Ð¼Ð°ÐµÑ 262 // инлайн-ÑÑÐ¸Ð»Ñ Ð¸ оÑкаÑÑÐ²Ð°ÐµÑ Ðº пÑÐ°Ð²Ð¸Ð»Ñ Ð¸Ð· ÑаблиÑÑ ÑÑилей) 263 qsa('.unit-cm', modal).forEach(el => (el.style.display = unit === 'inch' ? 'none' : 'inline')) 264 qsa('.unit-inch', modal).forEach(el => (el.style.display = unit === 'inch' ? 'inline' : 'none')) 265 } 266 267 select.onchange = () => apply(select.value) 268 apply(select.value) 269 } 270 271 // Ð¡Ð¸Ð½Ð¾Ð½Ð¸Ð¼Ñ Ð±ÑквеннÑÑ ÑазмеÑов Ð¾Ð´ÐµÐ¶Ð´Ñ (Ñе же, ÑÑо в admin's clothesSizeIndex, 272 // admin/view/template/catalog/sizechart_form.twig) â в ÑаблиÑе ÑазмеÑов 273 // бÑендом Ð¼Ð¾Ð¶ÐµÑ Ð±ÑÑÑ Ð¿Ð¾Ð´Ð¿Ð¸Ñано "1X"/"2X" и Ñ.п., а Ñ ÑоваÑа ÑеалÑнÑй 274 // ваÑÐ¸Ð°Ð½Ñ ÑазмеÑа пÑи ÑÑом назÑваеÑÑÑ "XL"/"XXL". Ðез ÑÑой ноÑмализаÑии 275 // Ñакой ÑÐ°Ð·Ð¼ÐµÑ Ð½Ð¸ÐºÐ¾Ð³Ð´Ð° не Ð½Ð°Ñ Ð¾Ð´Ð¸ÑÑÑ ÑÑеди ваÑианÑов ÑоваÑа и калÑкÑлÑÑÐ¾Ñ 276 // оÑибоÑно показÑÐ²Ð°ÐµÑ "ÑÐ°Ð·Ð¼ÐµÑ Ð½ÐµÐ´Ð¾ÑÑÑпен", Ñ Ð¾ÑÑ Ð²Ð°ÑÐ¸Ð°Ð½Ñ ÐµÑÑÑ. 277 const CLOTHES_SIZE_SYNONYMS = { 278 '2XL': 'XXL', 'XXXL': '3XL', 'XXXXL': '4XL', 'XXXXXL': '5XL', 279 '1X': 'XL', '2X': 'XXL', '3X': '3XL', '4X': '4XL', '5X': '5XL' 280 } 281 282 const normalizeClothesSize = raw => { 283 const norm = (raw || '').toString().trim().toUpperCase().replace(/\s+/g, '') 284 return CLOTHES_SIZE_SYNONYMS[norm] || norm 285 } 286 287 // ÐÑÐµÑ ÑÑеди Ñже оÑÑиÑованнÑÑ Ð½Ð° ÑÑÑаниÑе ÑоваÑа ÑазмеÑов (ÑпиÑок 288 // EUR/US/CM/RUS над кнопкой "Ghid de mÄrimi", .toggle_tab li) ÑазмеÑ, 289 // ÑовпадаÑÑий Ñ Ñем, ÑÑо вÑÑиÑлил калÑкÑлÑÑÐ¾Ñ â ÑÑÐ¾Ð±Ñ Ð¿Ð¾Ð½ÑÑÑ, еÑÑÑ Ð»Ð¸ Ñ 290 // ÑоваÑа Ñакой ÑÐ°Ð·Ð¼ÐµÑ Ð²Ð¾Ð¾Ð±Ñе (инаÑе показÑваем ÑкÑан "ÑÐ°Ð·Ð¼ÐµÑ Ð½ÐµÐ´Ð¾ÑÑÑпен") 291 function findProductSizeMatch(label) { 292 const items = qsa('.block-sizes .toggle_tab li') 293 if (!items.length) return { checked: false } 294 295 // ЯÑейка ÑаблиÑÑ Ð¼Ð¾Ð¶ÐµÑ Ð¿ÐµÑеÑиÑлÑÑÑ ÑеÑез запÑÑÑÑ Ð½ÐµÑколÑко ÑавнознаÑнÑÑ 296 // названий одного ÑазмеÑа (напÑ. "XXL,2XL,2X") â ÑÑавниваем ÑеалÑнÑÑ 297 // ÐºÐ½Ð¾Ð¿ÐºÑ Ñ ÐºÐ°Ð¶Ð´Ñм названием по оÑделÑноÑÑи, а не Ñо вÑей ÑÑÑокой Ñеликом 298 const labelParts = label.toString().split(',') 299 300 const sizesMatch = text => { 301 if (text === label) return true 302 303 for (const part of labelParts) { 304 if (normalizeClothesSize(text) === normalizeClothesSize(part)) return true 305 306 const a = parseFloat(text) 307 const b = parseFloat(part) 308 if (!isNaN(a) && !isNaN(b) && a === b) return true 309 } 310 311 return false 312 } 313 314 for (const li of items) { 315 const text = (li.textContent || '').trim() 316 if (!sizesMatch(text)) continue 317 // ÐозвÑаÑаем text (а не иÑÑ Ð¾Ð´Ð½Ñй label) â Ñак покÑпаÑÐµÐ»Ñ Ð²Ð¸Ð´Ð¸Ñ 318 // ÑиÑÑое название ÑеалÑного ваÑианÑа ÑоваÑа ("XXL"), а не ÑÑÑÑÑ 319 // ÑÑÐµÐ¹ÐºÑ ÑаблиÑÑ, коÑоÑÐ°Ñ Ð¼Ð¾Ð¶ÐµÑ Ð¿ÐµÑеÑиÑлÑÑÑ ÑеÑез запÑÑÑÑ 320 // неÑколÑко ÑавнознаÑнÑÑ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ð¹ ("XXL,2XL,2X") 321 const link = li.querySelector('a') 322 return { checked: true, available: true, href: link ? link.getAttribute('href') : null, text: text } 323 } 324 return { checked: true, available: false } 325 } 326 327 // ÐалÑкÑлÑÑÐ¾Ñ ÑазмеÑа (вкладка "Ghid de mÄrimi"): по введÑÐ½Ð½Ð¾Ð¼Ñ ÑоÑÑÑ/веÑÑ 328 // или длине ÑÑÐ¾Ð¿Ñ Ð½Ð°Ñ Ð¾Ð´Ð¸Ñ Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑÑÑ ÐºÐ¾Ð»Ð¾Ð½ÐºÑ Ð´Ð¸Ð°Ð¿Ð°Ð·Ð¾Ð½Ð¾Ð² из наÑÑÑоек 329 // админки и показÑÐ²Ð°ÐµÑ ÑооÑвеÑÑÑвÑÑÑий ÑазмеÑ, взÑÑÑй из ÑÑÑоки ÑаблиÑÑ, 330 // вÑбÑанной в админке как "ÑÑÑока Ñ ÑезÑлÑÑаÑом" (напÑ. EU Ð´Ð»Ñ Ð¾Ð±Ñви), 331 // либо из пеÑвой ÑÑÑоки по ÑмолÑÐ°Ð½Ð¸Ñ (Ð¿Ð¾Ð´Ñ Ð¾Ð´Ð¸Ñ Ð´Ð»Ñ Ð¾Ð´ÐµÐ¶Ð´Ñ â Ñам пеÑÐ²Ð°Ñ 332 // ÑÑÑока обÑÑно и еÑÑÑ Ñам ÑÐ°Ð·Ð¼ÐµÑ XS/S/M...). 333 // ÐкÑан калÑкÑлÑÑоÑа пеÑеклÑÑаеÑÑÑ Ð¼ÐµÐ¶Ð´Ñ ÑÑÐµÐ¼Ñ ÑоÑÑоÑниÑми: 334 // 'form' â Ð¿Ð¾Ð»Ñ Ð²Ð²Ð¾Ð´Ð° (по ÑмолÑаниÑ) 335 // 'result' â найденнÑй ÑÐ°Ð·Ð¼ÐµÑ + кнопки "SelecteazÄ"/"Ãnapoi" 336 // 'unavailable' â Ñ ÑоваÑа Ð½ÐµÑ Ñакого ÑазмеÑа + кнопка "Ãnapoi" 337 // ÐеÑеклÑÑение на 'result'/'unavailable' пÑоиÑÑ Ð¾Ð´Ð¸Ñ Ð¿Ð¾ кнопке OK в ÑÑÑеÑе
338 // модалки (Ñм. delegate('click', '.sizeguide-ok', ...) вÑÑе), коÑоÑÐ°Ñ 339 // вÑзÑÐ²Ð°ÐµÑ modal.__sizeGuideCalculator.tryConfirm() 340 function initSizeGuideCalculator(modal) { 341 const holder = qs('[data-calculator]', modal) 342 modal.__sizeGuideCalculator = null 343 if (!holder) return 344 345 let calculator 346 try { 347 calculator = JSON.parse(holder.dataset.calculator) 348 } catch (e) { 349 return 350 } 351 if (!calculator || !calculator.columns) return 352 353 const introEl = qs('.sizeguide-intro', holder) 354 const formEl = qs('.sizeguide-calculator-form', holder) 355 const resultEl = qs('.sizeguide-calculator-result', holder) 356 const unavailableEl = qs('.sizeguide-calculator-unavailable', holder) 357 const footerEl = qs('.sizeguide-footer', modal) 358 359 const resultLabelEl = qs('.sizeguide-result-label', resultEl) 360 const resultValueEl = qs('.sizeguide-result-value', resultEl) 361 const selectBtn = qs('.sizeguide-select', resultEl) 362 const unavailableTextEl = qs('.sizeguide-unavailable-text', unavailableEl) 363 364 if (resultLabelEl) resultLabelEl.textContent = holder.dataset.textResultLabel ?? '' 365 if (selectBtn) selectBtn.textContent = holder.dataset.textSelect ?? '' 366 if (unavailableTextEl) unavailableTextEl.textContent = holder.dataset.textUnavailable ?? '' 367 qsa('.sizeguide-back', holder).forEach(a => (a.textContent = holder.dataset.textBack ?? '')) 368 369 let state = 'form' 370 let selectHref = null 371 let matchedColumn = null 372 let confirm = () => {} 373 374 const setDisplay = (el, visible, display = 'block') => { 375 if (el) el.style.display = visible ? display : 'none' 376 } 377 378 const syncFooter = () => { 379 const paneActive = holder.closest('.tab-pane')?.classList.contains('active') 380 setDisplay(footerEl, !(paneActive && state !== 'form')) 381 } 382 383 const setState = next => { 384 state = next 385 setDisplay(introEl, state === 'form') 386 setDisplay(formEl, state === 'form') 387 setDisplay(resultEl, state === 'result', 'flex') 388 setDisplay(unavailableEl, state === 'unavailable', 'flex') 389 syncFooter() 390 } 391 392 // ÐÑÑÑ Ð»Ð¸ Ñ Ð¾ÑÑ Ð±Ñ Ð² одной колонке Ð·Ð°Ð´Ð°Ð½Ñ Ð³ÑаниÑÑ Ð´Ð»Ñ ÑÑого Ð¿Ð¾Ð»Ñ 393 // (напÑ. "weight") â иÑполÑзÑеÑÑÑ, ÑÑÐ¾Ð±Ñ Ð½Ðµ ÑÑебоваÑÑ Ð¾Ñ Ð¿Ð¾ÐºÑпаÑÐµÐ»Ñ 394 // знаÑение, коÑоÑого в ÑÑой конкÑеÑной ÑаблиÑе вÑÑ Ñавно Ð½ÐµÑ 395 // (Ñм. calculator.columns Ñ clothes/clothes_measure: ÑаÑÑÑ ÑÐ°Ð±Ð»Ð¸Ñ 396 // заполнена по ÑоÑÑÑ, ÑаÑÑÑ â по Ð¾Ð±Ñ Ð²Ð°Ñам, а не вÑе Ð¿Ð¾Ð»Ñ ÑÑазÑ) 397 const fieldHasData = field => { 398 for (const range of Object.values(calculator.columns)) { 399 if (range[`${field}_min`] !== undefined || range[`${field}_max`] !== undefined) return true 400 } 401 return false 402 } 403 404 const columnLabel = col => { 405 const rowSelector = 406 calculator.result_row !== undefined && calculator.result_row !== null 407 ? `[data-row-index="${calculator.result_row}"]` 408 : 'tr' 409 const row = qs(`.table_sizes table ${rowSelector}`, modal) 410 const cell = row && row.querySelector(`.long-${col}`) 411 if (!cell) return col 412 // в ÑÑейке Ð¼Ð¾Ð¶ÐµÑ Ð±ÑÑÑ Ð¿Ð°Ñа ÑкÑÑÑÑÑ span.unit-cm/unit-inch (Ñм/дÑймÑ) â 413 // textContent Ñклеил Ð±Ñ Ð¾Ð±Ð°, поÑÑÐ¾Ð¼Ñ Ð±ÐµÑÑм видимÑÑ ÐµÐ´Ð¸Ð½Ð¸ÑÑ Ñвно 414 const unitCm = cell.querySelector('.unit-cm') 415 return (unitCm ? unitCm.textContent : cell.textContent).trim() 416 } 417 418 const matchColumn = predicate => { 419 for (const [col, range] of Object.entries(calculator.columns)) { 420 if (predicate(range)) return col 421 } 422 return null 423 } 424 425 // Ðиапазон допÑÑÑимÑÑ Ð·Ð½Ð°Ñений по вÑем колонкам ÑÑÐ°Ð·Ñ â Ð´Ð»Ñ Ð¿Ð¾Ð´Ñказки 426 // под полем ввода (напÑ. "Ð¾Ñ 21.9 до 38.1") 427 const globalRange = (minKey, maxKey) => { 428 let min = Infinity 429 let max = -Infinity 430 for (const range of Object.values(calculator.columns)) { 431 if (range[minKey] !== undefined && range[minKey] !== null) min = Math.min(min, range[minKey]) 432 if (range[maxKey] !== undefined && range[maxKey] !== null) max = Math.max(max, range[maxKey]) 433 } 434 return min <= max ? { min, max } : null 435 } 436 437 const formatNum = n => { 438 const rounded = Math.round(n * 10) / 10 439 return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1) 440 } 441 442 const setHint = (hintEl, template, min, max) => { 443 if (!hintEl || !template) return 444 hintEl.textContent = template.replace('{min}
444', formatNum(min)).replace('{max}', formatNum(max)) 445 } 446 447 const setHintError = (hintEl, isError) => { 448 hintEl?.classList.toggle('sizeguide-hint--error', isError) 449 } 450 451 // ÐеÑÐµÐ²Ð¾Ð´Ð¸Ñ ÐºÐ°Ð»ÑкÑлÑÑÐ¾Ñ Ð² ÑкÑан ÑезÑлÑÑаÑа â еÑли Ñ ÑоваÑа Ð½ÐµÑ Ñакого 452 // ÑазмеÑа вообÑе (не найден ÑÑеди .toggle_tab li), показÑÐ²Ð°ÐµÑ ÑкÑан 453 // "ÑÐ°Ð·Ð¼ÐµÑ Ð½ÐµÐ´Ð¾ÑÑÑпен" вмеÑÑо ÑезÑлÑÑаÑа 454 const resolveResult = col => { 455 const label = columnLabel(col) 456 const match = findProductSizeMatch(label) 457 458 if (match.checked && !match.available) { 459 selectHref = null 460 setState('unavailable') 461 return 462 } 463 464 selectHref = match.href 465 // match.text â ÑиÑÑое название ÑеалÑного ваÑианÑа ÑоваÑа, найденное 466 // findProductSizeMatch (напÑ. "XXL"). ÐÑли ÑÐ¾Ð²Ð°Ñ Ð²Ð¾Ð¾Ð±Ñе не пÑовеÑÑлÑÑ 467 // (match.checked === false, Ð½ÐµÑ .toggle_tab на ÑÑÑаниÑе), Ñакого 468 // ÑекÑÑа Ð½ÐµÑ â Ñогда показÑваем пеÑвое Ð¸Ð¼Ñ Ð¸Ð· возможной ÑÑÑоки Ñ 469 // пеÑеÑиÑлением ÑеÑез запÑÑÑÑ ("XXL,2XL,2X" -> "XXL"), а не ÑÑÑÑÑ 470 // ÑÑÑÐ¾ÐºÑ Ñеликом 471 if (resultValueEl) resultValueEl.textContent = match.text || label.toString().split(',')[0].trim() 472 setState('result') 473 } 474 475 // ÐбÑий калÑкÑлÑÑÐ¾Ñ Ð´Ð»Ñ Ñипов Ñ Ð´Ð²ÑÐ¼Ñ Ð¸Ð·Ð¼ÐµÑÑемÑми полÑми: ÑоÑÑ+Ð²ÐµÑ 476 // (clothes) или Ð¾Ð±Ñ Ð²Ð°Ñ Ð³ÑÑди+Ñалии (clothes_measure). Ðоле, Ð´Ð»Ñ 477 // коÑоÑого ни Ñ Ð¾Ð´Ð½Ð¾Ð¹ колонки не Ð·Ð°Ð´Ð°Ð½Ñ Ð³ÑаниÑÑ (напÑ. "веÑ" в 478 // деÑÑкой ÑаблиÑе, где ÑеалÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ Ð¾ веÑе Ð½ÐµÑ â Ñм. fieldHasData), 479 // не показÑваеÑÑÑ Ð¸ не ÑÑебÑеÑÑÑ Ð¾Ñ Ð¿Ð¾ÐºÑпаÑелÑ. 480 const initTwoFieldCalculator = (fieldA, fieldB, hintKeyA, hintKeyB) => { 481 const activeA = fieldHasData(fieldA) 482 const activeB = fieldHasData(fieldB) 483 484 const inputA = qs(`[data-role="${fieldA}"]`, holder) 485 const inputB = qs(`[data-role="${fieldB}"]`, holder) 486 const hintA = qs(`[data-hint-for="${fieldA}"]`, holder) 487 const hintB = qs(`[data-hint-for="${fieldB}"]`, holder) 488 489 setDisplay(inputA?.closest('.sizeguide-field'), activeA) 490 setDisplay(inputB?.closest('.sizeguide-field'), activeB) 491 492 const rangeA = activeA ? globalRange(`${fieldA}_min`, `${fieldA}_max`) : null 493 const rangeB = activeB ? globalRange(`${fieldB}_min`, `${fieldB}_max`) : null 494 if (rangeA) setHint(hintA, holder.dataset[hintKeyA], rangeA.min, rangeA.max) 495 if (rangeB) setHint(hintB, holder.dataset[hintKeyB], rangeB.min, rangeB.max) 496 497 // ÐÑли ÑÑÑогий "ÑоÑÑ Ð Ð²ÐµÑ Ð¾Ð´Ð½Ð¾Ð²Ñеменно попадаÑÑ Ð² диапазон ÑÑой же 498 // колонки" не Ð½Ð°Ñ Ð¾Ð´Ð¸Ñ ÑÐ¾Ð²Ð¿Ð°Ð´ÐµÐ½Ð¸Ñ â не Ñеловек виноваÑ, а Ñама моделÑ: 499 // Ð´Ð¸Ð°Ð¿Ð°Ð·Ð¾Ð½Ñ ÑоÑÑа и веÑа ÑаÑÑÑÑ Ð² жÑÑÑкой ÑвÑзке (вÑÑе ÑоÑÑ = болÑÑе 500 // веÑ), а ÑеалÑнÑе лÑди бÑваÑÑ, напÑимеÑ, вÑÑокими и поджаÑÑми (ÑоÑÑ 501 // ÑÑÐ½ÐµÑ Ðº одной колонке, Ð²ÐµÑ â к ÑоÑедней, менÑÑей). Ð ÑÑом ÑлÑÑае 502 // беÑÑм колонкÑ, Ñей ÑенÑÑ (по обоим полÑм в ноÑмиÑованнÑÑ ÐµÐ´Ð¸Ð½Ð¸ÑÐ°Ñ ) 503 // ближе вÑего к введÑннÑм знаÑениÑм â Ñак ÑезÑлÑÑÐ°Ñ Ð¿Ð¾ÑÑи вÑегда 504 // Ð½Ð°Ñ Ð¾Ð´Ð¸ÑÑÑ, и "ÑÐ°Ð·Ð¼ÐµÑ Ð½ÐµÐ´Ð¾ÑÑÑпен" ÑÑабаÑÑÐ²Ð°ÐµÑ Ð¿Ð¾ ÑеалÑной пÑиÑине 505 // (Ñ ÑоваÑа Ð½ÐµÑ Ñакого ваÑианÑа), а не из-за пÑобела в ÑеÑке диапазонов. 506 const nearestColumn = (valueA, valueB) => { 507 let best = null 508 let bestDist = Infinity 509 510 for (const [col, r] of Object.entries(calculator.columns)) { 511 let dist = 0 512 513 if (activeA && r[`${fieldA}_min`] !== undefined && r[`${fieldA}_max`] !== undefined) { 514 const center = (r[`${fieldA}_min`] + r[`${fieldA}_max`]) / 2 515 const halfWidth = (r[`${fieldA}_max`] - r[`${fieldA}_min`]) / 2 || 1 516 dist += ((valueA - center) / halfWidth) ** 2 517 } 518 519 if (activeB && r[`${fieldB}_min`] !== undefined && r[`${fieldB}_max`] !== undefined) { 520 const center = (r[`${fieldB}_min`] + r[`${fieldB}_max`]) / 2 521 const halfWidth = (r[`${fieldB}_max`] - r[`${fieldB}_min`]) / 2 || 1 522 dist += ((valueB - center) / halfWidth) ** 2 523 } 524 525 if (dist < bestDist) { 526 bestDist = dist 527 best = col 528 } 529 } 530 531 return best 532 } 533 534 const recalc = () => { 535 setHintError(hintA, false) 536 setHintError(hintB, false) 537 538 const valueA = activeA ? parseFloat(inputA?.value) : null 539 const valueB = activeB ? parseFloat(inputB?.value) : null 540 if ((activeA && isNaN(valueA)) || (activeB && isNaN(valueB))) { 541 matchedColumn = null 542 return 543 } 544 matchedColumn = matchColumn( 545 r => 546 (!activeA || (valueA >= (r[`${fieldA}_min`] ?? -Infinity) && valueA <= (r[`${fieldA}_max`] ?? Infinity))) && 547 (!activeB || (valueB >= (r[`${fieldB}_min`] ?? -Infinity) && valueB <= (r[`${fieldB}_max`] ?? Infinity))) 548 ) 549 550 if (matchedColumn === null) { 551 matchedColumn = nearestColumn(valueA, valueB) 552 } 553 } 554 555 inputA?.addEventListener('input', recalc) 556 inputB?.addEventListener('input', recalc) 557 recalc() 558 559 confirm = () => { 560 if (matchedColumn === null) { 561 if (activeA) setHintError(hintA, true) 562 if (activeB) setHintError(hintB, true) 563 return 564 } 565 resolveResult(matchedColumn) 566 } 567 } 568 569 if (calculator.type === 'clothes') { 570 initTwoFieldCalculator('height', 'weight', 'hintHeight', 'hintWeight') 571 } 572 573 if (calculator.type === 'clothes_measure') { 574 initTwoFieldCalculator('chest', 'waist', 'hintChest', 'hintWaist') 575 } 576 577 if (calculator.type === 'shoes') { 578 const footInput = qs('[data-role="foot"]', holder) 579 const unitSelect = qs('[data-foot-unit-select]', holder) 580 const footHint = qs('[data-hint-for="foot"]', holder) 581 582 const footRangeCm = globalRange('foot_min', 'foot_max') 583 584 const updateHint = () => { 585 if (!footRangeCm) return 586 const factor = unitSelect?.value === 'inch' ? 1 / 2.54 : 1 587 setHint(footHint, holder.dataset.hintFoot, footRangeCm.min * factor, footRangeCm.max * factor) 588 } 589 updateHint() 590 591 const recalc = () => { 592 setHintError(footHint, false) 593 594 const raw = parseFloat(footInput?.value) 595 if (isNaN(raw)) { 596 matchedColumn = null 597 return 598 } 599 const cm = unitSelect?.value === 'inch' ? raw * 2.54 : raw 600 matchedColumn = matchColumn(r => cm >= (r.foot_min ?? -Infinity) && cm <= (r.foot_max ?? Infinity)) 601 } 602 603 unitSelect?.addEventListener('change', () => { 604 updateHint() 605 recalc() 606 }) 607 footInput?.addEventListener('input', recalc) 608 recalc() 609 610 confirm = () => { 611 if (matchedColumn === null) { 612 setHintError(footHint, true) 613 return 614 } 615 resolveResult(matchedColumn) 616 } 617 } 618 619 selectBtn?.addEventListener('click', () => { 620 if (selectHref) { 621 location.href = selectHref 622 } else { 623 closeAllModals() 624 } 625 }) 626
627 qsa('.sizeguide-back', holder).forEach(a => a.addEventListener('click', () => setState('form'))) 628 629 setState('form') 630 631 modal.__sizeGuideCalculator = { 632 tryConfirm: () => { 633 const paneActive = holder.closest('.tab-pane')?.classList.contains('active') 634 if (!paneActive || state !== 'form') return false 635 confirm() 636 return true 637 }, 638 syncFooter 639 } 640 } 641 642 /* ============================================================ 643 * 4. ÐÐ ÐÐÐÐÐÐÐ 644 * ============================================================ */ 645 646 // Ðодалка пÑедзаказа: оÑкÑÑÑие и оÑпÑавка ÑоÑÐ¼Ñ 647 function showPreorderModal(product_id) { 648 const modal = qs('#modal_preorder') 649 if (!modal) return 650 651 const idInput = modal.querySelector('[name="preorder_product_id"]') 652 if (idInput) idInput.value = product_id 653 show(modal) 654 655 const button = modal.querySelector('#button_preorder') 656 if (!button) return 657 658 // onclick-пÑиÑваивание вмеÑÑо addEventListener: модалка оÑкÑÑваеÑÑÑ 659 // многокÑаÑно, обÑабоÑÑик не должен накапливаÑÑÑÑ 660 button.onclick = e => { 661 e.preventDefault() 662 const fields = qsa(':scope input, :scope select, :scope textarea', modal) 663 const params = new URLSearchParams() 664 fields.forEach(f => { 665 if (!f.name || f.disabled) return 666 if ((f.type === 'checkbox' || f.type === 'radio') && !f.checked) return 667 params.append(f.name, f.value) 668 }) 669 670 button.classList.add('disabled') 671 postJSON('index.php?route=checkout/checkout/preorder', params) 672 .then(json => { 673 modal.querySelectorAll('.alert, .text-danger').forEach(el => el.remove()) 674 modal.classList.remove('has-error') 675 676 if (json.error_warning) { 677 modal 678 .querySelector('.modal-header') 679 ?.insertAdjacentHTML( 680 'afterend', 681 `<div class="alert alert-danger" style="margin:5px;"><i class="fa fa-exclamation-circle"></i> ${json.error_warning}</div>` 682 ) 683 } 684 if (json.error) { 685 for (const [index, value] of Object.entries(json.error)) { 686 const element = modal.querySelector(`#input-preorder_${index}`) 687 element?.parentElement?.classList.add('has-error') 688 insertAfter(element, `<div class="text-danger">${value}</div>`) 689 } 690 } 691 if (json.success) { 692 const common = qs('#modal_common') 693 if (common) { 694 const heading = common.querySelector('.main-heading') 695 const message = common.querySelector('.message_text') 696 if (heading) heading.innerHTML = json.success 697 if (message) message.innerHTML = json.success_info ?? '' 698 } 699 hide(modal) 700 show(common) 701 modal.querySelector('#form_preorder')?.reset() 702 } 703 }) 704 .catch(err => console.log(err)) 705 .finally(() => button.classList.remove('disabled')) 706 } 707 } 708 709 /* ============================================================ 710 * 5. ФÐÐ ÐЫ: ÐÐÐУСЫ, ÐÐÐТÐÐТЫ, Ð ÐССЫÐÐÐ, ÐÐÐРУÐÐРФÐÐÐÐÐ 711 * ============================================================ */ 712 713 // СобиÑÐ°ÐµÑ Ð·Ð½Ð°ÑÐµÐ½Ð¸Ñ Ð·Ð°Ð¿Ð¾Ð»Ð½ÑемÑÑ Ð¿Ð¾Ð»ÐµÐ¹ конÑейнеÑа (аналог jQuery-ÑеÑиализаÑии 714 // по ÑпиÑÐºÑ input/select/textarea Ñ checked-ÑилÑÑÑом) 715 const collectFields = container => { 716 const params = new URLSearchParams() 717 qsa('input, select, textarea', container).forEach(f => { 718 if (!f.name || f.disabled) return 719 if (f.type === 'submit' || f.type === 'button' || f.type === 'file') return 720 if ((f.type === 'checkbox' || f.type === 'radio') && !f.checked) return 721 params.append(f.name, f.value) 722 }) 723 return params 724 } 725 726 document.addEventListener('DOMContentLoaded', () => { 727 // пеÑеÑÑÑÑ ÑаблиÑÑ ÑазмеÑов пÑи изменении окна 728 window.addEventListener('resize', beautifyTableSizes) 729 730 // ÐоиÑк по бÑендам на ÑÑÑаниÑе пÑоизводиÑелей (ÑÑÑаниÑа «ÐÑендÑ») 731 delegate('keyup', '#product-manufacturer .brands_search_input', (e, input) => { 732 const slug = input.value 733 const root = qs('#product-manufacturer') 734 if (!root) return 735 736 if (slug !== '') { 737 qsa('.brand_category_letter', root).forEach(hide) 738 qsa('.brand_category_letter .brand_single', root).forEach(el => { 739 hide(el) 740 el.classList.remove('show_element') 741 }) 742 // показÑваем бÑендÑ, ÑодеÑжаÑие запÑÐ¾Ñ (ÑегиÑÑÑозавиÑимо, как бÑло) 743 qsa('.brand_category_letter .brand_single_value', root).forEach(value => { 744 if (!value.textContent.includes(slug)) return 745 const single = value.closest('.brand_single') 746 if (single) { 747 single.classList.add('show_element') 748 show(single) 749 } 750 }) 751 // показÑваем бÑквÑ, в коÑоÑÑÑ Ð¾ÑÑалиÑÑ Ð²Ð¸Ð´Ð¸Ð¼Ñе бÑÐµÐ½Ð´Ñ 752 qsa('.brand_category_letter .list .brand_single.show_element', root).forEach(el => { 753 const letterId = el.closest('.brand_category_letter')?.dataset.id 754 if (letterId !== undefined) { 755 qsa(`[data-id="${letterId}"].brand_category_letter`, root).forEach(letter => show(letter)) 756 } 757 }) 758 } else { 759 qsa('.brand_category_letter', root).forEach(el => show(el)) 760 qsa('.brand_category_letter .brand_single', root).forEach(el => show(el)) 761 } 762 }) 763 764 // ФоÑма бонÑÑной пÑогÑÐ°Ð¼Ð¼Ñ (ÑÑÑаниÑа «ÐонÑÑÑ») 765 delegate('click', '#information-bonuses #send-data-bonuses', (e, button) => { 766 e.preventDefault() 767 const root = qs('#information-bonuses') 768 if (!root) return 769 770 button.classList.add('disabled')
771 postJSON('index.php?route=information/bonuses/send_form', collectFields(root)) 772 .then(json => { 773 pushDataLayer(json) 774 qsa('.alert-dismissible, .text-danger', root).forEach(el => el.remove()) 775 776 if (json.error_warning) { 777 root.insertAdjacentHTML( 778 'afterbegin', 779 `<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> ${json.error_warning} <button type="button" class="close" data-dismiss="alert">×</button></div>` 780 ) 781 scrollBodyToEl(root.querySelector('.alert-dismissible')) 782 } 783 784 if (json.error) { 785 for (const key of Object.keys(json.error)) { 786 const element = root.querySelector(`#input-bonuses-${key.replace('_', '-')}`) 787 if (!element) continue 788 if (element.parentElement.classList.contains('check_it')) { 789 element.parentElement.insertAdjacentHTML('beforeend', `<div class="text-danger">${json.error[key]}</div>`) 790 } else { 791 insertAfter(element, `<div class="text-danger">${json.error[key]}</div>`) 792 } 793 } 794 } 795 796 if (json.success) { 797 root.querySelector('#bonuses_form')?.reset() 798 show(qs('#success_bonuses_form')) 799 } 800 }) 801 .catch(err => console.log(err)) 802 .finally(() => button.classList.remove('disabled')) 803 }) 804 805 // Ðнопка «ÑвÑзаÑÑÑÑ Ñ Ð¼Ð°Ð³Ð°Ð·Ð¸Ð½Ð¾Ð¼Â»: оÑкÑÑÐ²Ð°ÐµÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ ÐºÐ¾Ð½ÑакÑов Ñ Ñемой обÑаÑÐµÐ½Ð¸Ñ 806 delegate('click', '#information-contact .contact_store', (e, button) => { 807 const form = qs('#contact_form') 808 if (!form) return 809 810 const title = button.dataset.title ?? '' 811 const modalTitle = form.querySelector('.modal_title') 812 if (modalTitle) modalTitle.innerHTML = title 813 814 const nameInput = form.querySelector('input[name="form_name"]') 815 if (nameInput) nameInput.value = title 816 817 const langInput = form.querySelector('input[name="lang_form_name"]') 818 if (langInput) langInput.value = button.dataset.langTitle ?? title 819 820 show(form) 821 }) 822 823 // ÐÑпÑавка ÑоÑÐ¼Ñ ÐºÐ¾Ð½ÑакÑов 824 delegate('click', '#contact_form #send-data', (e, button) => { 825 const root = qs('#contact_form') 826 if (!root) return 827 828 button.classList.add('disabled') 829 postJSON('index.php?route=information/contact/send_form', collectFields(root)) 830 .then(json => { 831 pushDataLayer(json) 832 qsa('.alert-dismissible, .text-danger', root).forEach(el => el.remove()) 833 834 if (json.error_warning) { 835 root 836 .querySelector('.modal_body') 837 ?.insertAdjacentHTML( 838 'afterbegin', 839 `<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> ${json.error_warning} <button type="button" class="close" data-dismiss="alert">×</button></div>` 840 ) 841 } 842 843 if (json.error) { 844 for (const key of Object.keys(json.error)) { 845 const element = root.querySelector(`#input-contact-${key.replace('_', '-')}`) 846 if (!element) continue 847 if (element.parentElement.classList.contains('check_it')) { 848 element.parentElement.insertAdjacentHTML('beforeend', `<div class="text-danger">${json.error[key]}</div>`) 849 } else { 850 insertAfter(element, `<div class="text-danger">${json.error[key]}</div>`) 851 } 852 } 853 } 854 855 // пÑокÑÑÑиваем конÑÐµÐ¹Ð½ÐµÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÐ¸ к пеÑвой оÑибке 856 const container = root.querySelector('.modal_body') 857 if (container) { 858 if (json.error_warning) { 859 const elm = document.querySelector('.alert-danger') 860 if (elm) container.scrollTop = elm.offsetTop 861 } else if (json.error) { 862 const elm = document.querySelector('.text-danger')?.parentNode 863 if (elm) container.scrollTop = elm.offsetTop 864 } 865 } 866 867 if (json.success) { 868 root.querySelector('#contact_form_data')?.reset() 869 hide(root) 870 show(qs('#success_contact_form')) 871 } 872 }) 873 .catch(err => console.log(err)) 874 .finally(() => button.classList.remove('disabled')) 875 }) 876 877 // ÐагÑÑзка Ñайла Ð´Ð»Ñ Ð¾Ð¿Ñии ÑоваÑа (ÑÑандаÑÑнÑй Ð¼ÐµÑ Ð°Ð½Ð¸Ð·Ð¼ OpenCart) 878 let uploadTimer = null 879 delegate('click', "button[data-id^='button-upload']", (e, node) => { 880 qs('#form-upload')?.remove() 881 882 document.body.insertAdjacentHTML( 883 'afterbegin', 884 '<form enctype="multipart/form-data" id="form-upload" style="display: none;"><input type="file" name="file" /></form>' 885 ) 886 887 const fileInput = qs('#form-upload input[name="file"]') 888 fileInput.click() 889 890 clearInterval(uploadTimer) 891 uploadTimer = setInterval(() => { 892 if (!fileInput.value) return 893 clearInterval(uploadTimer) 894 895 node.classList.add('disabled') 896 fetch('index.php?route=tool/upload', { 897 method: 'POST', 898 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 899 body: new FormData(qs('#form-upload')) 900 }) 901 .then(r => r.json()) 902 .then(json => { 903 removeAll('.text-danger') 904 905 const input = node.parentElement?.querySelector('input') 906 if (json.error) { 907 insertAfter(input, `<div class="text-danger">${json.error}</div>`) 908 } 909 if (json.success) { 910 alert(json.success) 911 if (input) input.value = json.code 912 } 913 }) 914 .catch(err => alert(err)) 915 .finally(() => node.classList.remove('disabled')) 916 }, 500) 917 }) 918 919 // ÐодпиÑка на ÑаÑÑÑÐ»ÐºÑ (ÑоÑма в подвале) 920 delegate('click', '#newsletter_submit', (e, button) => { 921 e.preventDefault() 922 const form = button.closest('form') 923 if (!form) { 924 console.log('Form not found') 925 return 926 } 927 928 const setReadonly = readonly => { 929 ;['email', 'name', 'phone'].forEach(name => { 930 const input = form.querySelector(`input[name=${name}]`) 931 if (input) input.readOnly = readonly 932 }) 933 } 934 935 button.classList.add('disabled') 936 setReadonly(true) 937 938 postJSON('index.php?route=send_forms/send/subscribe', new URLSearchParams(new FormData(form))) 939 .then(json => { 940 pushDataLayer(json) 941 form.querySelectorAll('.text-danger').forEach(el => el.remove()) 942 943 if (json.success) { 944 const modal = qs('#discount_mail') 945 if (modal) { 946 const title = modal.querySelector('.modal-notification__title') 947 const text = modal.querySelector('.modal-notification__text') 948 if (title) title.innerHTML = json.success 949 if (text) text.innerHTML = json.text_success ?? '' 950 } 951 ;['email', 'name', 'phone'].forEach(name => { 952 const input = form.querySelector(`input[name=${name}]`) 953 if (input) input.value = '' 954 }) 955 const policy = form.querySelector('input[name="policy_agreement"]') 956 if (policy) policy.checked = false 957 show(modal) 958 } 959 960 if (json.error_email) { 961 insertAfter(form.querySelector('input[name=email]'), `<div class="text-danger">${json.error_email}</div>`) 962 } 963 if (json.error_phone) { 964 insertAfter(form.querySelector('input[name=phone]'), `<div class="text-danger">${json.error_phone}</div>`) 965 } 966 if (json.error_policy) { 967 insertAfter(form.querySelector('input[name=policy_agreement]'), `<div class="text-danger">${json.error_policy}</div>`) 968 } 969 970 // ÑообÑÐµÐ½Ð¸Ñ Ð¾Ð± оÑÐ¸Ð±ÐºÐ°Ñ Ð¸ÑÑезаÑÑ ÑеÑез 5 ÑекÑнд 971 setTimeout(() => removeAll('.text-danger'), 5000) 972 }) 973 .catch(err => console.log(err)) 974 .finally(() => { 975 button.classList.remove('disabled') 976 setReadonly(false) 977 }) 978 }) 979 980 // ÐодÑвеÑка полей Ñ Ð¾Ñибками, оÑÑиÑованнÑми ÑеÑвеÑом 981 qsa('.text-danger').forEach(el => { 982 const group = el.parentElement?.parentElement 983 if (group?.classList.contains('form-group')) group.classList.add('has-error') 984 }) 985 986 /* ============================================================ 987 * 7. ÐÐÐЮТÐ, ÐÐÐСÐ, ÐÐÐ ÐÐÐЮЧÐТÐÐЬ ÐÐÐРСÐÐСÐÐ 988 * ============================================================ */ 989 990 // ÐеÑеклÑÑение валÑÑÑ: пÑÐ½ÐºÑ ÑпиÑка пиÑÐµÑ ÐºÐ¾Ð´ в ÑкÑÑÑое поле и ÑабмиÑÐ¸Ñ ÑоÑÐ¼Ñ 991 qsa('#form-currency .currency-select').forEach(item => { 992 item.addEventListener('click', e => { 993 e.preventDefault() 994 const form = qs('#form-currency') 995 const code = form?.querySelector('input[name="code"]') 996 if (code) code.value = item.getAttribute('name') 997 form?.submit() 998 }) 999 }) 1000 1001 // ÐоиÑк в Ñапке: кнопка ведÑÑ Ð½Ð° ÑÑÑаниÑÑ Ð²ÑдаÑи, Enter лиÑÑ Ð½Ð°Ð¶Ð¸Ð¼Ð°ÐµÑ 1002 // кнопкÑ. Ð ÑекÑÑей вÑÑÑÑке кнопки Ð½ÐµÑ â пеÑÐµÑ Ð¾Ð´Ð¾Ð¼ на вÑдаÑÑ 1003 // (product/esearch) ÑпÑавлÑÐµÑ inline-ÑкÑÐ¸Ð¿Ñ common/search.twig 1004 const headerSearchInput = qs('header #search input[name="search"]') 1005 const headerSearchButton = headerSearchInput?.parentElement?.querySelector('button') 1006 1007 const goToSearch = () => { 1008 let url = `${qs('base')?.href ?? '/'}index.php?route=product/search` 1009 const value = headerSearchInput?.value 1010 if (value) url += `&search=${encodeURIComponent(value)}` 1011 location = url 1012 } 1013 1014 headerSearchButton?.addEventListener('click', goToSearch) 1015 headerSearchInput?.addEventListener('keydown', e => { 1016 if (e.key === 'Enter') headerSearchButton?.click() 1017 }) 1018 1019 // ÐеÑеклÑÑаÑÐµÐ»Ñ Ð²Ð¸Ð´Ð° «ÑпиÑок/ÑеÑка» (ÑÑÑаниÑа поиÑка, блог) 1020 const listView = qs('#list-view') 1021 const gridView = qs('#grid-view') 1022 1023 const applyListView = () => { 1024 qsa('#content .product-grid > .clearfix').forEach(el => el.remove()) 1025 qsa('#content .row > .product-grid').forEach(el => { 1026 el.className = 'product-layout product-list col-xs-12' 1027 }) 1028 gridView?.classList.remove('active') 1029 listView?.classList.add('active') 1030 localStorage.setItem('display', 'list') 1031 } 1032 1033 const applyGridView = () => { 1034 // колиÑеÑÑво боковÑÑ ÐºÐ¾Ð»Ð¾Ð½Ð¾Ðº опÑеделÑÐµÑ ÑиÑÐ¸Ð½Ñ ÐºÐ°ÑÑоÑек 1035 const cols = qsa('#column-right, #column-left').length 1036 const className = 1037 cols === 2 1038 ? 'product-layout product-grid col-lg-6 col-md-6 col-sm-12 col-xs-12' 1039 : cols === 1 1040 ? 'product-layout product-grid col-lg-4 col-md-4 col-sm-6 col-xs-12' 1041 : 'product-layout product-grid col-lg-3 col-md-3 col-sm-6 col-xs-12' 1042 qsa('#content .product-list').forEach(el => (el.className = className)) 1043 listView?.classList.remove('active') 1044 gridView?.classList.add('active') 1045 localStorage.setItem('display', 'grid') 1046 } 1047 1048 listView?.addEventListener('click', applyListView) 1049 gridView?.addEventListener('click', applyGridView) 1050 1051 if (listView || gridView) { 1052 if (localStorage.getItem('display') === 'list') { 1053 applyListView() 1054 } else { 1055 applyGridView() 1056 } 1057 } 1058 1059 // ФоÑма логина в блоке оÑÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð·Ð°ÐºÐ°Ð·Ð°: Enter оÑпÑавлÑÐµÑ 1060 delegate('keydown', "#collapse-checkout-option input[name='email'], #collapse-checkout-option input[name='password']", e => { 1061 if (e.key === 'Enter') qs('#collapse-checkout-option #button-login')?.click() 1062 }) 1063 1064 // Ðнопки «+/â» колиÑеÑÑва в коÑзине (js_count_cart): обновлÑÑÑ Ð¿Ð¾Ð·Ð¸ÑÐ¸Ñ Ð½Ð° ÑеÑвеÑе
1065 delegate('click', '.js_count_cart div', (e, control) => { 1066 if (control.getAttribute('onclick')) return 1067 const wrap = control.closest('.js_count_cart') 1068 wrap?.querySelector('.minus')?.classList.add('ui-state-disabled') 1069 wrap?.querySelector('.plus')?.classList.add('ui-state-disabled') 1070 const count = wrap?.querySelector('input') 1071 if (!count) return 1072 setTimeout(() => { 1073 cart.update(count.dataset.key, count.value) 1074 }, 100) 1075 }) 1076 1077 // ÐÑÑмой ввод колиÑеÑÑва в коÑзине 1078 delegate('change', '.count_cart input', (e, input) => { 1079 if (Number(input.value) <= 0) input.value = 1 1080 cart.update(input.dataset.key, input.value) 1081 }) 1082 1083 // Удаление пÑевÑÑ Ð·Ð°Ð³ÑÑженного Ñайла (dropzone) 1084 delegate('click', '.dz-error-mark', (e, mark) => { 1085 mark.closest('.dz-preview')?.remove() 1086 }) 1087 1088 /* ============================================================ 1089 * 6. ÐÐРТÐЧÐРТÐÐÐÐ Ð: ÐЫÐÐÐ ÐÐЦÐÐ, ÐÐÐ ÐÐ¥ÐÐЫ 1090 * ============================================================ */ 1091 1092 // ÐлиÑка-ÑÑÑлка Ñ data-href 1093 delegate('click', '.custom__go-data_href [data-href]:not(.disable)', (e, el) => { 1094 location = el.dataset.href 1095 }) 1096 1097 // ÐÑÐ±Ð¾Ñ Ð¾Ð¿Ñии (ÑазмеÑа) в попапе бÑÑÑÑой покÑпки: пеÑеÑиÑовка попапа 1098 delegate('click', '.render_product_popup_data_option [data-product-id]:not(.active):not(.disable)', (e, el) => { 1099 const product_id = parseInt(el.dataset.productId, 10) 1100 if (isNaN(product_id)) return 1101 1102 postJSON('index.php?route=product/product/getProductAjax', `product_id=${product_id}&option_popup=1`) 1103 .then(json => { 1104 removeAll('.render_product_popup_data_option .text-danger') 1105 1106 if (json.popup_modal_body_html) { 1107 const container = qs('.render_product_popup_data_option') 1108 if (container) container.outerHTML = json.popup_modal_body_html 1109 } 1110 1111 if (json.error_product) { 1112 el.classList.remove('active') 1113 el.classList.add('disable') 1114 const container = qs('.render_product_popup_data_option') 1115 container?.insertAdjacentHTML('beforeend', `<div class="text-danger">${json.error_product}</div>`) 1116 container?.querySelector('[data-active="active"]')?.classList.add('active') 1117 } 1118 }) 1119 .catch(err => console.log(err)) 1120 }) 1121 1122 // ÐÑÐ±Ð¾Ñ Ð¾Ð¿Ñии на каÑÑоÑке ÑоваÑа в лиÑÑинге: пеÑеÑиÑовка каÑÑоÑки 1123 delegate('click', '.render_product_data_option [data-product-id]:not(.active):not(.disable)', (e, el) => { 1124 const product_id = parseInt(el.dataset.productId, 10) 1125 const view = el.dataset.productView ?? '' 1126 let image_width = parseFloat(el.dataset.productImageWidth) 1127 let image_height = parseFloat(el.dataset.productImageHeight) 1128 1129 const container = el.closest('.render_product_data') 1130 if (isNaN(product_id) || !container) return 1131 1132 if (isNaN(image_width)) image_width = 0 1133 if (isNaN(image_height)) image_height = 0 1134 1135 postJSON( 1136 'index.php?route=product/product/getProductAjax', 1137 `product_id=${product_id}&view=${view}&image_width=${image_width}&image_height=${image_height}` 1138 ) 1139 .then(json => { 1140 container.querySelectorAll('.text-danger').forEach(elm => elm.remove()) 1141 1142 if (json.product) { 1143 container.outerHTML = json.product 1144 } 1145 1146 if (json.error_product) { 1147 el.classList.remove('active') 1148 el.classList.add('disable') 1149 container.insertAdjacentHTML('beforeend', `<div class="text-danger">${json.error_product}</div>`) 1150 container.querySelector('[data-active="active"]')?.classList.add('active') 1151 } 1152 }) 1153 .catch(err => console.log(err)) 1154 }) 1155 }) 1156 1157 /* ============================================================ 1158 * 8. ÐÐÐ ÐÐÐÐ (CART), ÐУÐÐÐЫ (VOUCHER) 1159 * ============================================================ */ 1160 1161 // ----------------------------- 1162 // ÐаÑÑоÑки Ñкидок (лÑÑÑÐ°Ñ Ñена / кÑпон / ÐºÐ¾Ð¼Ð¿Ð»ÐµÐºÑ / online) â обÑÐ°Ñ 1163 // логика Ð´Ð»Ñ Ð±Ð»Ð¾ÐºÐ° ÑÐµÐ½Ñ Ð½Ð° ÑÑÑаниÑе ÑоваÑа (product-core.js вÑзÑÐ²Ð°ÐµÑ 1164 // window.renderDiscountCards) и Ð´Ð»Ñ Ð¼Ð¾Ð´Ð°Ð»ÐºÐ¸ «ÑÐ¾Ð²Ð°Ñ Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½ в коÑзин
1164Ñ» 1165 // (ÑÑа модалка оÑкÑÑваеÑÑÑ Ñ Ð»Ñбой ÑÑÑаниÑÑ â Ñ ÐºÐ°ÑÑоÑки в каÑалоге, 1166 // Ñо ÑÑÑаниÑÑ ÑоваÑа, из коÑÐ·Ð¸Ð½Ñ â поÑÑÐ¾Ð¼Ñ Ð»Ð¾Ð³Ð¸ÐºÐ° живÑÑ Ð·Ð´ÐµÑÑ, в common.js, 1167 // коÑоÑÑй подклÑÑÑн глобалÑно, а не в product-core.js, коÑоÑÑй еÑÑÑ 1168 // ÑолÑко на ÑÑÑаниÑе ÑоваÑа). 1169 // ----------------------------- 1170 const DISCOUNT_ICON_BUNDLE = ` 1171 <svg class="discount-price-card__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"> 1172 <rect x="3" y="3" width="13" height="13" rx="2"></rect> 1173 <path d="M8 21h10a2 2 0 0 0 2-2V9"></path> 1174 </svg> 1175 ` 1176 const DISCOUNT_ICON_GIFT = ` 1177 <svg class="discount-compare__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"> 1178 <rect x="3" y="8" width="18" height="4" rx="1"></rect> 1179 <rect x="5" y="12" width="14" height="9" rx="1"></rect> 1180 <path d="M12 8v13"></path> 1181 <path d="M12 8c-1.6 0-3.2-1-3.2-2.6C8.8 3.9 10 3 12 3s3.2.9 3.2 2.4C15.2 7 13.6 8 12 8Z"></path> 1182 </svg> 1183 ` 1184 const DISCOUNT_ICON_TAG = ` 1185 <svg class="discount-compare__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"> 1186 <path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8Z"></path> 1187 <circle cx="7.5" cy="7.5" r="1.5"></circle> 1188 </svg> 1189 ` 1190 const DISCOUNT_ICON_CHEVRON = ` 1191 <svg class="discount-compare__cta-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"> 1192 <path d="m9 6 6 6-6 6"></path> 1193 </svg> 1194 ` 1195 1196 // ТÑлÑип-подÑказка Ñ ÑÑлÑка каÑÑоÑки: пеÑеиÑполÑзÑÐµÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑнÑй паÑÑеÑн 1197 // .pl_item/.pl_tooltip (клик â toggleTooltip() из common/footer.twig, 1198 // наведение â CSS-пÑавило .pl_item:hover в style.scss), поÑÑÐ¾Ð¼Ñ ÑабоÑÐ°ÐµÑ 1199 // ÑÑÐ°Ð·Ñ Ð¿Ð¾Ñле вÑÑавки ÑеÑез innerHTML без повÑоÑной иниÑиализаÑии JS. 1200 function renderInfoTooltip(text) { 1201 if (!text) return '' 1202 return ` 1203 <span class="discount-price-card__info pl_item" onclick="toggleTooltip(this)"> 1204 <svg class="discount-price-card__info-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 1205 <circle cx="12" cy="12" r="10"></circle> 1206 <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path> 1207 <line x1="12" y1="17" x2="12.01" y2="17"></line> 1208 </svg> 1209 <span class="pl_tooltip">${text}</span> 1210 </span> 1211 ` 1212 } 1213 1214 // ÐопиÑование кода кÑпона по ÐºÐ»Ð¸ÐºÑ Ð½Ð° кнопкÑ: на 1.5Ñ Ð¿Ð¾Ð´Ð¼ÐµÐ½Ñем ÐµÑ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑ 1215 // на data-label-done, без обÑего ÑоÑÑ-Ð¼ÐµÑ Ð°Ð½Ð¸Ð·Ð¼Ð° â ÑÑа каÑÑоÑка ÑендеÑиÑÑÑ 1216 // и на ÑÑÑаниÑе ÑоваÑа, и в модалке коÑзинÑ, где оÑделÑного ÑоÑÑа неÑ. 1217 // ÐодменÑем ÑекÑÑ ÑолÑко Ñ .discount-compare__cta-label, ÑÑÐ¾Ð±Ñ Ð½Ðµ ÑеÑÑÑÑ Ð¸ÐºÐ¾Ð½ÐºÑ. 1218 function copyPromoCode(el) { 1219 const { copy: code, labelDone } = el.dataset 1220 const label = el.querySelector('.discount-compare__cta-label') ?? el 1221 const initial = label.textContent 1222 1223 navigator.clipboard?.writeText(code).then(() => { 1224 label.textContent = labelDone 1225 setTimeout(() => { 1226 label.textContent = initial 1227 }, 1500) 1228 }) 1229 } 1230 window.copyPromoCode = copyPromoCode 1231 1232 function formatDiscountPrice(amount, decimalPoint, thousandPoint, currencySuffix) { 1233 const rounded = String(Math.round(Math.max(0, amount))) 1234 const grouped = rounded.replace(/\B(?=(\d{3})+(?!\d))/g, thousandPoint || '') 1235 return `${grouped} ${currencySuffix}`.trim() 1236 } 1237 1238 function formatDiscountAmount(amount, thousandPoint) { 1239 const rounded = String(Math.round(Math.max(0, amount))) 1240 return rounded.replace(/\B(?=(\d{3})+(?!\d))/g, thousandPoint || '') 1241 } 1242 1243 // container â ÑÐ»ÐµÐ¼ÐµÐ½Ñ .discount-price-rows, кÑда бÑдÑÑ Ð²ÑÑÐ°Ð²Ð»ÐµÐ½Ñ ÐºÐ°ÑÑоÑки. 1244 // basePrice â ÑекÑÑÐ°Ñ Ñена ÑоваÑа за единиÑÑ (Ñ ÑÑÑÑом special, без ÑкидоÐ
1244º 1245 // кÑпона/бонÑÑов). onlinePercent/onlineEligible â ÑÑÐ»Ð¾Ð²Ð¸Ñ Ñквозной online-Ñкидки 1246 // (total_special_discount), categoryPercent/categoryEligible â Ñкидки по каÑегоÑии 1247 // (total_category_discount); оба пÑÐ¸Ñ Ð¾Ð´ÑÑ Ð¸Ð· window.PRODUCT_PAGE на ÑÑÑаниÑе ÑоваÑа. 1248 // Рмодалке коÑÐ·Ð¸Ð½Ñ Ð½Ð¸ Ñа, ни дÑÑÐ³Ð°Ñ Ð½Ðµ ÑÑиÑаÑÑÑÑ â Ñм. cart_modal_body.twig. 1249 // ÐодÑли взаимоиÑклÑÑаÑÑие по ÑоваÑÑ (Ñм. ModelExtensionTotalCategoryDiscount / 1250 // ModelExtensionTotalSpecialDiscount), поÑÑÐ¾Ð¼Ñ Ð¿Ð¾ÐºÐ°Ð·Ñваем ÑолÑко Ð¾Ð´Ð½Ñ ÐºÐ°ÑÑоÑÐºÑ â 1251 // каÑегоÑÐ¸Ð¹Ð½Ð°Ñ Ñкидка пÑиоÑиÑеÑнее как более ÑпеÑиÑиÑнаÑ. 1252 function renderDiscountCards(container, productId, basePrice, currencySuffix, onlinePercent, onlineEligible, categoryPercent, categoryEligible) { 1253 if (!container || !productId || !basePrice) return 1254 1255 const categoryAmount = (categoryEligible && categoryPercent > 0) 1256 ? basePrice * categoryPercent / 100 1257 : 0 1258 1259 const onlineAmount = (!categoryAmount && onlineEligible && onlinePercent > 0) 1260 ? basePrice * onlinePercent / 100 1261 : 0 1262 1263 const lang = document.documentElement.lang 1264 const langParam = lang ? `&lang=${lang}` : '' 1265 1266 fetch(`index.php?route=extension/total/product_pricing/info&product_id=${productId}${langParam}`, { 1267 headers: { 'X-Requested-With': 'XMLHttpRequest' } 1268 }) 1269 .then(res => res.json()) 1270 .then(({ coupon, reward, bundle, one_plus_one, is_logged, text }) => { 1271 const fmt = amount => formatDiscountPrice(amount, text.decimal_point, text.thousand_point, currencySuffix) 1272 const rows = [] 1273 // ÐаÑианÑÑ ÐºÑпиÑÑ Ð´ÐµÑевле Ñ ÐºÐ¾Ð½ÐµÑной Ñеной (каÑегоÑиÑ/online/кÑпон/бонÑÑ) â 1274 // взаимоиÑклÑÑаÑÑие, из Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·ÑваеÑÑÑ ÑолÑко ÑамÑй вÑгоднÑй. 1275 const priceOptions = [] 1276 1277 if (bundle) { 1278 const bundleNote = text.bundle_note 1279 .replace('{percent}', bundle.percent) 1280 .replace('{count}', bundle.count) 1281 1282 rows.push(` 1283 <div class="discount-price-card discount-price-card--bundle"> 1284 <div class="discount-price-card__left"> 1285<!-- ${DISCOUNT_ICON_BUNDLE}--> 1286 <div class="discount-price-card__text"> 1287 <span class="discount-price-card__label">${renderInfoTooltip(text.bundle_tooltip)}${text.bundle_label}</span> 1288 <span class="discount-price-card__note">${bundleNote}</span> 1289 </div> 1290 </div> 1291 <a class="discount-price-card__cta" href="${bundle.url}">${text.bundle_cta}</a> 1292 </div> 1293 `) 1294 } 1295 1296 if (one_plus_one) { 1297 const opoFill = str => str 1298 .replace('{units_needed}', one_plus_one.units_needed) 1299 .replace('{group_size}', one_plus_one.group_size) 1300 .replace('{discount_price}', formatDiscountAmount(one_plus_one.discount_price, text.thousand_point)) 1301 1302 const note = opoFill(one_plus_one.is_free ? text.one_plus_one_note_free : text.one_plus_one_note) 1303 const tooltip = opoFill(one_plus_one.is_free ? text.one_plus_one_tooltip_free : text.one_plus_one_tooltip) 1304 const percent = one_plus_one.percent || 0 1305 1306 rows.push(` 1307 <div class="discount-price-card discount-price-card--bundle"> 1308 <div class="discount-price-card__left"> 1309 <div class="discount-price-card__text"> 1310 <span class="discount-price-card__label">${text.one_plus_one_label}${renderInfoTooltip(tooltip)}</span> 1311 <div class="discount-price-card__bar${percent >= 100 ? ' is-complete' : ''}"> 1312 <div class="discount-price-card__bar-fill" style="width: ${percent}%;"></div> 1313 </div> 1314 <span class="discount-price-card__note">${note}</span> 1315 </div> 1316 </div> 1317 </div> 1318 `) 1319 } 1320 1321 let couponAmount = 0 1322 let couponPrice = basePrice 1323 1324 if (coupon) { 1325 couponAmount = coupon.type === 'F' 1326 ? Math.min(coupon.discount, basePrice) 1327 : basePrice * coupon.discount / 100 1328 1329 couponPrice = basePrice - couponAmount 1330 if (coupon.allow_stack && (onlineAmount > 0 || categoryAmount > 0)) { 1331 couponPrice -= (onlineAmount || categoryAmount) 1332 } 1333 } 1334 1335 // ÐелÑÐ½Ð°Ñ ÐºÐ½Ð¾Ð¿ÐºÐ° каÑÑоÑки кÑпона/бонÑÑа â обÑабоÑÑик делегиÑован на document 1336 // (Ñм. «ÐÐ ÐÐÐÐÐÐÐÐ ÐÐÐУСÐ/ÐУÐÐÐÐ ÐÐ ÐÐРТÐЧÐРЦÐÐЫ» ниже): гоÑÑÑ â оÐ
1336ºÐ½Ð¾ Ð²Ñ Ð¾Ð´Ð°, 1337 // клиенÑÑ â ÑÐ¾Ð²Ð°Ñ Ð² коÑÐ·Ð¸Ð½Ñ + пÑименение бонÑÑа/кÑпона + пеÑÐµÑ Ð¾Ð´ на ÑекаÑÑ. 1338 const renderApplyCta = (label, attrs) => ` 1339 <button type="button" class="discount-compare__cta" ${attrs} data-logged="${is_logged ? 1 : 0}"> 1340 <span class="discount-compare__cta-label">${label}</span>${DISCOUNT_ICON_CHEVRON} 1341 </button> 1342 ` 1343 1344 // ÐодпиÑÑ Ð¿Ð¾Ð´ Ñеной: "<ÑÑловие> · ÑÐºÐ¾Ð½Ð¾Ð¼Ð¸Ñ X" â ÑÑловие (напÑÐ¸Ð¼ÐµÑ 1345 // "ÐоÑле ÑегиÑÑÑаÑии") необÑзаÑелÑно, Ñогда оÑÑаÑÑÑÑ ÑолÑко ÑкономиÑ. 1346 const renderNote = (prefix, savingsAmount) => { 1347 const savings = text.savings_note.replace('{amount}', fmt(savingsAmount)) 1348 return prefix ? `${prefix} · ${savings.charAt(0).toLowerCase()}${savings.slice(1)}` : savings 1349 } 1350 1351 if (categoryAmount > 0) { 1352 priceOptions.push({ 1353 label: text.category_label, 1354 icon: DISCOUNT_ICON_TAG, 1355 note: text.category_note, 1356 finalPrice: basePrice - categoryAmount, 1357 }) 1358 } 1359 1360 if (onlineAmount > 0) { 1361 priceOptions.push({ 1362 label: text.online_label, 1363 icon: DISCOUNT_ICON_TAG, 1364 note: text.online_note, 1365 finalPrice: basePrice - onlineAmount, 1366 }) 1367 } 1368 1369 if (coupon) { 1370 priceOptions.push({ 1371 label: text.coupon_label, 1372 icon: DISCOUNT_ICON_TAG, 1373 // Ðод â Ñипом напÑоÑив ÑайÑла (клик копиÑÑеÑ), подпиÑÑ â как Ñ Ð±Ð¾Ð½ÑÑной 1374 // каÑÑоÑки: гоÑÑÑ "ÐоÑле ÑегиÑÑÑаÑии · ÑÐºÐ¾Ð½Ð¾Ð¼Ð¸Ñ X", клиенÑÑ â ÑолÑко ÑкономиÑ. 1375 chip: `<span class="discount-compare__chip" data-copy="${coupon.code}" data-label-done="${text.coupon_copied}" onclick="copyPromoCode(this)">${coupon.code}</span>`, 1376 note: is_logged ? '' : text.reward_option_note, 1377 finalPrice: couponPrice, 1378 cta: renderApplyCta(text.coupon_cta, `data-discount-apply="coupon" data-coupon="${coupon.code}"`), 1379 }) 1380 } 1381 1382 // ÐонÑÑ ÑÑиÑаеÑÑÑ Ð¾Ñ basePrice (оÑновной/special-ÑенÑ), Ñак же, как 1383 // в ÑеалÑном ÑаÑÑÑÑе коÑÐ·Ð¸Ð½Ñ (ModelExtensionTotalReward::getTotal()), 1384 // где иÑполÑзÑеÑÑÑ $product['total'] без ÑÑÑÑа кÑпона/online-Ñкидки. 1385 // Math.ceil â окÑÑглÑем в полÑÐ·Ñ Ð¿Ð¾ÐºÑпаÑелÑ. ЧекаÑÑ (ControllerExtensionTotalReward::index() 1386 // и ::reward()) ÑепеÑÑ Ñоже ÑÑиÑÐ°ÐµÑ Ñвой макÑимÑм ÑеÑез ceil(), Ñак ÑÑо показанное здеÑÑ 1387 // ÑиÑло вÑегда пÑÐ¾Ñ Ð¾Ð´Ð¸Ñ Ð²Ð°Ð»Ð¸Ð´Ð°ÑÐ¸Ñ Ð¿Ñи пÑименении на ÑекаÑÑе. 1388 const bonusAmount = (reward && reward.percent > 0) 1389 ? Math.ceil(Math.min(reward.balance, basePrice * reward.percent / 100)) 1390 : 0 1391 1392 if (bonusAmount > 0) { 1393 priceOptions.push({ 1394 label: text.reward_label, 1395 icon: DISCOUNT_ICON_GIFT, 1396 // ÐоÑÑÑ â "ÐоÑле ÑегиÑÑÑаÑии" (бонÑÑ ÐµÑÑ Ð½Ðµ наÑиÑлен), клиенÑÑ â его баланÑ. 1397 note: reward.is_logged 1398 ? text.reward_note.replace('{balance}', fmt(reward.balance)) 1399 : text.reward_option_note, 1400 finalPrice: basePrice - bonusAmount, 1401 cta: renderApplyCta(reward.is_logged ? text.reward_cta_logged : text.reward_cta, 'data-discount-apply="reward"'), 1402 }) 1403 } 1404 1405 // СпоÑÐ¾Ð±Ñ Ð²Ð·Ð°Ð¸Ð¼Ð¾Ð¸ÑклÑÑаÑÑие (на ÑекаÑÑе пÑименÑеÑÑÑ ÑолÑко один), 1406 // поÑÑÐ¾Ð¼Ñ Ð¿Ð¾ÐºÐ°Ð·Ñваем Ð¾Ð´Ð½Ñ ÐºÐ°ÑÑоÑÐºÑ â ÑамÑй вÑгоднÑй ваÑианÑ. 1407 if (priceOptions.length) { 1408 const best = priceOptions.reduce((acc, o) => o.finalPrice < acc.finalPrice ? o : acc) 1409 1410 rows.push(` 1411 <div class="discount-compare__option"> 1412 <span class="discount-compare__head"> 1413 <span class="discount-compare__title">${best.icon}${best.label}</span> 1414 ${best.chip ?? ''} 1415 </span> 1416 <span class="discount-compare__price"> 1417 <span class="discount-compare__number">
1417${formatDiscountAmount(best.finalPrice, text.thousand_point)}</span> 1418 <span class="discount-compare__currency">${currencySuffix}</span> 1419 </span> 1420 <span class="discount-compare__note">${renderNote(best.note, basePrice - best.finalPrice)}</span> 1421 ${best.cta ?? ''} 1422 </div> 1423 `) 1424 } 1425 1426 // ÐолÑÑе одной каÑÑоÑки (акÑÐ¸Ñ bundle/1+1 и/или лÑÑÑий ваÑÐ¸Ð°Ð½Ñ 1427 // ÑенÑ) â над ними Ñапка "вÑбеÑи ваÑианÑ", под ними â "не ÑÑммиÑÑÑÑÑÑ". 1428 if (rows.length > 1) { 1429 rows.unshift(`<div class="discount-compare__header"><span>${text.choose_note}</span>${renderInfoTooltip(text.compare_header_tooltip)}</div>`) 1430 rows.push(`<div class="discount-compare__footer">${renderInfoTooltip(text.compare_footer_tooltip)}<span>${text.stack_footer_note}</span></div>`) 1431 } 1432 1433 container.innerHTML = rows.join('') 1434 }) 1435 .catch(() => {}) 1436 } 1437 1438 window.renderDiscountCards = renderDiscountCards 1439 1440 /* ============================================================ 1441 * ÐÐ ÐÐÐÐÐÐÐÐ ÐÐÐУСÐ/ÐУÐÐÐÐ ÐÐ ÐÐРТÐЧÐРЦÐÐЫ 1442 * ============================================================ 1443 * Ðнопка "FoloseÈte bonusul" / кÑпона в каÑÑоÑке renderDiscountCards: 1444 * - гоÑÑÑ â оÑкÑÑваем #login_modal (Ð²Ñ Ð¾Ð´/ÑегиÑÑÑаÑÐ¸Ñ Ð¿Ð¾ ÑелеÑонÑ; поÑле 1445 * Ð²Ñ Ð¾Ð´Ð° ÑÑÑаниÑа пеÑезагÑÑжаеÑÑÑ, и кнопка Ñже ÑабоÑÐ°ÐµÑ ÐºÐ°Ðº Ñ ÐºÐ»Ð¸ÐµÐ½Ñа); 1446 * - ÐºÐ»Ð¸ÐµÐ½Ñ Ð½Ð° ÑÑÑаниÑе ÑоваÑа (#discount-price-rows) â ÑнаÑала добавлÑем 1447 * ÑÐ¾Ð²Ð°Ñ Ð² коÑÐ·Ð¸Ð½Ñ Ñем же запÑоÑом, ÑÑо и #button-cart 1448 * (window.productAddToCart из product-features.js, Ñ Ð²ÑбÑаннÑм ÑазмеÑом); 1449 * в модалке коÑÐ·Ð¸Ð½Ñ ÑÐ¾Ð²Ð°Ñ Ñже добавлен â ÑÑÐ¾Ñ Ñаг пÑопÑÑкаеÑÑÑ; 1450 * - заÑем пÑименÑем бонÑÑ (extension/total/product_pricing/applyReward â 1451 * макÑимÑм, доÑÑÑпнÑй по коÑзине) или кÑпон (ÑÑаÑнÑй 1452 * extension/total/coupon/coupon) и пеÑÐµÑ Ð¾Ð´Ð¸Ð¼ на ÑекаÑÑ. 1453 */ 1454 document.addEventListener('click', async e => { 1455 const btn = e.target.closest('[data-discount-apply]') 1456 if (!btn || btn.disabled) return 1457 e.preventDefault() 1458 1459 if (btn.dataset.logged !== '1') { 1460 const loginModal = document.getElementById('login_modal') 1461 if (loginModal) loginModal.style.display = 'block' 1462 return 1463 } 1464 1465 const option = btn.closest('.discount-compare__option') 1466 const showError = message => { 1467 let box = option?.querySelector('.discount-compare__cta-error') 1468 if (!box && option) { 1469 box = document.createElement('span') 1470 box.className = 'discount-compare__cta-error' 1471 option.appendChild(box) 1472 } 1473 if (box) box.textContent = message 1474 } 1475 option?.querySelector('.discount-compare__cta-error')?.remove() 1476 1477 btn.disabled = true 1478 1479 try { 1480 if (btn.closest('#discount-price-rows') && typeof window.productAddToCart === 'function') { 1481 const added = await window.productAddToCart() 1482 if (!added) { 1483 // ÐÑибки опÑий (не вÑбÑан ÑÐ°Ð·Ð¼ÐµÑ Ð¸ Ñ.п.) вÑÐ²Ð¾Ð´Ð¸Ñ Ñам productAddToCart â 1484 // пÑокÑÑÑиваем к пеÑвой из Ð½Ð¸Ñ . 1485 qs('#product .text-danger')?.scrollIntoView({ behavior: 'smooth', block: 'center' }) 1486 return 1487 } 1488 } 1489 1490 const lang = document.documentElement.lang 1491 const langParam = lang ? `&lang=${lang}` : '' 1492 const isCoupon = btn.dataset.discountApply === 'coupon' 1493 const body = new URLSearchParams() 1494 if (isCoupon) body.append('coupon', btn.dataset.coupon || '') 1495 1496 const res = await fetch( 1497 `index.php?route=${isCoupon ? 'extension/total/coupon/coupon' : 'extension/total/product_pricing/applyReward'}${langParam}`, 1498 { 1499 method: 'POST', 1500 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 1501 body 1502 } 1503 ) 1504 const json = await res.json() 1505 1506 if (json.error) { 1507 showError(json.error) 1508 return 1509 } 1510 1511 window.location.href = json.redirect || `/index.php?route=checkout/checkout${langParam}` 1512 } catch (err) { 1513 console.error(err) 1514 } finally { 1515 btn.disabled = false 1516 } 1517 }) 1518
1519 // ÐÑÑиÑовка модалки коÑÐ·Ð¸Ð½Ñ Ð¸Ð· оÑвеÑа ÑеÑвеÑа 1520 const renderCartModal = json => { 1521 const body = qs('#cartmodal .modal_body .render__modal_body') 1522 if (body) body.innerHTML = json.modal_body_html ?? '' 1523 1524 const discountRows = body?.querySelector('.discount-price-rows') 1525 if (discountRows) { 1526 const basePrice = parseFloat(discountRows.dataset.basePrice) 1527 renderDiscountCards( 1528 discountRows, 1529 discountRows.dataset.productId, 1530 basePrice, 1531 discountRows.dataset.currency || '' 1532 ) 1533 } 1534 1535 /* 1536 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: каÑÑÑÐµÐ»Ñ slick Ð´Ð»Ñ Ð±Ð»Ð¾ÐºÐ° Â«Ñ ÑÑим покÑпаÑÑ» в модалке â 1537 * библиоÑека slick Ñдалена Ñ ÑайÑа, вÑзов бÑл под пÑовеÑкой typeof $.fn.slick. 1538 */ 1539 1540 updateCartCounters(json) 1541 show(qs('#cartmodal')) 1542 1543 if (json.fbq && typeof fbq !== 'undefined') fbq(json.fbq) 1544 } 1545 1546 // ÐÑвод оÑибок опÑий в попапе вÑбоÑа ÑазмеÑа (#chose_sizes) 1547 const renderCartOptionErrors = json => { 1548 if (json.error?.option) { 1549 for (const key of Object.keys(json.error.option)) { 1550 const element = qs(`#chose_sizes [data-input-option="${key}"]`) 1551 if (!element) continue 1552 if (element.parentElement.classList.contains('input-group')) { 1553 insertAfter(element.parentElement, `<div class="text-danger">${json.error.option[key]}</div>`) 1554 } else { 1555 insertAfter(element, `<div class="text-danger">${json.error.option[key]}</div>`) 1556 } 1557 } 1558 } else if (json.redirect && json.error?.recurring) { 1559 location = json.redirect 1560 } 1561 } 1562 1563 // Ðодмена даннÑÑ Ð»Ð¸ÑÑинга (item_list_id и Ñ.п.) из localStorage Ð´Ð»Ñ GA 1564 const applySavedListInfo = (json, product_id) => { 1565 if (!json.dataLayer) return 1566 const savedItems = JSON.parse(localStorage.getItem('selectedItems')) || {} 1567 if (!Object.prototype.hasOwnProperty.call(savedItems, product_id)) return 1568 const local = savedItems[product_id] 1569 const item = json.dataLayer.ecommerce?.items?.[0] 1570 if (!item) return 1571 if (local.item_list_id) item.item_list_id = local.item_list_id 1572 if (local.item_list_name) item.item_list_name = local.item_list_name 1573 if (local.index) item.index = local.index 1574 } 1575 1576 // ÐбÑий ÑÑенаÑий Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð² коÑÐ·Ð¸Ð½Ñ (иÑполÑзÑеÑÑÑ add и add_single). 1577 // option: ÑÑÑока паÑамеÑÑов ÐÐÐ null â Ñогда ÑоÑма опÑий иÑеÑÑÑ ÑÑдом Ñ _this 1578 const cartAddRequest = (product_id, quantity, option, _this) => { 1579 const self = _this instanceof Element ? _this : null 1580 1581 if (option == null && self) option = serializeOptionForm(self) 1582 1583 const data = `product_id=${product_id}&quantity=${quantity ?? 1}${option ? `&${option}` : ''}` 1584 1585 self?.classList.add('disabled') 1586 qsa('#cart > button').forEach(b => b.classList.add('disabled')) 1587 1588 // lang обÑзаÑелен: без ÑзÑкового пÑеÑикÑа в URL ÑÑого AJAX-запÑоÑа 1589 // startup.php Ð¿Ð°Ð´Ð°ÐµÑ Ð½Ð° деÑолÑнÑй ÑзÑк магазина (ro) вмеÑÑо ÑекÑÑего ÑзÑка ÑÑÑаниÑÑ 1590 const lang = document.documentElement.lang 1591 postJSON(`index.php?route=checkout/cart/add${lang ? `&lang=${lang}` : ''}`, data) 1592 .then(json => { 1593 removeAll('.alert-dismissible, .text-danger') 1594 if (json.error) { 1595 renderCartOptionErrors(json) 1596 } else if (json.success) { 1597 applySavedListInfo(json, product_id) 1598 pushDataLayer(json) 1599 hide(qs('#chose_sizes')) 1600 renderCartModal(json) 1601 } 1602 }) 1603 .catch(err => alert(err)) 1604 .finally(() => { 1605 self?.classList.remove('disabled') 1606 qsa('#cart > button').forEach(b => b.classList.remove('disabled')) 1607 }) 1608 } 1609 1610 const cart = { 1611 // ÐÑÑÑÑÐ°Ñ Ð¿Ð¾ÐºÑпка из лиÑÑинга: ÑнаÑала попап вÑбоÑа ÑазмеÑа, еÑли нÑÐ¶Ð½Ñ Ð¾Ð¿Ñии 1612 preview(product_id, quantity, option, _this) { 1613 const self = _this instanceof Element ? _this : null 1614 1615 if (option == null && self) option = serializeOptionForm(self, true) 1616 1617 const data = `product_id=${product_id}&quantity=${quantity ?? 1}${option ? `&${option}` : ''}` 1618 1619 self?.classList.add('disabled') 1620 qsa('#cart > button').forEach(b => b.classList.add('disabled')) 1621 1622 const lang = document.documentElement.lang 1623 postJSON(`index.php?route=checkout/cart/prev_add${lang ? `&lang=${lang}` : ''}`, data) 1624 .then(json => { 1625 if (json.success) { 1626 pushDataLayer(json) 1627 renderCartModal(json) 1628 } else if (json.popup) { 1629 // нÑÐ¶Ð½Ñ Ð¾Ð¿Ñии â показÑваем попап вÑбоÑа ÑазмеÑа 1630 const body = qs('#chose_sizes .modal_body .render__modal_body') 1631 if (body) body.innerHTML = json.popup_modal_body_html ?? '' 1632 show(qs('#chose_sizes')) 1633 } else if (json.redirect) { 1634 location = json.redirect 1635 } 1636 }) 1637 .catch(err => console.log(err)) 1638 .finally(() => { 1639 self?.classList.remove('disabled') 1640 qsa('#cart > button').forEach(b => b.classList.remove('disabled')) 1641 }) 1642 }, 1643
1644 // ÐбÑÑное добавление в коÑÐ·Ð¸Ð½Ñ 1645 add(product_id, quantity, option, _this) { 1646 cartAddRequest(product_id, quantity, option, _this) 1647 }, 1648 1649 // Ðобавление «оÑобого» ÑоваÑа â ÑнаÑала модалка подÑвеÑÐ¶Ð´ÐµÐ½Ð¸Ñ 1650 add_single(product_id, quantity, option, _this) { 1651 // ÑоÑÐ¼Ñ Ð¾Ð¿Ñий ÑеÑиализÑем ÑÑазÑ: к моменÑÑ Ð¿Ð¾Ð´ÑвеÑÐ¶Ð´ÐµÐ½Ð¸Ñ DOM попапа Ð¼Ð¾Ð¶ÐµÑ ÑмениÑÑÑÑ 1652 const self = _this instanceof Element ? _this : null 1653 if (option == null && self) option = serializeOptionForm(self) 1654 1655 show(qs('#specialProductModal')) 1656 1657 const confirm = qs('#confirm-add-to-cart') 1658 if (confirm) { 1659 confirm.onclick = () => { 1660 hide(qs('#specialProductModal')) 1661 cartAddRequest(product_id, quantity, option, _this) 1662 } 1663 } 1664 }, 1665 1666 // Ðзменение колиÑеÑÑва позиÑии 1667 update(key, quantity) { 1668 postJSON('index.php?route=checkout/cart/update', `key=${key}&quantity=${quantity ?? 1}`) 1669 .then(json => { 1670 pushDataLayer(json) 1671 1672 qsa('.cart > .cart_counter').forEach(el => (el.innerHTML = json.total ?? '')) 1673 1674 // пÑедÑпÑеждение о пÑевÑÑении оÑÑаÑка на Ñкладе 1675 const limitDiv = qs(`.quantity_limit_check_${key}`) 1676 if (limitDiv) { 1677 if (json.quantity_limit) { 1678 limitDiv.innerHTML = json.quantity_limit 1679 limitDiv.classList.add('show') 1680 setTimeout(() => limitDiv.classList.remove('show'), 1000) 1681 } else { 1682 limitDiv.classList.remove('show') 1683 limitDiv.innerHTML = '' 1684 } 1685 } 1686 1687 if (json.cart_total) {
1688 qsa('.cart-fragment_header--price').forEach(el => (el.innerHTML = json.cart_total)) 1689 } 1690 if (json.total) { 1691 qsa('.cart-fragment_header--counter').forEach(el => (el.innerHTML = json.total)) 1692 } 1693 1694 // пеÑеÑиÑовÑваем ÑодеÑжимое ÑÑÑаниÑÑ ÐºÐ¾ÑÐ·Ð¸Ð½Ñ 1695 const cartContent = qs('.cart_page .cart_content') 1696 if (cartContent) { 1697 fetch('index.php?route=checkout/cart', { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) 1698 .then(r => r.text()) 1699 .then(html => { 1700 // аналог jQuery .load(url + ' .cart_content .js_load') 1701 const tmp = document.createElement('div') 1702 tmp.innerHTML = html 1703 const fresh = tmp.querySelectorAll('.cart_content .js_load') 1704 if (fresh.length) { 1705 cartContent.replaceChildren(...fresh) 1706 } 1707 }) 1708 .catch(() => {}) 1709 } 1710 }) 1711 .catch(() => {}) 1712 }, 1713 1714 // Удаление позиÑии Ñ Ð¿ÐµÑÐµÑ Ð¾Ð´Ð¾Ð¼ (кнопка «ÑдалиÑÑ» в коÑзине) 1715 remove(key, cart_link) { 1716 qsa('#cart > button').forEach(b => b.classList.add('disabled')) 1717 postJSON('index.php?route=checkout/cart/remove', `key=${key}`) 1718 .then(json => { 1719 pushDataLayer(json) 1720 setTimeout(() => { 1721 qsa('.cart > .cart_counter').forEach(el => (el.innerHTML = json.total ?? '')) 1722 }, 100) 1723 location = cart_link 1724 }) 1725 .catch(err => alert(err)) 1726 .finally(() => qsa('#cart > button').forEach(b => b.classList.remove('disabled'))) 1727 }, 1728 1729 // Ð¢Ð¸Ñ Ð¾Ðµ Ñдаление позиÑии (без пеÑÐµÑ Ð¾Ð´Ð°) â Ð´Ð»Ñ Â«ÑдалиÑÑ Ð²ÑбÑанное» 1730 remove_key(key) { 1731 postJSON('index.php?route=checkout/cart/remove', `key=${key}`) 1732 .then(json => { 1733 pushDataLayer(json) 1734 setTimeout(() => { 1735 qsa('.cart > .cart_counter').forEach(el => (el.innerHTML = json.total ?? '')) 1736 }, 100) 1737 }) 1738 .catch(() => {}) 1739 }, 1740 1741 // ÐÐ¾Ð»Ð½Ð°Ñ Ð¾ÑиÑÑка коÑÐ·Ð¸Ð½Ñ 1742 clear(cart_link) { 1743 postJSON('index.php?route=checkout/cart/clear', '') 1744 .then(json => { 1745 pushDataLayer(json) 1746 location = cart_link 1747 }) 1748 .catch(() => {}) 1749 } 1750 } 1751 1752 // ÐÑоÑмление заказа пÑи Ð½ÐµÑ Ð²Ð°Ñке оÑÑаÑка: checkout/checkout.php Ñам пÑоÑÑо 1753 // молÑа ÑедиÑекÑÐ¸Ñ Ð¾Ð±ÑаÑно в коÑÐ·Ð¸Ð½Ñ (никакого ÑообÑÐµÐ½Ð¸Ñ Ð½Ðµ вÑÑÑавлÑеÑ) â 1754 // без ÑÑого клик по кнопке вÑглÑÐ´Ð¸Ñ ÐºÐ°Ðº «ÑÑÑаниÑа пÑоÑÑо пеÑезагÑÑзилаÑÑ». 1755 // ÐодÑвеÑиваем пÑоблемнÑе позиÑии и не даÑм ÑйÑи Ñо ÑÑÑаниÑÑ ÑанÑÑе вÑемени. 1756 document.addEventListener('DOMContentLoaded', () => { 1757 const checkoutBtn = document.querySelector('#checkout-cart .order_confirm') 1758 if (!checkoutBtn) return 1759 1760 checkoutBtn.addEventListener('click', e => { 1761 const problems = qsa('#checkout-cart .cart_item[data-stock="0"]') 1762 if (!problems.length) return 1763 1764 e.preventDefault() 1765 1766 problems.forEach(item => item.classList.add('is-shake')) 1767 problems[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) 1768 setTimeout(() => problems.forEach(item => item.classList.remove('is-shake')), 600) 1769 }) 1770 }) 1771 1772 const voucher = { 1773 add() {}, 1774 remove(key) { 1775 qsa('#cart > button').forEach(b => b.classList.add('disabled')) 1776 postJSON('index.php?route=checkout/cart/remove', `key=${key}`) 1777 .then(json => { 1778 setTimeout(() => { 1779 qsa('.cart > .cart_counter').forEach(el => (el.innerHTML = json.total ?? '')) 1780 }, 100) 1781 1782 if (getURLVar('route') === 'checkout/cart' || getURLVar('route') === 'checkout/checkout') { 1783 location = 'index.php?route=checkout/cart' 1784 } 1785 }) 1786 .catch(err => alert(err)) 1787 .finally(() => qsa('#cart > button').forEach(b => b.classList.remove('disabled'))) 1788 } 1789 } 1790 1791 /* ============================================================ 1792 * 9. ÐÐÐÐ ÐÐÐÐÐ (WISHLIST) 1793 * ============================================================ */ 1794 1795 const wishlist = (() => { 1796 let _productIds = [] 1797 1798 // ÐодÑвеÑка кнопок-ÑеÑдеÑек на вÑÐµÑ ÐºÐ°ÑÑоÑÐºÐ°Ñ 1799 const _setButtons = productIds => { 1800 _productIds = productIds || [] 1801 qsa('.wishlist-btn[data-product-id], .wishlist-btn-product[data-product-id]').forEach(btn => { 1802 const pid = parseInt(btn.dataset.productId, 10) 1803 if (!pid) return 1804 const inList = _productIds.includes(pid) 1805 btn.classList.toggle('active', inList) 1806 const icon = btn.querySelector('i') 1807 icon?.classList.toggle('ri-heart-3-fill', inList) 1808 icon?.classList.toggle('ri-heart-3-line', !inList) 1809 }) 1810 } 1811 1812 const _setCounter = total => { 1813 qsa('.wishlist_counter').forEach(counter => { 1814 counter.textContent = total > 0 ? total : '' 1815 counter.classList.toggle('has-items', total > 0) 1816 }) 1817 } 1818 1819 return { 1820 has(product_id) { 1821 return _productIds.includes(parseInt(product_id, 10)) 1822 }, 1823 // ÐовÑоÑÐ½Ð°Ñ Ð¿Ð¾Ð´ÑвеÑка â Ð´Ð»Ñ ÐºÐ°ÑÑоÑек, добавленнÑÑ Ð² DOM поÑле init (ленивÑе модÑли главной) 1824 refresh() { 1825 _setButtons(_productIds) 1826 }, 1827 // ÐаÑалÑное ÑоÑÑоÑние: ÑÑÑÑÑик и подÑвеÑка кнопок 1828 init() { 1829 getJSON('index.php?route=account/wishlist/info') 1830 .then(json => {
1831 if (json && typeof json.total !== 'undefined') { 1832 _setCounter(json.total) 1833 _setButtons(json.product_ids || []) 1834 } 1835 }) 1836 .catch(() => {}) 1837 }, 1838 add(product_id) { 1839 postJSON('index.php?route=account/wishlist/add', `product_id=${product_id}`) 1840 .then(json => { 1841 if (json.success) { 1842 if (!_productIds.includes(product_id)) _productIds.push(product_id) 1843 _setCounter(json.total) 1844 _setButtons(_productIds) 1845 } 1846 }) 1847 .catch(err => console.log(err)) 1848 }, 1849 remove(product_id) { 1850 postJSON('index.php?route=account/wishlist/remove', `product_id=${product_id}`) 1851 .then(json => { 1852 if (!json.success) return 1853 const idx = _productIds.indexOf(product_id) 1854 if (idx !== -1) _productIds.splice(idx, 1) 1855 _setCounter(json.total) 1856 _setButtons(_productIds) 1857 1858 // на ÑÑÑаниÑе избÑанного ÑбиÑаем каÑÑоÑÐºÑ ÑоваÑа 1859 if (qs('#account-wishlist')) { 1860 const btns = qsa( 1861 `.wishlist-btn[data-product-id="${product_id}"], .wishlist-btn-product[data-product-id="${product_id}"]` 1862 ) 1863 btns.forEach(btn => btn.closest('.container.render_product_data')?.remove()) 1864 1865 if (!qs('#account-wishlist .render_product_data')) { 1866 const row = qs('#account-wishlist #content .row') 1867 if (row) { 1868 const emptyText = 1869 typeof wishlist_text_empty !== 'undefined' ? wishlist_text_empty : 'Your wish list is empty.' 1870 const continueText = 1871 typeof wishlist_text_continue !== 'undefined' ? wishlist_text_continue : 'Continue' 1872 row.innerHTML = `<div class="wishlist-empty"><i class="ri-heart-3-line"></i><p>${emptyText}</p><a href="/" class="btn-continue">${continueText}</a></div>` 1873 } 1874 } 1875 } 1876 }) 1877 .catch(err => console.log(err)) 1878 }, 1879 toggle(product_id) { 1880 product_id = parseInt(product_id, 10) 1881 if (_productIds.includes(product_id)) { 1882 wishlist.remove(product_id) 1883 } else { 1884 wishlist.add(product_id) 1885 } 1886 } 1887 } 1888 })() 1889 1890 document.addEventListener('DOMContentLoaded', () =>
1890 wishlist.init()) 1891 // ленивÑе модÑли главной (app.js â initLazyModules) пÑÐ¸Ñ Ð¾Ð´ÑÑ Ð¿Ð¾Ñле DOMContentLoaded 1892 document.addEventListener('lazy-module:loaded', () => wishlist.refresh()) 1893 1894 /* ============================================================ 1895 * 10. СРÐÐÐÐÐÐÐ (PRODUCTCOMPARE, COMPARE) 1896 * ============================================================ */ 1897 1898 const productCompare = (() => { 1899 let _productIds = [] 1900 1901 const _setButtons = productIds => { 1902 _productIds = productIds || [] 1903 qsa('[data-compare-id]').forEach(btn => { 1904 const pid = parseInt(btn.dataset.compareId, 10) 1905 if (!pid) return 1906 const inList = _productIds.includes(pid) 1907 btn.classList.toggle('active', inList) 1908 const icon = btn.querySelector('i') 1909 icon?.classList.toggle('ri-arrow-left-right-fill', inList) 1910 icon?.classList.toggle('ri-arrow-left-right-line', !inList) 1911 }) 1912 } 1913 1914 const _setCounter = total => { 1915 qsa('.compare_counter, #footer-compare-total').forEach(counter => { 1916 counter.textContent = total > 0 ? total : '' 1917 counter.classList.toggle('has-items', total > 0) 1918 }) 1919 } 1920 1921 return { 1922 has(product_id) { 1923 return _productIds.includes(parseInt(product_id, 10)) 1924 }, 1925 // ÐовÑоÑÐ½Ð°Ñ Ð¿Ð¾Ð´ÑвеÑка â Ð´Ð»Ñ ÐºÐ°ÑÑоÑек, добавленнÑÑ Ð² DOM поÑле init (ленивÑе модÑли главной) 1926 refresh() { 1927 _setButtons(_productIds) 1928 }, 1929 init() { 1930 getJSON('index.php?route=product/compare/info') 1931 .then(json => { 1932 if (json && typeof json.total !== 'undefined') { 1933 _setCounter(json.total) 1934 _setButtons(json.product_ids || []) 1935 } 1936 }) 1937 .catch(() => {}) 1938 }, 1939 add(product_id) { 1940 product_id = parseInt(product_id, 10) 1941 postJSON('index.php?route=product/compare/add', `product_id=${product_id}`) 1942 .then(json => { 1943 if (json.success) { 1944 if (!_productIds.includes(product_id)) _productIds.push(product_id) 1945 _setCounter(json.total) 1946 _setButtons(_productIds) 1947 } 1948 }) 1949 .catch(err => console.log(err)) 1950 }, 1951 remove(product_id) { 1952 product_id = parseInt(product_id, 10) 1953 postJSON('index.php?route=product/compare/remove', `product_id=${product_id}`) 1954 .then(json => { 1955 if (!json.success) return 1956 const idx = _productIds.indexOf(product_id) 1957 if (idx !== -1) _productIds.splice(idx, 1) 1958 _setCounter(json.total) 1959 _setButtons(_productIds) 1960 1961 // на ÑÑÑаниÑе ÑÑÐ°Ð²Ð½ÐµÐ½Ð¸Ñ ÑбиÑаем ÐºÐ¾Ð»Ð¾Ð½ÐºÑ ÑоваÑа из ÑаблиÑÑ 1962 const table = qs('#product-compare table') 1963 if (table) { 1964 const col = table.querySelector(`[data-compare-id="${product_id}"]`)?.closest('td') 1965 if (col) { 1966 const colIndex = [...col.parentElement.children].indexOf(col) 1967 const totalCols = table.querySelectorAll('thead tr:first-child td').length 1968 1969 table.querySelectorAll('tr').forEach(tr => { 1970 const cells = tr.querySelectorAll('td, th') 1971 if (cells.length >= totalCols) { 1972 cells[colIndex]?.remove() 1973 } else { 1974 cells.forEach(cell => { 1975 const span = parseInt(cell.getAttribute('colspan') || '1', 10) 1976 if (span > 1) cell.setAttribute('colspan', span - 1) 1977 }) 1978 } 1979 }) 1980 1981 if (table.querySelectorAll('thead tr:first-child td').length === 0) { 1982 hide(table.closest('.compare__table-wrap')) 1983 show(qs('.compare__empty')) 1984 } 1985 } 1986 } 1987 }) 1988 .catch(err => console.log(err)) 1989 }, 1990 toggle(product_id) { 1991 product_id = parseInt(product_id, 10) 1992 if (_productIds.includes(product_id)) { 1993 productCompare.remove(product_id) 1994 } else { 1995 productCompare.add(product_id) 1996 } 1997 } 1998 } 1999 })() 2000 2001 document.addEventListener('lazy-module:loaded', () => productCompare.refresh()) 2002 2003 document.addEventListener('DOMContentLoaded', () => { 2004 productCompare.init() 2005 2006 delegate('click', '[data-compare-id]', (e, btn) => { 2007 const product_id = parseInt(btn.dataset.compareId, 10) 2008 if (!product_id) return 2009 if (qs('#product-compare') && btn.classList.contains('btn-danger')) {
2010 productCompare.remove(product_id) 2011 } else { 2012 productCompare.toggle(product_id) 2013 } 2014 }) 2015 }) 2016 2017 // СÑаÑÑй API ÑÑÐ°Ð²Ð½ÐµÐ½Ð¸Ñ â иÑполÑзÑеÑÑÑ Ð² blog/article.twig 2018 const compare = { 2019 add(product_id) { 2020 postJSON('index.php?route=product/compare/add', `product_id=${product_id}`) 2021 .then(json => { 2022 removeAll('.alert-dismissible') 2023 2024 if (json.success) { 2025 qs('#content')?.parentElement?.insertAdjacentHTML( 2026 'beforebegin', 2027 `<div class="alert alert-success alert-dismissible"><i class="fa fa-check-circle"></i> ${json.success} <button type="button" class="close" data-dismiss="alert">×</button></div>` 2028 ) 2029 2030 const total = qs('#compare-total') 2031 if (total) total.innerHTML = json.total 2032 2033 document.body.scrollTo({ top: 0, behavior: 'smooth' }) 2034 } 2035 }) 2036 .catch(err => alert(err)) 2037 }, 2038 remove() {} 2039 } 2040 2041 /* ============================================================ 2042 * 11. ÐÐÐÐÐÐР«УСÐÐÐÐЯ СÐÐÐÐШÐÐÐЯ» (.agree) 2043 * ============================================================ */ 2044 2045 // СÑÑлки на ÑÑловиÑ/полиÑÐ¸ÐºÑ Ð² ÑоÑÐ¼Ð°Ñ : подгÑÑжаÑÑ ÑекÑÑ Ð¸ показÑваÑÑ Ð¼Ð¾Ð´Ð°Ð»ÐºÑ. 2046 // РанÑÑе показ делал bootstrap ($.modal) â заменено на vanilla 2047 delegate('click', '.agree', (e, link) => { 2048 e.preventDefault() 2049 2050 qs('#modal-agree')?.remove() 2051 2052 fetch(link.href, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) 2053 .then(r => r.text()) 2054 .then(data => { 2055 const html = ` 2056 <div id="modal-agree" class="modal" style="display:block; position:fixed; inset:0; z-index:1050; overflow-y:auto; background:rgba(0,0,0,.5);"> 2057 <div class="modal-dialog" style="max-width:800px; margin:40px auto; background:#fff; border-radius:6px;"> 2058 <div class="modal-content"> 2059 <div class="modal-header"> 2060 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> 2061 <h4 class="modal-title">${link.textContent}</h4> 2062 </div> 2063 <div class="modal-body">${data}</div> 2064 </div> 2065 </div> 2066 </div>` 2067 2068 document.body.insertAdjacentHTML('beforeend', html) 2069 }) 2070 .catch(() => {}) 2071 }) 2072 2073 // ÐакÑÑÑие модалки ÑоглаÑениÑ: кÑеÑÑик или клик по подложке 2074 delegate('click', '#modal-agree .close, #modal-agree [data-dismiss="modal"]', () => { 2075 qs('#modal-agree')?.remove() 2076 }) 2077 document.addEventListener('click', e => { 2078 if (e.target.id === 'modal-agree') e.target.remove() 2079 }) 2080 2081 /* ============================================================ 2082 * 12. ÐÐТÐÐÐÐÐÐÐТ (initAutocomplete) 2083 * ============================================================ */ 2084 2085 // Vanilla-замена jQuery-плагина $.fn.autocomplete из OpenCart. 2086 // ÐонÑÑÐ°ÐºÑ ÑÐ¾Ñ ÑанÑн: source(value, responseCallback), select(item) 2087 function initAutocomplete(input, { source, select }) { 2088 if (!input) return 2089 2090 let timer = null 2091 const items = {} 2092 2093 input.setAttribute('autocomplete', 'off') 2094 2095 // вÑпадаÑÑий ÑпиÑок â ÑоÑедний ul, как в оÑигиналÑном плагине 2096 const dropdown = document.createElement('ul') 2097 dropdown.className = 'dropdown-menu' 2098 input.after(dropdown) 2099 2100 const showList = () => { 2101 dropdown.style.top = `${input.offsetTop + input.offsetHeight}px` 2102 dropdown.style.left = `${input.offsetLeft}px` 2103 dropdown.style.display = 'block' 2104 } 2105 const hideList = () => { 2106 dropdown.style.display = 'none' 2107 } 2108 2109 const response = json => { 2110 let html = '' 2111 2112 if (json.length) { 2113 json.forEach(item => { 2114 items[item.value] = item 2115 }) 2116 2117 // ÑнаÑала ÑлеменÑÑ Ð±ÐµÐ· каÑегоÑии 2118 json.filter(item => !item.category).forEach(item => { 2119 html += `<li data-value="${item.value}"><a href="#">${item.label}</a></li>` 2120 }) 2121 2122 // заÑем â ÑгÑÑппиÑованнÑе по каÑегоÑиÑм 2123 const categories = {} 2124 json.filter(item => item.category).forEach(item => { 2125 ;(categories[item.category] ??= []).push(item) 2126 }) 2127 for (const [name, list] of Object.entries(categories)) { 2128 html += `<li class="dropdown-header">${name}</li>` 2129 list.forEach(item => { 2130 html += `<li data-value="${item.value}"><a href="#"> ${item.label}</a></li>` 2131 }) 2132 } 2133 } 2134 2135 if (html) { 2136 showList() 2137 } else { 2138 hideList() 2139 } 2140 2141 dropdown.innerHTML = html 2142 } 2143 2144 const request = () => { 2145 clearTimeout(timer) 2146 timer = setTimeout(() => source(input.value, response), 200) 2147 } 2148 2149 input.addEventListener('focus', request) 2150 input.addEventListener('blur', () => setTimeout(hideList, 200)) 2151 input.addEventListener('keydown', e => { 2152 if (e.key === 'Escape') { 2153 hideList() 2154 } else { 2155 request() 2156 } 2157 }) 2158 2159 dropdown.addEventListener('click', e => { 2160 e.preventDefault() 2161 const li = e.target.closest('li[data-value]') 2162 if (!li) return 2163 const value = li.dataset.value 2164 if (value && items[value]) select(items[value]) 2165 }) 2166 } 2167 2168 /* ============================================================ 2169 * 13. ÐÐÐÐÐТÐÐÐ GA4 2170 * ============================================================ */ 2171 2172 // view_item_list: оÑпÑавка показаннÑÑ ÐºÐ°ÑÑоÑек ÑоваÑа (поÑÑиÑми, ÑеÑез IO) 2173 document.addEventListener('DOMContentLoaded', function () { 2174 // ÐножеÑÑво Ð´Ð»Ñ Ñ ÑÐ°Ð½ÐµÐ½Ð¸Ñ ÑникалÑнÑÑ Ð¸Ð´ÐµÐ½ÑиÑикаÑоÑов добавленнÑÑ ÑоваÑов 2175 const addedItems = new Set() 2176 2177 // ÐаÑÑив Ð´Ð»Ñ Ñ ÑÐ°Ð½ÐµÐ½Ð¸Ñ ÑоваÑов, коÑоÑÑе бÑдÑÑ Ð¾ÑпÑÐ°Ð²Ð»ÐµÐ½Ñ Ð² dataLayer 2178 let itemsToSend = [] 2179 2180 // ФÑнкÑÐ¸Ñ Ð´Ð»Ñ Ð¾ÑпÑавки даннÑÑ Ð² dataLayer 2181 const sendDataLayerEvent = () => { 2182 if (itemsToSend.length > 0) { 2183 window.dataLayer = window.dataLayer || [] 2184 window.dataLayer.push({ 2185 event: 'view_item_list', 2186 currency: 'MDL', 2187 ecommerce: { 2188 items: itemsToSend 2189 } 2190 }) 2191 // ÐÑиÑаем маÑÑив, ÑÑÐ¾Ð±Ñ Ð¸Ð·Ð±ÐµÐ¶Ð°ÑÑ Ð¿Ð¾Ð²ÑоÑной оÑпÑавки ÑÐµÑ Ð¶Ðµ ÑоваÑов 2192 itemsToSend = [] 2193 } 2194 } 2195 2196 const itemListTitle = document.querySelector('.ecommerce_title')?.getAttribute('data-item_list_title') 2197 2198 // ÐниÑиализаÑÐ¸Ñ Intersection Observer Ñ Ð¾Ð¿ÑимизаÑией 2199 const observer = new IntersectionObserver( 2200 entries => {
2201 entries.forEach(entry => { 2202 if (entry.isIntersecting) { 2203 const productData = entry.target 2204 2205 // ÐолÑÑаем ÑникалÑнÑй иденÑиÑикаÑÐ¾Ñ ÑоваÑа 2206 const productId = productData.getAttribute('data-product_id') 2207 2208 // ÐÑовеÑÑем, бÑл ли ÑÐ¾Ð²Ð°Ñ Ñже добавлен в Set Ð´Ð»Ñ Ð¿ÑедоÑвÑаÑÐµÐ½Ð¸Ñ Ð¿Ð¾Ð²ÑоÑной оÑпÑавки 2209 if (!addedItems.has(productId)) { 2210 addedItems.add(productId) 2211 2212 // СобиÑаем даннÑе ÑоваÑа из data-аÑÑибÑÑов 2213 const item = { 2214 item_id: productId, 2215 item_name: productData.getAttribute('data-item_name'), 2216 price: productData.getAttribute('data-price'), 2217 item_variant: productData.getAttribute('data-variant'), 2218 item_brand: productData.getAttribute('data-brand'), 2219 index: productData.getAttribute('data-index'), 2220 item_list_id: productData.getAttribute('data-item_list_id'), 2221 item_list_name: itemListTitle ?? productData.getAttribute('data-item_list_name') 2222 } 2223 2224 // ÐобавлÑем дополниÑелÑнÑе даннÑе, еÑли они пÑиÑÑÑÑÑвÑÑÑ 2225 if (productData.hasAttribute('data-discount')) { 2226 item.discount = productData.getAttribute('data-discount') 2227 } 2228 if (productData.hasAttribute('data-color')) { 2229 item.color = productData.getAttribute('data-color') 2230 } 2231 if (productData.hasAttribute('data-size')) { 2232 item.size = productData.getAttribute('data-size') 2233 } 2234 2235 // ÐÑовеÑка налиÑÐ¸Ñ ÐºÐ°ÑегоÑий и Ð¸Ñ Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ðµ 2236 for (let i = 0; i <= 6; i++) { 2237 const categoryAttr = `data-category-${i}` 2238 if (productData.hasAttribute(categoryAttr)) { 2239 if (i === 1) { 2240 item[`item_category`] = productData.getAttribute(categoryAttr) 2241 } else { 2242 item[`item_category${i ? i : ''}`] = productData.getAttribute(categoryAttr) 2243 } 2244 } 2245 } 2246 2247 itemsToSend.push(item) 2248 2249 // ÐÑклÑÑаем оÑÑлеживание Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ ÑлеменÑа 2250 observer.unobserve(productData) 2251 } 2252 } 2253 }) 2254 2255 // ÐапланиÑоваÑÑ Ð¾ÑпÑÐ°Ð²ÐºÑ Ð´Ð°Ð½Ð½ÑÑ Ð² dataLayer (200 Ð¼Ñ â баÑÑинг) 2256 clearTimeout(window.ecommerceTimeout) 2257 window.ecommerceTimeout = setTimeout(sendDataLayerEvent, 200) 2258 }, 2259 { 2260 threshold: 0.5 // Ð¢Ð¾Ð²Ð°Ñ Ð´Ð¾Ð»Ð¶ÐµÐ½ бÑÑÑ Ð²Ð¸Ð´ÐµÐ½ на 50% Ð´Ð»Ñ ÑÑабаÑÑÐ²Ð°Ð½Ð¸Ñ 2261 } 2262 ) 2263 2264 document.querySelectorAll('.ecommerce_data').forEach(product => observer.observe(product)) 2265 }) 2266 2267 // select_item: оÑпÑавка ÑобÑÑÐ¸Ñ Ð¿Ñи клике на каÑÑоÑÐºÑ + ÑÐ¾Ñ Ñанение 2268 // даннÑÑ Ð»Ð¸ÑÑинга в localStorage (Ð¿Ð¾Ð´Ñ Ð²Ð°ÑÑваÑÑÑÑ Ð¿Ñи add to cart) 2269 function sendSelectItemEvent(element) { 2270 const ecommerceData = element.closest('.item').querySelector('.ecommerce_data') 2271 2272 const ecommerceItemData = { 2273 item_name: ecommerceData.dataset.item_name, 2274 item_id: ecommerceData.dataset.product_id, 2275 price: ecommerceData.getAttribute('data-price') || '', 2276 item_brand: ecommerceData.dataset.brand, 2277 item_variant: ecommerceData.dataset.variant 2278 } 2279 2280 // ÐобавлÑем каÑегоÑии ÑоваÑов 2281 for (let i = 0; i <= 6; i++) { 2282 const categoryAttr = `data-category-${i}` 2283 if (ecommerceData.hasAttribute(categoryAttr)) { 2284 if (i === 1) { 2285 ecommerceItemData['item_category'] = ecommerceData.getAttribute(categoryAttr) 2286 } else { 2287 ecommerceItemData[`item_category${i ? i : ''}`] = ecommerceData.getAttribute(categoryAttr) 2288 } 2289 } 2290 } 2291 2292 if (ecommerceData.hasAttribute('data-discount')) { 2293 ecommerceItemData['discount'] = ecommerceData.getAttribute('data-discount') 2294 } 2295 2296 // Ð¡Ð¾Ñ Ñанение даннÑÑ Ð»Ð¸ÑÑинга в localStorage по item_id 2297 const selectedFields = { 2298 item_list_id: ecommerceData.dataset.item_list_id || '', 2299 item_list_name: ecommerceData.dataset.item_list_name || '', 2300 index: ecommerceData.getAttribute('data-index') || '' 2301 } 2302 2303 const savedItems = JSON.parse(localStorage.getItem('selectedItems')) || {} 2304 savedItems[ecommerceData.dataset.product_id] = selectedFields 2305 localStorage.setItem('selectedItems', JSON.stringify(savedItems)) 2306 2307 window.dataLayer = window.dataLayer || [] 2308 window.dataLayer.push({ 2309 event: 'select_item', 2310 currency: 'MDL', 2311 ecommerce: { 2312 items: [ecommerceItemData] 2313 } 2314 }) 2315 } 2316 2317 // ÐавеÑÐ¸Ð²Ð°ÐµÑ Ð¾ÑпÑÐ°Ð²ÐºÑ select_item на ÑÑÑлки каÑÑоÑек 2318 function addSelectItemEventListeners() { 2319 document.querySelectorAll('.item .images a').forEach(link => { 2320 link.addEventListener('click', event => { 2321 if (event.ctrlKey || event.metaKey) { 2322 sendSelectItemEvent(event.currentTarget) 2323 } else { 2324 sendSelectItemEvent(event.currentTarget) 2325 window.location.href = event.currentTarget.href 2326 event.preventDefault() 2327 } 2328 }) 2329 2330 // клик колеÑиком (оÑкÑÑÑие в новой вкладке) 2331 link.addEventListener('mousedown', event => { 2332 if (event.button === 1) sendSelectItemEvent(event.currentTarget) 2333 }) 2334 2335 // пÑавÑй клик (конÑекÑÑное менÑ) 2336 link.addEventListener('contextmenu', event => { 2337 sendSelectItemEvent(event.currentTarget) 2338 }) 2339 }) 2340 2341 document.querySelectorAll('.item .add_to_cart').forEach(link => { 2342 link.addEventListener('click', event => { 2343 if (event.ctrlKey || event.metaKey) { 2344 sendSelectItemEvent(event.currentTarget) 2345 } else { 2346 sendSelectItemEvent(event.currentTarget) 2347 window.location.href = event.currentTarget.href 2348 event.preventDefault() 2349 } 2350 }) 2351 }) 2352 } 2353 2354 document.addEventListener('DOMContentLoaded', addSelectItemEventListeners) 2355 2356 // view_search_results: оÑпÑавка поиÑкового запÑоÑа (Ð¾Ñ 3 Ñимволов, Ñ Ð¿Ð°Ñзой) 2357 document.addEventListener('DOMContentLoaded', () => { 2358 const searchInput = document.querySelector('.search_form input[name="search"]') 2359 if (!searchInput) return 2360 2361 let searchTimeout 2362 searchInput.addEventListener('input', () => { 2363 const searchQuery = searchInput.value.trim() 2364 clearTimeout(searchTimeout) 2365 searchTimeout = setTimeout(() => { 2366 if (searchQuery.length >= 3) { 2367 window.dataLayer = window.dataLayer || [] 2368 window.dataLayer.push({ 2369 event: 'view_search_results', 2370 search_query: searchQuery 2371 }) 2372 } 2373 }, 1000) 2374 }) 2375 }) 2376 2377 // СбÑÐ¾Ñ Ð¿Ð¾Ð²ÑеждÑнной кÑки _ga (заÑиÑа Ð¾Ñ Ñпам-знаÑениÑ) 2378 document.addEventListener('DOMContentLoaded', () => { 2379 const cookieName = '_ga=' 2380 const targetValue = '@^_2965645322497708761' 2381 const cookies = document.cookie.split('; ') 2382 2383 for (const cookie of cookies) { 2384 if (cookie.startsWith(cookieName) && cookie.substring(cookieName.length) === targetValue) { 2385 document.cookie = `_ga=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=${window.location.hostname}` 2386 console.log("ÐÑка '_ga' ÑбÑоÑена") 2387 return 2388 } 2389 } 2390 }) 2391 2392 /* ============================================================ 2393 * 14. ÐÐÐÐÐСÐÐ ÐРТÐÐÐÐ ÐÐ Ð ÐÐÐÐЧÐÐ 2394 * ============================================================ */ 2395 2396 document.addEventListener('DOMContentLoaded', () => { 2397 const form = document.getElementById('stock_notify_form') 2398 const emailInput = document.getElementById('stock_notify_email') 2399 const messageBox = document.getElementById('stock_notify_message') 2400 const lang = document.documentElement.lang || 'ro' 2401 let url = `/index.php?route=extension/module/stock_notify/add` 2402 2403 if (lang !== 'ro') { 2404 url = `/${lang}${url}` 2405 } 2406 2407 if (!form) return 2408 2409 form.addEventListener('submit', async event => { 2410 event.preventDefault() 2411 2412 const email = emailInput.value.trim() 2413 messageBox.textContent = '' 2414 messageBox.className = 'stock-notify-message' 2415 2416 // пÑоÑÑÐ°Ñ Ð²Ð°Ð»Ð¸Ð´Ð°ÑÐ¸Ñ 2417 const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 2418 if (!emailPattern.test(email)) { 2419 emailInput.classList.add('error') 2420 messageBox.textContent = '{{ error_email }}' 2421 messageBox.classList.add('error-text') 2422 return 2423 } 2424 2425 emailInput.classList.remove('error') 2426 form.classList.add('loading') 2427 2428 try { 2429 const response = await fetch(url, { 2430 method: 'POST', 2431 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 2432 body: new URLSearchParams(new FormData(form)) 2433 }) 2434 2435 const data = await response.json() 2436 form.classList.remove('loading') 2437 2438 messageBox.textContent = data.success || data.error || 'ÐÑибка' 2439 messageBox.classList.add(data.success ? 'success-text' : 'error-text') 2440 2441 if (data.success) form.reset() 2442 } catch (err) { 2443 form.classList.remove('loading') 2444 messageBox.textContent = 'ÐÑибка ÑоединениÑ' 2445 messageBox.classList.add('error-text') 2446 } 2447 }) 2448 2449 emailInput.addEventListener('input', () => { 2450 emailInput.classList.remove('error') 2451 messageBox.textContent = '' 2452 }) 2453 }) 2454 2455 /* ============================================================ 2456 * ÐÐÐÐÐÐ BOOTSTRAP 3 JS (data-toggle) ÐÐ VANILLA 2457 * ============================================================ 2458 * bootstrap.min.js оÑклÑÑÑн вмеÑÑе Ñ jQuery. Ðз его компоненÑов на ÑайÑе 2459 * ÑеалÑно ÑабоÑали ÑолÑко вкладки (ÑаблиÑа ÑазмеÑов) и ÑаÑÑиÑно collapse. 2460 * ТÑлÑÐ¸Ð¿Ñ ([data-toggle="tooltip"]) не иниÑиализиÑовалиÑÑ Ð¸ до ÑÑого 2461 * (вÑзов .tooltip() бÑл закомменÑиÑован), bootstrap CSS не подклÑÑÑн. 2462 */ 2463 2464 // Ðкладки (data-toggle="tab"): акÑÐ¸Ð²Ð½Ð°Ñ ÑÑÑлка + показ панели .tab-pane
2465 delegate('click', '[data-toggle="tab"]', (e, toggle) => { 2466 e.preventDefault() 2467 const target = qs(toggle.getAttribute('href') || toggle.dataset.target || '#') 2468 if (!target) return 2469 2470 toggle.closest('ul')?.querySelectorAll('li').forEach(li => li.classList.remove('active')) 2471 toggle.closest('li')?.classList.add('active') 2472 2473 target.parentElement?.querySelectorAll(':scope > .tab-pane').forEach(pane => pane.classList.remove('active', 'in')) 2474 target.classList.add('active', 'in') 2475 }) 2476 2477 // СвоÑаÑиваемÑе блоки (data-toggle="collapse") Ñ Ð¿Ð¾Ð´Ð´ÐµÑжкой data-parent (аккоÑдеон). 2478 // ЧекаÑÑ Ð½Ðµ ÑÑогаем â Ñ Ð½ÐµÐ³Ð¾ ÑобÑÑвеннÑй аккоÑдеон в checkout.twig 2479 delegate('click', '[data-toggle="collapse"]', (e, toggle) => { 2480 if (toggle.closest('#checkout-checkout')) return 2481 e.preventDefault() 2482 const target = qs(toggle.dataset.target || toggle.getAttribute('href') || '#') 2483 if (!target) return 2484 2485 const parentSelector = toggle.dataset.parent 2486 if (parentSelector) { 2487 qsa(`${parentSelector} .panel-collapse.in`).forEach(panel => { 2488 if (panel !== target) panel.classList.remove('in') 2489 }) 2490 } 2491 2492 target.classList.toggle('in') 2493 toggle.setAttribute('aria-expanded', target.classList.contains('in') ? 'true' : 'false') 2494 }) 2495 2496 // ÐÑпадаÑÑие Ð¼ÐµÐ½Ñ (data-toggle="dropdown"): клаÑÑ .open на ÑодиÑеле 2497 delegate('click', '[data-toggle="dropdown"]', (e, toggle) => { 2498 e.preventDefault() 2499 toggle.parentElement?.classList.toggle('open') 2500 }) 2501 2502 // клик вне дÑопдаÑна закÑÑÐ²Ð°ÐµÑ ÐµÐ³Ð¾ 2503 document.addEventListener('click', e => { 2504 qsa('.btn-group.open, .dropdown.open').forEach(group => { 2505 if (!group.contains(e.target)) group.classList.remove('open') 2506 }) 2507 }) 2508 2509 /* ============================================================ 2510 * ÐÐТÐÐТÐРЫТÐÐ ÐÐÐÐÐÐÐ ÐÐ¥ÐÐÐ ÐРССЫÐÐÐ (?show_login=1) 2511 * ============================================================ 2512 * ÐÑжно Ð´Ð»Ñ ÑÑÑлок из email-ÑаÑÑÑлок и Ñ.п.: гоÑÑÑ ÑедиÑекÑÑÑ Ð½Ð° 2513 * ÑÑÑаниÑÑ Ñ ÑÑим паÑамеÑÑом, ÑÑÐ¾Ð±Ñ ÑÑÐ°Ð·Ñ Ð¿Ð¾ÐºÐ°Ð·Ð°ÑÑ Ð²Ñ Ð¾Ð´/ÑегиÑÑÑаÑÐ¸Ñ 2514 * (Ñм. account/invite.php â незалогиненного оÑпÑавлÑÐµÑ Ð½Ð° 2515 * common/home?show_login=1 вмеÑÑо полноÑенной ÑÑÑаниÑÑ Ð»Ð¾Ð³Ð¸Ð½Ð°). 2516 * #login_modal â ÑÐ¾Ñ Ð¶Ðµ модал, ÑÑо оÑкÑÑваÑÑ .quick_signup и Ð¿Ð¾Ð¿Ð°Ð¿Ñ 2517 * бонÑÑов (onclick="...getElementById('login_modal').style.display=..."). 2518 * ÐаÑамеÑÑ ÑиÑаеÑÑÑ Ð½Ð° клиенÑе, а не ÑеÑез ÑеÑÑиÑ/PHP-Ñлаг â ÑÑÐ°Ð³Ð¼ÐµÐ½Ñ 2519 * common/quicksignup кÑÑиÑÑеÑÑÑ Ð¿Ð¾ ÑзÑÐºÑ Ð² footer.php, Ñак ÑÑо ÑеÑвеÑнÑй 2520 * Ñлаг на конкÑеÑного поÑеÑиÑÐµÐ»Ñ Ð½Ð°Ð´Ñжно не ÑÑабоÑаеÑ. 2521 */ 2522 document.addEventListener('DOMContentLoaded', () => { 2523 if (!new URLSearchParams(window.location.search).has('show_login')) return 2524 const modal = document.getElementById('login_modal') 2525 if (modal) modal.style.display = 'block' 2526 }) 2527 2528 /* ============================================================ 2529 * ÐÐСÐÐРТ ÐÐÐÐÐÐЬÐÐÐÐ API 2530 * ============================================================ */ 2531 2532 Object.assign(window, { 2533 getURLVar, 2534 remove_product, 2535 beautifyTableSizes, 2536 chozeSystemSize, 2537 showSizechart, 2538 showPreorderModal, 2539 cart, 2540 voucher, 2541 wishlist, 2542 productCompare, 2543 compare, 2544 initAutocomplete, 2545 sendSelectItemEvent 2546 }) 2547})() 2548 2549/* ============================================================ 2550 * 15. ÐÐÐСÐÐÐЬÐУÐÐЫРÐÐÐ (закомменÑиÑован) 2551 * ============================================================ */ 2552 2553/* 2554 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: AJAX-пеÑÐµÑ Ð¾Ð´Ñ Ð¿Ð¾ ÑилÑÑÑÑ #ocfilter-content1 â 2555 * ÑлеменÑа Ð½ÐµÑ Ð½Ð¸ в одном Ñаблоне (OCFilter ÑабоÑÐ°ÐµÑ ÑеÑез Ñвой ocfilter.js). 2556 * 2557 * $("#ocfilter-content1").on("click", "a", function (e) { ...загÑÑзка #content... }); 2558 */ 2559 2560/* 2561 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: позиÑиониÑование вÑпадаÑÑÐ¸Ñ Ð¼ÐµÐ½Ñ Ð±ÑÑÑÑÑап-навигаÑии #menu â 2562 * ÑлеменÑа #menu Ð½ÐµÑ Ð² ÑÐ°Ð±Ð»Ð¾Ð½Ð°Ñ ÑемÑ. 2563 * 2564 * $('#menu .dropdown-menu').each(function () { ...margin-left... }); 2565 */ 2566 2567/* 2568 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: AJAX-оÑпÑавка ÑоÑм .custom__forms Ñ bootstrap-модалками â 2569 * клаÑÑа .custom__forms Ð½ÐµÑ Ð½Ð¸ в одном Ñаблоне. 2570 * 2571 * $('.custom__forms input, .custom__forms textarea').on('focus', ...); 2572 * $(document).on('submit', ".custom__forms", function (e) { ...$.ajax + $('.modal').modal()... }); 2573 */ 2574 2575/* 2576 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: пеÑеклÑÑение ÑзÑка ÑеÑез .language-select â 2577 * клаÑÑа Ð½ÐµÑ Ð² ÑÐ°Ð±Ð»Ð¾Ð½Ð°Ñ (ÑзÑк пеÑеклÑÑаеÑÑÑ ÑÑÑлками в language.twig). 2578 * 2579 * $('.form-language .language-select').on('click', function (e) { ... }); 2580 */ 2581 2582/* 2583 * ÐÐ ÐСÐÐÐЬÐУÐТСЯ: в cart.add бÑл вÑвод .quantity_limit_check_ + key, 2584 * но пеÑÐµÐ¼ÐµÐ½Ð½Ð°Ñ key в ÑÑой облаÑÑи не ÑÑÑеÑÑвовала (ReferenceError) â 2585 * ÑабоÑий ваÑÐ¸Ð°Ð½Ñ ÑÑого кода живÑÑ Ð² cart.update. 2586 */ 2587 2588/* Phone Register Module â ÑÑаÑÑе веÑÑии, давно закомменÑиÑованÑ. 2589 * ÐкÑÑалÑÐ½Ð°Ñ ÑеализаÑÐ¸Ñ â initPhoneRegister в assets/src/js/app.js. 2590 * (два болÑÑÐ¸Ñ Ð·Ð°ÐºÐ¾Ð¼Ð¼ÐµÐ½ÑиÑованнÑÑ Ð±Ð»Ð¾ÐºÐ° ÑÐ´Ð°Ð»ÐµÐ½Ñ Ð¿Ñи пеÑепиÑÑвании Ñайла; 2591 * Ñм. git-иÑÑоÑиÑ, коммиÑÑ Ð´Ð¾ пеÑевода на ÑиÑÑÑй JS) 2592 */
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.