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.