/* ===== 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);
})();