PageSourceSearch

https://sundadigi.com/assets/js/sundadigi.js?v=1789702012

js sundadigi.com collected 2026-10-02 06:59:31 UTC 17,989 bytes, 637 lines download raw bytes

1/**
2 * sundadigi.com: Main JS
3 * Developer: Muhammad Ridwan Hakim, S.T., CPITA, ITPMCP
4 */
5
6(function () {
7  "use strict";
8
9  /**
10   * Easy selector helper function
11   */
12  const select = (el, all = false) => {
13    if (!el) return null;
14
15    el = el.trim();
16    
17    // Exclude Google Custom Search hash
18    if (el.startsWith('#gsc')) return null;
19
20    try {
21      if (all) {
22        return [...document.querySelectorAll(el)];
23      } else {
24        return document.querySelector(el);
25      }
26    } catch (error) {
27      return null;
28    }
29  };
30
31  /**
32   * Easy event listener function
33   */
34  const on = (type, el, listener, all = false) => {
35    if (all) {
36      select(el, all).forEach((e) => e.addEventListener(type, listener));
37    } else {
38      select(el, all).addEventListener(type, listener);
39    }
40  };
41
42  /**
43   * Easy on scroll event listener
44   */
45  const onscroll = (el, listener) => {
46    el.addEventListener("scroll", listener);
47  };
48
49  /**
50   * Navbar links active state on scroll
51   */
52  let navbarlinks = select("#navbar .scrollto", true);
53  const navbarlinksActive = () => {
54    let position = window.scrollY + 200;
55    navbarlinks.forEach((navbarlink) => {
56      if (!navbarlink.hash) return;
57      let section = select(navbarlink.hash);
58      if (!section) return;
59      if (
60        position >= section.offsetTop &&
61        position <= section.offsetTop + section.offsetHeight
62      ) {
63        navbarlink.classList.add("active");
64      } else {
65        navbarlink.classList.remove("active");
66      }
67    });
68  };
69  window.addEventListener("load", navbarlinksActive);
70  onscroll(document, navbarlinksActive);
71
72  /**
73   * Scrolls to an element with header offset
74   */
75  const scrollto = (el) => {
76    let header = select("#header");
77    let offset = header.offsetHeight;
78
79    if (!header.classList.contains("header-scrolled")) {
80      offset -= 10;
81    }
82
83    let elementPos = select(el).offsetTop;
84    window.scrollTo({
85      top: elementPos - offset,
86      behavior: "smooth",
87    });
88  };
89
90  /**
91   * Toggle .header-scrolled class to #header when page is scrolled
92   */
93  let selectHeader = select("#header");
94  if (selectHeader) {
95    const headerScrolled = () => {
96      if (window.scrollY > 100) {
97        selectHeader.classList.add("header-scrolled");
98      } else {
99        selectHeader.classList.remove("header-scrolled");
100      }
101    };
102    window.addEventListener("load", headerScrolled);
103    onscroll(document, headerScrolled);
104  }
105
106  /**
107   * Back to top button
108   */
109  let backtotop = select(".back-to-top");
110  if (backtotop) {
111    const toggleBacktotop = () => {
112      if (window.scrollY > 100) {
113        backtotop.classList.add("active");
114      } else {
115        backtotop.classList.remove("active");
116      }
117    };
118    window.addEventListener("load", toggleBacktotop);
119    onscroll(document, toggleBacktotop);
120  }
121
122  /**
123   * Zoom buttons
124   */
125  document.addEventListener("DOMContentLoaded", function () {
126    // Check if current page is in the whitelist and if device is mobile
127    const path = window.location.pathname;
128    const isWhitelisted =
129      path.includes("/chapter_") ||
130      path.includes("/chapter") ||
131      path.includes("/detail") ||
132      path.endsWith("/bewara") ||
133      path === "/bewara/";
134    const isMobile = window.innerWidth <= 768;
135
136    // Only initialize zoom buttons for whitelisted pages on mobile devices
137    if (isWhitelisted && isMobile) {
138      const zoomInButton = document.querySelector(".zoom-in-button");
139      const zoomResetButton = document.querySelector(".zoom-reset-button");
140
141      if (zoomInButton && zoomResetButton) {
142        let currentZoomLevel = 100;
143
144        // Remove existing event listeners by cloning and replacing the buttons
145        const newZoomInButton = zoomInButton.cloneNode(true);
146        const newZoomResetButton = zoomResetButton.cloneNode(true);
147        zoomInButton.parentNode.replaceChild(newZoomInButton, zoomInButton);
148        zoomResetButton.parentNode.replaceChild(
149          newZoomResetButton,
150          zoomResetButton
151        );
152
153        // Add click event listeners to the new buttons
154        newZoomInButton.addEventListener("click", function (e) {
155          e.preventDefault(); // Prevent default anchor behavior
156          currentZoomLevel += 50;
157          applyZoom(currentZoomLevel);
158        });
159
160        newZoomResetButton.addEventListener("click", function (e) {
161          e.preventDefault(); // Prevent default anchor behavior
162          currentZoomLevel = 100;
163          applyZoom(currentZoomLevel);
164        });
165
166        function applyZoom(zoomLevel) {
167          const images = document.querySelectorAll("img");
168          images.forEach((img) => {
169            const excludedIds = [
170              "logoDPJ",
171              "logoGapura",
172              "logoKITLV",
173              "logoUBL",
174              "rambo",
175              "jumbotron-1",
176              "jumbotron-2",
177              "jumbotron-3",
178              "menu-icons",
179              "unpadLogo",
180              "pdpbsLogo",
181              "dpjLogo",
182              "pssLogo",
183              "mangleLogo",
184            ];
185            if (!excludedIds.includes(img.id)) {
186              img.style.width = `${zoomLevel}%`;
187            }
188          });
189        }
190      }
191    }
192  });
193
194  // Check path condition
195  // console.log(window.location.pathname);
196
197  // Check mobile condition
198  // console.log(window.innerWidth <= 768);
199
200  // Check if buttons exist
201  // console.log(document.querySelector(".zoom-in-button"));
202  // console.log(document.querySelector(".zoom-reset-button"));
203
204  /**
205   * Mobile nav toggle
206   */
207  on("click", ".mobile-nav-toggle", function (e) {
208    select("#navbar").classList.toggle("navbar-mobile");
209    // The icon is the inner <i>, not the <button> itself
210    let icon = this.querySelector("i") || this;
211    icon.classList.toggle("bi-list");
212    icon.classList.toggle("bi-x");
213  });
214
215  /**
216   * Mobile nav dropdowns activate
217   */
218  on(
219    "click",
220    ".navbar .dropdown > a",
221    function (e) {
222      if (select("#navbar").classList.contains("navbar-mobile")) {
223        e.preventDefault();
224        this.nextElementSibling.classList.toggle("dropdown-active");
225      }
226    },
227    true
228  );
229
230  /**
231   * Scrool with ofset on links with a class name .scrollto
232   */
233  on(
234    "click",
235    ".scrollto",
236    function (e) {
237      if (select(this.hash)) {
238        e.preventDefault();
239
240        let navbar = select("#navbar");
241        if (navbar.classList.contains("navbar-mobile")) {
242          navbar.classList.remove("navbar-mobile");
243          let navbarToggle = select(".mobile-nav-toggle");
244          let icon = navbarToggle.querySelector("i") || navbarToggle;
245          icon.classList.toggle("bi-list");
246          icon.classList.toggle("bi-x");
247        }
248        scrollto(this.hash);
249      }
250    },
251    true
252  );
253
254  /**
255   * Scroll with ofset on page load with hash links in the url
256   */
257  window.addEventListener("load", () => {
258    if (window.location.hash) {
259      if (select(window.location.hash)) {
260        scrollto(window.location.hash);
261      }
262    }
263  });
264
265  /**
266   * Clients Slider
267   */
268  new Swiper(".clients-slider", {
269    speed: 400,
270    loop: true,
271    autoplay: {
272      delay: 5000,
273      disableOnInteraction: false,
274    },
275    slidesPerView: "auto",
276    pagination: {
277      el: ".swiper-pagination",
278      type: "bullets",
279      clickable: true,
280    },
281    breakpoints: {
282      320: {
283        slidesPerView: 2,
284        spaceBetween: 40,
285      },
286      480: {
287        slidesPerView: 3,
288        spaceBetween: 60,
289      },
290      640: {
291        slidesPerView: 4,
292        spaceBetween: 80,
293      },
294      992: {
295        slidesPerView: 6,
296        spaceBetween: 120,
297      },
298    },
299  });
300
301  /**
302   * Porfolio isotope and filter
303   */
304  window.addEventListener("load", () => {
305    let portfolioContainer = select(".portfolio-container");
306    if (portfolioContainer) {
307      let portfolioIsotope = new Isotope(portfolioContainer, {
308        itemSelector: ".portfolio-item",
309        layoutMode: "fitRows",
310      });
311
312      let portfolioFilters = select("#portfolio-flters li", true);
313
314      on(
315        "click",
316        "#portfolio-flters li",
317        function (e) {
318          e.preventDefault();
319          portfolioFilters.forEach(function (el) {
320            el.classList.remove("filter-active");
321          });
322          this.classList.add("filter-active");
323
324          portfolioIsotope.arrange({
325            filter: this.getAttribute("data-filter"),
326          });
327          aos_init();
328        },
329        true
330      );
331    }
332  });
333
334  /**
335   * Initiate portfolio lightbox
336   */
337  const portfolioLightbox = GLightbox({
338    selector: ".portfokio-lightbox",
339  });
340
341  /**
342   * Portfolio details slider
343   */
344  new Swiper(".portfolio-details-slider", {
345    speed: 400,
346    autoplay: {
347      delay: 5000,
348      disableOnInteraction: false,
349    },
350    pagination: {
351      el: ".swiper-pagination",
352      type: "bullets",
353      clickable: true,
354    },
355  });
356
357  /**
358   * Testimonials slider
359   */
360  new Swiper(".testimonials-slider", {
361    speed: 600,
362    loop: true,
363    autoplay: {
364      delay: 5000,
365      disableOnInteraction: false,
366    },
367    slidesPerView: "auto",
368    pagination: {
369      el: ".swiper-pagination",
370      type: "bullets",
371      clickable: true,
372    },
373    breakpoints: {
374      320: {
375        slidesPerView: 1,
376        spaceBetween: 40,
377      },
378
379      1200: {
380        slidesPerView: 3,
381      },
382    },
383  });
384
385  /**
386   * Animation on scroll
387   */
388  function aos_init() {
389    AOS.init({
390      duration: 1000,
391      easing: "ease-in-out",
392      once: true,
393      mirror: false,
394    });
395  }
396  window.addEventListener("load", () => {
397    aos_init();
398  });
399
400  /**
401   * Custom Search Autocomplete
402   */
403  const searchInput      = document.getElementById('custom-search-input');
404  const suggestionsBox   = document.getElementById('search-suggestions');
405
406  if (searchInput && suggestionsBox) {
407    let debounceTimer   = null;
408    let activeIndex     = -1;
409    let currentSuggestions = [];
410
411    const showSuggestions = (items) => {
412      suggestionsBox.innerHTML = '';
413      activeIndex = -1;
414      currentSuggestions = items;
415
416      if (!items.length) {
417        suggestionsBox.classList.remove('active');
418        return;
419      }
420
421      items.forEach((text, idx) => {
422        const li = document.createElement('li');
423        li.innerHTML = `<i class="fas fa-search suggestion-icon"></i><span>${text}</span>`;
424        li.addEventListener('mousedown', (e) => {
425          e.preventDefault();
426          searchInput.value = text;
427          suggestionsBox.classList.remove('active');
428          document.getElementById('custom-search-form').dispatchEvent(new Event('submit', { cancelable: true }));
429        });
430        suggestionsBox.appendChild(li);
431      });
432
433      suggestionsBox.classList.add('active');
434    };
435
436    const fetchSuggestions = (query) => {
437      if (!query.trim()) {
438        suggestionsBox.classList.remove('active');
439        return;
440      }
441
442      // Use Google's public JSONP autocomplete endpoint
443      const callbackName = '_gacb_' + Date.now();
444      const script = document.createElement('script');
445
446      window[callbackName] = (data) => {
447        delete window[callbackName];
448        script.remove();
449        const suggestions = (data[1] || []).slice(0, 7);
450        showSuggestions(suggestions);
451      };
452
453      script.src = `https://suggestqueries.google.com/complete/search?client=firefox&q=${encodeURIComponent(query)}&callback=${callbackName}`;
454      script.onerror = () => { delete window[callbackName]; script.remove(); };
455      document.head.appendChild(script);
456    };
457
458    searchInput.addEventListener('input', () => {
459      clearTimeout(debounceTimer);
460      const query = searchInput.value;
461      if (!query.trim()) {
462        suggestionsBox.classList.remove('active');
463        return;
464      }
465      debounceTimer = setTimeout(() => fetchSuggestions(query), 250);
466    });
467
468    searchInput.addEventListener('keydown', (e) => {
469      const items = suggestionsBox.querySelectorAll('li');
470      if (!items.length) return;
471
472      if (e.key === 'ArrowDown') {
473        e.preventDefault();
474        activeIndex = Math.min(activeIndex + 1, items.length - 1);
475      } else if (e.key === 'ArrowUp') {
476        e.preventDefault();
477        activeIndex = Math.max(activeIndex - 1, -1);
478      } else if (e.key === 'Escape') {
479        suggestionsBox.classList.remove('active');
480        return;
481      } else {
482        return;
483      }
484
485      items.forEach((li, i) => li.classList.toggle('active', i === activeIndex));
486      if (activeIndex >= 0) searchInput.value = currentSuggestions[activeIndex];
487    });
488
489    // Hide on click outside
490    document.addEventListener('click', (e) => {
491      if (!e.target.closest('.search-autocomplete-container')) {
492        suggestionsBox.classList.remove('active');
493      }
494    });
495
496    // Hide on form submit
497    document.getElementById('custom-search-form').addEventListener('submit', () => {
498      suggestionsBox.classList.remove('active');
499    });
500  }
501
502  /**
503   * Sanitize Google CSE internal links.
504   * Remove Google tracking params like &fexp from internal URLs before navigation.
505   */
506  const sanitizeGscInternalHref = (href) => {
507    if (!href) return href;
508
509    const isAbsoluteUrl = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
510    const isHashOrAnchor = href.startsWith('#');
511    const isMailtoOrJs = href.startsWith('mailto:') || href.startsWith('javascript:');
512    const isInternal = href.includes('sundadigi.com') || !isAbsoluteUrl && !isHashOrAnchor && !isMailtoOrJs;
513    if (!isInternal) return href;
514
515    let sanitized = href
516      .replace(/([?&])fexp=[^&]*/g, '')
517      .replace(/([?&])ved=[^&]*/g, '')
518      .replace(/([?&])usg=[^&]*/g, '')
519      .replace(/([?&])sa=[^&]*/g, '')
520      .replace(/([?&])source=[^&]*/g, '')
521      .replace(/([?&])client=[^&]*/g, '');
522
523    sanitized = sanitized.replace(/[?&]$/, '');
524    return sanitized;
525  };
526
527  /**
528   * Fix Google CSE inline width overrides on desktop
529   * Google injects style="width:Xpx" on elements dynamically.
530   * CSS !important cannot beat inline styles, so we strip them with JS.
531   */
532  const fixGscLayout = () => {
533    const overlay = document.querySelector('.gsc-results-wrapper-overlay');
534    if (!overlay) return;
535
536    // Selectors whose inline widths must be removed
537    const targets = [
538      '.gsc-results-wrapper-nooverlay',
539      '.gsc-above-wrapper-area',
540      '.gsc-above-wrapper-area-container',
541      '.gsc-tabsArea',
542      '.gsc-orderby-container',
543      '.gsc-results',
544      '.gsc-result',
545      '.gsc-webResult',
546      '.gsc-adBlock',
547      '.gsc-wrapper',
548      '.gsc-resultsbox-visible',
549      '.gsc-resultsRoot',
550      '.gsc-positioningWrapper',
551      '.gsc-expansionArea'
552    ];
553
554    targets.forEach((sel) => {
555      overlay.querySelectorAll(sel).forEach((el) => {
556        el.style.removeProperty('width');
557        el.style.removeProperty('max-width');
558        el.style.removeProperty('float');
559        el.style.setProperty('width', '100%', 'important');
560        el.style.setProperty('box-sizing', 'border-box', 'important');
561        el.style.setProperty('float', 'none', 'important');
562        el.style.setProperty('display', 'block', 'important');
563      });
564    });
565
566    // Hide Branding and "Find more on Google"
567    const hideTargets = [
568      '.gcsc-find-more-on-google-root',
569      '.gcsc-more-maybe-branding-box',
570      '.gcsc-more-maybe-branding-root'
571    ];
572    hideTargets.forEach((sel) => {
573      overlay.querySelectorAll(sel).forEach((el) => {
574        el.style.setProperty('display', 'none', 'important');
575      });
576    });
577
578    // Collapse the top-level wrapper table columns
579    overlay.querySelectorAll('table, tbody, tr, td').forEach((el) => {
580      if (el.closest('.gsc-table-result')) return; // skip result-row tables
581      el.style.removeProperty('width');
582      el.style.setProperty('width', '100%', 'important');
583      el.style.setProperty('display', 'block', 'important');
584      el.style.setProperty('box-sizing', 'border-box', 'important');
585    });
586
587    // Hide iframes (video embeds)
588    overlay.querySelectorAll('iframe').forEach((el) => {
589      el.style.setProperty('display', 'none', 'important');
590    });
591
592    // Make external links open in a new tab and sanitize internal links
593    overlay.querySelectorAll('a').forEach((link) => {
594      let href = link.getAttribute('href') || link.href;
595      if (!href) return;
596
597      const sanitizedHref = sanitizeGscInternalHref(href);
598      if (sanitizedHref !== href) {
599        link.setAttribute('href', sanitizedHref);
600        link.href = sanitizedHref;
601        href = sanitizedHref;
602      }
603
604      if (!href.includes('sundadigi.com') && !href.startsWith('/') && !href.startsWith('#')) {
605        link.setAttribute('target', '_blank');
606        link.setAttribute('rel', 'noopener noreferrer');
607      } else if (href.includes('sundadigi.com')) {
608        link.removeAttribute('target'); // Ensure internal links open in the same tab
609      }
610
611      // Protect against dynamic click rewrite by Google ads/search scripts
612      link.addEventListener('click', (event) => {
613        const currentHref = link.getAttribute('href') || link.href;
614        const currentSanitized = sanitizeGscInternalHref(currentHref);
615        if (currentSanitized !== currentHref) {
616          event.preventDefault();
617          window.location.href = currentSanitized;
618        }
619      });
620    });
621  };
622
623  // Watch for Google CSE overlay being added / mutated
624  const gscObserver = new MutationObserver(() => {
625    const overlay = document.querySelector('.gsc-results-wrapper-overlay');
626    if (overlay) {
627      fixGscLayout();
628      // Also watch inside the overlay for late mutations (pagination etc.)
629      const innerObserver = new MutationObserver(fixGscLayout);
630      innerObserver.observe(overlay, { childList: true, subtree: true });
631      gscObserver.disconnect(); // outer observer no longer needed
632    }
633  });
634
635  gscObserver.observe(document.body, { childList: true, subtree: true });
636
637})();

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.