PageSourceSearch

https://mobstep.com/assets/js/main_new.js?v=bf6711f8

js mobstep.com collected 2026-10-02 01:36:09 UTC 11,463 bytes, 273 lines download raw bytes

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.