/* ========================================================== KIWICUB · SALLA CUSTOM JS (paste into the JS field only) Contents 0. Settings colors and links, edit these first 1. Mobile bottom bar "اطلب الآن" on product pages, Instagram everywhere else 2. Scroll progress bar thin line at the top of the screen 3. Section reveals sections fade in as you scroll 4. Reviews: read more tap a review to open the full text 5. Cart bump cart count bounces when a product is added 6. Reviews: auto-swipe reviews move on their own every 2 seconds 7. Buy now button rounded, same size as "أضف إلى السلة" 8. Bought together removes the "كمّل طلبك" block from product pages 9. Quick buy on cards Apple Pay / "اشترِ الآن" under products on the homepage ========================================================== */ /* ---------- 0. SETTINGS ---------- */ var KIWI = { color: '#8E4B5E', // bottom bar button color (Kiwicub rose) progressColor: '#4A8B5C', // scroll progress line color (Kiwicub green) instagram: 'https://instagram.com/kiwicub.sa' // link for the "تابعنا" button }; /* ---------- 1. MOBILE BOTTOM BAR ---------- Phones only (768px and below). - Product pages: "اطلب الآن" scrolls to the add-to-cart button, and the bar hides while that button is already on screen. - All other pages: "تابعنا" opens Instagram in a new tab (or the app). - The bar also hides while typing or when a popup is open. */ (function () { function init() { // Only build the bar once if (document.getElementById('otus-cta-bar')) return; // A product page has the product form with its add-to-cart button. // (Product cards on the homepage have add-to-cart buttons too, // so a plain button search would wrongly treat the homepage as a product page) var addBtn = document.querySelector( 'form.product-form salla-add-product-button, .sticky-product-bar salla-add-product-button' ); var isProduct = !!addBtn; // Bar styles var css = document.createElement('style'); css.textContent = '#otus-cta-bar{display:none;position:fixed;left:0;right:0;bottom:0;z-index:40;' + 'background:#fff;border-top:1px solid rgba(0,0,0,.08);' + 'padding:10px 16px calc(10px + env(safe-area-inset-bottom));direction:rtl;' + 'align-items:center;justify-content:center;gap:14px;text-align:center;' + // centered text + button 'font-family:var(--font-main),sans-serif}' + '#otus-cta-bar span{font-size:14px;color:#333}' + '#otus-cta-bar a{display:inline-flex;align-items:center;gap:8px;margin-inline-start:12px;' + // icon + text side by side; nudged left 'background:' + KIWI.color + ';color:#fff;padding:10px 22px;border-radius:999px;' + 'font-size:15px;font-weight:700;text-decoration:none;white-space:nowrap}' + '#otus-cta-bar a svg{width:18px;height:18px;flex:0 0 18px}' + '@media(max-width:768px){#otus-cta-bar{display:flex}body{padding-bottom:70px!important}}' + '#otus-cta-bar.otus-cta-off{display:none!important}'; document.head.appendChild(css); // Instagram icon (white outline, sits inside the button) var IG_ICON = ''; // Bar content: product page vs every other page var bar = document.createElement('div'); bar.id = 'otus-cta-bar'; bar.innerHTML = isProduct ? 'عجبك المنتج؟' + 'اطلب الآن' : 'تابع جديدنا على انستقرام' + '' + IG_ICON + 'تابعنا'; document.body.appendChild(bar); // Product page: button scrolls to the real add-to-cart button, // and the bar hides while that button is visible var addVisible = false; if (isProduct) { bar.querySelector('#otus-cta-btn').addEventListener('click', function (e) { e.preventDefault(); addBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); new IntersectionObserver(function (entries) { addVisible = entries[0].isIntersecting; update(); }).observe(addBtn); } // Is a popup (cart, login, options...) open right now? function popupOpen() { var els = document.querySelectorAll( '[role="dialog"], [aria-modal="true"], salla-modal, .s-modal-overlay, .s-modal-wrapper' ); for (var i = 0; i < els.length; i++) { var r = els[i].getBoundingClientRect(); if (r.width > 0 && r.height > 0) return true; } return false; } // Is the visitor typing in a field? (checks inside Salla components too) function typing() { var a = document.activeElement; while (a && a.shadowRoot && a.shadowRoot.activeElement) a = a.shadowRoot.activeElement; return !!a && /^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName); } // Show or hide the bar function update() { bar.classList.toggle('otus-cta-off', typing() || popupOpen() || addVisible); } document.addEventListener('focusin', update, true); document.addEventListener('focusout', function () { setTimeout(update, 50); }, true); document.addEventListener('click', function () { setTimeout(update, 300); }, true); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })(); /* ---------- 2. SCROLL PROGRESS BAR ---------- A thin line at the very top that fills (right to left) as you scroll down. */ (function () { function init() { if (document.getElementById('otus-progress')) return; var css = document.createElement('style'); css.textContent = '#otus-progress{position:fixed;top:0;left:0;right:0;height:3px;z-index:9999;' + 'background:' + KIWI.progressColor + ';transform:scaleX(0);transform-origin:right;' + 'pointer-events:none;transition:transform .1s linear}'; document.head.appendChild(css); var bar = document.createElement('div'); bar.id = 'otus-progress'; document.body.appendChild(bar); // Update at most once per frame while scrolling var ticking = false; function update() { var max = document.documentElement.scrollHeight - window.innerHeight; var p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0; bar.style.transform = 'scaleX(' + p + ')'; ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true }); window.addEventListener('resize', update); update(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })(); /* ---------- 3. SECTION REVEALS ---------- Each section fades in as it enters the screen, cycling through 4 directions (up, right, left, zoom). Images and FAQ items inside fade in one after another. Product cards are NOT animated one by one: that breaks them on touch. Turned off for visitors who ask their phone for reduced motion. */ (function () { var REVEAL = '.s-block, .banner-entry'; // what fades in as a whole var CHILD = '.image-item, .faq-item'; // items inside that fade in one by one var VARIANTS = ['v-up', 'v-right', 'v-left', 'v-zoom']; var STAGGER = 90; // delay between items (ms) var MAX_STEPS = 8; // longest delay = 8 × 90ms var variantIndex = 0; var revealCss = '.otus-hide{opacity:0;transition:opacity .8s ease,transform .8s cubic-bezier(.2,.7,.2,1);}' + '.otus-hide.v-up{transform:translateY(40px) scale(.96);}' + '.otus-hide.v-left{transform:translateX(-60px);}' + '.otus-hide.v-right{transform:translateX(60px);}' + '.otus-hide.v-zoom{transform:scale(.88);}' + '.otus-hide.otus-above{transform:translateY(-40px) scale(.96);}' + '.otus-hide.otus-show{opacity:1;transform:none;}' + '.otus-child{opacity:0;transform:translateY(24px);' + 'transition:opacity .6s ease,transform .6s cubic-bezier(.2,.7,.2,1);}' + '.otus-show .otus-child{opacity:1;transform:none;}'; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; // Sideways animations must not create a horizontal scrollbar var clip = document.createElement('style'); clip.textContent = 'html,body{overflow-x:clip}'; document.head.appendChild(clip); // Show a section when 15% of it is on screen; reset it once fully off screen var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { var el = e.target; if (e.intersectionRatio >= 0.15) { el.classList.add('otus-show'); } else if (!e.isIntersecting) { el.classList.toggle('otus-above', e.boundingClientRect.top < 0); el.classList.remove('otus-show'); } }); }, { threshold: [0, 0.15] }); // Add the animation styles once per document / component function addStyle(root, id, text) { if (root.getElementById && root.getElementById(id)) return; if (root.querySelector && root.querySelector('#' + id)) return; var s = document.createElement('style'); s.id = id; s.textContent = text; (root === document ? document.head : root).appendChild(s); } function reveal(root) { var targets = root.querySelectorAll(REVEAL); if (!targets.length) return; addStyle(root, 'otus-reveal-style', revealCss); // Sections (never the header or footer) targets.forEach(function (el) { if (el.dataset.otus || el.closest('header, footer')) return; el.dataset.otus = '1'; el.classList.add('otus-hide', VARIANTS[variantIndex++ % VARIANTS.length]); if (el.getBoundingClientRect().top < window.innerHeight) el.classList.add('otus-show'); io.observe(el); }); // Items inside sections, each a little later than the one before root.querySelectorAll(CHILD).forEach(function (c) { if (c.dataset.otusChild) return; var section = c.closest(REVEAL); if (!section || !section.dataset.otus) return; c.dataset.otusChild = '1'; var i = Array.prototype.indexOf.call(c.parentElement.children, c); c.style.transitionDelay = (Math.min(i, MAX_STEPS) * STAGGER) + 'ms'; c.classList.add('otus-child'); }); } // Look in the page and inside Salla components function scan(root) { reveal(root); root.querySelectorAll('*').forEach(function (el) { if (el.shadowRoot) scan(el.shadowRoot); }); } // Salla loads sections gradually, so check every 0.5s for 10 seconds var tries = 0; var timer = setInterval(function () { scan(document); if (++tries > 20) clearInterval(timer); }, 500); })(); /* ---------- 4. REVIEWS: READ MORE ---------- Reviews are cut to 4 lines by the CSS. This marks which ones are actually cut (so only those show "اقرأ المزيد"), and opens/closes a review on tap. A swipe doesn't count as a tap. */ (function () { var SLIDES = ':is(.s-block--testimonials, .s-block-trust-testimonials) .swiper-slide'; // Mark reviews whose text is cut off function mark(root) { root.querySelectorAll(SLIDES).forEach(function (s) { if (s.classList.contains('otus-open')) return; var cut = false; s.querySelectorAll('p').forEach(function (p) { if (p.scrollHeight > p.clientHeight + 1) cut = true; }); s.classList.toggle('otus-clamped', cut); }); } function scan(root) { mark(root); root.querySelectorAll('*').forEach(function (el) { if (el.shadowRoot) scan(el.shadowRoot); }); } // Tap to open / close document.addEventListener('click', function (e) { var path = e.composedPath ? e.composedPath() : []; var inReviews = path.some(function (n) { return n.classList && (n.classList.contains('s-block--testimonials') || n.classList.contains('s-block-trust-testimonials')); }); if (!inReviews) return; for (var i = 0; i < path.length; i++) { var el = path[i]; if (el.classList && el.classList.contains('swiper-slide')) { if (el.classList.contains('otus-clamped') || el.classList.contains('otus-open')) { el.classList.toggle('otus-open'); } break; } } }); // Check for reviews every 0.5s for 10 seconds, and again on resize var tries = 0; var timer = setInterval(function () { scan(document); if (++tries > 20) clearInterval(timer); }, 500); window.addEventListener('resize', function () { scan(document); }); })(); /* ---------- 5. CART BUMP ---------- The cart count in the header bounces when a product is added. */ (function () { var css = document.createElement('style'); css.textContent = '@keyframes otusBump{0%{transform:scale(1)}30%{transform:scale(1.35)}60%{transform:scale(.9)}100%{transform:scale(1)}}' + '.otus-bump{display:inline-block;animation:otusBump .5s ease}'; document.head.appendChild(css); // Read the count, including Arabic digits (٠-٩) var AR = '٠١٢٣٤٥٦٧٨٩'; function cartCount(el) { var t = (el.textContent || '').replace(/[٠-٩]/g, function (d) { return AR.indexOf(d); }); return parseInt(t.replace(/\D/g, ''), 10) || 0; } // Watch the count and bounce only when it goes up function watchCart() { var count = document.querySelector('.store-header .s-cart-summary-count'); if (!count) return false; var last = cartCount(count); new MutationObserver(function () { var n = cartCount(count); if (n > last) { count.classList.remove('otus-bump'); void count.offsetWidth; // restart the animation count.classList.add('otus-bump'); } last = n; }).observe(count, { childList: true, characterData: true, subtree: true }); return true; } // Wait for the header to load (up to 10 seconds) var tries = 0; var timer = setInterval(function () { if (watchCart() || ++tries > 20) clearInterval(timer); }, 500); })(); /* ---------- 6. REVIEWS: AUTO-SWIPE ---------- Moves to the next review every 2 seconds and loops back to the first. Pauses while touched (resumes 8 seconds later), on mouse hover, while a review is opened, and when the tab is in the background. */ (function () { var BLOCK = '.s-block--testimonials, .s-block-trust-testimonials'; var DELAY = 2000; // time per review (ms) var RESUME_AFTER = 8000; // wait after the visitor touches it (ms) var tries = 0; function start(block) { if (block.dataset.otusAuto) return; var el = block.querySelector('.swiper'); var sw = el && el.swiper; var btn = block.querySelector('.swiper-button-next, [class*="nav"] button:last-child'); // Prefer the slider itself; fall back to its "next" button if needed var goNext; if (sw) { goNext = function () { if (sw.isEnd) sw.slideTo(0); else sw.slideNext(); }; } else if (btn && tries > 10) { goNext = function () { btn.click(); }; } else { return; // not ready yet, try again on the next check } block.dataset.otusAuto = '1'; // Pausing var paused = false, resumeTimer; function pauseForAWhile() { paused = true; clearTimeout(resumeTimer); resumeTimer = setTimeout(function () { paused = false; }, RESUME_AFTER); } block.addEventListener('touchstart', pauseForAWhile, { passive: true }); block.addEventListener('mouseenter', function () { paused = true; }); block.addEventListener('mouseleave', function () { paused = false; }); // Move on every DELAY ms setInterval(function () { if (paused || document.hidden) return; if (block.querySelector('.swiper-slide.otus-open')) return; // a review is opened goNext(); }, DELAY); } function scan(root) { root.querySelectorAll(BLOCK).forEach(start); root.querySelectorAll('*').forEach(function (el) { if (el.shadowRoot) scan(el.shadowRoot); }); } // Look for review blocks every 0.5s for 20 seconds var finder = setInterval(function () { scan(document); if (++tries > 40) clearInterval(finder); }, 500); })(); /* ---------- 7. BUY NOW BUTTON ---------- "اشترِ الآن" is drawn by Salla inside a sealed widget (shadow root), so the store CSS can't reach it. This adds a small style inside the widget: rounded corners and the same height and text size as "أضف إلى السلة". */ (function () { var STYLE = 'button,.s-fast-checkout-button{' + // the button inside the widget 'border-radius:10px!important;' + 'height:44px!important;min-height:44px!important;' + 'font-size:14px!important;font-weight:700!important}'; function fix() { document.querySelectorAll('salla-mini-checkout-widget').forEach(function (w) { var root = w.shadowRoot; if (!root || root.getElementById('otus-mini-style')) return; // not ready, or already done var s = document.createElement('style'); s.id = 'otus-mini-style'; s.textContent = STYLE; root.appendChild(s); }); } // The widget loads after the page, so check every 0.5s for 10 seconds var tries = 0; var timer = setInterval(function () { fix(); if (++tries > 20) clearInterval(timer); }, 500); })(); /* ---------- 8. REMOVE "BOUGHT TOGETHER" ---------- Removes Salla's "كمّل طلبك / اشتريها معًا" block from product pages. (The CSS hides it too; this is a backup in case the CSS doesn't load fully.) */ (function () { function remove() { document.querySelectorAll('salla-bought-together, .s-bought-together-entry') .forEach(function (el) { el.remove(); }); } // The block can load late, so check every 0.5s for 10 seconds var tries = 0; var timer = setInterval(function () { remove(); if (++tries > 20) clearInterval(timer); }, 500); })(); /* ---------- 9. QUICK BUY ON PRODUCT CARDS ---------- Turns on Salla's quick-buy button (Apple Pay on iPhone/Safari, "اشترِ الآن" elsewhere) under products on the homepage. It takes the customer straight to payment, skipping the cart. ALL_CARDS: false = only the big offer card, true = every product card. */ (function () { var ALL_CARDS = false; var CARDS = ALL_CARDS ? '.s-product-card-entry' : '.s-product-card-special'; function enable() { document.querySelectorAll(CARDS + ' salla-add-product-button:not([quick-buy])') .forEach(function (btn) { btn.setAttribute('quick-buy', ''); // ask Salla to add the quick-buy button if ('quickBuy' in btn) btn.quickBuy = true; // same thing, set on the component directly }); } // Product cards load gradually, so check every 0.5s for 10 seconds var tries = 0; var timer = setInterval(function () { enable(); if (++tries > 20) clearInterval(timer); }, 500); })();