1/** 2 * Crypto Québec / CRYPTON1TE â Outils de Trading Interactifs 3 * 1. Calculateur de Taille de Position & Gestion du Risque 4 * 2. Checklist Interactive Pre-Trade (Filtre de Confluence) 5 * 3. Journal de Trading & Analytics (LocalStorage) 6 */ 7 8(function () { 9 'use strict'; 10 11 function getLang() { 12 return localStorage.getItem('selected_language') || localStorage.getItem('cq_lang') || document.documentElement.lang || 'fr'; 13 } 14 15 let refreshAllTools = function() {}; 16 17 // âââ 1. CALCULATEUR DE TAILLE DE POSITION âââââââââââââââââââââââââââââââ 18 function initPositionCalculator() { 19 const capitalInput = document.getElementById('cq-calc-capital'); 20 const riskInput = document.getElementById('cq-calc-risk-pct'); 21 const entryInput = document.getElementById('cq-calc-entry'); 22 const stopInput = document.getElementById('cq-calc-stop'); 23 const tpInput = document.getElementById('cq-calc-tp'); 24 25 if (!capitalInput || !entryInput || !stopInput) return () => {}; 26 27 function calculate() { 28 const capital = parseFloat(capitalInput.value) || 0; 29 const riskPct = parseFloat(riskInput.value) || 0; 30 const entry = parseFloat(entryInput.value) || 0; 31 const stop = parseFloat(stopInput.value) || 0; 32 const tp = parseFloat(tpInput.value) || 0; 33 34 const riskAmount = (capital * riskPct) / 100; 35 const stopDistance = Math.abs(entry - stop); 36 37 let positionUnits = 0; 38 let totalPosValue = 0; 39 let leverageReq = 0; 40 let rewardAmount = 0; 41 let rrRatio = 0; 42 43 if (entry > 0 && stopDistance > 0) { 44 positionUnits = riskAmount / stopDistance; 45 totalPosValue = positionUnits * entry; 46 if (capital > 0) { 47 leverageReq = totalPosValue / capital; 48 } 49 } 50 51 if (tp > 0 && entry > 0 && stopDistance > 0) { 52 const tpDistance = Math.abs(tp - entry); 53 rewardAmount = positionUnits * tpDistance; 54 rrRatio = tpDistance / stopDistance; 55 } 56 57 // Update UI 58 document.getElementById('cq-res-risk-amt').textContent = `$${riskAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 59 document.getElementById('cq-res-pos-units').textContent = positionUnits > 0 ? positionUnits.toFixed(4) : '0.0000'; 60 document.getElementById('cq-res-pos-val').textContent = `$${totalPosValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 61 document.getElementById('cq-res-leverage').textContent = `${leverageReq.toFixed(1)}x`; 62 63 const rrEl = document.getElementById('cq-res-rr'); 64 const rrStatusEl = document.getElementById('cq-res-rr-status'); 65 66 const lang = getLang(); 67 68 const statusTextDict = { 69 fr: { 70 excellent: 'Excellent (R:R ⥠1:2)', 71 acceptable: 'Acceptable (R:R ⥠1:1.5)', 72 highRisk: 'Risque Ãlevé (R:R < 1:1.5)', 73 awaiting: 'Attente d\'Objectif (TP)' 74 }, 75 en: { 76 excellent: 'Excellent (R:R ⥠1:2)', 77 acceptable: 'Acceptable (R:R ⥠1:1.5)', 78 highRisk: 'High Risk (R:R < 1:1.5)', 79 awaiting: 'Awaiting Target (TP)' 80 }, 81 es: { 82 excellent: 'Excelente (R:R ⥠1:2)', 83 acceptable: 'Aceptable (R:R ⥠1:1.5)', 84 highRisk: 'Riesgo Alto (R:R < 1:1.5)', 85 awaiting: 'Esperando Objetivo (TP)' 86 } 87 }; 88 const statusText = statusTextDict[lang] || statusTextDict['fr']; 89 90 if (rrRatio > 0) { 91 rrEl.textContent = `1 : ${rrRatio.toFixed(2)}`; 92 if (rrRatio >= 2.0) { 93 rrEl.style.color = '#00e676'; 94 if (rrStatusEl) rrStatusEl.className = 'badge bg-success'; 95 if (rrStatusEl) rrStatusEl.textContent = statusText.excellent; 96 } else if (rrRatio >= 1.5) { 97 rrEl.style.color = '#00e5ff'; 98 if (rrStatusEl) rrStatusEl.className = 'badge bg-info text-dark'; 99 if (rrStatusEl) rrStatusEl.textContent = statusText.acceptable; 100 } else { 101 rrEl.style.color = '#ff3d00'; 102 if (rrStatusEl) rrStatusEl.className = 'badge bg-danger'; 103 if (rrStatusEl) rrStatusEl.textContent = statusText.highRisk; 104 } 105 } else { 106 rrEl.textContent = '1 : 0.00'; 107 rrEl.style.color = '#7b839a'; 108 if (rrStatusEl) rrStatusEl.className = 'badge bg-secondary'; 109 if (rrStatusEl) rrStatusEl.textContent = statusText.awaiting; 110 } 111 } 112 113 const inputs = [capitalInput, riskInput, entryInput, stopInput, tpInput];
114 inputs.forEach(input => { 115 if (input) input.addEventListener('input', calculate); 116 }); 117 118 // Quick Risk Preset Buttons 119 document.querySelectorAll('.cq-risk-preset-btn').forEach(btn => { 120 btn.addEventListener('click', function () { 121 document.querySelectorAll('.cq-risk-preset-btn').forEach(b => b.classList.remove('active')); 122 this.classList.add('active'); 123 riskInput.value = this.getAttribute('data-risk'); 124 calculate(); 125 }); 126 }); 127 128 calculate(); 129 return calculate; 130 } 131 132 // âââ 2. CHECKLIST PRE-TRADE âââââââââââââââââââââââââââââââââââââââââââââ 133 function initPreTradeChecklist() { 134 const checkboxes = document.querySelectorAll('.cq-chk-item'); 135 const scoreEl = document.getElementById('cq-chk-score-val'); 136 const barEl = document.getElementById('cq-chk-score-bar'); 137 const statusEl = document.getElementById('cq-chk-status'); 138 139 if (!checkboxes.length || !scoreEl) return () => {}; 140 141 function updateChecklistScore() { 142 let total = checkboxes.length; 143 let checked = 0; 144 145 checkboxes.forEach(chk => { 146 if (chk.checked) checked++; 147 }); 148 149 const percent = Math.round((checked / total) * 100); 150 scoreEl.textContent = `${checked} / ${total} (${percent}%)`; 151 if (barEl) barEl.style.width = `${percent}%`; 152 153 const lang = getLang(); 154 155 if (statusEl) { 156 if (percent === 100) { 157 statusEl.className = 'alert alert-success mt-3 mb-0'; 158 if (lang === 'en') { 159 statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>HIGH CONFLUENCE (100%)</strong> â All theoretical criteria configured are satisfied.'; 160 } else if (lang === 'es') { 161 statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>ALTA CONFLUENCIA (100%)</strong> â Todos los criterios teóricos configurados se han cumplido.'; 162 } else { 163 statusEl.innerHTML = '<i class="fa-solid fa-circle-check me-2"></i><strong>CONFLUENCE ÃLEVÃE (100%)</strong> â Tous les critères théoriques configurés sont validés.'; 164 } 165 } else if (percent >= 66) { 166 statusEl.className = 'alert alert-warning mt-3 mb-0'; 167 if (lang === 'en') { 168 statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>MODERATE CONFLUENCE (' + percent + '%)</strong> â The majority of theoretical criteria are satisfied.'; 169 } else if (lang === 'es') { 170 statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>CONFLUENCIA MODERADA (' + percent + '%)</strong> â La mayorÃa de los criterios teóricos se han cumplido.'; 171 } else { 172 statusEl.innerHTML = '<i class="fa-solid fa-triangle-exclamation me-2"></i><strong>CONFLUENCE MODÃRÃE (' + percent + '%)</strong> â La majorité des critères théoriques sont validés.'; 173 } 174 } else { 175 statusEl.className = 'alert alert-danger mt-3 mb-0'; 176 if (lang === 'en') { 177 statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>LOW CONFLUENCE (' + percent + '%)</strong> â Low confluence level based on this evaluation model.'; 178 } else if (lang === 'es') { 179 statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>CONFLUENCIA BAJA (' + percent + '%)</strong> â Bajo nivel de confluencia según este modelo de evaluación.'; 180 } else { 181 statusEl.innerHTML = '<i class="fa-solid fa-circle-info me-2"></i><strong>CONFLUENCE FAIBLE (' + percent + '%)</strong> â Faible niveau de confluence selon ce modèle d\'évaluation.'; 182 } 183 } 184 } 185 } 186
187 checkboxes.forEach(chk => chk.addEventListener('change', updateChecklistScore)); 188 updateChecklistScore(); 189 return updateChecklistScore; 190 } 191 192 // âââ 3. JOURNAL DE TRADING & ANALYTICS ââââââââââââââââââââââââââââââââââ 193 const STORAGE_KEY = 'cq_trading_journal_v1'; 194 195 function getJournalEntries() { 196 try { 197 const data = localStorage.getItem(STORAGE_KEY); 198 return data ? JSON.parse(data) : []; 199 } catch (e) { 200 console.error('Error reading trading journal from localStorage', e); 201 return []; 202 } 203 } 204 205 function saveJournalEntries(entries) { 206 try { 207 localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)); 208 } catch (e) { 209 console.error('Error saving trading journal to localStorage', e); 210 } 211 } 212 213 function renderJournal() { 214 const tbody = document.getElementById('cq-journal-tbody'); 215 const entries = getJournalEntries(); 216 217 if (!tbody) return; 218 219 const lang = getLang(); 220 const emptyMsg = { 221 fr: 'Aucun trade enregistré pour le moment. Remplissez le formulaire ci-dessus pour suivre vos performances !', 222 en: 'No trades recorded yet. Fill out the form above to track your performance!', 223 es: '¡No hay operaciones registradas aún. Complete el formulario arriba para seguir su rendimiento!' 224 }[lang] || 'Aucun trade enregistré pour le moment. Remplissez le formulaire ci-dessus pour suivre vos performances !'; 225 226 if (entries.length === 0) { 227 tbody.innerHTML = ` 228 <tr> 229 <td colspan="7" class="text-center py-4" style="color: #e2e8f0 !important; font-size: 14px; font-weight: 500;"> 230 <i class="fa-solid fa-journal-whills fa-2x mb-2 d-block" style="color: #00e5ff !important;"></i> 231 ${emptyMsg} 232 </td> 233 </tr>`; 234 updateJournalStats(entries); 235 return; 236 } 237 238 tbody.innerHTML = entries.map((entry, index) => { 239 const isWin = entry.pnl >= 0; 240 const pnlFormatted = `${isWin ? '+' : ''}$${parseFloat(entry.pnl).toLocaleString('en-US', { minimumFractionDigits: 2 })}`; 241 const dirBadge = entry.direction === 'LONG' 242 ? '<span class="badge bg-success">LONG</span>' 243 : '<span class="badge bg-danger">SHORT</span>'; 244 245 return ` 246 <tr> 247 <td class="text-nowrap" style="color: #a0aec0;">${entry.date || '-'}</td> 248 <td class="font-weight-bold text-white">${entry.pair}</td> 249 <td>${dirBadge}</td> 250 <td>$${parseFloat(entry.entryPrice).toLocaleString()}</td> 251 <td>$${parseFloat(entry.exitPrice).toLocaleString()}</td> 252 <td class="${isWin ? 'text-success font-weight-bold' : 'text-danger font-weight-bold'}">${pnlFormatted}</td> 253 <td> 254 <button class="btn btn-sm btn-outline-danger py-0 px-2 cq-del-trade-btn" data-index="${index}"> 255 <i class="fa-solid fa-trash-can"></i> 256 </button> 257 </td> 258 </tr>`; 259 }).join(''); 260 261 // Attach delete listeners 262 document.querySelectorAll('.cq-del-trade-btn').forEach(btn => { 263 btn.addEventListener('click', function () { 264 const idx = parseInt(this.getAttribute('data-index'), 10); 265 deleteTrade(idx); 266 }); 267 }); 268 269 updateJournalStats(entries); 270 } 271 272 function updateJournalStats(entries) { 273 const totalTradesEl = document.getElementById('cq-j-total-trades'); 274 const winRateEl = document.getElementById('cq-j-win-rate'); 275 const totalPnlEl = document.getElementById('cq-j-total-pnl'); 276 277 if (!totalTradesEl) return; 278 279 const total = entries.length; 280 if (total === 0) { 281 totalTradesEl.textContent = '0'; 282 winRateEl.textContent = '0%'; 283 totalPnlEl.textContent = '$0.00'; 284 totalPnlEl.className = 'h4 mb-0 text-white'; 285 return; 286 } 287
288 const wins = entries.filter(e => parseFloat(e.pnl) >= 0).length; 289 const winRate = Math.round((wins / total) * 100); 290 const totalPnl = entries.reduce((acc, curr) => acc + parseFloat(curr.pnl || 0), 0); 291 292 totalTradesEl.textContent = total.toString(); 293 winRateEl.textContent = `${winRate}%`; 294 totalPnlEl.textContent = `${totalPnl >= 0 ? '+' : ''}$${totalPnl.toLocaleString('en-US', { minimumFractionDigits: 2 })}`; 295 totalPnlEl.className = `h4 mb-0 ${totalPnl >= 0 ? 'text-success' : 'text-danger'}`; 296 } 297 298 function deleteTrade(index) { 299 const entries = getJournalEntries(); 300 entries.splice(index, 1); 301 saveJournalEntries(entries); 302 renderJournal(); 303 } 304 305 function initJournal() { 306 const form = document.getElementById('cq-journal-form'); 307 const clearBtn = document.getElementById('cq-j-clear-btn'); 308 const exportBtn = document.getElementById('cq-j-export-btn'); 309 310 if (form) { 311 form.addEventListener('submit', function (e) { 312 e.preventDefault(); 313 const pair = document.getElementById('cq-j-pair').value.toUpperCase().trim(); 314 const direction = document.getElementById('cq-j-dir').value; 315 const entryPrice = parseFloat(document.getElementById('cq-j-entry').value) || 0; 316 const exitPrice = parseFloat(document.getElementById('cq-j-exit').value) || 0; 317 const pnl = parseFloat(document.getElementById('cq-j-pnl').value) || 0; 318 const date = new Date().toISOString().split('T')[0]; 319 320 if (!pair || !entryPrice || !exitPrice) return; 321 322 const newTrade = { pair, direction, entryPrice, exitPrice, pnl, date }; 323 const entries = getJournalEntries(); 324 entries.unshift(newTrade); 325 saveJournalEntries(entries); 326 renderJournal(); 327 form.reset(); 328 }); 329 } 330 331 if (clearBtn) { 332 clearBtn.addEventListener('click', function () { 333 if (confirm('Voulez-vous vraiment réinitialiser l\'ensemble de votre journal de trading ?')) { 334 saveJournalEntries([]); 335 renderJournal(); 336 } 337 }); 338 } 339 340 if (exportBtn) { 341 exportBtn.addEventListener('click', function () { 342 const entries = getJournalEntries(); 343 const jsonStr = JSON.stringify(entries, null, 2); 344 const blob = new Blob([jsonStr], { type: 'application/json' }); 345 const url = URL.createObjectURL(blob); 346 const a = document.createElement('a'); 347 a.href = url; 348 a.download = `CQ_Trading_Journal_${new Date().toISOString().split('T')[0]}.json`; 349 a.click(); 350 URL.revokeObjectURL(url); 351 }); 352 } 353 354 const importBtn = document.getElementById('cq-j-import-btn'); 355 const importFile = document.getElementById('cq-j-import-file'); 356 357 if (importBtn && importFile) { 358 importBtn.addEventListener('click', function () { 359 importFile.click(); 360 }); 361 362 importFile.addEventListener('change', function (e) { 363 const file = e.target.files[0]; 364 if (!file) return; 365 366 const reader = new FileReader(); 367 reader.onload = function (event) { 368 try { 369 const importedData = JSON.parse(event.target.result); 370 if (Array.isArray(importedData)) { 371 const existing = getJournalEntries(); 372 const merged = [...importedData, ...existing]; 373 saveJournalEntries(merged); 374 renderJournal(); 375 alert(`${importedData.length} trade(s) importé(s) avec succès !`); 376 } else {
377 alert("Format de fichier JSON invalide."); 378 } 379 } catch (err) { 380 alert("Erreur lors de la lecture du fichier JSON."); 381 } 382 }; 383 reader.readAsText(file); 384 importFile.value = ''; 385 }); 386 } 387 388 renderJournal(); 389 return renderJournal; 390 } 391 392 // âââ INITIALISATION GLOBALE AU CHARGEMENT DU DOM âââââââââââââââââââââââââ 393 394 // âââ 4. UNIVERSAL INFOBULLE / TOOLTIP ENGINE ââââââââââââââââââââââââââââââ 395 function initUniversalTooltips() { 396 let tooltipBox = document.getElementById('cq-custom-tooltip'); 397 if (!tooltipBox) { 398 tooltipBox = document.createElement('div'); 399 tooltipBox.id = 'cq-custom-tooltip'; 400 tooltipBox.style.cssText = 'position: fixed; display: none; background: #0d1926; color: #e2e8f0; border: 1px solid #00e5ff; box-shadow: 0 8px 32px rgba(0, 229, 255, 0.35); border-radius: 8px; padding: 10px 14px; font-size: 12.5px; font-weight: 500; line-height: 1.45; max-width: 290px; z-index: 99999999; pointer-events: none; backdrop-filter: blur(8px); font-family: system-ui, -apple-system, sans-serif; opacity: 0; transition: opacity 0.15s ease-in-out;'; 401 document.body.appendChild(tooltipBox); 402 } 403 404 let activeIcon = null; 405 406 function showTooltip(icon) { 407 let text = icon.getAttribute('title') || icon.getAttribute('data-bs-title') || icon.getAttribute('data-original-title') || icon.getAttribute('data-stored-title'); 408 if (!text) return; 409 410 if (icon.hasAttribute('title')) { 411 icon.setAttribute('data-stored-title', icon.getAttribute('title')); 412 icon.removeAttribute('title'); 413 } 414 415 tooltipBox.innerHTML = '<div style="color: #00e5ff; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; display: flex; align-items: center; gap: 5px;"><i class="fa-solid fa-circle-info"></i> INFO / CONFLUENCE</div><div>
415' + text + '</div>'; 416 417 const rect = icon.getBoundingClientRect(); 418 tooltipBox.style.display = 'block'; 419 420 let top = rect.top - tooltipBox.offsetHeight - 8; 421 let left = rect.left + (rect.width / 2) - (tooltipBox.offsetWidth / 2); 422 423 if (top < 10) { 424 top = rect.bottom + 8; 425 } 426 if (left < 10) left = 10; 427 if (left + tooltipBox.offsetWidth > window.innerWidth - 10) { 428 left = window.innerWidth - tooltipBox.offsetWidth - 10; 429 } 430 431 tooltipBox.style.top = top + 'px'; 432 tooltipBox.style.left = left + 'px'; 433 tooltipBox.style.opacity = '1'; 434 activeIcon = icon; 435 } 436 437 function hideTooltip() { 438 if (!tooltipBox) return; 439 tooltipBox.style.opacity = '0'; 440 setTimeout(function() { 441 if (tooltipBox.style.opacity === '0') { 442 tooltipBox.style.display = 'none'; 443 } 444 }, 150); 445 446 if (activeIcon && activeIcon.hasAttribute('data-stored-title')) { 447 activeIcon.setAttribute('title', activeIcon.getAttribute('data-stored-title')); 448 activeIcon.removeAttribute('data-stored-title'); 449 activeIcon = null; 450 } 451 } 452 453 // Tooltip click handler (non-interfering bubble phase) 454 document.addEventListener('click', function(e) { 455 const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info'); 456 if (icon) { 457 // If icon is purely standalone (not inside button/link with another action) 458 var interactiveParent = icon.closest('button, a, .btn, .indicator-card, .chart-tf-btn'); 459 if (!interactiveParent) { 460 e.preventDefault(); 461 } 462 if (activeIcon === icon && tooltipBox.style.display === 'block' && tooltipBox.style.opacity === '1') { 463 hideTooltip(); 464 } else { 465 showTooltip(icon); 466 } 467 } else { 468 hideTooltip(); 469 } 470 }, false); 471 472 document.addEventListener('mouseover', function(e) { 473 const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info'); 474 if (icon) { 475 showTooltip(icon); 476 } 477 }); 478 479 document.addEventListener('mouseout', function(e) { 480 const icon = e.target.closest('[data-bs-toggle="tooltip"], .fa-circle-info'); 481 if (icon) { 482 hideTooltip(); 483 } 484 }); 485 486 window.addEventListener('scroll', hideTooltip, { passive: true }); 487 document.addEventListener('keydown', function(e) { 488 if (e.key === 'Escape') hideTooltip(); 489 }); 490 } 491 492 document.addEventListener('DOMContentLoaded', function () { 493 initUniversalTooltips(); 494 const recalc = initPositionCalculator(); 495 const recheck = initPreTradeChecklist(); 496 const rejournal = initJournal(); 497 498 refreshAllTools = function() { 499 if (typeof recalc === 'function') recalc(); 500 if (typeof recheck === 'function') recheck(); 501 if (typeof rejournal === 'function') rejournal(); 502 }; 503 504 // Hook window.setLanguage for live i18n dynamic refreshes 505 if (window.setLanguage && !window.setLanguage._cqToolsHooked) { 506 const origSetLang = window.setLanguage; 507 window.setLanguage = function(lang) { 508 origSetLang(lang); 509 setTimeout(refreshAllTools, 50); 510 }; 511 window.setLanguage._cqToolsHooked = true; 512 } 513 514 // Listen for language selector button clicks as backup 515 document.querySelectorAll('.lang-btn, [data-lang]').forEach(btn => { 516 btn.addEventListener('click', function() { 517 setTimeout(refreshAllTools, 100); 518 }); 519 }); 520 521 // Initialisation automatique des Tooltips Bootstrap 522 if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { 523 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); 524 tooltipTriggerList.map(function (tooltipTriggerEl) { 525 return new bootstrap.Tooltip(tooltipTriggerEl); 526 }); 527 } else if (typeof $ !== 'undefined' && $.fn.tooltip) { 528 $('[data-bs-toggle="tooltip"]').tooltip(); 529 } 530 });
531 532})(); 533 534 535// Global Tool Modal Handlers (openToolModal / closeToolModal) 536window.openToolModal = function(modalId) { 537 if (!modalId) return; 538 try { 539 var modalEl = document.getElementById(modalId); 540 if (modalEl) { 541 modalEl.style.display = 'flex'; 542 modalEl.classList.add('show', 'active'); 543 document.body.style.overflow = 'hidden'; 544 545 var lang = localStorage.getItem('selected_language') || document.documentElement.lang || 'fr'; 546 547 if (modalId === 'modalPodcast') { 548 if (window.updatePodcastModalLanguage) { 549 try { window.updatePodcastModalLanguage(lang); } catch(e) { console.error(e); } 550 } 551 } 552 if (modalId === 'modalGlossaire') { 553 if (window.initCQGlossary) { 554 try { window.initCQGlossary('glossary-app-container'); } catch(e) { console.error(e); } 555 } 556 } 557 } 558 } catch(err) { 559 console.error('Error in openToolModal:', err); 560 } 561}; 562 563window.closeToolModal = function(modalId) { 564 try { 565 var modalEl = (typeof modalId === 'string') ? document.getElementById(modalId) : modalId; 566 if (modalEl) { 567 modalEl.style.display = 'none'; 568 modalEl.classList.remove('show', 'active'); 569 document.body.style.overflow = ''; 570 571 // Stop any playing iframe video inside the modal 572 var iframe = modalEl.querySelector('iframe'); 573 if (iframe) { 574 iframe.src = 'about:blank'; 575 } 576 var cover = modalEl.querySelector('#podcast-cover-poster'); 577 if (cover) { 578 cover.style.display = 'flex'; 579 } 580 } 581 } catch(err) { 582 console.error('Error in closeToolModal:', err); 583 } 584};
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.