1/* ===================================================================== 2 Mobstep â index_new.html interactions (main_new.js) 3 Vanilla JS. No dependency on legacy main.js. 4 ===================================================================== */ 5(function () { 6 "use strict"; 7 8 document.addEventListener("DOMContentLoaded", function () { 9 /* ---------- Sticky header ---------- */ 10 var header = document.getElementById("ms-header"); 11 var toTop = document.getElementById("ms-totop"); 12 function onScroll() { 13 var y = window.pageYOffset || document.documentElement.scrollTop; 14 if (header) header.classList.toggle("ms-scrolled", y > 20); 15 if (toTop) toTop.classList.toggle("is-visible", y > 600); 16 } 17 window.addEventListener("scroll", onScroll, { passive: true }); 18 onScroll(); 19 if (toTop) { 20 toTop.addEventListener("click", function () { 21 window.scrollTo({ top: 0, behavior: "smooth" }); 22 }); 23 } 24 25 /* ---------- Theme toggle (dark-first, persisted) ---------- */ 26 var THEME_KEY = "ms-theme"; 27 var themeBtns = document.querySelectorAll("[data-theme-toggle]"); 28 function applyTheme(theme) { 29 document.body.classList.toggle("ms-light", theme === "light"); 30 themeBtns.forEach(function (b) { 31 var icon = b.querySelector("i"); 32 if (icon) icon.className = theme === "light" ? "las la-moon" : "las la-sun"; 33 }); 34 } 35 var stored = null; 36 try { stored = localStorage.getItem(THEME_KEY); } catch (e) {} 37 applyTheme(stored === "light" ? "light" : "dark"); 38 themeBtns.forEach(function (b) { 39 b.addEventListener("click", function () { 40 var next = document.body.classList.contains("ms-light") ? "dark" : "light"; 41 applyTheme(next); 42 try { localStorage.setItem(THEME_KEY, next); } catch (e) {} 43 }); 44 }); 45 46 /* ---------- Mobile menu ---------- */ 47 var mobile = document.getElementById("ms-mobile-menu"); 48 document.querySelectorAll("[data-menu-open]").forEach(function (b) { 49 b.addEventListener("click", function () { 50 if (mobile) { mobile.classList.add("is-open"); document.body.style.overflow = "hidden"; } 51 }); 52 }); 53 function closeMobile() { 54 if (mobile) { mobile.classList.remove("is-open"); document.body.style.overflow = ""; } 55 } 56 document.querySelectorAll("[data-menu-close]").forEach(function (b) { 57 b.addEventListener("click", closeMobile); 58 }); 59 if (mobile) { 60 mobile.querySelectorAll("a").forEach(function (a) { 61 if (a.getAttribute("href") && a.getAttribute("href").indexOf("#") === 0) { 62 a.addEventListener("click", closeMobile); 63 } 64 }); 65 } 66 67 /* ---------- Tabs ---------- */ 68 document.querySelectorAll("[data-tabs]").forEach(function (group) { 69 var btns = group.querySelectorAll(".ms-tab-btn"); 70 var panels = group.querySelectorAll(".ms-tab-panel"); 71 btns.forEach(function (btn) { 72 btn.addEventListener("click", function () { 73 var target = btn.getAttribute("data-tab"); 74 btns.forEach(function (b) { b.classList.remove("is-active"); }); 75 panels.forEach(function (p) { p.classList.remove("is-active"); }); 76 btn.classList.add("is-active"); 77 var panel = group.querySelector('.ms-tab-panel[data-panel="' + target + '"]'); 78 if (panel) panel.classList.add("is-active"); 79 }); 80 }); 81 }); 82 83 /* ---------- Pricing toggle ---------- */ 84 var pTog = document.getElementById("ms-price-toggle"); 85 var pWrap = document.getElementById("ms-pricing"); 86 var lblM = document.getElementById("ms-lbl-monthly"); 87 var lblA = document.getElementById("ms-lbl-annual"); 88 if (pTog && pWrap) { 89 pTog.addEventListener("click", function () { 90 var annual = pTog.classList.toggle("is-annual"); 91 pWrap.classList.toggle("is-annual", annual); 92 pTog.setAttribute("aria-checked", annual ? "true" : "false"); 93 if (lblM) lblM.classList.toggle("is-active", !annual); 94 if (lblA) lblA.classList.toggle("is-active", annual); 95 }); 96 } 97 98 /* ---------- Currency switcher ---------- 99 Prices are published in USD; SAR and EGP are the local list prices, not 100 a live FX conversion. Every amount lives in MS_PRICES so the pages only 101 carry a USD fallback for no-JS visitors. */ 102 var MS_CUR = { 103 USD: { symbol: "$" }, 104 SAR: { symbol: "SAR " }, 105 EGP: { symbol: "EGP " } 106 }; 107 /* [monthly, annual] â annual is ten months, i.e. "2 months free". */ 108 var MS_PRICES = { 109 basic: { USD: [49, 490], SAR: [179, 1790], EGP: [2499, 24990] }, 110 premium: { USD: [99, 990], SAR: [369, 3690], EGP: [4999, 49990] }, 111 delivery: { USD: [45, 450], SAR: [169, 1690], EGP: [2249, 22490] }, 112 delivery_was: { USD: [60, 600], SAR: [229, 2290], EGP: [2999, 29990] }, 113 care: { USD: [19, 190], SAR: [69, 690], EGP: [949, 9490] }, 114 care_was: { USD: [25, 250], SAR: [89, 890], EGP: [1249, 12490] }, 115 /* Per 1,000 AI replies in Customer Support; usage, so only [0] is shown. */ 116 ai_replies: { USD: [15, 150], SAR: [59, 590], EGP: [749, 7490] }, 117 marketing: { USD: [29, 290], SAR: [109, 1090], EGP: [1449, 14490] }, 118 marketing_was: { USD: [40, 400], SAR: [149, 1490], EGP: [1999, 19990] } 119 }; 120 /* Countries whose visitors see a local price by default. */ 121 var MS_CUR_BY_COUNTRY = { SA: "SAR", EG: "EGP" }; 122 var MS_CUR_BY_TZ = { "Asia/Riyadh": "SAR", "Africa/Cairo": "EGP", "Egypt": "EGP" }; 123 var MS_CUR_KEY = "ms-currency"; 124 125 var curBox = document.getElementById("ms-currency"); 126 if (curBox && pWrap) { 127 var curBtns = curBox.querySelectorAll("[data-ms-cur]"); 128 var priceEls = document.querySelectorAll("[data-ms-price]"); 129 var saveEls = document.querySelectorAll("[data-ms-save]"); 130 var userPicked = false;
131 132 var fmt = function (cur, amount) { 133 return MS_CUR[cur].symbol + amount.toLocaleString("en-US"); 134 }; 135 136 var render = function (cur) { 137 if (!MS_CUR[cur]) cur = "USD"; 138 priceEls.forEach(function (el) { 139 var row = MS_PRICES[el.getAttribute("data-ms-price")]; 140 if (!row || !row[cur]) return; 141 el.textContent = fmt(cur, row[cur][el.getAttribute("data-ms-period") === "yr" ? 1 : 0]); 142 }); 143 saveEls.forEach(function (el) { 144 var key = el.getAttribute("data-ms-save"); 145 var now = MS_PRICES[key], was = MS_PRICES[key + "_was"]; 146 if (!now || !was || !now[cur] || !was[cur]) return; 147 el.textContent = (el.getAttribute("data-ms-save-label") || "Save") + " " + 148 Math.round((1 - now[cur][0] / was[cur][0]) * 100) + "%"; 149 }); 150 curBtns.forEach(function (b) { 151 var on = b.getAttribute("data-ms-cur") === cur; 152 b.classList.toggle("is-active", on); 153 b.setAttribute("aria-pressed", on ? "true" : "false"); 154 }); 155 pWrap.setAttribute("data-currency", cur); 156 }; 157 158 var stored = null; 159 try { stored = localStorage.getItem(MS_CUR_KEY); } catch (e) {} 160 161 var param = null; 162 try { 163 var m = /[?&]cur=([a-zA-Z]{3})/.exec(window.location.search); 164 if (m) param = m[1].toUpperCase(); 165 } catch (e) {} 166 167 /* Best guess without a network round-trip: timezone first, then the 168 region on the browser locale. */ 169 var guess = function () { 170 try { 171 var tz = Intl.DateTimeFormat().resolvedOptions().timeZone; 172 if (tz && MS_CUR_BY_TZ[tz]) return MS_CUR_BY_TZ[tz]; 173 } catch (e) {} 174 var langs = navigator.languages || [navigator.language || ""]; 175 for (var i = 0; i < langs.length; i++) { 176 var r = /[-_]([A-Za-z]{2})\b/.exec(langs[i] || ""); 177 if (r && MS_CUR_BY_COUNTRY[r[1].toUpperCase()]) { 178 return MS_CUR_BY_COUNTRY[r[1].toUpperCase()]; 179 } 180 } 181 return "USD"; 182 }; 183 184 if (param && MS_CUR[param]) { 185 userPicked = true; 186 render(param); 187 } else if (stored && MS_CUR[stored]) { 188 userPicked = true; 189 render(stored); 190 } else { 191 render(guess()); 192 /* Refine with the edge's view of the visitor's country. Silently 193 ignored when the trace endpoint isn't there. */ 194 if (window.fetch) { 195 fetch("/cdn-cgi/trace", { cache: "no-store" }).then(function (r) { 196 return r.ok ? r.text() : ""; 197 }).then(function (txt) { 198 var loc = /(?:^|\n)loc=([A-Z]{2})/.exec(txt || ""); 199 if (!loc || userPicked) return; 200 render(MS_CUR_BY_COUNTRY[loc[1]] || "USD"); 201 })["catch"](function () {}); 202 } 203 } 204 205 curBtns.forEach(function (btn) { 206 btn.addEventListener("click", function () { 207 var cur = btn.getAttribute("data-ms-cur"); 208 userPicked = true; 209 render(cur); 210 try { localStorage.setItem(MS_CUR_KEY, cur); } catch (e) {} 211 }); 212 }); 213 } 214 215 /* ---------- FAQ accordion ---------- */ 216 document.querySelectorAll(".ms-faq__item").forEach(function (item) { 217 var q = item.querySelector(".ms-faq__q"); 218 var a = item.querySelector(".ms-faq__a"); 219 if (!q || !a) return; 220 q.addEventListener("click", function () { 221 var open = item.classList.contains("is-open"); 222 document.querySelectorAll(".ms-faq__item").forEach(function (it) { 223 it.classList.remove("is-open"); 224 var ia = it.querySelector(".ms-faq__a"); 225 if (ia) ia.style.maxHeight = null; 226 }); 227 if (!open) { 228 item.classList.add("is-open"); 229 a.style.maxHeight = a.scrollHeight + "px"; 230 } 231 }); 232 }); 233 234 /* ---------- Scroll reveal ---------- */ 235 if ("IntersectionObserver" in window) { 236 var io = new IntersectionObserver(function (entries) { 237 entries.forEach(function (en) { 238 if (en.isIntersecting) { en.target.classList.add("is-in"); io.unobserve(en.target); } 239 }); 240 }, { threshold: 0.12 }); 241 document.querySelectorAll(".ms-reveal").forEach(function (el) { io.observe(el); }); 242 } else { 243 document.querySelectorAll(".ms-reveal").forEach(function (el) { el.classList.add("is-in"); }); 244 } 245 246 /* ---------- Count-up stats ---------- */ 247 function countUp(el) { 248 var target = parseFloat(el.getAttribute("data-count")) || 0; 249 var suffix = el.getAttribute("data-suffix") || ""; 250 var prefix = el.getAttribute("data-prefix") || ""; 251 var dur = 1600, start = null; 252 function step(ts) { 253 if (!start) start = ts; 254 var p = Math.min((ts - start) / dur, 1); 255 var eased = 1 - Math.pow(1 - p, 3); 256 var val = target * eased; 257 var display = target >
257= 100 ? Math.floor(val) : val.toFixed(1); 258 el.textContent = prefix + Number(display).toLocaleString() + suffix; 259 if (p < 1) requestAnimationFrame(step); 260 else el.textContent = prefix + target.toLocaleString() + suffix; 261 } 262 requestAnimationFrame(step); 263 } 264 if ("IntersectionObserver" in window) { 265 var sio = new IntersectionObserver(function (entries) { 266 entries.forEach(function (en) { 267 if (en.isIntersecting) { countUp(en.target); sio.unobserve(en.target); } 268 }); 269 }, { threshold: 0.5 }); 270 document.querySelectorAll("[data-count]").forEach(function (el) { sio.observe(el); }); 271 } 272 }); 273})();
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.