1/*--- Write Javascript Here ---*/ 2 3// Safe DOM manipulation helper functions 4const SafeDOM = { 5 // Safely get element by ID 6 getById: function(id) { 7 const element = document.getElementById(id); 8 return element || null; 9 }, 10 11 // Safely get elements by class name 12 getByClass: function(className) { 13 const elements = document.getElementsByClassName(className); 14 return elements && elements.length > 0 ? elements : null; 15 }, 16 17 // Safely get first element by class name 18 getFirstByClass: function(className) { 19 const elements = this.getByClass(className); 20 return elements ? elements[0] : null; 21 }, 22 23 // Safely set className for element by ID 24 setClassNameById: function(id, className) { 25 const element = this.getById(id); 26 if (element) { 27 element.className = className; 28 return true; 29 } 30 return false; 31 }, 32 33 // Safely set className for element by class 34 setClassNameByClass: function(className, newClassName) { 35 const element = this.getFirstByClass(className); 36 if (element) { 37 element.className = newClassName; 38 return true; 39 } 40 return false; 41 }, 42 43 // Safely add event listener 44 addEventListener: function(element, event, handler) { 45 if (element && typeof handler === 'function') { 46 element.addEventListener(event, handler); 47 return true; 48 } 49 return false; 50 } 51}; 52 53$(document).ready(function(){ 54 55}); 56 57new WOW().init(); 58 59// Hamburger Mobile Menu 60var $hamburger = $(".hamburger"); 61if ($hamburger.length > 0) { 62 $hamburger.on("click", function(e) { 63 $hamburger.toggleClass("is-active"); 64 $('#nav-header-mobile').toggleClass('active'); 65 $('.overlay-menu-mobile').toggleClass('active'); 66 }); 67} 68 69var $overlayMenu = $('.overlay-menu-mobile'); 70if ($overlayMenu.length > 0) { 71 $overlayMenu.on('click', function(e) { 72 $hamburger.toggleClass("is-active"); 73 $('#nav-header-mobile').toggleClass('active'); 74 $('.overlay-menu-mobile').toggleClass('active'); 75 }); 76} 77 78// Header on scroll 79window.onscroll = function() {headerScroll()}; 80 81function headerScroll() { 82 const scrollTop = document.body.scrollTop || document.documentElement.scrollTop; 83 const isScrolled = scrollTop > 50; 84 85 if (isScrolled) { 86 SafeDOM.setClassNameById("header-top", "navbar navbar-expand-lg navbar-light bg-transparent d-none d-lg-flex active"); 87 SafeDOM.setClassNameById("header-top-mobile", "navbar navbar-expand-lg navbar-light d-flex d-lg-none w-100 active"); 88 SafeDOM.setClassNameById("header-menu-section", "navbar navbar-expand-lg navbar-light d-none d-lg-flex mb-3 w-100 active"); 89 SafeDOM.setClassNameById("header-menu", "bg-theme-primary rounded-pill w-100 py-2 active"); 90 SafeDOM.setClassNameById("logo-header-menu", "navbar-brand d-inline-flex me-0 bg-theme-dark px-3 py-2 rounded-pill active"); 91 // SafeDOM.setClassNameByClass("audio-controls", "d-flex justify-content-between align-items-center bg-theme-primary rounded-pill px-3 py-2 w-100 h-100 audio-controls scrolled"); 92 } else { 93 SafeDOM.setClassNameById("header-top", "navbar navbar-expand-lg navbar-light bg-transparent d-none d-lg-flex"); 94 SafeDOM.setClassNameById("header-top-mobile", "navbar navbar-expand-lg navbar-light d-flex d-lg-none w-100 active"); 95 SafeDOM.setClassNameById("header-menu-section", "navbar navbar-expand-lg navbar-light d-none d-lg-flex mb-3 w-100"); 96 SafeDOM.setClassNameById("header-menu", "bg-theme-primary rounded-pill w-100 py-2"); 97 SafeDOM.setClassNameById("logo-header-menu", "navbar-brand d-none me-0"); 98 // SafeDOM.setClassNameByClass("audio-controls", "d-flex justify-content-between align-items-center bg-theme-primary rounded-pill px-3 py-2 w-100 h-100 audio-controls"); 99 } 100} 101 102// Article Reading Functionality 103document.addEventListener("DOMContentLoaded", function () { 104 // Font size control elements 105 const decreaseBtn = document.getElementById("decreaseFont"); 106 const increaseBtn = document.getElementById("increaseFont"); 107 const resetBtn = document.getElementById("resetFont"); 108 const currentSizeSpan = document.getElementById("currentFontSize"); 109 const contentDiv = document.querySelector(".news-content"); 110 111 // Font family control elements 112 const fontFamilySelect = document.getElementById("fontFamily"); 113 const resetFontFamilyBtn = document.getElementById("resetFontFamily"); 114 115 // Text-to-speech control elements 116 const playAudioBtn = document.getElementById("playAudio"); 117 const pauseAudioBtn = document.getElementById("pauseAudio"); 118 const stopAudioBtn = document.getElementById("stopAudio"); 119 120 // If elements don't exist (not on news page), exit early 121 if (!contentDiv) { 122 return; 123 } 124 125 // Font size configuration 126 const fontSizes = { 127 "fs-11": "0.688rem", 128 "fs-12": "0.75rem", 129 "fs-13": "0.813rem", 130 "fs-14": "0.875rem", 131 "fs-15": "0.938rem", 132 "fs-16": "1rem", 133 "fs-17": "1.063rem", 134 "fs-18": "1.125rem", 135 "fs-19": "1.188rem", 136 "fs-20": "1.25rem", 137 "fs-21": "1.313rem", 138 "fs-22": "1.375rem", 139 "fs-23": "1.438rem", 140 "fs-24": "1.5rem", 141 "fs-25": "1.563rem", 142 }; 143 144 // Font family configuration 145 const fontFamilies = { 146 inter: "Inter, sans-serif", 147 roboto: "Roboto, sans-serif",
148 poppins: "Poppins, sans-serif", 149 newsreader: "Newsreader, serif", 150 arial: "Arial, sans-serif", 151 georgia: "Georgia, serif", 152 times: "Times New Roman, serif", 153 }; 154 155 // Text-to-speech configuration 156 let speechSynthesis = window.speechSynthesis; 157 let speechUtterance = null; 158 let isPlaying = false; 159 let isPaused = false; 160 let currentText = ""; 161 let currentPosition = 0; 162 let pauseStartTime = 0; 163 let totalPauseTime = 0; 164 let startTime = 0; 165 let voicesLoaded = false; 166 let availableVoices = []; 167 168 // Chrome-compatible voice loading function 169 function loadVoices() { 170 return new Promise((resolve) => { 171 // Chrome fix: Sometimes voices aren't loaded immediately 172 const voices = speechSynthesis.getVoices(); 173 174 if (voices.length > 0) { 175 availableVoices = voices; 176 voicesLoaded = true; 177 console.log("Voices loaded immediately:", voices.length); 178 resolve(voices); 179 return; 180 } 181 182 // Wait for voices to load 183 const onVoicesChanged = () => { 184 const loadedVoices = speechSynthesis.getVoices(); 185 if (loadedVoices.length > 0) { 186 availableVoices = loadedVoices; 187 voicesLoaded = true; 188 console.log("Voices loaded via event:", loadedVoices.length); 189 190 // Remove event listener to prevent multiple calls 191 speechSynthesis.removeEventListener("voiceschanged", onVoicesChanged); 192 resolve(loadedVoices); 193 } 194 }; 195 196 // Add event listener 197 speechSynthesis.addEventListener("voiceschanged", onVoicesChanged); 198 199 // Chrome fix: Sometimes we need to trigger voice loading manually 200 setTimeout(() => { 201 const retryVoices = speechSynthesis.getVoices(); 202 if (retryVoices.length > 0) { 203 availableVoices = retryVoices; 204 voicesLoaded = true; 205 console.log("Voices loaded via timeout:", retryVoices.length); 206 speechSynthesis.removeEventListener("voiceschanged", onVoicesChanged); 207 resolve(retryVoices); 208 } 209 }, 1000); 210 211 // Trigger voices loading 212 speechSynthesis.getVoices(); 213 }); 214 } 215 216 // Check if speech synthesis is supported 217 if (!speechSynthesis) { 218 // Hide audio controls if not supported 219 [playAudioBtn, pauseAudioBtn, stopAudioBtn].forEach((btn) => { 220 if (btn) btn.style.display = "none"; 221 }); 222 } else { 223 // Load voices when page loads 224 loadVoices() 225 .then((voices) => { 226 const indonesianVoices = voices.filter( 227 (voice) => 228 voice.lang === "id-ID" || 229 voice.lang === "id" || 230 voice.name.toLowerCase().includes("indonesia") || 231 voice.name.toLowerCase().includes("indonesian") 232 ); 233 234 if (indonesianVoices.length > 0) { 235 console.log( 236 "Available Indonesian voices:", 237 indonesianVoices.map((v) => `${v.name} (${v.lang})`) 238 ); 239 } else { 240 console.log("No Indonesian voices found, will use fallback"); 241 } 242 }) 243 .catch((error) => { 244 console.error("Error loading voices:", error); 245 }); 246 } 247 248 let currentFontIndex = 5; // Start with fs-14 (index 5) 249 const fontKeys = Object.keys(fontSizes); 250 251 // Initialize font size 252 function updateFontSize() { 253 const currentFontClass = fontKeys[currentFontIndex]; 254 const currentSize = fontSizes[currentFontClass]; 255 256 // Remove all font size classes 257 fontKeys.forEach((key) => { 258 contentDiv.classList.remove(key); 259 }); 260 261 // Add current font size class 262 contentDiv.classList.add(currentFontClass); 263 264 // Update display - convert rem to px for better readability 265 const fontSizeInPx = Math.round(parseFloat(currentSize) * 16); // 1rem = 16px 266 if (currentSizeSpan) { 267 currentSizeSpan.textContent = fontSizeInPx; 268 } 269 270 // Store in localStorage 271 localStorage.setItem("articleFontSize", currentFontIndex); 272 273 // Update button states 274 if (decreaseBtn) { 275 decreaseBtn.disabled = currentFontIndex === 0; 276 if (decreaseBtn.disabled) { 277 decreaseBtn.style.opacity = "0.5"; 278 decreaseBtn.style.cursor = "not-allowed"; 279 } else { 280 decreaseBtn.style.opacity = "1"; 281 decreaseBtn.style.cursor = "pointer"; 282 } 283 } 284 285 if (increaseBtn) { 286 increaseBtn.disabled = currentFontIndex === fontKeys.length - 1; 287 if (increaseBtn.disabled) { 288 increaseBtn.style.opacity = "0.5"; 289 increaseBtn.style.cursor = "not-allowed"; 290 } else { 291 increaseBtn.style.opacity = "1"; 292 increaseBtn.style.cursor = "pointer"; 293 } 294 } 295 } 296 297 // Initialize font family 298 function updateFontFamily() { 299 if (!fontFamilySelect) return; 300 301 const selectedFont = fontFamilySelect.value; 302 const fontFamilyValue = fontFamilies[selectedFont]; 303 304 // Apply font family to content 305 contentDiv.style.fontFamily = fontFamilyValue; 306 307 // Store in localStorage 308 localStorage.setItem("articleFontFamily", selectedFont); 309 } 310 311 // Text-to-speech functions 312 function playArticle() {
313 if (speechSynthesis && !isPlaying) { 314 try { 315 // Chrome fix: Ensure speech synthesis is not paused 316 if (speechSynthesis.paused) { 317 speechSynthesis.resume(); 318 } 319 320 // Chrome fix: Cancel any existing speech to prevent conflicts 321 if (speechSynthesis.speaking) { 322 speechSynthesis.cancel(); 323 } 324 325 // If resuming from pause, calculate the exact position 326 if (isPaused && currentText) { 327 // Calculate how much time has passed since we started 328 const currentTime = Date.now(); 329 const totalElapsedTime = 330 (currentTime - startTime - totalPauseTime) / 1000; 331 332 // Estimate position based on time (assuming ~150 words per minute) 333 const wordsPerMinute = 150; 334 const wordsElapsed = Math.floor( 335 (totalElapsedTime * wordsPerMinute) / 60 336 ); 337 const words = currentText.split(" "); 338 339 if (wordsElapsed < words.length) { 340 // Resume from calculated position 341 const remainingWords = words.slice(wordsElapsed); 342 const remainingText = remainingWords.join(" "); 343 speechUtterance = new SpeechSynthesisUtterance(remainingText); 344 345 // Update position tracking 346 currentPosition = wordsElapsed; 347 isPaused = false; 348 349 console.log( 350 `Resuming from word ${wordsElapsed} of ${ 351 words.length 352 }: "${remainingWords.slice(0, 3).join(" ")}..."` 353 ); 354 } else { 355 // If we've reached the end, start over 356 currentText = contentDiv.innerText || contentDiv.textContent; 357 currentPosition = 0; 358 speechUtterance = new SpeechSynthesisUtterance(currentText); 359 isPaused = false; 360 } 361 } else { 362 // Start fresh - get the article content 363 currentText = contentDiv.innerText || contentDiv.textContent; 364 currentPosition = 0; 365 speechUtterance = new SpeechSynthesisUtterance(currentText); 366 isPaused = false; 367 totalPauseTime = 0; 368 } 369 370 // Configure speech settings 371 speechUtterance.lang = "id-ID"; // Indonesian language 372 speechUtterance.rate = 0.85; // Slightly slower for better Indonesian pronunciation 373 speechUtterance.pitch = 1.0; // Normal pitch 374 speechUtterance.volume = 1.0; // Full volume 375 376 // Use the pre-loaded voices 377 if (!voicesLoaded || availableVoices.length === 0) { 378 console.log("Voices not loaded yet, loading now..."); 379 loadVoices() 380 .then((voices) => { 381 availableVoices = voices; 382 selectVoiceAndSpeak(); 383 }) 384 .catch((error) => { 385 console.error("Failed to load voices:", error); 386 console.log( 387 "Gagal memuat voice. Silakan refresh halaman dan coba lagi." 388 ); 389 }); 390 return; 391 } 392 393 selectVoiceAndSpeak(); 394 395 function selectVoiceAndSpeak() { 396 const indonesianVoice = availableVoices.find( 397 (voice) => 398 voice.lang === "id-ID" || 399 voice.lang === "id" || 400 voice.name.toLowerCase().includes("indonesia") || 401 voice.name.toLowerCase().includes("indonesian") 402 ); 403 404 if (indonesianVoice) { 405 speechUtterance.voice = indonesianVoice; 406 console.log("Using Indonesian voice:", indonesianVoice.name); 407 } else { 408 // Fallback to any available voice 409 const fallbackVoices = availableVoices.filter( 410 (voice) => 411 voice.lang.startsWith("id") || voice.lang.startsWith("en") 412 ); 413 if (fallbackVoices.length > 0) { 414 speechUtterance.voice = fallbackVoices[0]; 415 console.log( 416 "Using fallback voice:", 417 fallbackVoices[0].name, 418 "for language:", 419 fallbackVoices[0].lang 420 ); 421 } else { 422 // Use first available voice
423 if (availableVoices.length > 0) { 424 speechUtterance.voice = availableVoices[0]; 425 console.log( 426 "Using first available voice:", 427 availableVoices[0].name, 428 "for language:", 429 availableVoices[0].lang 430 ); 431 } else { 432 console.log("No voices available"); 433 } 434 } 435 } 436 437 // Event handlers 438 speechUtterance.onstart = function () { 439 isPlaying = true; 440 if (isPaused) { 441 // If resuming, add the pause time to total 442 totalPauseTime += Date.now() - pauseStartTime; 443 } else { 444 // If starting fresh, reset pause time 445 startTime = Date.now(); 446 totalPauseTime = 0; 447 } 448 updateAudioButtonStates(); 449 450 // Start progress updates 451 startProgressUpdates(); 452 }; 453 454 speechUtterance.onend = function () { 455 isPlaying = false; 456 isPaused = false; 457 currentPosition = 0; 458 totalPauseTime = 0; 459 updateAudioButtonStates(); 460 }; 461 462 speechUtterance.onerror = function (event) { 463 isPlaying = false; 464 isPaused = false; 465 currentPosition = 0; 466 totalPauseTime = 0; 467 updateAudioButtonStates(); 468 console.error("Speech synthesis error:", event.error); 469 470 // Chrome-specific error handling 471 if (event.error === "not-allowed") { 472 console.log( 473 "Audio tidak diizinkan. Pastikan halaman sudah diinteraksi." 474 ); 475 } else { 476 console.log("Error audio: " + event.error); 477 } 478 }; 479 480 // Start speaking 481 try { 482 speechSynthesis.speak(speechUtterance); 483 console.log("Speech synthesis started"); 484 } catch (error) { 485 console.error("Error starting speech synthesis:", error); 486 console.log("Gagal memulai audio. Silakan coba lagi."); 487 } 488 } 489 } catch (error) { 490 console.error("Error in playArticle:", error); 491 console.log("Error: " + error.message); 492 } 493 } 494 } 495 496 function pauseArticle() { 497 if (speechSynthesis && isPlaying) { 498 // Calculate current position before pausing 499 const currentTime = Date.now(); 500 const elapsedTime = (currentTime - startTime - totalPauseTime) / 1000; 501 502 // Estimate position based on time (assuming ~150 words per minute) 503 const wordsPerMinute = 150; 504 const wordsElapsed = Math.floor((elapsedTime * wordsPerMinute) / 60); 505 const words = currentText.split(" "); 506 507 currentPosition = Math.min(wordsElapsed, words.length); 508 509 // Store pause start time 510 pauseStartTime = currentTime; 511 512 // Cancel current speech 513 speechSynthesis.cancel(); 514 isPlaying = false; 515 isPaused = true; 516 517 // Stop progress updates 518 stopProgressUpdates(); 519 updateAudioButtonStates(); 520 521 console.log( 522 `Paused at word ${currentPosition} of ${words.length}: "${words 523 .slice(Math.max(0, currentPosition - 2), currentPosition + 1) 524 .join(" ")}..."` 525 ); 526 } 527 } 528 529 function stopArticle() { 530 if (speechSynthesis) { 531 speechSynthesis.cancel(); 532 isPlaying = false; 533 isPaused = false; 534 currentPosition = 0; 535 currentText = ""; 536 totalPauseTime = 0; 537 538 // Stop progress updates 539 stopProgressUpdates(); 540 updateAudioButtonStates(); 541 } 542 } 543 544 function updateAudioButtonStates() { 545 if (!playAudioBtn || !pauseAudioBtn || !stopAudioBtn) return; 546 547 playAudioBtn.disabled = isPlaying; 548 pauseAudioBtn.disabled = !isPlaying; 549 stopAudioBtn.disabled = !isPlaying && !isPaused; 550 551 // Update button styles and text 552 if (isPlaying) { 553 playAudioBtn.style.opacity = "0.5"; 554 playAudioBtn.style.cursor = "not-allowed"; 555 playAudioBtn.innerHTML = '<i class="fas fa-play"></i>'; 556 } else if (isPaused) { 557 playAudioBtn.style.opacity = "1"; 558 playAudioBtn.style.cursor = "pointer"; 559 playAudioBtn.innerHTML = '<i class="fas fa-play"></i>'; 560 } else { 561 playAudioBtn.style.opacity = "1"; 562 playAudioBtn.style.cursor = "pointer"; 563 playAudioBtn.innerHTML = '<i class="fas fa-play"></i>'; 564 } 565 566 // Update progress display 567 updateAudioProgress(); 568 } 569 570 function updateAudioProgress() { 571 const progressSpan = document.getElementById("audioProgress"); 572 if (progressSpan) { 573 if (isPlaying) { 574 const currentTime = Date.now(); 575 const elapsedTime = (currentTime - startTime - totalPauseTime) / 1000; 576 const wordsPerMinute = 150; 577 const wordsElapsed = Math.floor((elapsedTime * wordsPerMinute) / 60); 578 const words = currentText.split(" "); 579 const progress = Math.min( 580 Math.round((wordsElapsed / words.length) * 100), 581 100 582 ); 583 progressSpan.textContent = `${progress}% (${wordsElapsed}/${words.length} words)`; 584 } else if (isPaused) { 585 const words = currentText.split(" "); 586 const progress = Math.min( 587 Math.round((currentPosition / words.length) * 100), 588 100 589 ); 590 progressSpan.textContent = `Paused: ${progress}% (${currentPosition}/${words.length} words)`; 591 } else { 592 progressSpan.textContent = ""; 593 } 594 } 595 } 596 597 let progressInterval = null; 598 599 function startProgressUpdates() { 600 // Clear any existing interval 601 if (progressInterval) { 602 clearInterval(progressInterval); 603 } 604
605 // Update progress every 500ms while playing 606 progressInterval = setInterval(() => { 607 if (isPlaying) { 608 updateAudioProgress(); 609 } else { 610 clearInterval(progressInterval); 611 progressInterval = null; 612 } 613 }, 500); 614 } 615 616 function stopProgressUpdates() { 617 if (progressInterval) { 618 clearInterval(progressInterval); 619 progressInterval = null; 620 } 621 } 622 623 // Event listeners 624 if (decreaseBtn) { 625 decreaseBtn.addEventListener("click", function () { 626 if (currentFontIndex > 0) { 627 currentFontIndex--; 628 updateFontSize(); 629 } 630 }); 631 } 632 633 if (increaseBtn) { 634 increaseBtn.addEventListener("click", function () { 635 if (currentFontIndex < fontKeys.length - 1) { 636 currentFontIndex++; 637 updateFontSize(); 638 } 639 }); 640 } 641 642 if (resetBtn) { 643 resetBtn.addEventListener("click", function () { 644 currentFontIndex = 5; // Reset to fs-14 645 updateFontSize(); 646 }); 647 } 648 649 // Font family event listeners 650 if (fontFamilySelect) { 651 fontFamilySelect.addEventListener("change", updateFontFamily); 652 } 653 654 if (resetFontFamilyBtn) { 655 resetFontFamilyBtn.addEventListener("click", function () { 656 fontFamilySelect.value = "inter"; // Reset to default Inter font 657 updateFontFamily(); 658 }); 659 } 660 661 // Text-to-speech event listeners 662 if (playAudioBtn) { 663 playAudioBtn.addEventListener("click", function () { 664 playArticle(); 665 }); 666 } 667 668 if (pauseAudioBtn) { 669 pauseAudioBtn.addEventListener("click", pauseArticle); 670 } 671 672 if (stopAudioBtn) { 673 stopAudioBtn.addEventListener("click", stopArticle); 674 } 675 676 // Keyboard support 677 document.addEventListener("keydown", function (e) { 678 if (e.ctrlKey || e.metaKey) { 679 if (e.key === "=" || e.key === "+") { 680 e.preventDefault(); 681 if (currentFontIndex < fontKeys.length - 1) { 682 currentFontIndex++; 683 updateFontSize(); 684 } 685 } else if (e.key === "-") { 686 e.preventDefault(); 687 if (currentFontIndex > 0) { 688 currentFontIndex--; 689 updateFontSize(); 690 } 691 } else if (e.key === "0") { 692 e.preventDefault(); 693 currentFontIndex = 5; // Reset to fs-14 694 updateFontSize(); 695 } else if (e.key === "f") { 696 e.preventDefault(); 697 if (fontFamilySelect) fontFamilySelect.focus(); 698 } else if (e.key === "p") { 699 e.preventDefault(); 700 if (isPlaying) { 701 pauseArticle(); 702 } else if (isPaused) { 703 playArticle(); // Resume from pause 704 } else { 705 playArticle(); // Start fresh 706 } 707 } else if (e.key === "s") { 708 e.preventDefault(); 709 stopArticle(); 710 } 711 } 712 }); 713 714 // Load saved font size from localStorage 715 const savedFontSize = localStorage.getItem("articleFontSize"); 716 if (savedFontSize !== null) { 717 currentFontIndex = parseInt(savedFontSize); 718 } 719 720 // Load saved font family from localStorage 721 const savedFontFamily = localStorage.getItem("articleFontFamily"); 722 if (savedFontFamily !== null && fontFamilySelect) { 723 fontFamilySelect.value = savedFontFamily; 724 } 725 726 // Initialize 727 updateFontSize(); 728 updateFontFamily(); 729 updateAudioButtonStates(); 730});
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.