/* ===== Otus Branding - custom JS for Salla (JS field only) ===== */ /* 1. Mobile bottom bar: link to portfolio (sits below popups and hides while typing or when a popup is open) */ (function () { var portfolio = 'https://otusprojekt.net'; // your portfolio link function init() { if (document.getElementById('otus-cta-bar')) return; 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(31,94,67,.15);' + 'padding:10px 16px calc(10px + env(safe-area-inset-bottom));direction:rtl;' + 'align-items:center;justify-content:space-between;gap:12px;' + 'font-family:var(--font-main),sans-serif}' + '#otus-cta-bar span{font-size:14px;color:#333}' + '#otus-cta-bar a{background:#1f5e43;color:#fff;padding:10px 22px;border-radius:999px;' + 'font-size:15px;font-weight:700;text-decoration:none;white-space:nowrap}' + '@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); var bar = document.createElement('div'); bar.id = 'otus-cta-bar'; bar.innerHTML = 'Contact us now on Whatsapp' + 'شاهد أعمالنا'; document.body.appendChild(bar); // Is any popup/dialog open on the page? 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? (also looks inside shadow roots) 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); } function update() { bar.classList.toggle('otus-cta-off', typing() || popupOpen()); } 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: thin green line at the top that fills as you scroll */ (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:#1f5e43;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); 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. Gallery in one row + varied, staggered reveals + count-up numbers (Salla puts sections inside shadow roots, so everything looks inside them) */ (function () { var galleryCss = ` .s-block--images-gallery .lp-container.wrapper { display: flex !important; flex-wrap: nowrap !important; gap: 8px !important; overflow-x: auto !important; scroll-snap-type: x mandatory; } .s-block--images-gallery .image-item { flex: 1 1 0 !important; min-width: 0 !important; width: auto !important; height: auto !important; aspect-ratio: 1 / 1; scroll-snap-align: start; } @media (max-width: 767px) { .s-block--images-gallery .image-item { flex: 0 0 40% !important; } } `; // Sections: four entrance styles that rotate down the page // Items inside a section: fade up one after another 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;}'; var REVEAL = '.s-block, .banner-entry'; var CHILD = '.image-item, .faq-item, [class*="statistic"] [class*="item"]'; var VARIANTS = ['v-up', 'v-right', 'v-left', 'v-zoom']; var STAGGER = 90; // ms between items var MAX_STEPS = 8; // cap so long lists don't wait too long var variantIndex = 0; var noMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; // Keep sideways slides from causing a horizontal scrollbar if (!noMotion) { var clip = document.createElement('style'); clip.textContent = 'html,body{overflow-x:clip}'; document.head.appendChild(clip); } var io = noMotion ? null : 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) { // Fully off screen: reset so it animates again next time. // Above the screen = it comes back from the top when scrolling up. el.classList.toggle('otus-above', e.boundingClientRect.top < 0); el.classList.remove('otus-show'); } }); }, { threshold: [0, 0.15] }); 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) { if (!io) return; var targets = root.querySelectorAll(REVEAL); if (!targets.length) return; addStyle(root, 'otus-reveal-style', revealCss); // Sections 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]); // Already on screen: start visible so the first view never flashes if (el.getBoundingClientRect().top < window.innerHeight) el.classList.add('otus-show'); io.observe(el); }); // Items inside sections (products, gallery images, FAQs, stats) root.querySelectorAll(CHILD).forEach(function (c) { if (c.dataset.otusChild) return; var section = c.closest(REVEAL); // Only stagger items whose section is animated, otherwise they'd stay hidden 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'); }); } /* ----- Count-up numbers in the statistics block ----- */ var AR = '٠١٢٣٤٥٦٧٨٩'; function toWestern(s) { return s.replace(/[٠-٩]/g, function (d) { return AR.indexOf(d); }); } function toArabic(s) { return s.replace(/[0-9]/g, function (d) { return AR[d]; }); } var countIo = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (!e.isIntersecting) return; countIo.unobserve(e.target); runCount(e.target); }); }, { threshold: 0.5 }); function runCount(el) { var d = el._otusCount; if (noMotion) { el.textContent = d.original; return; } var start = null, duration = 1600; function step(t) { if (!start) start = t; var p = Math.min(1, (t - start) / duration); var eased = 1 - Math.pow(1 - p, 3); // ease-out var n = Math.round(d.target * eased); var s = d.commas ? n.toLocaleString('en-US') : String(n); if (d.arabic) s = toArabic(s); el.textContent = d.prefix + s + d.suffix; if (p < 1) requestAnimationFrame(step); else el.textContent = d.original; // end exactly on the original text } requestAnimationFrame(step); } function counters(root) { var inStats = root.host && root.host.closest && root.host.closest('[class*="statistic"]'); var candidates = root.querySelectorAll(inStats ? '*' : '[class*="statistic"] *'); candidates.forEach(function (el) { if (el.dataset.otusCount || el.children.length) return; var text = el.textContent; var m = /^(\s*\D{0,3}?)([0-9٠-٩][0-9٠-٩,]*)(\s*\D{0,8}\s*)$/.exec(text); if (!m) return; var target = parseInt(toWestern(m[2]).replace(/,/g, ''), 10); if (!target || target < 2) return; el.dataset.otusCount = '1'; el._otusCount = { original: text, prefix: m[1], suffix: m[3], target: target, commas: m[2].indexOf(',') > -1, arabic: /[٠-٩]/.test(m[2]) }; countIo.observe(el); }); } function scan(root) { reveal(root); counters(root); root.querySelectorAll('*').forEach(function (el) { var sr = el.shadowRoot; if (!sr) return; if (sr.querySelector('.s-block--images-gallery')) { addStyle(sr, 'otus-gallery-style', galleryCss); } scan(sr); // nested shadow roots too }); } // Salla loads sections gradually, so retry for a few seconds var tries = 0; var timer = setInterval(function () { scan(document); if (++tries > 20) clearInterval(timer); }, 500); })(); /* 4. Reviews: tap a review to read the full text, tap again to shrink (reviews sit inside a shadow root, so this looks inside it) */ (function () { // Mark reviews whose text is cut off, so only those get "اقرأ المزيد" function mark(root) { root.querySelectorAll('.s-block-trust-testimonials .swiper-slide').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 (a swipe doesn't count as a tap) 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-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; } } }); var tries = 0; var timer = setInterval(function () { scan(document); if (++tries > 20) clearInterval(timer); }, 500); window.addEventListener('resize', function () { scan(document); }); })(); /* 5. Header: WhatsApp button + hide the cart while it's empty */ (function () { var WHATSAPP = '9665XXXXXXXX'; // ← your WhatsApp number: country code + number, no + or spaces var MESSAGE = 'مرحبا، أبغى أستفسر عن خدمات التصميم'; var ICON = ''; function addWhatsApp() { if (document.getElementById('otus-wa-header')) return true; var anchor = document.querySelector('.store-header salla-user-menu'); if (!anchor) return false; var a = document.createElement('a'); a.id = 'otus-wa-header'; a.href = 'https://wa.me/' + WHATSAPP + '?text=' + encodeURIComponent(MESSAGE); a.target = '_blank'; a.rel = 'noopener'; a.setAttribute('aria-label', 'WhatsApp'); a.innerHTML = ICON + 'تواصل معنا'; anchor.parentNode.insertBefore(a, anchor); return true; } // Hide the cart button while the count is 0, show it when something is added 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; } var watched = false; function updateCart() { var count = document.querySelector('.store-header .s-cart-summary-count'); var cart = document.querySelector('.store-header salla-cart-summary'); if (!count || !cart) return false; cart.style.display = cartCount(count) > 0 ? '' : 'none'; if (!watched) { watched = true; new MutationObserver(updateCart).observe(count, { childList: true, characterData: true, subtree: true }); } return true; } var tries = 0; var timer = setInterval(function () { var done = addWhatsApp() & updateCart(); if (done || ++tries > 20) clearInterval(timer); }, 500); })(); /* 6. Reviews: auto-swipe every 2 seconds (reviews sit inside a shadow root, so this looks inside it) */ (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'; 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; }); 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); }); } var finder = setInterval(function () { scan(document); if (++tries > 40) clearInterval(finder); }, 500); })();