1/** 2 * Lysonsaky Theme â main.js v5.0 3 * Pure Vanilla JS â no jQuery, no Slick, no Fancybox, no external libs. 4 */ 5(function () { 6 'use strict'; 7 8 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 9 1. HERO SLIDER â Pure CSS transitions 10 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 11 function initHeroSlider() { 12 var wrap = document.querySelector('.lssk-slides'); 13 if (!wrap) return; 14 var slides = Array.from(wrap.querySelectorAll('.lssk-slide')); 15 if (slides.length < 2) { 16 if (slides[0]) slides[0].classList.add('is-active'); 17 return; 18 } 19 var cur = 0, timer, SPEED = 5500; 20 21 function goTo(n) { 22 slides[cur].classList.remove('is-active', 'is-prev'); 23 slides[cur].classList.add('is-prev'); 24 cur = ((n % slides.length) + slides.length) % slides.length; 25 slides[cur].classList.add('is-active'); 26 setTimeout(function () { 27 slides.forEach(function (s) { s.classList.remove('is-prev'); }); 28 }, 800); 29 updateDots(); 30 } 31 function next() { goTo(cur + 1); } 32 function prev() { goTo(cur - 1); } 33 function startAuto() { timer = setInterval(next, SPEED); } 34 function stopAuto() { clearInterval(timer); } 35 36 slides[0].classList.add('is-active'); 37 wrap.classList.add('js-ready'); 38 39 /* Arrows */ 40 var prevBtn = wrap.parentElement.querySelector('.lssk-slide-prev'); 41 var nextBtn = wrap.parentElement.querySelector('.lssk-slide-next'); 42 if (!prevBtn) { 43 prevBtn = document.createElement('button'); 44 prevBtn.type = 'button'; prevBtn.className = 'lssk-slide-prev'; 45 prevBtn.setAttribute('aria-label', 'Trưá»c'); 46 prevBtn.innerHTML = '‹'; 47 nextBtn = document.createElement('button'); 48 nextBtn.type = 'button'; nextBtn.className = 'lssk-slide-next'; 49 nextBtn.setAttribute('aria-label', 'Sau'); 50 nextBtn.innerHTML = '›'; 51 var hero = wrap.closest('.lssk-hero') || wrap.parentElement; 52 hero.appendChild(prevBtn); 53 hero.appendChild(nextBtn); 54 } 55 prevBtn.addEventListener('click', function () { stopAuto(); prev(); startAuto(); }); 56 nextBtn.addEventListener('click', function () { stopAuto(); next(); startAuto(); }); 57 58 /* Dots */ 59 var dotsWrap = document.createElement('div'); 60 dotsWrap.className = 'lssk-slide-dots'; 61 slides.forEach(function (_, i) { 62 var d = document.createElement('button'); 63 d.type = 'button'; d.className = 'lssk-slide-dot'; 64 if (i === 0) d.classList.add('is-active'); 65 d.setAttribute('aria-label', 'Slide ' + (i + 1)); 66 d.addEventListener('click', function () { stopAuto(); goTo(i); startAuto(); }); 67 dotsWrap.appendChild(d); 68 }); 69 (wrap.closest('.lssk-hero') || wrap.parentElement).appendChild(dotsWrap); 70 71 function updateDots() { 72 Array.from(dotsWrap.children).forEach(function (d, i) { 73 d.classList.toggle('is-active', i === cur); 74 }); 75 } 76 77 /* Touch swipe */ 78 var sx = 0; 79 wrap.addEventListener('touchstart', function (e) { sx = e.touches[0].clientX; }, { passive: true }); 80 wrap.addEventListener('touchend', function (e) { 81 var dx = e.changedTouches[0].clientX - sx; 82 if (Math.abs(dx) > 50) { stopAuto(); dx < 0 ? next() : prev(); startAuto(); } 83 }); 84 85 /* Pause on hover */ 86 wrap.addEventListener('mouseenter', stopAuto); 87 wrap.addEventListener('mouseleave', startAuto); 88 89 startAuto(); 90 } 91 92 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 93 2. MOBILE NAV + MULTI-LEVEL 94 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 95 function initNav() { 96 var toggle = document.getElementById('lssk-hamburger'); 97 var nav = document.getElementById('lssk-nav');
98 var overlay = document.getElementById('lssk-nav-overlay'); 99 var close = document.getElementById('lssk-nav-close'); 100 var BREAK = 1280; 101 if (!toggle || !nav) return; 102 103 var _navScrollY = 0; 104 105 function closeNav() { 106 nav.classList.remove('is-open'); 107 toggle.classList.remove('is-active'); 108 toggle.setAttribute('aria-expanded', 'false'); 109 overlay && overlay.classList.remove('is-visible'); 110 document.body.classList.remove('nav-open'); 111 // iOS Safari: khôi phục scroll position sau khi bá» position:fixed 112 document.body.style.top = ''; 113 window.scrollTo(0, _navScrollY); 114 nav.querySelectorAll('.lssk-submenu').forEach(function (s) { 115 s.classList.remove('is-open'); 116 s.style.display = ''; 117 }); 118 nav.querySelectorAll('.lssk-has-children').forEach(function (li) { 119 li.classList.remove('sub-open'); 120 }); 121 } 122 function openNav() { 123 _navScrollY = window.scrollY; 124 // iOS Safari: ghi nhá» vá» trà scroll, position:fixed reset vá» 0 125 document.body.style.top = '-' + _navScrollY + 'px'; 126 nav.classList.add('is-open'); 127 toggle.classList.add('is-active'); 128 toggle.setAttribute('aria-expanded', 'true'); 129 overlay && overlay.classList.add('is-visible'); 130 document.body.classList.add('nav-open'); 131 } 132 133 toggle.addEventListener('click', function () { 134 nav.classList.contains('is-open') ? closeNav() : openNav(); 135 }); 136 close && close.addEventListener('click', closeNav); 137 overlay && overlay.addEventListener('click', closeNav); 138 document.addEventListener('keydown', function (e) { 139 if (e.key === 'Escape') closeNav(); 140 }); 141 142 /* Mobile: slide toggle sub-menu */ 143 nav.addEventListener('click', function (e) { 144 var link = e.target.closest('.lssk-has-children > a'); 145 if (!link || window.innerWidth >= BREAK) return; 146 e.preventDefault(); 147 var li = link.parentElement; 148 var sub = li.querySelector(':scope > .lssk-submenu'); 149 if (!sub) return; 150 /* close siblings */ 151 li.parentElement.querySelectorAll(':scope > .lssk-has-children.sub-open').forEach(function (sib) { 152 if (sib !== li) { 153 sib.classList.remove('sub-open'); 154 var sibSub = sib.querySelector(':scope > .lssk-submenu'); 155 if (sibSub) slideUp(sibSub); 156 } 157 }); 158 li.classList.toggle('sub-open'); 159 li.classList.contains('sub-open') ? slideDown(sub) : slideUp(sub); 160 }); 161 162 window.addEventListener('resize', function () { 163 if (window.innerWidth >= BREAK) closeNav(); 164 }); 165 166 /* Desktop hover â JS managed (không dùng CSS :hover Äá» tránh gap flicker) 167 Dùng mouseenter/mouseleave trên <li> â vì padding-bottom 10px extend hit area, 168 cursor không rá»i <li> khi di chuyá»n qua khoảng trá»ng và o dropdown */ 169 function initDesktopHover() { 170 var hoverTimers = new Map(); 171 172 nav.querySelectorAll('.lssk-menu-item--d0').forEach(function (li) { 173 var sub = li.querySelector(':scope > .lssk-submenu--d0'); 174 if (!sub) return; // không có submenu â bá» qua 175 176 var DELAY = 120; // ms 177 178 function openItem() { 179 /* Äóng anh chá» em trưá»c */ 180 li.parentElement && li.parentElement.querySelectorAll(':scope > .lssk-menu-item--d0.is-hovered').forEach(function (s) { 181 if (s !== li) { 182 clearTimeout(hoverTimers.get(s)); 183 s.classList.remove('is-hovered'); 184 } 185 }); 186 clearTimeout(hoverTimers.get(li)); 187 li.classList.add('is-hovered'); 188 } 189 190 function scheduleClose() { 191 hoverTimers.set(li, setTimeout(function () { 192 li.classList.remove('is-hovered'); 193 }, DELAY)); 194 } 195 196 function cancelClose() { 197 clearTimeout(hoverTimers.get(li)); 198 } 199 200 /* LI hover */ 201 li.addEventListener('mouseenter', openItem); 202 li.addEventListener('mouseleave', scheduleClose); 203 204 /* Submenu hover â há»§y timer Äá» không Äóng khi cursor và o dropdown */ 205 sub.addEventListener('mouseenter', cancelClose); 206 sub.addEventListener('mouseleave', scheduleClose); 207 208 /* Keyboard: Enter/Space má» dropdown, Escape Äóng */ 209 li.querySelector(':scope > a') && li.querySelector(':scope > a').addEventListener('keydown', function (e) { 210 if (window.innerWidth < BREAK) return; 211 if (e.key === 'Enter' || e.key === ' ') { 212 if (li.classList.contains('is-hovered')) { 213 li.classList.remove('is-hovered'); e.preventDefault(); 214 } else { 215 openItem(); e.preventDefault(); 216 } 217 } 218 if (e.key === 'Escape') { li.classList.remove('is-hovered'); } 219 }); 220 }); 221 222 /* Click outside â close all */ 223 document.addEventListener('click', function (e) { 224 if (window.innerWidth < BREAK) return; 225 if (!e.target.closest('#lssk-nav')) { 226 nav.querySelectorAll('.lssk-menu-item--d0.is-hovered').forEach(function (li) { 227 li.classList.remove('is-hovered'); 228 }); 229 } 230 }); 231 } 232 233 var _hoverInited = false;
234 function maybeInitHover() { 235 if (window.innerWidth >= BREAK && !_hoverInited) { 236 _hoverInited = true; 237 initDesktopHover(); 238 } 239 } 240 maybeInitHover(); 241 window.addEventListener('resize', maybeInitHover); 242 } 243 244 /* â slideDown / slideUp â */ 245 function slideDown(el) { 246 el.style.display = 'block'; 247 var h = el.scrollHeight; 248 el.style.overflow = 'hidden'; 249 el.style.height = '0'; 250 el.style.transition = 'height .22s ease'; 251 requestAnimationFrame(function () { el.style.height = h + 'px'; }); 252 el.addEventListener('transitionend', function done() { 253 el.style.height = el.style.overflow = el.style.transition = ''; 254 el.removeEventListener('transitionend', done); 255 }); 256 } 257 function slideUp(el) { 258 el.style.overflow = 'hidden'; 259 el.style.height = el.scrollHeight + 'px'; 260 el.style.transition = 'height .22s ease'; 261 requestAnimationFrame(function () { el.style.height = '0'; }); 262 el.addEventListener('transitionend', function done() { 263 el.style.display = ''; 264 el.style.height = el.style.overflow = el.style.transition = ''; 265 el.removeEventListener('transitionend', done); 266 }); 267 } 268 269 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 270 3. LIVE SEARCH 271 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 272 function initSearch() { 273 var btn = document.getElementById('lssk-search-btn'); 274 var overlay = document.getElementById('lssk-search-overlay'); 275 var closeBtn= document.getElementById('lssk-search-close'); 276 var input = document.getElementById('lssk-search-input'); 277 var results = document.getElementById('lssk-search-results'); 278 if (!btn || !overlay) return; 279 var timer; 280 281 function openSearch() { 282 _searchScrollY = window.scrollY; 283 document.body.style.top = '-' + _searchScrollY + 'px'; 284 overlay.classList.add('is-active'); 285 document.body.classList.add('search-open'); 286 setTimeout(function () { input && input.focus(); }, 80); 287 } 288 var _searchScrollY = 0; 289 290 function closeSearch() { 291 overlay.classList.remove('is-active'); 292 document.body.classList.remove('search-open'); 293 document.body.style.top = ''; 294 window.scrollTo(0, _searchScrollY); 295 if (input) input.value = ''; 296 if (results) results.innerHTML = ''; 297 } 298 299 btn.addEventListener('click', openSearch); 300 closeBtn && closeBtn.addEventListener('click', closeSearch); 301 overlay.addEventListener('click', function (e) { 302 if (e.target === overlay) closeSearch(); 303 }); 304 document.addEventListener('keydown', function (e) { 305 if (e.key === 'Escape' && overlay.classList.contains('is-active')) closeSearch(); 306 }); 307 308 if (!input || !results) return; 309 input.addEventListener('input', function () { 310 clearTimeout(timer); 311 var kw = input.value.trim(); 312 if (kw.length < 2) { results.innerHTML = ''; return; } 313 results.innerHTML = '<div class="lssk-search-loading">Äang tìm...</div>'; 314 timer = setTimeout(function () { 315 var fd = new FormData(); 316 fd.append('action', 'lssk_live_search'); 317 fd.append('nonce', lssk.nonce); 318 fd.append('keyword', kw);
319 fetch(lssk.ajax_url, { method: 'POST', body: fd }) 320 .then(function (r) { return r.json(); }) 321 .then(function (res) { 322 if (!res.success || !res.data || !res.data.length) { 323 results.innerHTML = '<div class="lssk-search-empty">Không tìm thấy kết quả cho "<strong>' + escHtml(kw) + '</strong>"</div>'; 324 return; 325 } 326 var html = '<ul class="lssk-search-list">'; 327 res.data.forEach(function (item) { 328 html += '<li><a href="' + item.url + '">' + 329 '<img src="' + item.thumb + '" alt="" loading="lazy">' + 330 '<div class="lssk-search-item-body">' + 331 '<span class="lssk-search-type">' + escHtml(item.type) + '</span>' + 332 '<strong>' + escHtml(item.title) + '</strong>' + 333 '<span class="lssk-search-excerpt">' + escHtml(item.excerpt) + '</span>' + 334 '</div></a></li>'; 335 }); 336 html += '</ul><div class="lssk-search-all-row"><a href="' + 337 lssk.search_url + encodeURIComponent(kw) + '">Xem tất cả kết quả ›</a></div>'; 338 results.innerHTML = html; 339 }) 340 .catch(function () { 341 results.innerHTML = '<div class="lssk-search-empty">Lá»i kết ná»i, thá» lại.</div>'; 342 }); 343 }, 350); 344 }); 345 input.addEventListener('keydown', function (e) { 346 if (e.key === 'Enter') { 347 window.location.href = lssk.search_url + encodeURIComponent(input.value.trim()); 348 } 349 }); 350 } 351 352 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 353 3b. LIVE SEARCH INLINE (Header Mẫu 2 â desktop + mobile) 354 Dùng chung API/markup lssk_live_search vá»i initSearch(), chá» khác 355 là kết quả render ngay dưá»i ô input (.lssk-inline-search-form) thay 356 vì trong overlay toà n mà n hình. 357 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 358 function initInlineLiveSearch() { 359 var forms = document.querySelectorAll('.lssk-inline-search-form'); 360 if (!forms.length) return; 361 362 forms.forEach(function (form) { 363 var input = form.querySelector('input[type="search"]'); 364 var results = form.querySelector('.lssk-search-results'); 365 if (!input || !results) return; 366 var timer; 367 368 input.addEventListener('input', function () { 369 clearTimeout(timer); 370 var kw = input.value.trim(); 371 if (kw.length < 2) { results.innerHTML = ''; return; } 372 results.innerHTML = '<div class="lssk-search-loading">Äang tìm...</div>'; 373 timer = setTimeout(function () { 374 var fd = new FormData(); 375 fd.append('action', 'lssk_live_search'); 376 fd.append('nonce', lssk.nonce); 377 fd.append('keyword', kw);
378 fetch(lssk.ajax_url, { method: 'POST', body: fd }) 379 .then(function (r) { return r.json(); }) 380 .then(function (res) { 381 if (!res.success || !res.data || !res.data.length) { 382 results.innerHTML = '<div class="lssk-search-empty">Không tìm thấy kết quả cho "<strong>' + escHtml(kw) + '</strong>"</div>'; 383 return; 384 } 385 var html = '<ul class="lssk-search-list">'; 386 res.data.forEach(function (item) { 387 html += '<li><a href="' + item.url + '">' + 388 '<img src="' + item.thumb + '" alt="" loading="lazy">' + 389 '<div class="lssk-search-item-body">' + 390 '<span class="lssk-search-type">' + escHtml(item.type) + '</span>' + 391 '<strong>' + escHtml(item.title) + '</strong>' + 392 '<span class="lssk-search-excerpt">' + escHtml(item.excerpt) + '</span>' + 393 '</div></a></li>'; 394 }); 395 html += '</ul><div class="lssk-search-all-row"><a href="' + 396 lssk.search_url + encodeURIComponent(kw) + '">Xem tất cả kết quả ›</a></div>'; 397 results.innerHTML = html; 398 }) 399 .catch(function () { 400 results.innerHTML = '<div class="lssk-search-empty">Lá»i kết ná»i, thá» lại.</div>'; 401 }); 402 }, 350); 403 }); 404 405 document.addEventListener('click', function (e) { 406 if (!form.contains(e.target)) results.innerHTML = ''; 407 }); 408 document.addEventListener('keydown', function (e) { 409 if (e.key === 'Escape') results.innerHTML = ''; 410 }); 411 }); 412 } 413 414 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 415 4. SCROLL-TO-TOP 416 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 417 function initStickyUI() { 418 var toTop = document.getElementById('lssk-to-top'); 419 if (!toTop) return; 420 window.addEventListener('scroll', function () { 421 toTop.classList.toggle('visible', window.scrollY > 400); 422 }, { passive: true }); 423 toTop.addEventListener('click', function () { 424 window.scrollTo({ top: 0, behavior: 'smooth' }); 425 }); 426 } 427 428 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 429 5. ENTRANCE ANIMATIONS â IntersectionObserver 430 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 431 function initAnimations() { 432 var els = document.querySelectorAll('.lssk-animate'); 433 if (!('IntersectionObserver' in window)) { 434 els.forEach(function (el) { el.classList.add('is-visible'); }); 435 return; 436 } 437 var obs = new IntersectionObserver(function (entries) { 438 entries.forEach(function (entry) { 439 if (entry.isIntersecting) { 440 entry.target.classList.add('is-visible'); 441 obs.unobserve(entry.target); 442 } 443 }); 444 }, { threshold: 0.1 }); 445 els.forEach(function (el) { obs.observe(el); }); 446 } 447 448 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 449 6. NUMBER COUNTER â IntersectionObserver 450 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 451 function initCounters() { 452 // Chá» animate bá» Äếm sá» liá»u (thẻ lá chá» chứa sá»); Bá» QUA gallery có data-count (có phần tá» con) 453 var els = Array.prototype.filter.call( 454 document.querySelectorAll('[data-count]'), 455 function (el) { return el.children.length === 0; } 456 ); 457 if (!els.length || !('IntersectionObserver' in window)) return; 458 var obs = new IntersectionObserver(function (entries) {
459 entries.forEach(function (entry) { 460 if (!entry.isIntersecting) return; 461 var el = entry.target; 462 var end = parseInt(el.getAttribute('data-count'), 10); 463 var dur = 1600, step = 16; 464 var inc = end / (dur / step), cur = 0; 465 var iv = setInterval(function () { 466 cur = Math.min(cur + inc, end); 467 el.textContent = Math.floor(cur).toLocaleString('vi-VN'); 468 if (cur >= end) clearInterval(iv); 469 }, step); 470 obs.unobserve(el); 471 }); 472 }, { threshold: 0.5 }); 473 els.forEach(function (el) { obs.observe(el); }); 474 } 475 476 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 477 CONTACT FORM VALIDATION â lssk-contact-form 478 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 479 function initFormValidation() { 480 document.querySelectorAll('.lssk-contact-form, .lssk-form--validate').forEach(function (form) { 481 form.setAttribute('novalidate', ''); 482 483 function showError(field, msg) { 484 field.classList.add('lssk-field--error'); 485 var err = field.parentNode.querySelector('.lssk-field-error'); 486 if (!err) { 487 err = document.createElement('span'); 488 err.className = 'lssk-field-error'; 489 field.parentNode.appendChild(err); 490 } 491 err.textContent = msg; 492 } 493 function clearError(field) { 494 field.classList.remove('lssk-field--error'); 495 var err = field.parentNode.querySelector('.lssk-field-error'); 496 if (err) err.remove(); 497 } 498 499 /* Clear on input */ 500 form.querySelectorAll('input, textarea, select').forEach(function (f) { 501 f.addEventListener('input', function () { clearError(f); }); 502 }); 503 504 form.addEventListener('submit', function (e) { 505 var valid = true; 506 form.querySelectorAll('[required]').forEach(function (field) { 507 var v = field.value.trim(); 508 clearError(field); 509 if (!v) { 510 showError(field, 'Vui lòng Äiá»n và o trưá»ng nà y'); 511 valid = false; 512 } else if (field.type === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) { 513 showError(field, 'Email không Äúng Äá»nh dạng'); 514 valid = false; 515 } else if (field.type === 'tel' && v.replace(/\D/g, '').length < 9) { 516 showError(field, 'Sá» Äiá»n thoại không hợp lá»'); 517 valid = false; 518 } 519 }); 520 if (!valid) { 521 e.preventDefault(); 522 var firstErr = form.querySelector('.lssk-field--error'); 523 if (firstErr) firstErr.scrollIntoView({ behavior: 'smooth', block: 'center' }); 524 } 525 }); 526 }); 527 } 528 529 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 530 LAZY IMAGES â IntersectionObserver fallback 531 Native loading="lazy" handles most cases; 532 this adds fade-in for better UX 533 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 534 function initLazyImages() { 535 if (!('IntersectionObserver' in window)) return; 536 var imgs = document.querySelectorAll('img[loading="lazy"]:not([data-lssk-observed])'); 537 if (!imgs.length) return; 538 var obs = new IntersectionObserver(function (entries) {
539 entries.forEach(function (entry) { 540 if (entry.isIntersecting) { 541 var img = entry.target; 542 img.addEventListener('load', function () { 543 img.style.opacity = '1'; 544 }); 545 img.style.opacity = img.complete ? '1' : '0'; 546 img.style.transition = 'opacity .3s ease'; 547 if (img.complete) img.style.opacity = '1'; 548 obs.unobserve(img); 549 } 550 }); 551 }, { rootMargin: '200px 0px' }); 552 553 imgs.forEach(function (img) { 554 img.setAttribute('data-lssk-observed', '1'); 555 obs.observe(img); 556 }); 557 } 558 559 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 560 6b. PRODUCT TABS â Mô tả / Video / Mô hình 3D 561 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 562 function initProductTabs() { 563 document.querySelectorAll('.lssk-sp-tabs').forEach(function (wrap) { 564 var btns = Array.from(wrap.querySelectorAll('.lssk-sp-tab-btn')); 565 if (!btns.length) return; 566 567 /* ââ Activate má»t tab theo target id ââ */ 568 function activate(target, updateHash) { 569 /* Ẩn tất cả panels, deselect tất cả buttons */ 570 btns.forEach(function (b) { 571 var isTarget = b.dataset.tab === target; 572 b.classList.toggle('is-active', isTarget); 573 b.setAttribute('aria-selected', isTarget ? 'true' : 'false'); 574 }); 575 wrap.querySelectorAll('.lssk-sp-tab-panel').forEach(function (p) { 576 var isTarget = p.id === 'sp-tab-' + target; 577 p.classList.toggle('is-active', isTarget); 578 /* hidden attribute: loại bá» khá»i tab order + ẩn vá»i AT */ 579 if (isTarget) p.removeAttribute('hidden'); 580 else p.setAttribute('hidden', ''); 581 }); 582 583 /* Lazy-init model-viewer khi má» tab model lần Äầu */ 584 if (target === 'model') { 585 var panel = wrap.querySelector('#sp-tab-model'); 586 var mv = panel && panel.querySelector('model-viewer:not([data-loaded])'); 587 if (mv) { 588 mv.setAttribute('data-loaded', '1'); 589 setTimeout(function () { window.dispatchEvent(new Event('resize')); }, 120); 590 } 591 } 592 593 /* Cáºp nháºt URL hash (không scroll lại) */ 594 if (updateHash && history.replaceState) { 595 history.replaceState(null, '', '#sp-' + target); 596 } 597 } 598 599 /* ââ Click ââ */ 600 btns.forEach(function (btn) { 601 btn.addEventListener('click', function () { 602 activate(btn.dataset.tab, true); 603 }); 604 }); 605 606 /* ââ Keyboard: â â Home End (ARIA tabs pattern) ââ */ 607 wrap.querySelector('.lssk-sp-tab-nav').addEventListener('keydown', function (e) { 608 var cur = btns.indexOf(document.activeElement); 609 if (cur === -1) return; 610 var next = cur; 611 if (e.key === 'ArrowRight') next = (cur + 1) % btns.length; 612 else if (e.key === 'ArrowLeft') next = (cur - 1 + btns.length) % btns.length; 613 else if (e.key === 'Home') next = 0; 614 else if (e.key === 'End') next = btns.length - 1; 615 else return; 616 e.preventDefault(); 617 btns[next].focus(); 618 activate(btns[next].dataset.tab, true); 619 }); 620 621 /* ââ Hash on load â /san-pham/slug/#sp-video má» Äúng tab ââ */ 622 var hash = window.location.hash.replace('#sp-', ''); 623 var hashBtn = hash && btns.find(function (b) { return b.dataset.tab === hash; }); 624 if (hashBtn) { 625 /* Scroll và o tab section sau khi DOM ready */ 626 setTimeout(function () { 627 activate(hash, false); 628 wrap.scrollIntoView({ behavior: 'smooth', block: 'start' }); 629 }, 300); 630 } 631 }); 632 } 633 634 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 635 7. ENTRY-CONTENT IMAGE LIGHTBOX 636 Dùng lssk-lb (gallery.js) thay Fancybox 637 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 638 function initContentImages() { 639 document.querySelectorAll('.lssk-entry-content').forEach(function (content) { 640 var imageLinks = Array.from(content.querySelectorAll('a')).filter(function (a) { 641 return /\.(jpg|jpeg|png|webp|gif|avif)(\?|$)/i.test(a.href); 642 }); 643 if (!imageLinks.length) return; 644 645 /* Build group for lightbox navigation */ 646 var group = imageLinks.map(function (a) { 647 var img = a.querySelector('img'); 648 return { src: a.href, thumb: a.href, alt: img ? (img.alt || '') : '' }; 649 }); 650 651 imageLinks.forEach(function (a, idx) { 652 a.addEventListener('click', function (e) { 653 e.preventDefault(); 654 document.dispatchEvent(new CustomEvent('lssk:open-image', {
655 detail: { group: group, index: idx }, 656 })); 657 }); 658 }); 659 }); 660 } 661 662 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 663 UTILITY 664 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 665 function escHtml(str) { 666 return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); 667 } 668 669 /* âââââââââââââââââââââââââââââââââââââââââââââââââââ 670 INIT 671 âââââââââââââââââââââââââââââââââââââââââââââââââââ */ 672 function init() { 673 initHeroSlider(); 674 initNav(); 675 initSearch(); 676 initInlineLiveSearch(); 677 initStickyUI(); 678 initAnimations(); 679 initCounters(); 680 initLazyImages(); 681 initContentImages(); 682 initProductTabs(); 683 } 684 685 if (document.readyState === 'loading') { 686 document.addEventListener('DOMContentLoaded', init); 687 } else { 688 init(); 689 } 690 691})();
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.