Topic 31 of 57 6. HTML Head Elements Interactive Tutorial

HTML Head Tag: Structure, Elements, and Examples

12 mins read Last updated: 12 Nov 2026 16,200 views

Topic Overview & Definition

Key Question
What is the HTML <head> element and what critical metadata tags does it house?
Direct Answer & Overview
The HTML element acts as the metadata container for a webpage, positioned between and . Content inside is not rendered visually on the screen; instead, it defines the document title, character encoding, viewport settings, external CSS stylesheets, favicon icons, and SEO metadata.

Comprehensive Step-by-Step Concepts

1) Role of the Element: The communicates essential document instructions to the browser, search engines, and social media scrapers before any visible body pixels are painted. 2) Permitted Children of : - : Defines the page title in the browser tab and search results. - <meta>: Declares character set, viewport, descriptions, and keywords. - <link>: Connects external stylesheets, fonts, and favicons. - <style>:Embeds internal CSS style rules.- <script>:Embeds or links client-side JavaScript.- <base>:Defines default base URL for all relative URLs.3) Critical Order of Execution:- Always declare <meta charset="UTF-8">first so the parser correctly decodes characters immediately.- Next,include <meta name="viewport">for mobile layout calculation.- Place <title>and critical stylesheet <link>tags high in <head>to prevent Flash of Unstyled Content (FOUC).4) Preloading and Resource Hints:- <link rel="preload">:Prioritizes downloading critical web fonts or hero images.- <link rel="preconnect">:Establishes early TCP/TLS handshakes with third-party origins (e.g. Google Fonts).5) Social Media Meta Tags:Open Graph (<meta property="og:title">) and Twitter Cards provide rich preview cards when links are shared on LinkedIn,WhatsApp,Facebook,or X.</div></section><section class="tutorial-section mb-4"><h2 class="h4 fw-bold text-dark mb-3"><i class="fas fa-code me-2 text-danger"></i>Runnable HTML5 Code Example &Practical Implementation</h2><article class="code-card"><div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2 pb-2 border-bottom"><div class="d-flex align-items-center gap-2"><span class="terminal-dots d-inline-flex gap-1"><span class="t-dot t-red"></span><span class="t-dot t-yellow"></span><span class="t-dot t-green"></span></span><span class="badge bg-dark text-warning border border-secondary px-2 py-1 small">index.html</span></div><div class="d-flex gap-2"><button type="button" class="btn btn-sm btn-outline-light copy-code-btn px-3 fw-semibold" onclick="copyCode(this)"><i class="far fa-copy me-1"></i>Copy Code </button><button type="button" class="btn btn-sm btn-success preview-code-btn px-3 fw-semibold" onclick="previewHtmlModal(this)"><i class="fas fa-play me-1"></i>Try it Yourself </button><a href="/html-editor" target="_blank" class="btn btn-sm btn-primary run-code-btn px-3 fw-semibold" aria-label="Open in HTML Editor" style="background-color:#e34f26;border-color:#e34f26;"><i class="fab fa-html5 me-1"></i>HTML Editor </a></div></div><pre class="code-box"><code class="language-html"><!DOCTYPE html><html lang="en"><head><!-- 1. Character Encoding (Must be first) --><meta charset="UTF-8"><!-- 2. Responsive Mobile Viewport --><meta name="viewport"content="width=device-width,initial-scale=1.0"><!-- 3. Document Title --><title>High Performance Web Design - OperateTools</title><!-- 4. SEO Meta Description --><meta name="description"content="Master front-end web development with our high-speed,interactive HTML tutorials and tools."><!-- 5. Favicon Link --><link rel="icon"type="image/svg+xml"href="/favicon.svg"><!-- 6. Preconnect to Font Servers --><link rel="preconnect"href="https://fonts.googleapis.com"><!-- 7. External CSS Stylesheet --><link rel="stylesheet"href="/css/main.css"><!-- 8. Deferred JavaScript Execution --><script src="/js/app.js"defer></script></head><body><h1>Clean and Optimized Head Section</h1></body></html></code></pre></article></section><section class="tutorial-section mb-4"><h2 class="h4 fw-bold text-dark mb-3"><i class="fas fa-lightbulb text-warning me-2"></i>Key Takeaways,Best Practices &Standards</h2><div class="topic-box topic-best-practice-box mb-4"><div class="d-flex align-items-start gap-3"><div class="topic-icon-pill bg-opacity-20 text-warning flex-shrink-0"><i class="fas fa-check-double fs-4 text-warning"></i></div><div class="flex-grow-1"><div class="text-uppercase tracking-wider fw-bold text-warning small mb-1">Best Practice &Standards</div><div class="text-dark fw-medium fs-6 lh-base">Place <meta charset="UTF-8">first,followed by <meta name="viewport">,<title>,and stylesheet <link>tags. Defer all non-critical scripts.</div></div></div></div></section><section class="tutorial-section mb-4"><h2 class="h4 fw-bold text-dark mb-3"><i class="fas fa-comments text-danger me-2"></i>Common Technical &Interview Questions</h2><div class="interview-qa-item mb-3 p-3 bg-white rounded-3 border border-danger border-opacity-25 shadow-xs"><div class="fw-bold text-danger mb-2 d-flex align-items-start gap-2"><span class="badge bg-danger text-white rounded-circle p-1 d-inline-flex align-items-center justify-content-center flex-shrink-0" style="width:22px;height:22px;font-size:0.75rem;">Q</span><h3 class="h6 fw-bold text-danger mb-0 lh-base">Q1:What is FOUC (Flash of Unstyled Content) and how does correct <head>ordering prevent it?</h3></div><div class="text-dark fw-medium fs-6 ps-4"><div class="d-flex align-items-start gap-2"><i class="fas fa-check-circle text-success mt-1 flex-shrink-0"></i><div class="lh-base">A1:FOUC occurs when HTML text renders before CSS stylesheets have downloaded,causing an ugly unstyled flash. Placing CSS <link>tags in <head>blocks visual rendering until styles are parsed.</div></div></div></div><div class="interview-qa-item mb-3 p-3 bg-white rounded-3 border border-danger border-opacity-25 shadow-xs"><div class="fw-bold text-danger mb-2 d-flex align-items-start gap-2"><span class="badge bg-danger text-white rounded-circle p-1 d-inline-flex align-items-center justify-content-center flex-shrink-0" style="width:22px;height:22px;font-size:0.75rem;">Q</span><h3 class="h6 fw-bold text-danger mb-0 lh-base">Q2:What happens if <meta charset="UTF-8">is placed at the very bottom of <head>?</h3></div><div class="text-dark fw-medium fs-6 ps-4"><div class="d-flex align-items-start gap-2"><i class="fas fa-check-circle text-success mt-1 flex-shrink-0"></i><div class="lh-base">A2:Browsers may start parsing characters using a default fallback encoding. When they hit the UTF-8 declaration later,they may restart HTML parsing from scratch,causing noticeable performance degradation.</div></div></div></div><div class="interview-qa-item mb-3 p-3 bg-white rounded-3 border border-danger border-opacity-25 shadow-xs"><div class="fw-bold text-danger mb-2 d-flex align-items-start gap-2"><span class="badge bg-danger text-white rounded-circle p-1 d-inline-flex align-items-center justify-content-center flex-shrink-0" style="width:22px;height:22px;font-size:0.75rem;">Q</span><h3 class="h6 fw-bold text-danger mb-0 lh-base">Q3:Can a <script>tag be placed inside <head>?</h3></div><div class="text-dark fw-medium fs-6 ps-4"><div class="d-flex align-items-start gap-2"><i class="fas fa-check-circle text-success mt-1 flex-shrink-0"></i><div class="lh-base">A3:Yes,but without 'defer' or 'async',synchronous scripts block the HTML parser until the script is fully downloaded and executed. Always use <script defer>in <head>.</div></div></div></div></section></div></div></div><!-- Bottom Navigation Bar:Previous & Next Topic --><div class="tutorial-nav-bottom pt-4 mt-5 border-top"><div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2"><button type="button" id="btn-toggle-complete" class="btn btn-outline-success rounded-pill px-3 py-2 fw-semibold d-inline-flex align-items-center gap-2" style="font-size:13px;"><i class="fas fa-check-circle"></i><span id="toggle-complete-text">Mark as Completed</span></button><a href="https://www.operatetools.com/tutorial/html-tutorial" class="btn btn-outline-secondary rounded-pill px-3 py-2 fw-semibold d-inline-flex align-items-center gap-2" style="font-size:13px;"><i class="fas fa-list-ul"></i>All HTML Topics </a></div><div class="row g-3 align-items-center" style="font-size:12px;"><div class="col-sm-6"><a href="https://www.operatetools.com/tools/html-programming-tutorial/html-main-tag-syntax-uses-and-examples" class="topic-nav-card text-decoration-none d-flex align-items-center gap-3 p-3" aria-label="Previous Topic:HTML Main Tag:Syntax,Uses,and Examples"><div class="topic-nav-arrow flex-shrink-0"><i class="fas fa-arrow-left text-primary"></i></div><div class="overflow-hidden"><small class="text-muted text-uppercase fw-bold d-block mb-1 topic-nav-label" style="font-size:11px;">Previous Topic</small><span class="topic-nav-title text-dark fw-bold text-truncate d-block" style="font-size:12px;">HTML Main Tag:Syntax,Uses,and Examples</span></div></a></div><div class="col-sm-6"><a href="https://www.operatetools.com/tools/html-programming-tutorial/html-title-tag-syntax-seo-best-practices-and-examples" class="topic-nav-card text-decoration-none d-flex align-items-center justify-content-between gap-3 p-3" aria-label="Next Topic:HTML Title Tag:Syntax,SEO Best Practices,and Examples"><div class="overflow-hidden text-start text-sm-end w-100"><small class="text-muted text-uppercase fw-bold d-block mb-1 topic-nav-label" style="font-size:11px;">Next Topic</small><span class="topic-nav-title text-dark fw-bold text-truncate d-block" style="font-size:12px;">HTML Title Tag:Syntax,SEO Best Practices,and Examples</span></div><div class="topic-nav-arrow flex-shrink-0"><i class="fas fa-arrow-right text-primary"></i></div></a></div></div></div></div></div></div><!-- Attractive Social Media Share Modal --><div class="modal fade tutorial-share-modal" id="tutorialShareModal" tabindex="-1" aria-labelledby="tutorialShareModalLabel" aria-hidden="true"><div class="modal-dialog modal-dialog-centered"><div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius:20px;"><!-- Modal Header with vibrant gradient --><div class="modal-header border-0 text-white p-4" style="background:linear-gradient(135deg,#1e3a8a 0%,#2563eb 60%,#3b82f6 100%);"><div class="d-flex align-items-center gap-3"><div class="share-header-icon-box"><i class="fas fa-share-nodes"></i></div><div><h5 class="modal-title fw-bold mb-1" id="tutorialShareModalLabel" style="font-size:1.15rem;color:#ffffff;">Share This Tutorial</h5><p class="mb-0 text-white-50 small" style="font-size:0.8rem;">Share with friends,students,or your developer network</p></div></div><button type="button" class="btn-close btn-close-white ms-auto shadow-none" onclick="closeTutorialShareModal()" data-bs-dismiss="modal" aria-label="Close"></button></div><!-- Modal Body --><div class="modal-body p-4 bg-white"><!-- Social Media Platforms Grid --><div class="share-platforms-grid mb-4"><!-- WhatsApp --><button type="button" class="share-platform-btn share-whatsapp" onclick="shareToPlatform('whatsapp',event)" title="Share on WhatsApp"><div class="share-btn-icon" style="background:#25D366;color:#fff;"><i class="fab fa-whatsapp"></i></div><span class="share-btn-label">WhatsApp</span></button><!-- LinkedIn --><button type="button" class="share-platform-btn share-linkedin" onclick="shareToPlatform('linkedin',event)" title="Share on LinkedIn"><div class="share-btn-icon" style="background:#0A66C2;color:#fff;"><i class="fab fa-linkedin-in"></i></div><span class="share-btn-label">LinkedIn</span></button><!-- X / Twitter --><button type="button" class="share-platform-btn share-twitter" onclick="shareToPlatform('twitter',event)" title="Share on X (Twitter)"><div class="share-btn-icon" style="background:#0f1419;color:#fff;"><i class="fab fa-x-twitter"></i></div><span class="share-btn-label">X / Twitter</span></button><!-- Facebook --><button type="button" class="share-platform-btn share-facebook" onclick="shareToPlatform('facebook',event)" title="Share on Facebook"><div class="share-btn-icon" style="background:#1877F2;color:#fff;"><i class="fab fa-facebook-f"></i></div><span class="share-btn-label">Facebook</span></button><!-- Telegram --><button type="button" class="share-platform-btn share-telegram" onclick="shareToPlatform('telegram',event)" title="Share on Telegram"><div class="share-btn-icon" style="background:#229ED9;color:#fff;"><i class="fab fa-telegram-plane"></i></div><span class="share-btn-label">Telegram</span></button><!-- Reddit --><button type="button" class="share-platform-btn share-reddit" onclick="shareToPlatform('reddit',event)" title="Share on Reddit"><div class="share-btn-icon" style="background:#FF4500;color:#fff;"><i class="fab fa-reddit-alien"></i></div><span class="share-btn-label">Reddit</span></button><!-- Threads --><button type="button" class="share-platform-btn share-threads" onclick="shareToPlatform('threads',event)" title="Share on Threads"><div class="share-btn-icon" style="background:#000000;color:#fff;"><i class="fas fa-at"></i></div><span class="share-btn-label">Threads</span></button><!-- Email --><button type="button" class="share-platform-btn share-email" onclick="shareToPlatform('email',event)" title="Share via Email"><div class="share-btn-icon" style="background:#64748b;color:#fff;"><i class="fas fa-envelope"></i></div><span class="share-btn-label">Email</span></button></div><!-- Copy Link Container --><div class="share-link-wrapper p-3 rounded-3" style="background:#f8fafc;border:1.5px solid #e2e8f0;"><label class="form-label fw-bold text-dark small mb-2 d-flex align-items-center justify-content-between"><span><i class="fas fa-link text-primary me-1"></i>Page Link</span><span class="text-muted" style="font-size:0.72rem;">Click button to copy</span></label><div class="input-group"><input type="text" class="form-control form-control-sm bg-white text-secondary font-monospace" id="tutorialShareUrlInput" readonly style="font-size:0.8rem;border-color:#cbd5e1;"><button type="button" class="btn btn-sm btn-primary px-3 fw-semibold" id="tutorialCopyLinkBtn" onclick="copyTutorialShareLink()" style="background-color:#2563eb;border-color:#2563eb;"><i class="far fa-copy me-1" id="tutorialCopyLinkIcon"></i><span id="tutorialCopyLinkText">Copy Link</span></button></div></div><!-- Native Device Share Button (if supported) --><button type="button" class="btn btn-outline-primary btn-sm w-100 rounded-pill mt-3 py-2 fw-semibold d-none" id="nativeShareBtn" onclick="triggerNativeShare()"><i class="fas fa-mobile-screen-button me-2"></i>More Share Options (Device) </button></div></div></div></div><!-- Copy Code Script & Progress Tracking --><script>function openTutorialShareModal(){const urlInput = document.getElementById('tutorialShareUrlInput');if (urlInput){urlInput.value = window.location.href}const nativeBtn = document.getElementById('nativeShareBtn');if (nativeBtn && navigator.share){nativeBtn.classList.remove('d-none')}const modalEl = document.getElementById('tutorialShareModal');if (!modalEl) return;if (typeof bootstrap !== 'undefined' && bootstrap.Modal){const modalInstance = bootstrap.Modal.getOrCreateInstance(modalEl);modalInstance.show()}else{modalEl.classList.add('show');modalEl.style.display = 'block';document.body.classList.add('modal-open');let backdrop = document.querySelector('.modal-backdrop');if (!backdrop){backdrop = document.createElement('div');backdrop.className = 'modal-backdrop fade show';document.body.appendChild(backdrop);backdrop.onclick = closeTutorialShareModal}}}function closeTutorialShareModal(){const modalEl = document.getElementById('tutorialShareModal');if (!modalEl) return;if (typeof bootstrap !== 'undefined' && bootstrap.Modal){const modalInstance = bootstrap.Modal.getInstance(modalEl);if (modalInstance) modalInstance.hide()}else{modalEl.classList.remove('show');modalEl.style.display = 'none';document.body.classList.remove('modal-open');const backdrop = document.querySelector('.modal-backdrop');if (backdrop) backdrop.remove()}}function copyTutorialShareLink(){const url = window.location.href;const copyBtn = document.getElementById('tutorialCopyLinkBtn');const copyText = document.getElementById('tutorialCopyLinkText');const copyIcon = document.getElementById('tutorialCopyLinkIcon');const onSuccess = () =>{if (copyBtn){copyBtn.classList.remove('btn-primary');copyBtn.classList.add('btn-success')}if (copyText) copyText.textContent = 'Copied!';if (copyIcon) copyIcon.className = 'fas fa-check me-1';setTimeout(() =>{if (copyBtn){copyBtn.classList.remove('btn-success');copyBtn.classList.add('btn-primary')}if (copyText) copyText.textContent = 'Copy Link';if (copyIcon) copyIcon.className = 'far fa-copy me-1'},2500)};if (navigator.clipboard && navigator.clipboard.writeText){navigator.clipboard.writeText(url).then(onSuccess).catch(() =>{fallbackCopy(url,onSuccess)})}else{fallbackCopy(url,onSuccess)}}function fallbackCopy(text,callback){const input = document.createElement('input');input.value = text;document.body.appendChild(input);input.select();try{document.execCommand('copy');if (callback) callback()}catch(e){}document.body.removeChild(input)}function shareToPlatform(platform,event){if (event) event.preventDefault();const url = encodeURIComponent(window.location.href);const title = encodeURIComponent(document.title || 'Check out this Programming Tutorial on OperateTools');let shareUrl = '';switch (platform){case 'whatsapp':shareUrl = `https://api.whatsapp.com/send?text=${title}%20${url}`;break;case 'linkedin':shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${url}`;break;case 'twitter':shareUrl = `https://twitter.com/intent/tweet?url=${url}&text=${title}`;break;case 'facebook':shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${url}`;break;case 'telegram':shareUrl = `https://t.me/share/url?url=${url}&text=${title}`;break;case 'reddit':shareUrl = `https://reddit.com/submit?url=${url}&title=${title}`;break;case 'threads':shareUrl = `https://www.threads.net/intent/post?text=${title}%20${url}`;break;case 'email':shareUrl = `mailto:?subject=${title}&body=Check%20out%20this%20tutorial:%20${url}`;break}if (shareUrl){window.open(shareUrl,'_blank','width=600,height=500,location=no,menubar=no,status=no')}}function triggerNativeShare(){if (navigator.share){navigator.share({title:document.title,text:'Learn with runnable code on OperateTools:',url:window.location.href}).catch(() =>{})}}function copyCode(btn){let text = '';const wrapper = btn.closest('.code-card') || btn.closest('.code-block-wrapper');if (wrapper){const pre = wrapper.querySelector('pre');if (pre) text = pre.innerText || pre.textContent || ''}else{const targetId = btn.getAttribute('data-target');if (targetId){const el = document.getElementById(targetId);if (el) text = el.innerText || el.textContent || ''}}if (!text && btn.parentNode){const siblingPre = btn.parentNode.querySelector('pre');if (siblingPre) text = siblingPre.innerText || siblingPre.textContent || ''}if (text){if (navigator.clipboard && window.isSecureContext){navigator.clipboard.writeText(text).then(() =>{showCopySuccess(btn)})}else{const textarea = document.createElement('textarea');textarea.value = text;textarea.style.position = 'fixed';textarea.style.opacity = '0';document.body.appendChild(textarea);textarea.select();try{document.execCommand('copy');showCopySuccess(btn)}catch (err){console.error('Fallback copy failed',err)}document.body.removeChild(textarea)}}}function showCopySuccess(btn){const originalHtml = btn.innerHTML;const hasText = btn.textContent.trim().length>0;if (hasText){btn.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';btn.classList.remove('btn-outline-primary');btn.classList.add('btn-success')}else{btn.innerHTML = '<i class="fas fa-check text-success"></i>'}setTimeout(() =>{btn.innerHTML = originalHtml;if (hasText){btn.classList.remove('btn-success');btn.classList.add('btn-outline-primary')}},1500)}function escapeHtmlTokens(str){return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}function highlightJSCode(element){if (!element) return;let text = element.innerText || element.textContent || '';if (!text.trim()) return;// Split inline step arrows and numbered steps into clean top-to-bottom lines text = text.replace(/\s*([⇣↓⬇]|↓|↓|⇩)\s*/g,'\n$1\n') .replace(/([^\n])\s+(\d+\.\s+)/g,'$1\n$2');if (!text.includes('\n')){if (text.includes('<') || text.includes('<')){text = text.replace(/(>|>)\s*(<|<)/gi,'$1\n$2')}else if (text.includes(';') || text.includes('{')){text = text.replace(/\{\s*/g,'{\n ') .replace(/;\s*(?=[a-zA-Z_$]|\bif\b|\blet\b|\bconst\b|\bvar\b|\breturn\b|\bconsole\b|\bfunction\b)/g,';\n') .replace(/\s*\}\s*/g,'\n}\n') .trim()}}const tokenRegex = /(\/\/.*|\/\*[\s\S]*?\*\/)|("(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`)|(\b(?:let|var|const|if|else|return|function|for|while|switch|case|break|continue|try|catch|new|typeof|class|import|export|from|async|await|of|in|instanceof|default|do|void|this|super|throw|debugger|yield)\b)|(\b(?:console|document|window|Math|JSON|Promise|Array|Object|String|Number|Boolean|Date|RegExp|Set|Map|parseInt|parseFloat|setTimeout|setInterval)\b)|(\b(?:true|false|null|undefined|NaN|Infinity)\b)|(\b\d+(?:\.\d+)?\b)|(\b[a-zA-Z_$][a-zA-Z0-9_$]*\b)|([=+\-*\/%><!&|^~?:;.,(){}]+)/g;let highlighted = text.replace(tokenRegex,function(match,comment,string,keyword,builtin,literal,number,identifier,operator){if (comment) return `<span class="js-cm">${escapeHtmlTokens(comment)}</span>`;if (string) return `<span class="js-str">${escapeHtmlTokens(string)}</span>`;if (keyword) return `<span class="js-kw">${escapeHtmlTokens(keyword)}</span>`;if (builtin) return `<span class="js-id">${escapeHtmlTokens(builtin)}</span>`;if (literal) return `<span class="js-num">${escapeHtmlTokens(literal)}</span>`;if (number) return `<span class="js-num">${escapeHtmlTokens(number)}</span>`;if (identifier) return `<span class="js-id">${escapeHtmlTokens(identifier)}</span>`;if (operator) return `<span class="js-op">${escapeHtmlTokens(operator)}</span>`;return escapeHtmlTokens(match)});element.innerHTML = highlighted}// Global Try-it-Yourself Live Preview Functionalitywindow.previewHtmlModal = function(btn){const card = btn.closest('.code-card');const codeEl = card ? (card.querySelector('code') || card.querySelector('pre')):null;const rawCode = codeEl ? codeEl.innerText.trim():'<!DOCTYPE html>\n<html>\n<body>\n<h1>Hello World</h1>\n</body>\n</html>';const editorArea = document.getElementById('liveHtmlEditorArea');if (editorArea){editorArea.value = rawCode}const modalEl = document.getElementById('livePreviewModal');if (modalEl){const modal = bootstrap.Modal.getOrCreateInstance(modalEl);modal.show();setTimeout(runLivePreview,150)}};window.runLivePreview = function(){const editorArea = document.getElementById('liveHtmlEditorArea');const iframe = document.getElementById('liveHtmlPreviewFrame');if (!editorArea || !iframe) return;const htmlContent = editorArea.value;const frameDoc = iframe.contentDocument || iframe.contentWindow.document;frameDoc.open();frameDoc.write(htmlContent);frameDoc.close()};window.copyModalCode = function(){const editorArea = document.getElementById('liveHtmlEditorArea');if (!editorArea) return;navigator.clipboard.writeText(editorArea.value).then(() =>{alert('HTML Code copied to clipboard!')})};// Real-time debounce runner on keystroke inside modaldocument.addEventListener('DOMContentLoaded',function(){const editorArea = document.getElementById('liveHtmlEditorArea');if (editorArea){let timeout = null;editorArea.addEventListener('input',function(){clearTimeout(timeout);timeout = setTimeout(runLivePreview,300)})}});function initCodeBlocks(){// 1. Remove duplicate/unwanted wrapper clutter and header artifacts document.querySelectorAll('[class*="pointer-events-none"],[class*="sticky top-2"],[class*="-mt-10"]').forEach(el =>el.remove());document.querySelectorAll('[class*="border-border"],[class*="bg-background"]').forEach(el =>{el.classList.remove('border','border-border','bg-background','p-4','p-3','rounded-md','text-sm','overflow-y-auto','overflow-x-auto')});// 2. Wrap block-level standalone code elements into pre.code-box document.querySelectorAll('.article-explanation-body code,.tutorial-body code').forEach(function(code){if (code.closest('pre') || code.closest('.code-card') || code.closest('.code-block-wrapper')) return;const cls = code.className || '';const isBlockCode = cls.includes('hljs') || cls.includes('javascript') || cls.includes('language-') || (code.textContent && code.textContent.includes('\n'));if (!isBlockCode) return;const parent = code.parentNode;if (parent){const pre = document.createElement('pre');pre.className = 'code-box';parent.insertBefore(pre,code);pre.appendChild(code)}});// 3. Ensure all pre elements have .code-box class and are wrapped in .code-card document.querySelectorAll('pre,.tutorial-topic-container pre,.tutorial-body pre,.article-explanation-body pre').forEach(function(pre){pre.classList.add('code-box');const codeEl = pre.querySelector('code') || pre;if (!codeEl.innerHTML.includes('hljs-') && !codeEl.innerHTML.includes('js-')){highlightJSCode(codeEl)}if (pre.closest('.code-card') || pre.closest('.code-block-wrapper')){return}const card = document.createElement('article');card.className = 'code-card my-3';card.innerHTML = ` <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2 pb-2 border-bottom"><div class="d-flex align-items-center gap-2"><span class="terminal-dots d-inline-flex gap-1"><span class="t-dot t-red"></span><span class="t-dot t-yellow"></span><span class="t-dot t-green"></span></span><span class="badge bg-dark text-warning border border-secondary px-2 py-1 small">index.html</span></div><div class="d-flex gap-2"><button type="button" class="btn btn-sm btn-outline-primary copy-code-btn px-3 fw-semibold" onclick="copyCode(this)"><i class="far fa-copy me-1"></i>Copy Code </button><a href="/html-editor" target="_blank" class="btn btn-sm btn-primary run-code-btn px-3 fw-semibold" aria-label="Run JavaScript code online in compiler" style="background-color:#2563eb;border-color:#2563eb;"><i class="fas fa-play me-1"></i>Run Online </a></div></div>`;pre.parentNode.insertBefore(card,pre);card.appendChild(pre)});// Remove any leftover text-muted font-monospace labels from code-cards document.querySelectorAll('.code-card .text-muted.font-monospace,.code-card .font-monospace.text-muted,.article-explanation-body .text-muted.font-monospace').forEach(function(el){el.remove()})}function initTableOfContents(){const tocBox = document.getElementById('pageTocBox');const tocList = document.getElementById('tocPillsList');const countBadge = document.getElementById('tocSectionCount');const toggleBtn = document.getElementById('tocToggleBtn');const tocBody = document.getElementById('tocBody');const toggleText = document.getElementById('tocToggleText');const toggleIcon = document.getElementById('tocToggleIcon');if (!tocBox || !tocList) return;const contentEl = document.querySelector('.tutorial-body') || document.querySelector('.tutorial-content-card');if (!contentEl){tocBox.style.display = 'none';return}const headings = Array.from(contentEl.querySelectorAll('h2,h3')).filter(h =>{return h.textContent.trim().length>0 && !h.closest('.article-hero-header') && !h.closest('.article-toc-box')});if (headings.length < 2){tocBox.style.display = 'none';return}if (countBadge){countBadge.textContent = headings.length+' sections'}tocList.innerHTML = '';const pillMap = new Map();headings.forEach((heading,idx) =>{if (!heading.id){const slug = heading.textContent.trim().toLowerCase() .replace(/[^a-z0-9]+/g,'-') .replace(/^-+|-+$/g,'');heading.id = slug || ('sec-'+(idx+1))}const pill = document.createElement('a');pill.href = '#'+heading.id;pill.className = 'toc-pill-link';pill.innerHTML = `<i class="fas fa-hashtag toc-pill-icon"></i><span>${heading.textContent.trim()}</span>`;pill.addEventListener('click',function(e){e.preventDefault();const target = document.getElementById(heading.id);if (target){target.scrollIntoView({behavior:'smooth',block:'start'});target.classList.remove('heading-target-pulse');void target.offsetWidth;// Trigger reflow to restart animation target.classList.add('heading-target-pulse');setTimeout(() =>target.classList.remove('heading-target-pulse'),2000);if (history.pushState){history.pushState(null,null,'#'+heading.id)}}});tocList.appendChild(pill);pillMap.set(heading,pill)});function toggleToc(){if (!tocBody) return;const isCollapsed = tocBody.classList.toggle('collapsed');if (toggleText) toggleText.textContent = isCollapsed ? 'Expand':'Collapse';if (toggleIcon){toggleIcon.className = isCollapsed ? 'fas fa-chevron-down ms-1':'fas fa-chevron-up ms-1'}if (tocBox){tocBox.classList.toggle('is-collapsed',isCollapsed)}}const tocHeader = tocBox.querySelector('.toc-header');if (tocHeader){tocHeader.addEventListener('click',function(e){toggleToc()})}if (toggleBtn){toggleBtn.addEventListener('click',function(e){e.stopPropagation();toggleToc()})}if ('IntersectionObserver' in window){const observer = new IntersectionObserver((entries) =>{entries.forEach(entry =>{if (entry.isIntersecting){document.querySelectorAll('.toc-pill-link').forEach(p =>p.classList.remove('active'));const activePill = pillMap.get(entry.target);if (activePill){activePill.classList.add('active')}}})},{rootMargin:'-80px 0px -70% 0px',threshold:0});headings.forEach(heading =>observer.observe(heading))}}function initHtmlTutorialApp(){initCodeBlocks();initTableOfContents();// Mobile Sidebar Drawer Elements & Handlers const openFab = document.getElementById('openTutorialSidebarBtn');const closeBtn = document.getElementById('closeSidebarDrawerBtn');const sidebar = document.getElementById('tutorialSidebar');const backdrop = document.getElementById('tutorialSidebarBackdrop');// Ensure floating elements are attached to document.body to avoid parent container containment if (openFab && openFab.parentElement !== document.body){document.body.appendChild(openFab)}if (backdrop && backdrop.parentElement !== document.body){document.body.appendChild(backdrop)}function openSidebarDrawer(){if (sidebar) sidebar.classList.add('show');if (backdrop) backdrop.classList.add('show');document.body.style.overflow = 'hidden';const activeLink = document.querySelector('.step-link.active');if (activeLink && sidebar){setTimeout(() =>{activeLink.scrollIntoView({behavior:'smooth',block:'center'})},150)}}function closeSidebarDrawer(){if (sidebar) sidebar.classList.remove('show');if (backdrop) backdrop.classList.remove('show');document.body.style.overflow = ''}if (openFab) openFab.addEventListener('click',openSidebarDrawer);if (closeBtn) closeBtn.addEventListener('click',closeSidebarDrawer);if (backdrop) backdrop.addEventListener('click',closeSidebarDrawer);document.addEventListener('keydown',function(e){if (e.key === 'Escape') closeSidebarDrawer()});const totalTopics = 57;const currentTopicSlug = "html-head-tag-structure-elements-and-examples";// HTML Tutorial Progress Tracking (Unified LocalStorage) function getHtmlProgressMap(){let progress ={};try{const raw = localStorage.getItem('html_tutorial_progress');if (raw){const parsed = JSON.parse(raw);if (Array.isArray(parsed)){parsed.forEach(s =>{if (s) progress[s] = true})}else if (typeof parsed === 'object' && parsed !== null){progress = parsed}}}catch(e){}if (Object.keys(progress).length === 0){try{const rawArr = localStorage.getItem('html_completed_topics');if (rawArr){const parsedArr = JSON.parse(rawArr);if (Array.isArray(parsedArr)){parsedArr.forEach(s =>{if (s) progress[s] = true})}}}catch(e){}}return progress}function saveHtmlProgressMap(progress){try{localStorage.setItem('html_tutorial_progress',JSON.stringify(progress));const arr = Object.keys(progress).filter(k =>!!progress[k]);localStorage.setItem('html_completed_topics',JSON.stringify(arr));localStorage.setItem('completed_topics_html',JSON.stringify(arr))}catch(e){}}let progressMap = getHtmlProgressMap();// Auto mark currently visited topic as completed if (currentTopicSlug && !progressMap[currentTopicSlug]){progressMap[currentTopicSlug] = true;saveHtmlProgressMap(progressMap)}function updateAllProgressUI(){const completedSlugs = Object.keys(progressMap).filter(k =>!!progressMap[k]);const percent = totalTopics>0 ? ((completedSlugs.length / totalTopics) * 100).toFixed(2):'0.00';const badgeEl = document.getElementById('user-progress-percent');if (badgeEl){badgeEl.textContent = percent+'%'}// Toggle button update const toggleBtn = document.getElementById('btn-toggle-complete');if (toggleBtn && currentTopicSlug){if (progressMap[currentTopicSlug]){toggleBtn.className = 'btn btn-success rounded-pill px-3 py-2 fw-semibold d-inline-flex align-items-center gap-2 text-white';toggleBtn.innerHTML = '<i class="fas fa-check-circle"></i><span>Completed</span>'}else{toggleBtn.className = 'btn btn-outline-success rounded-pill px-3 py-2 fw-semibold d-inline-flex align-items-center gap-2';toggleBtn.innerHTML = '<i class="far fa-circle"></i><span>Mark as Completed</span>'}}// Update sidebar step items document.querySelectorAll('.step-item').forEach(function(item){const slug = item.getAttribute('data-topic-slug');const dotEl = item.querySelector('.step-dot');const linkEl = item.querySelector('.step-link');if (progressMap[slug]){item.classList.add('completed-step');if (dotEl && !dotEl.classList.contains('active')){dotEl.style.backgroundColor = '#22c55e'}}else{item.classList.remove('completed-step');if (dotEl && !dotEl.classList.contains('active')){dotEl.style.backgroundColor = ''}}if (currentTopicSlug && slug === currentTopicSlug){item.classList.add('active-step');if (dotEl){dotEl.classList.add('active');dotEl.style.backgroundColor = '#2563eb'}if (linkEl) linkEl.classList.add('active')}});// Update per-module progress badges and SVG circular gauges in sidebar document.querySelectorAll('.sidebar-module-item').forEach(function(mod){const total = parseInt(mod.getAttribute('data-module-total') || '0',10);let completedCount = 0;mod.querySelectorAll('.step-item').forEach(function(step){const slug = step.getAttribute('data-topic-slug');if (progressMap[slug]){completedCount++}});const countBadge = mod.querySelector('.module-count-badge');if (countBadge){countBadge.textContent = completedCount+'/'+total;if (completedCount === total && total>0){countBadge.className = 'badge is-completed module-count-badge'}else if (completedCount>0){countBadge.className = 'badge has-progress module-count-badge'}else{countBadge.className = 'badge module-count-badge'}}const ratio = total>0 ? (completedCount / total) * 100:0;const formattedPercent = (ratio === 0) ? '0%':(Number.isInteger(ratio) ? ratio+'%':ratio.toFixed(2)+'%');const percentEl = mod.querySelector('.module-percent-text');if (percentEl){percentEl.textContent = formattedPercent}const svgGauge = mod.querySelector('.module-svg-gauge');if (svgGauge && total>0){svgGauge.setAttribute('stroke-dasharray',ratio.toFixed(2)+',100')}})}// Toggle button handler const toggleBtn = document.getElementById('btn-toggle-complete');if (toggleBtn && currentTopicSlug){toggleBtn.addEventListener('click',function(){if (progressMap[currentTopicSlug]){delete progressMap[currentTopicSlug]}else{progressMap[currentTopicSlug] = true}saveHtmlProgressMap(progressMap);updateAllProgressUI()})}// Auto expand active module container in sidebar if (currentTopicSlug){const activeItem = document.querySelector(`.step-item[data-topic-slug="${currentTopicSlug}"]`);if (activeItem){const moduleContainer = activeItem.closest('.sidebar-module-item');if (moduleContainer){const collapseDiv = moduleContainer.querySelector('.collapse');const toggleBtn = moduleContainer.querySelector('.module-toggle-btn');const chevronIcon = moduleContainer.querySelector('.module-chevron-icon');if (collapseDiv && !collapseDiv.classList.contains('show')){collapseDiv.classList.add('show')}if (toggleBtn){toggleBtn.classList.remove('collapsed');toggleBtn.setAttribute('aria-expanded','true')}if (chevronIcon){chevronIcon.className = 'fas fa-chevron-down text-muted module-chevron-icon'}}}}// Attach Bootstrap collapse show/hide listeners for dynamic chevron icon toggling document.querySelectorAll('.sidebar-module-item').forEach(function(mod){const collapseDiv = mod.querySelector('.collapse');const chevronIcon = mod.querySelector('.module-chevron-icon');if (collapseDiv && chevronIcon){collapseDiv.addEventListener('show.bs.collapse',function(){chevronIcon.className = 'fas fa-chevron-down text-muted module-chevron-icon'});collapseDiv.addEventListener('hide.bs.collapse',function(){chevronIcon.className = 'fas fa-chevron-right text-muted module-chevron-icon'})}});// Run initial UI update updateAllProgressUI();// Listen to storage events from other tabs window.addEventListener('storage',function(e){if (e.key === 'html_tutorial_progress' || e.key === 'html_completed_topics' || e.key === 'completed_topics_html'){progressMap = getHtmlProgressMap();updateAllProgressUI()}});window.addEventListener('pageshow',function(){progressMap = getHtmlProgressMap();updateAllProgressUI()})}if (document.readyState === 'loading'){document.addEventListener('DOMContentLoaded',initHtmlTutorialApp)}else{initHtmlTutorialApp()}</script></main><!-- Footer --><!-- SEO-Optimized Footer Component --><footer class="main-footer" itemscope itemtype="https://schema.org/WPFooter"><div class="container"><div class="row"><!-- Brand Section --><div class="col-lg-4 col-md-4 mb-4" itemscope itemtype="https://schema.org/Organization"><p class="brand-title" itemprop="name"><i class="fas fa-tools me-2" aria-hidden="true"></i>OperateTools </p><p itemprop="description" class="brand-description">Free,powerful and efficient tools. Explore our suite of 300+professional web tools to help you convert,compress,analyze,and edit files online for free. </p><!-- Contact Links --><div class="footer-contact mb-3" style="font-size:0.875rem;"><p class="mb-1"><i class="fas fa-envelope me-2" style="color:#60a5fa;width:16px"></i><a href="mailto:support@operatetools.com" style="color:#94a3b8;text-decoration:none;"><span itemprop="email">support@operatetools.com</span></a></p><p class="mb-0"><i class="fas fa-phone me-2" style="color:#60a5fa;width:16px"></i><a href="tel:+919434668218" itemprop="telephone" style="color:#94a3b8;text-decoration:none;">+91 94346 68218</a></p></div><!-- Social Media Links --><div class="social-links mt-3" role="navigation" aria-label="Social media links"><a href="https://www.facebook.com/operatetools" aria-label="Facebook" itemprop="sameAs" rel="noopener noreferrer" target="_blank"><i class="fab fa-facebook-f" aria-hidden="true"></i></a><a href="https://twitter.com/operatetools" aria-label="Twitter" itemprop="sameAs" rel="noopener noreferrer" target="_blank"><i class="fab fa-twitter" aria-hidden="true"></i></a><a href="https://www.linkedin.com/company/operatetools" aria-label="LinkedIn" itemprop="sameAs" rel="noopener noreferrer" target="_blank"><i class="fab fa-linkedin-in" aria-hidden="true"></i></a><a href="https://www.instagram.com/operatetools" aria-label="Instagram" itemprop="sameAs" rel="noopener noreferrer" target="_blank"><i class="fab fa-instagram" aria-hidden="true"></i></a></div></div><!-- Image Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/image-tools" class="footer-heading-link">Image Tools</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/image-compressor">Image Compressor</a></li><li><a href="https://www.operatetools.com/tools/image-resizer">Image Resizer</a></li><li><a href="https://www.operatetools.com/image/resize-image-for-whatsapp-dp">Resize for WhatsApp DP</a></li><li><a href="https://www.operatetools.com/tools/crop-round-image">Crop Round Image</a></li><li><a href="https://www.operatetools.com/tools/convert-image">Universal Converter</a></li><li><a href="https://www.operatetools.com/png-to-jpg">PNG to JPG</a></li><li><a href="https://www.operatetools.com/image-tools"><strong>View all Image Tools →</strong></a></li></ul></div><!-- Calculator Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/calculator-tools" class="footer-heading-link">Calculators</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/currency-converter">Currency Converter</a></li><li><a href="https://www.operatetools.com/cost-of-living">Cost of Living Index</a></li><li><a href="https://www.operatetools.com/salary-after-tax">Salary Calculator</a></li><li><a href="https://www.operatetools.com/tools/bmi-calculator">BMI Calculator</a></li><li><a href="https://www.operatetools.com/tools/emi-calculator">EMI Calculator</a></li><li><a href="https://www.operatetools.com/tools/percentage-calculator">Percentage Calculator</a></li><li><a href="https://www.operatetools.com/calculator-tools"><strong>View all Calculators →</strong></a></li></ul></div><!-- Text Analysis Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/text-analysis-tools" class="footer-heading-link">Text Tools</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/plagiarism-checker">Plagiarism Checker</a></li><li><a href="https://www.operatetools.com/tools/word-counter">Word Counter</a></li><li><a href="https://www.operatetools.com/tools/text-case-converter">Case Converter</a></li><li><a href="https://www.operatetools.com/text-analysis-tools"><strong>View all Text Tools →</strong></a></li></ul></div><!-- PDF Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/pdf-tools" class="footer-heading-link">PDF Tools</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/pdf-to-word">PDF to Word</a></li><li><a href="https://www.operatetools.com/tools/merge-pdf">Merge PDF</a></li><li><a href="https://www.operatetools.com/tools/split-pdf">Split PDF</a></li><li><a href="https://www.operatetools.com/tools/organize-pdf">Organize PDF</a></li><li><a href="https://www.operatetools.com/tools/crop-pdf">Crop PDF</a></li><li><a href="https://www.operatetools.com/tools/sign-pdf">Sign PDF Online</a></li><li><a href="https://www.operatetools.com/sample-files">Sample Files Download</a></li><li><a href="https://www.operatetools.com/pdf-tools"><strong>View all PDF Tools →</strong></a></li></ul></div></div><div class="row"><!-- Development Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/development-tools" class="footer-heading-link">Development</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/json-editor">JSON Editor</a></li><li><a href="https://www.operatetools.com/css-compressor">CSS Minifier</a></li><li><a href="https://www.operatetools.com/tools/javascript-minifier">JS Minifier</a></li><li><a href="https://www.operatetools.com/development-tools"><strong>View all Dev Tools →</strong></a></li></ul></div><!-- Unit Converters --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/unit-converter-tools" class="footer-heading-link">Converters</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/length-converter">Length Converter</a></li><li><a href="https://www.operatetools.com/tools/weight-converter">Weight Converter</a></li><li><a href="https://www.operatetools.com/tools/temperature-conversion">Temperature</a></li><li><a href="https://www.operatetools.com/unit-converter-tools"><strong>View all Converters →</strong></a></li></ul></div><!-- Code Editors --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading"><a href="https://www.operatetools.com/code-editor-tools" class="footer-heading-link">Code Editors</a></p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/javascript-projects">JavaScript Projects</a></li><li><a href="https://www.operatetools.com/tools/python-compiler">Python Compiler</a></li><li><a href="https://www.operatetools.com/tools/javascript-compiler-editor">JavaScript Editor</a></li><li><a href="https://www.operatetools.com/tools/c-compiler">C Compiler</a></li><li><a href="https://www.operatetools.com/code-editor-tools"><strong>View all Code Editors →</strong></a></li></ul></div><!-- SEO & Website Tools --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading">SEO Tools</p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/tools/backlink-checker">Backlink Checker</a></li><li><a href="https://www.operatetools.com/sitemap-generator">Sitemap Generator</a></li><li><a href="https://www.operatetools.com/tools/open-graph-checker">OG Tag Checker</a></li><li><a href="https://www.operatetools.com/backlink-tools"><strong>View all SEO Tools →</strong></a></li></ul></div><!-- Company Links --><div class="col-lg-2 col-md-2 mb-4"><p class="footer-heading">Company</p><ul class="footer-links list-unstyled"><li><a href="https://www.operatetools.com/about">About Us</a></li><li><a href="https://www.operatetools.com/contact">Contact Us</a></li><li><a href="https://www.operatetools.com/privacy-policy">Privacy Policy</a></li><li><a href="https://www.operatetools.com/terms-of-service">Terms of Service</a></li><li><a href="https://www.operatetools.com/tools">All Tools</a></li><li><a href="https://www.operatetools.com/sample-files">Sample Files</a></li><li><a href="https://www.operatetools.com/sitemap">Sitemap</a></li></ul></div></div></div><!-- Footer Bottom --><div class="footer-bottom"><hr class="footer-divider my-4"><div class="row align-items-center"><div class="col-md-6"><p class="copyright mb-0" itemprop="copyrightNotice">©2026 <span itemprop="name">OperateTools</span>. All rights reserved. </p></div><div class="col-md-6 text-md-end mt-2 mt-md-0"><div class="footer-legal-links small"><a href="https://www.operatetools.com/privacy-policy" class="text-secondary text-decoration-none me-3">Privacy Policy</a><a href="https://www.operatetools.com/terms-of-service" class="text-secondary text-decoration-none me-3">Terms of Service</a><a href="https://www.operatetools.com/contact" class="text-secondary text-decoration-none">Support</a></div></div></div></div></div></footer><!-- Footer Styles --><style>.main-footer{background:linear-gradient(180deg,#0d1520 0%,#0f1923 100%);color:#c9d3e0;padding:3rem 0 1rem;position:relative;overflow:hidden;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;border-top:2px solid #1e3a5f}.main-footer::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');opacity:0.3;z-index:1}.main-footer .container{position:relative;z-index:2;max-width:1200px;width:100%;margin:0 auto;padding:0 15px}.brand-title{font-size:1.3rem;font-weight:600;margin-bottom:1rem;color:#fff}.brand-description{font-size:0.95rem;line-height:1.6;color:#ecf0f1;margin-bottom:1.5rem}.main-footer .row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.main-footer .col-lg-4,.main-footer .col-lg-2,.main-footer .col-md-6{position:relative;width:100%;padding-right:15px;padding-left:15px}.footer-heading{color:#60a5fa;font-weight:600;font-size:1rem;margin-bottom:1rem;text-transform:uppercase;letter-spacing:0.5px}.footer-links{padding-left:0;list-style:none}.footer-links li{margin-bottom:0.5rem}.footer-heading a.footer-heading-link{color:#60a5fa;text-decoration:none;display:inline-block}.footer-heading a.footer-heading-link:hover{color:#93c5fd;text-decoration:underline}.footer-links a{color:#94a3b8;text-decoration:none;transition:all 0.2s ease;font-size:0.875rem;position:relative;display:inline-block;padding:2px 0}.footer-links a:hover{color:#60a5fa;padding-left:5px}.footer-links a::before{content:'→';position:absolute;left:-15px;opacity:0;transition:all 0.3s ease}.footer-links a:hover::before{opacity:1;left:-10px}.social-links a{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;background:rgba(255,255,255,0.1);border-radius:50%;color:#ecf0f1;text-decoration:none;transition:all 0.3s ease;border:1px solid rgba(255,255,255,0.2);margin-right:8px}.social-links a:hover{background:#2563eb;color:white;transform:translateY(-2px);box-shadow:0 5px 15px rgba(37,99,235,.4)}.footer-divider{border-color:rgba(255,255,255,0.08);margin:2rem 0;border-top:1px solid rgba(255,255,255,0.08)}.footer-bottom{padding-top:1.5rem;margin-top:2rem}.copyright{color:#95a5a6;font-size:0.9rem;margin-bottom:0}.tagline{color:#bdc3c7;font-size:0.9rem;font-style:italic;margin-bottom:0}.seo-text-section{background:rgba(0,0,0,0.2);border-radius:10px;padding:1.5rem;margin-top:2rem}.seo-heading{color:#3498db;font-size:1rem;font-weight:600;margin-bottom:1rem;text-transform:uppercase;letter-spacing:0.5px}.seo-text{color:#bdc3c7;font-size:0.85rem;line-height:1.6;margin-bottom:1rem;opacity:0.9}.seo-text:last-child{margin-bottom:0}@media (min-width:992px){.main-footer .col-lg-4{flex:0 0 33.333333%;max-width:33.333333%}.main-footer .col-lg-2{flex:0 0 16.666667%;max-width:16.666667%}}@media (min-width:768px){.main-footer .col-md-6{flex:0 0 50%;max-width:50%}.text-md-end{text-align:right !important}}@media (max-width:991px){.main-footer .mb-4{margin-bottom:1.5rem !important}}@media (max-width:768px){.main-footer{padding:2rem 0 1rem}.footer-heading{margin-top:1rem;margin-bottom:0.75rem;font-size:0.95rem}.social-links{margin-bottom:1.5rem}.footer-bottom .row>div{text-align:center !important;margin-bottom:0.5rem}.seo-text-section{padding:1rem}.footer-links a{font-size:0.875rem}}@media (max-width:576px){.main-footer{padding:1.5rem 0 1rem}.footer-heading{font-size:0.875rem}.footer-links a{font-size:0.75rem}.social-links a{width:32px;height:32px}.seo-text{font-size:0.75rem}}@media (prefers-reduced-motion:reduce){.footer-links a,.social-links a,.footer-links a::before{transition:none}}@media (prefers-contrast:high){.main-footer{background:#000;color:#fff}.footer-links a{color:#fff}.footer-links a:hover{color:#64b5f6}}@media print{.main-footer{background:#fff !important;color:#000 !important;border-top:1px solid #000}.social-links,.seo-text-section{display:none}}</style><div id="cookieConsentBanner" class="position-fixed bottom-0 start-0 end-0 p-3 z-3 d-none" style="z-index: 1080;"><div class="container"><div class="card border-0 shadow-lg bg-dark text-white rounded-4 overflow-hidden" style="background: rgba(15, 23, 42, 0.96) !important; backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.12) !important;"><div class="card-body p-3 p-md-4"><div class="row align-items-center g-3"><div class="col-lg-8"><div class="d-flex align-items-start gap-3"><div class="p-2.5 rounded-circle bg-primary bg-opacity-25 text-primary flex-shrink-0 d-none d-sm-flex align-items-center justify-content-center" style="width: 44px; height: 44px;"><i class="fas fa-cookie-bite fs-5"></i></div><div><div class="h6 fw-bold text-white mb-1"> We value your privacy & transparency </div><p class="text-white-50 small mb-0 lh-base"> OperateTools uses cookies and local storage to ensure basic tool functionality, preserve preferences, and analyze aggregated traffic. We never sell your personal data or index your document files. Learn more in our <a href="https://www.operatetools.com/privacy-policy" class="text-primary text-decoration-underline">Privacy Policy</a>. </p></div></div></div><div class="col-lg-4 text-lg-end"><div class="d-flex flex-wrap align-items-center justify-content-lg-end gap-2"><button type="button" id="btnRejectCookies" class="btn btn-outline-light btn-sm px-3 rounded-pill fw-semibold"> Essential Only </button><button type="button" id="btnAcceptCookies" class="btn btn-primary btn-sm px-4 rounded-pill fw-bold shadow-sm"> Accept All </button></div></div></div></div></div></div></div><script> (function() { var consentKey = 'operatetools_cookie_consent'; function initCookieConsent() { var banner = document.getElementById('cookieConsentBanner'); if (!banner) return; var consent = localStorage.getItem(consentKey); if (!consent) { setTimeout(function() { banner.classList.remove('d-none'); }, 1200); } var btnAccept = document.getElementById('btnAcceptCookies'); var btnReject = document.getElementById('btnRejectCookies'); if (btnAccept) { btnAccept.addEventListener('click', function() { localStorage.setItem(consentKey, 'all'); banner.classList.add('d-none'); }); } if (btnReject) { btnReject.addEventListener('click', function() { localStorage.setItem(consentKey, 'essential'); banner.classList.add('d-none'); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCookieConsent); } else { initCookieConsent(); } })(); </script><script src="https://www.operatetools.com/js/jquery-3.6.0.min.js" defer></script><script> (function() { var loadedBootstrapJs = false; var triggerEvents = ['mousemove', 'touchstart', 'pointerdown']; function loadBootstrapJs() { if (loadedBootstrapJs) return; loadedBootstrapJs = true; var script = document.createElement('script'); script.src = "https://www.operatetools.com/js/bootstrap.bundle.min.js"; script.crossOrigin = 'anonymous'; document.head.appendChild(script); // Clean up listeners triggerEvents.forEach(function(evt) { document.removeEventListener(evt, loadBootstrapJs); }); } triggerEvents.forEach(function(evt) { document.addEventListener(evt, loadBootstrapJs, { once: true, passive: true }); }); })(); </script><script> document.addEventListener('DOMContentLoaded', function() { // Ensure dropdowns are accessible via keyboard document.querySelectorAll('.dropdown-toggle').forEach(function(toggle) { toggle.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); } }); }); }); </script><script> // Monitor Core Web Vitals function measureCoreWebVitals() { if ('PerformanceObserver' in window) { // Largest Contentful Paint (LCP) new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { console.log('LCP:', entry.startTime); } }).observe({ entryTypes: ['largest-contentful-paint'] }); // Interaction to Next Paint (INP - Google Core Web Vital replacing FID) try { new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (entry.interactionId) { console.log('INP:', entry.duration); } } }).observe({ type: 'event', buffered: true, durationThreshold: 16 }); } catch (e) { try { new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { console.log('FID:', entry.processingStart - entry.startTime); } }).observe({ type: 'first-input', buffered: true }); } catch (e2) {} } // Cumulative Layout Shift (CLS) let clsValue = 0; new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; console.log('CLS:', clsValue); } } }).observe({ entryTypes: ['layout-shift'] }); } } // Initialize performance monitoring window.addEventListener('load', measureCoreWebVitals); // Optimize scroll performance let ticking = false; function updateScrollPosition() { ticking = false; } window.addEventListener('scroll', function() { if (!ticking) { requestAnimationFrame(updateScrollPosition); ticking = true; } }); // Accessibility & Mobile Navbar Toggle Handler (#navbarNav) document.addEventListener('DOMContentLoaded', function() { const navbarToggler = document.querySelector('.navbar-toggler'); const navbarCollapse = document.querySelector('.navbar-collapse'); if (navbarToggler && navbarCollapse) { // Update aria-expanded when collapse state changes navbarCollapse.addEventListener('shown.bs.collapse', function() { navbarToggler.setAttribute('aria-expanded', 'true'); }); navbarCollapse.addEventListener('hidden.bs.collapse', function() { navbarToggler.setAttribute('aria-expanded', 'false'); }); // Dedicated toggle handler for mobile navbar navbarToggler.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); if (window.bootstrap && typeof window.bootstrap.Collapse === 'function') { var bsCollapse = window.bootstrap.Collapse.getInstance(navbarCollapse); if (!bsCollapse) { bsCollapse = new window.bootstrap.Collapse(navbarCollapse, { toggle: false }); } bsCollapse.toggle(); } else { var isShown = navbarCollapse.classList.contains('show'); if (isShown) { navbarCollapse.classList.remove('show'); navbarToggler.setAttribute('aria-expanded', 'false'); } else { navbarCollapse.classList.add('show'); navbarToggler.setAttribute('aria-expanded', 'true'); } } }); // Auto-close mobile navbar when clicking navigation links document.querySelectorAll('#navbarNav .nav-link:not(.dropdown-toggle):not(.mega-toggle):not(.mega-dropdown > .nav-link), #navbarNav .mega-menu a').forEach(function(link) { link.addEventListener('click', function() { if (window.innerWidth < 992 && navbarCollapse.classList.contains('show')) { if (window.bootstrap && typeof window.bootstrap.Collapse === 'function') { var bsCollapse = window.bootstrap.Collapse.getInstance(navbarCollapse); if (bsCollapse) bsCollapse.hide(); } else { navbarCollapse.classList.remove('show'); navbarToggler.setAttribute('aria-expanded', 'false'); } } }); }); } // Universal dropdown fallback (handles nav-profile-btn & data-bs-toggle="dropdown") document.addEventListener('click', function(e) { var toggle = e.target.closest('[data-bs-toggle="dropdown"], .nav-profile-btn'); if (toggle) { var parent = toggle.closest('.dropdown') || toggle.parentElement; if (parent) { var menu = parent.querySelector('.dropdown-menu'); if (menu) { var hasBootstrapDropdown = window.bootstrap && typeof window.bootstrap.Dropdown === 'function'; if (!hasBootstrapDropdown) { e.preventDefault(); e.stopPropagation(); var isOpen = menu.classList.contains('show'); document.querySelectorAll('.dropdown-menu.show').forEach(function(m) { m.classList.remove('show'); m.style.display = ''; }); if (!isOpen) { menu.classList.add('show'); menu.style.display = 'block'; } } } } } else if (!e.target.closest('.dropdown-menu')) { document.querySelectorAll('.dropdown-menu.show').forEach(function(m) { m.classList.remove('show'); m.style.display = ''; }); } }); // Initialize Bootstrap dropdowns explicitly (in case auto-init fails) try { if (window.bootstrap && typeof window.bootstrap.Dropdown === 'function') { document.querySelectorAll('.dropdown-toggle:not(.mega-dropdown > .nav-link), .nav-profile-btn').forEach(function(el) { new window.bootstrap.Dropdown(el); }); } } catch (e) { console.warn('Dropdown init warning:', e); } // Robust Accordion & Collapse Toggle Handler (fallback if Bootstrap JS is not active) document.addEventListener('click', function(ev) { const trigger = ev.target.closest('[data-bs-toggle="collapse"]'); if (!trigger) return; const targetSelector = trigger.getAttribute('data-bs-target') || trigger.getAttribute('href'); if (!targetSelector || targetSelector === '#') return; const targetEl = document.querySelector(targetSelector); if (!targetEl) return; // If Bootstrap JS is active, let Bootstrap handle collapse natively if (window.bootstrap && typeof window.bootstrap.Collapse === 'function') { return; } ev.preventDefault(); const isCurrentlyShown = targetEl.classList.contains('show'); const parentSelector = targetEl.getAttribute('data-bs-parent') || trigger.getAttribute('data-bs-parent'); // Handle accordion group closing siblings if (parentSelector && !isCurrentlyShown) { const parentEl = document.querySelector(parentSelector); if (parentEl) { parentEl.querySelectorAll('.accordion-collapse.show, .collapse.show').forEach(function(openEl) { if (openEl !== targetEl) { openEl.classList.remove('show'); const openId = openEl.getAttribute('id'); if (openId) { parentEl.querySelectorAll('[data-bs-target="#' + openId + '"]').forEach(function(btn) { btn.classList.add('collapsed'); btn.setAttribute('aria-expanded', 'false'); }); } } }); } } // Toggle target collapse state & button collapsed class if (isCurrentlyShown) { targetEl.classList.remove('show'); trigger.classList.add('collapsed'); trigger.setAttribute('aria-expanded', 'false'); } else { targetEl.classList.add('show'); trigger.classList.remove('collapsed'); trigger.setAttribute('aria-expanded', 'true'); } }); }); </script><div id="pushOptinToast" style="display: none; position: fixed; bottom: 24px; left: 24px; z-index: 100049 !important; width: 360px; max-width: calc(100vw - 32px); background: #ffffff; border-radius: 16px; box-shadow: 0 16px 36px -4px rgba(15, 23, 42, 0.16), 0 4px 12px rgba(0, 0, 0, 0.06); border: 1px solid rgba(226, 232, 240, 0.9); overflow: hidden; animation: newsHubSlideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;"><div style="background: linear-gradient(135deg, #1a73e8 0%, #0d5bbd 100%); color: #ffffff; padding: 9px 14px; display: flex; align-items: center; justify-content: space-between;"><div style="display: flex; align-items: center; gap: 8px; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase;"><i class="fas fa-bell text-warning" style="color: #fbbf24 !important;"></i><span>Stay Updated</span></div><button onclick="dismissPushOptinToast()" type="button" aria-label="Close" style="background: rgba(255, 255, 255, 0.18); border: none; color: #ffffff; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.72rem; cursor: pointer; transition: background 0.2s ease;"><i class="fas fa-times"></i></button></div><div style="padding: 16px;"><div style="display: flex; align-items: flex-start; gap: 14px;"><div style="position: relative; width: 44px; height: 44px; border-radius: 12px; background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border: 1px solid #fde68a; display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 4px 10px rgba(245, 158, 11, 0.15);"><i class="fas fa-bell text-warning fs-4" style="color: #f59e0b !important; filter: drop-shadow(0 2px 4px rgba(245, 158, 11, 0.3));"></i></div><div style="flex-grow: 1;"><div style="font-size: 0.94rem; font-weight: 700; color: #0f172a; margin-bottom: 3px; line-height: 1.25;"> Enable Notifications? </div><div style="font-size: 0.78rem; color: #64748b; line-height: 1.4; margin-bottom: 14px;"> Get real-time updates, instant tool alerts & daily news directly on your device. </div><div style="display: flex; align-items: center; gap: 8px;"><button onclick="requestFCMNotificationPermission()" type="button" style="flex: 1; background: linear-gradient(135deg, #1a73e8 0%, #1557b0 100%); color: #ffffff; border: none; border-radius: 8px; padding: 8px 12px; font-size: 0.78rem; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(26, 115, 232, 0.35); transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 6px;"><i class="fas fa-check-circle"></i><span>Allow Notifications</span></button><button onclick="dismissPushOptinToast()" type="button" style="background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px 12px; font-size: 0.78rem; font-weight: 600; cursor: pointer; transition: background 0.2s ease;"> Later </button></div></div></div></div></div><div id="floatingPushWidgetContainer" style="position: fixed; bottom: 20px; right: 20px; z-index: 100050 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;"><button id="floatingBellBtn" onclick="togglePushNewsHub()" type="button" aria-label="Notifications" style="width: 52px; height: 52px; border-radius: 50%; background: linear-gradient(135deg, #1a73e8 0%, #0056b3 100%); border: none; color: #ffffff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 18px rgba(0, 123, 255, 0.45); cursor: pointer; transition: all 0.25s ease; position: relative;"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg><span id="floatingBellBadge" style="display: none; position: absolute; top: -2px; right: -2px; background: #dc2626; color: #ffffff; font-size: 0.68rem; font-weight: 700; padding: 2px 6px; border-radius: 10px; border: 2px solid #ffffff; line-height: 1;">0</span></button><div id="newsHubPopupCard" style="display: none; position: absolute; bottom: 64px; right: 0; width: 350px; max-height: 480px; background: #ffffff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.08); border: 1px solid #e2e8f0; overflow: hidden; animation: newsHubSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);"><div style="background: #1a73e8; color: #ffffff; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between;"><div style="font-size: 1rem; font-weight: 700; letter-spacing: 0.2px;">News Hub</div><button onclick="togglePushNewsHub()" type="button" aria-label="Close" style="background: transparent; border: none; color: #ffffff; font-size: 1.2rem; cursor: pointer; opacity: 0.9; line-height: 1; padding: 0 4px;"><i class="fas fa-times"></i></button></div><div id="fcmPermissionBanner" style="display: none; background: #eff6ff; border-bottom: 1px solid #dbeafe; padding: 10px 14px; align-items: center; justify-content: space-between; gap: 8px;"><div style="font-size: 0.78rem; color: #1e40af; font-weight: 500;"> Get real-time updates & instant tool alerts! </div><button onclick="requestFCMNotificationPermission()" type="button" style="background: #1a73e8; color: #fff; border: none; border-radius: 6px; padding: 4px 10px; font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap;"> Enable </button></div><div id="newsHubContentList" style="max-height: 410px; overflow-y: auto; background: #ffffff;"><div style="padding: 30px 20px; text-align: center; color: #64748b;"><i class="fas fa-spinner fa-spin" style="font-size: 1.5rem; color: #1a73e8; margin-bottom: 8px;"></i><div style="font-size: 0.8rem;">Loading notifications...</div></div></div></div></div><style>@keyframes newsHubSlideUp{from{opacity:0;transform:translateY(12px) scale(0.96)}to{opacity:1;transform:translateY(0) scale(1)}}#floatingBellBtn:hover{transform:scale(1.08);box-shadow:0 6px 22px rgba(0,123,255,0.6)}.news-hub-item-wrapper{border-bottom:1px solid #f1f5f9;transition:background 0.18s ease}.news-hub-item-wrapper:hover{background:#f8fafc}.news-hub-item{display:flex;align-items:flex-start;gap:12px;padding:12px 14px 6px 14px;text-decoration:none;color:#1e293b}.news-hub-img{width:48px;height:48px;border-radius:10px;object-fit:cover;flex-shrink:0;background:#f1f5f9;border:1px solid #e2e8f0}.news-hub-img-fallback{width:48px;height:48px;border-radius:10px;flex-shrink:0;background:linear-gradient(135deg,#fffbeb 0%,#fef3c7 100%);border:1px solid #fde68a;display:flex;align-items:center;justify-content:center;font-size:1.25rem}.news-hub-title{font-size:0.84rem;font-weight:700;color:#1e293b;line-height:1.35;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.news-hub-time{font-size:0.72rem;color:#64748b}.news-hub-share-bar{display:inline-flex;align-items:center;gap:5px}.news-hub-share-btn{width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#ffffff !important;text-decoration:none;transition:transform 0.15s ease,filter 0.15s ease,background 0.15s ease;border:none;cursor:pointer;padding:0;outline:none}.news-hub-share-btn:hover{transform:scale(1.18);filter:brightness(1.1)}.news-hub-share-btn.btn-whatsapp{background:#25D366}.news-hub-share-btn.btn-telegram{background:#0088cc}.news-hub-share-btn.btn-twitter{background:#0f172a}.news-hub-share-btn.btn-facebook{background:#1877F2}.news-hub-share-btn.btn-copy{background:#64748b}</style><script> // Firebase Cloud Messaging Configuration const firebaseConfig = { apiKey: "AIzaSyBrBrPT3THe36gyZcScE6AZ-Hil8vMi8o4", authDomain: "operatetools-cf96b.firebaseapp.com", projectId: "operatetools-cf96b", storageBucket: "operatetools-cf96b.firebasestorage.app", messagingSenderId: "924761704144", appId: "1:924761704144:web:50008178501a86259700d5", measurementId: "G-NF8Z3LBE87" }; let fcmMessaging = null; let swRegistration = null; let firebaseLoadingPromise = null; function loadScriptAsync(src) { return new Promise(function(resolve, reject) { var existing = document.querySelector('script[src="' + src + '"]'); if (existing) { if (existing.getAttribute('data-loaded') === 'true' || typeof firebase !== 'undefined') { return resolve(); } existing.addEventListener('load', resolve); existing.addEventListener('error', reject); return; } var s = document.createElement('script'); s.src = src; s.async = true; s.onload = function() { s.setAttribute('data-loaded', 'true'); resolve(); }; s.onerror = reject; document.head.appendChild(s); }); } let swRegistrationPromise = null; function getServiceWorkerRegistration() { if (!('serviceWorker' in navigator)) return Promise.resolve(null); if (swRegistrationPromise) return swRegistrationPromise; swRegistrationPromise = navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/' }) .then(function(reg) { return navigator.serviceWorker.ready.then(function(readyReg) { swRegistration = readyReg || reg; return swRegistration; }); }) .catch(function(err) { console.warn('Service Worker registration error:', err); return null; }); return swRegistrationPromise; } function ensureFirebaseLoaded() { if (typeof firebase !== 'undefined' && typeof firebase.messaging === 'function') { return Promise.resolve(); } if (!firebaseLoadingPromise) { firebaseLoadingPromise = loadScriptAsync('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js') .then(function() { return loadScriptAsync('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js'); }); } return firebaseLoadingPromise; } function initFCM() { if (!('Notification' in window) || !('serviceWorker' in navigator)) return; // Eagerly initiate Service Worker registration getServiceWorkerRegistration(); ensureFirebaseLoaded() .then(function() { if (typeof firebase !== 'undefined' && typeof firebase.messaging === 'function') { if (!firebase.apps.length) { firebase.initializeApp(firebaseConfig); } fcmMessaging = firebase.messaging(); // Foreground Message Handler (auto Open tracking & refresh News Hub) fcmMessaging.onMessage(function(payload) { console.log('Foreground Push Message:', payload); const notificationId = payload.data?.notification_id || payload.notification_id || payload.data?.id; if (notificationId) { fetch('/push/track/open/' + notificationId, { method: 'GET', cache: 'no-cache' }).catch(() => {}); } const title = payload.notification?.title || payload.data?.title || 'OperateTools Notification'; const targetUrl = payload.data?.target_url || '/'; const trackUrl = notificationId ? ('/push/track/click/' + notificationId) : targetUrl; const options = { body: (payload.notification?.body || payload.data?.message || payload.data?.body || '').toString(), icon: payload.notification?.icon || payload.data?.icon_url || (window.location.origin + '/favicon.ico'), badge: window.location.origin + '/favicon.ico', data: { url: trackUrl, target_url: targetUrl, notification_id: notificationId }, requireInteraction: true }; const imgUrl = payload.notification?.image || payload.data?.image_url; if (imgUrl && typeof imgUrl === 'string' && imgUrl.trim() !== '') { options.image = imgUrl.trim(); } if ('Notification' in window && Notification.permission === 'granted') { if (swRegistration && typeof swRegistration.showNotification === 'function') { swRegistration.showNotification(title, options); } else if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(function(reg) { reg.showNotification(title, options); }).catch(function() { try { new Notification(title, options); } catch(e) {} }); } else { try { new Notification(title, options); } catch(e) {} } } loadFloatingPushNotifications(); }); if (Notification.permission === 'granted') { obtainAndRegisterFCMToken(); } else if (Notification.permission === 'default') { showPushOptinUi(); } } }) .catch(function(err) { console.warn('Firebase lazy initialization notice:', err); if ('Notification' in window && Notification.permission === 'default') { showPushOptinUi(); } }); } // Run non-blocking initialization after document load & idle if (document.readyState === 'complete') { setTimeout(initFCM, 300); } else { window.addEventListener('load', function() { if ('requestIdleCallback' in window) { requestIdleCallback(initFCM, { timeout: 3000 }); } else { setTimeout(initFCM, 300); } }); } function showPushOptinUi() { const banner = document.getElementById('fcmPermissionBanner'); if (banner) banner.style.display = 'flex'; var dismissedUntil = localStorage.getItem('push_optin_dismissed_until'); if (dismissedUntil && parseInt(dismissedUntil, 10) > Date.now()) { return; } if (!sessionStorage.getItem('push_optin_dismissed')) { setTimeout(function() { const toast = document.getElementById('pushOptinToast'); if (toast && Notification.permission === 'default') { toast.style.display = 'block'; } }, 6000); } } function dismissPushOptinToast() { const toast = document.getElementById('pushOptinToast'); if (toast) toast.style.display = 'none'; sessionStorage.setItem('push_optin_dismissed', '1'); // Suppress for 14 days in localStorage localStorage.setItem('push_optin_dismissed_until', (Date.now() + 14 * 86400000).toString()); } function obtainAndRegisterFCMToken() { if (!('Notification' in window) || Notification.permission !== 'granted') return; if (!('serviceWorker' in navigator)) return; Promise.all([ensureFirebaseLoaded(), getServiceWorkerRegistration()]) .then(function(results) { const reg = results[1] || swRegistration; if (!reg) { console.warn('Service Worker is not ready yet for FCM token registration'); return; } if (typeof firebase !== 'undefined' && typeof firebase.messaging === 'function') { if (!firebase.apps.length) { firebase.initializeApp(firebaseConfig); } fcmMessaging = firebase.messaging(); const vapidKey = "BCV5frZsyui7oAJReUqiqHEj3Y3wQ1a0HF7an-eWn1hiEbWEDpEsA7WAuJRj0GyYqMD1uqXgf8vyGSiD7hUy12A"; const tokenParams = { serviceWorkerRegistration: reg }; if (vapidKey && vapidKey.trim() !== '') { tokenParams.vapidKey = vapidKey.trim(); } fcmMessaging.getToken(tokenParams) .then(function(currentToken) { if (currentToken) { console.log('FCM Device Token obtained successfully'); sendTokenToServer(currentToken); } }) .catch(function(err) { console.warn('Unable to get FCM token:', err); }); } }) .catch(function(err) { console.warn('Failed to obtain FCM token:', err); }); } // User-triggered event handler for permission request function requestFCMNotificationPermission() { if (!('Notification' in window)) { alert('This browser does not support desktop push notifications.'); return; } Notification.requestPermission().then(function(permission) { const banner = document.getElementById('fcmPermissionBanner'); if (banner) banner.style.display = 'none'; const toast = document.getElementById('pushOptinToast'); if (toast) toast.style.display = 'none'; if (permission === 'granted') { obtainAndRegisterFCMToken(); } }).catch(function(err) { console.warn('Notification permission error:', err); }); } function sendTokenToServer(token) { let payload = { token: token, subscribe_url: window.location.href, subscribe_page_title: document.title }; try { payload.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch(e) {} fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(payload) }) .then(res => res.json()) .then(data => { if (data.success) { localStorage.setItem('fcm_device_token', token); } }) .catch(err => { console.warn('Error saving FCM token:', err); }); } // Floating News Hub Popup Logic function togglePushNewsHub() { const popup = document.getElementById('newsHubPopupCard'); if (!popup) return; if (popup.style.display === 'none' || popup.style.display === '') { popup.style.display = 'block'; loadFloatingPushNotifications(); if (Notification.permission === 'default') { const banner = document.getElementById('fcmPermissionBanner'); if (banner) banner.style.display = 'flex'; } } else { popup.style.display = 'none'; } } let currentPushAlert = null; function getShareButtonsHtml(title, targetUrl) { const safeTitle = (title || '').replace(/'/g, "\\'").replace(/"/g, '"'); const safeUrl = (targetUrl || '').replace(/'/g, "\\'").replace(/"/g, '"'); return ` <div class="news-hub-share-bar" onclick="event.stopPropagation();"> <button type="button" class="news-hub-share-btn btn-whatsapp" title="Share on WhatsApp" onclick="sharePushItem(event, 'whatsapp', '${safeTitle}', '${safeUrl}', this)"> <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg> </button> <button type="button" class="news-hub-share-btn btn-telegram" title="Share on Telegram" onclick="sharePushItem(event, 'telegram', '${safeTitle}', '${safeUrl}', this)"> <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg> </button> <button type="button" class="news-hub-share-btn btn-twitter" title="Share on X / Twitter" onclick="sharePushItem(event, 'twitter', '${safeTitle}', '${safeUrl}', this)"> <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> </button> <button type="button" class="news-hub-share-btn btn-facebook" title="Share on Facebook" onclick="sharePushItem(event, 'facebook', '${safeTitle}', '${safeUrl}', this)"> <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 8H6v4h3v12h5V12h3.642L18 8h-4V6.333C14 5.374 14.5 5 15.688 5H18V0h-3.808C10.595 0 9 1.582 9 4.615V8z"/></svg> </button> <button type="button" class="news-hub-share-btn btn-copy" title="Copy Link" onclick="sharePushItem(event, 'copy', '${safeTitle}', '${safeUrl}', this)"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> </button> </div> `; } function sharePushItem(e, platform, title, targetUrl, btnEl) { if (e) { e.preventDefault(); e.stopPropagation(); } let absoluteUrl = targetUrl || '/'; try { if (!absoluteUrl.startsWith('http://') && !absoluteUrl.startsWith('https://')) { absoluteUrl = new URL(targetUrl, window.location.origin).href; } } catch(err) { absoluteUrl = window.location.origin + (targetUrl.startsWith('/') ? '' : '/') + targetUrl; } const shareTitle = title || document.title || 'Check this out'; const encUrl = encodeURIComponent(absoluteUrl); const encText = encodeURIComponent(shareTitle + ' ' + absoluteUrl); const encTitleOnly = encodeURIComponent(shareTitle); let shareUrl = ''; switch (platform) { case 'whatsapp': shareUrl = 'https://api.whatsapp.com/send?text=' + encText; break; case 'telegram': shareUrl = 'https://t.me/share/url?url=' + encUrl + '&text=' + encTitleOnly; break; case 'twitter': case 'x': shareUrl = 'https://twitter.com/intent/tweet?text=' + encTitleOnly + '&url=' + encUrl; break; case 'facebook': shareUrl = 'https://www.facebook.com/sharer/sharer.php?u=' + encUrl; break; case 'copy': if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(absoluteUrl).then(() => { showCopiedFeedback(btnEl); }).catch(() => { fallbackCopy(absoluteUrl, btnEl); }); } else { fallbackCopy(absoluteUrl, btnEl); } return; } if (shareUrl) { const width = 600, height = 500; const left = Math.max(0, (window.innerWidth - width) / 2); const top = Math.max(0, (window.innerHeight - height) / 2); window.open(shareUrl, '_blank', `width=${width},height=${height},top=${top},left=${left},toolbar=0,menubar=0,location=0,status=0,scrollbars=1,resizable=1`); } } function showCopiedFeedback(btnEl) { if (!btnEl) return; const origHtml = btnEl.innerHTML; const origBg = btnEl.style.background; btnEl.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>'; btnEl.title = 'Link Copied!'; btnEl.style.background = '#10b981'; setTimeout(() => { btnEl.innerHTML = origHtml; btnEl.title = 'Copy Link'; btnEl.style.background = origBg; }, 2000); } function fallbackCopy(text, btnEl) { try { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand('copy'); textArea.remove(); showCopiedFeedback(btnEl); } catch (e) { prompt('Copy this link:', text); } } function getClickedPushIds() { try { const stored = localStorage.getItem('clicked_push_notification_ids'); return stored ? JSON.parse(stored) : []; } catch(e) { return []; } } function markPushNotificationClicked(id) { if (!id) return; try { const ids = getClickedPushIds(); const idStr = String(id); if (!ids.includes(idStr)) { ids.push(idStr); localStorage.setItem('clicked_push_notification_ids', JSON.stringify(ids)); } } catch(e) {} } function handleNewsHubItemClick(event, id, url) { markPushNotificationClicked(id); const itemEl = document.getElementById('news_hub_item_' + id); if (itemEl) { itemEl.style.transition = 'all 0.2s ease'; itemEl.style.opacity = '0'; itemEl.style.transform = 'translateX(20px)'; setTimeout(() => { if (itemEl) itemEl.remove(); const container = document.getElementById('newsHubContentList'); const remaining = container ? container.querySelectorAll('.news-hub-item-wrapper').length : 0; const badge = document.getElementById('floatingBellBadge'); if (badge) { if (remaining > 0) { badge.innerText = remaining; badge.style.display = 'inline-block'; } else { badge.style.display = 'none'; } } if (remaining === 0 && container) { container.innerHTML = ` <div style="padding: 40px 20px; text-align: center; color: #64748b;"> <i class="fas fa-bell-slash" style="font-size: 2rem; color: #cbd5e1; margin-bottom: 10px; display: block;"></i> <div style="font-weight: 600; font-size: 0.88rem; color: #334155;">No New Notifications</div> <div style="font-size: 0.75rem; color: #94a3b8; margin-top: 4px;">You are all caught up! Check back later for fresh updates.</div> </div> `; } }, 200); } } function loadFloatingPushNotifications() { const container = document.getElementById('newsHubContentList'); const badge = document.getElementById('floatingBellBadge'); if (!container) return; fetch('/api/notifications/recent') .then(res => res.json()) .then(data => { if (data.success && data.notifications && data.notifications.length > 0) { const clickedIds = getClickedPushIds(); const unreadNotifications = data.notifications.filter(item => !clickedIds.includes(String(item.id))); if (unreadNotifications.length > 0) { if (badge) { badge.innerText = unreadNotifications.length; badge.style.display = 'inline-block'; } let html = ''; unreadNotifications.forEach(item => { const trackUrl = item.id ? ('/push/track/click/' + item.id) : '#'; const rawTargetUrl = (item.target_url && String(item.target_url).trim().toLowerCase() !== 'null') ? item.target_url : ''; const rawItemUrl = (item.url && String(item.url).trim().toLowerCase() !== 'null') ? item.url : ''; const finalShareUrl = rawTargetUrl || rawItemUrl || trackUrl; const rawImgSrc = item.image_url || item.icon_url; const imgSrc = rawImgSrc ? String(rawImgSrc).trim() : ''; let dateFormatted = 'Recently'; if (item.created_at) { try { const d = new Date(item.created_at); dateFormatted = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } catch(e) {} } let imgElement = ''; if (imgSrc && imgSrc.toLowerCase() !== 'null' && imgSrc.toLowerCase() !== 'undefined' && imgSrc !== '/favicon.ico' && (imgSrc.startsWith('/') || imgSrc.startsWith('http') || imgSrc.startsWith('data:'))) { imgElement = `<img src="${imgSrc}" class="news-hub-img" alt="${item.title ? item.title.replace(/"/g, '"') : 'Notification'}" onerror="this.onerror=null; this.outerHTML='<div class=\\'news-hub-img-fallback\\'><i class=\\'fas fa-bell text-warning\\' style=\\'color:#f59e0b !important\\'></i></div>';">`; } else { imgElement = `<div class="news-hub-img-fallback"><i class="fas fa-bell text-warning" style="color:#f59e0b !important"></i></div>`; } const shareButtonsHtml = getShareButtonsHtml(item.title, finalShareUrl); html += ` <div class="news-hub-item-wrapper" id="news_hub_item_${item.id}"> <a href="${trackUrl}" class="news-hub-item" onclick="handleNewsHubItemClick(event, '${item.id}', '${trackUrl}')"> ${imgElement} <div style="flex-grow: 1; overflow: hidden;"> <div class="news-hub-title">${item.title}</div> <div class="news-hub-time">${dateFormatted}</div> </div> </a> <div style="display: flex; align-items: center; justify-content: space-between; padding: 0 14px 10px 74px;"> <span style="font-size: 0.68rem; color: #94a3b8; font-weight: 500;">Share link:</span> ${shareButtonsHtml} </div> </div> `; }); container.innerHTML = html; } else { if (badge) badge.style.display = 'none'; container.innerHTML = ` <div style="padding: 40px 20px; text-align: center; color: #64748b;"> <i class="fas fa-bell-slash" style="font-size: 2rem; color: #cbd5e1; margin-bottom: 10px; display: block;"></i> <div style="font-weight: 600; font-size: 0.88rem; color: #334155;">No New Notifications</div> <div style="font-size: 0.75rem; color: #94a3b8; margin-top: 4px;">You are all caught up! Check back later for fresh updates.</div> </div> `; } } else { if (badge) badge.style.display = 'none'; container.innerHTML = ` <div style="padding: 40px 20px; text-align: center; color: #64748b;"> <i class="fas fa-bell-slash" style="font-size: 2rem; color: #cbd5e1; margin-bottom: 10px; display: block;"></i> <div style="font-weight: 600; font-size: 0.88rem; color: #334155;">No Notifications Yet</div> <div style="font-size: 0.75rem; color: #94a3b8; margin-top: 4px;">Check back later for recent news and updates.</div> </div> `; } }) .catch(err => { console.warn('Error loading push notifications:', err); if (container) { container.innerHTML = ` <div style="padding: 30px 20px; text-align: center; color: #94a3b8; font-size: 0.78rem;"> Unable to load notifications at this moment. </div> `; } }); } // Auto-fetch badge count on page load document.addEventListener('DOMContentLoaded', function() { loadFloatingPushNotifications(); }); </script><script> (function() { window.hasUnsavedWork = false; window.setWorkInProgress = function() {}; })(); </script><script> // Show a Bootstrap modal by id. If Bootstrap JS hasn't loaded yet (it's lazy-loaded), // load the bundle on demand and then open the modal. function showAuthModalById(modalId) { var el = document.getElementById(modalId); if (!el) return; if (el.parentNode !== document.body) { document.body.appendChild(el); } function open() { if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { bootstrap.Modal.getOrCreateInstance(el).show(); } else if (window.jQuery && typeof window.jQuery.fn.modal === 'function') { window.jQuery(el).modal('show'); } } if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { open(); return; } // Bootstrap not ready yet: load the bundle, then open once available. var existing = document.getElementById('bootstrapBundleScript'); if (!existing) { var script = document.createElement('script'); script.id = 'bootstrapBundleScript'; script.src = "https://www.operatetools.com/js/bootstrap.bundle.min.js"; script.onload = open; document.head.appendChild(script); } else { // Script tag exists but may still be loading; poll briefly. var tries = 0; var timer = setInterval(function() { tries++; if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { clearInterval(timer); open(); } else if (tries > 40) { clearInterval(timer); } }, 50); } } // Open a login/register/etc auth modal from a header link, keeping the href as a // no-JS fallback (the anchor still points to the real page). function openAuthModal(e, modalId) { if (e) e.preventDefault(); showAuthModalById(modalId); return false; } function requireLogin(e) { e.preventDefault(); showAuthModalById('loginModal'); return false; return true; } function togglePassword(inputId, btn) { const input = document.getElementById(inputId); const icon = btn.querySelector('i'); if (input.type === 'password') { input.type = 'text'; icon.classList.replace('fa-eye', 'fa-eye-slash'); } else { input.type = 'password'; icon.classList.replace('fa-eye-slash', 'fa-eye'); } } document.addEventListener('DOMContentLoaded', function() { var megaDropdowns = document.querySelectorAll('.mega-dropdown'); var closeTimers = {}; function isDesktop() { return window.innerWidth >= 992; } function openMegaMenu(dropdown, idx) { if (closeTimers[idx]) { clearTimeout(closeTimers[idx]); closeTimers[idx] = null; } // Close other mega menus megaDropdowns.forEach(function(d, i) { if (i !== idx) { closeMegaMenu(d, i, true); } }); dropdown.classList.add('show'); var toggle = dropdown.querySelector('.dropdown-toggle') || dropdown.querySelector('.nav-link'); if (toggle) toggle.setAttribute('aria-expanded', 'true'); } function closeMegaMenu(dropdown, idx, immediate) { function doClose() { dropdown.classList.remove('show'); var toggle = dropdown.querySelector('.dropdown-toggle') || dropdown.querySelector('.nav-link'); if (toggle) toggle.setAttribute('aria-expanded', 'false'); } if (immediate) { if (closeTimers[idx]) { clearTimeout(closeTimers[idx]); closeTimers[idx] = null; } doClose(); } else { closeTimers[idx] = setTimeout(doClose, 180); } } megaDropdowns.forEach(function(dropdown, idx) { var toggle = dropdown.querySelector('.dropdown-toggle') || dropdown.querySelector('.nav-link'); // 1. Desktop Hover dropdown.addEventListener('mouseenter', function() { if (isDesktop()) { openMegaMenu(dropdown, idx); } }); dropdown.addEventListener('mouseleave', function() { if (isDesktop()) { closeMegaMenu(dropdown, idx, false); } }); // 2. Click / Tap (Desktop & Mobile) if (toggle) { toggle.addEventListener('click', function(e) { var isOpen = dropdown.classList.contains('show'); if (!isDesktop()) { // Mobile: toggle accordion e.preventDefault(); if (isOpen) { closeMegaMenu(dropdown, idx, true); } else { openMegaMenu(dropdown, idx); } } else { // Desktop: if clicked, toggle if (!isOpen) { openMegaMenu(dropdown, idx); } } }); // 3. Keyboard accessibility on Toggle toggle.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); var isOpen = dropdown.classList.contains('show'); if (!isOpen) { openMegaMenu(dropdown, idx); } // Focus first link in menu var firstLink = dropdown.querySelector('.mega-menu a'); if (firstLink) { setTimeout(function() { firstLink.focus(); }, 60); } } else if (e.key === 'Escape') { closeMegaMenu(dropdown, idx, true); } }); } // 4. Focus trapping & Escape key inside dropdown dropdown.addEventListener('focusin', function() { if (isDesktop()) { openMegaMenu(dropdown, idx); } }); dropdown.addEventListener('focusout', function(e) { if (!dropdown.contains(e.relatedTarget)) { closeMegaMenu(dropdown, idx, true); } }); dropdown.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeMegaMenu(dropdown, idx, true); if (toggle) toggle.focus(); } }); }); // Close on click outside document.addEventListener('click', function(e) { if (!e.target.closest('.mega-dropdown')) { megaDropdowns.forEach(function(d, i) { closeMegaMenu(d, i, true); }); } }); // Window resize / orientation change: close menus cleanly window.addEventListener('resize', function() { megaDropdowns.forEach(function(d, i) { closeMegaMenu(d, i, true); }); }); // Auto-open login modal if redirected from auth middleware const loginForm = document.getElementById('loginForm'); const registerForm = document.getElementById('registerForm'); // Auto location detection for Create Account (silent IP-based lookup on modal open) var locationDetected = false; function detectUserLocation() { if (locationDetected) return; fetch('https://www.operatetools.com/api/detect-location') .then(r => r.json()) .then(data => { if (data.success) { locationDetected = true; var sId = document.getElementById('regStateId'); if (sId && data.state_id) sId.value = data.state_id; var cId = document.getElementById('regCityId'); if (cId && data.city_id) cId.value = data.city_id; var sName = document.getElementById('regStateName'); if (sName && data.state_name) sName.value = data.state_name; var cName = document.getElementById('regCityName'); if (cName && data.city_name) cName.value = data.city_name; var countryInput = document.getElementById('regCountry'); if (countryInput && data.country && !countryInput.value) countryInput.value = data.country; if (data.city_name || data.state_name || data.country) { const badge = document.getElementById('detectedLocationBadge'); const locText = document.getElementById('locName'); if (badge && locText) { locText.textContent = [data.city_name, data.state_name, data.country].filter(Boolean).join(', '); badge.classList.remove('d-none'); } } } }) .catch(() => {}); } // Attach to register modal open event var regModalEl = document.getElementById('registerModal'); if (regModalEl) { regModalEl.addEventListener('show.bs.modal', detectUserLocation); } // Helper: show error / success function showError(el, msg) { el.innerHTML = msg; el.classList.remove('d-none'); } function showSuccess(el, msg) { el.innerHTML = msg; el.classList.remove('d-none'); } function hideError(el) { el.innerHTML = ''; el.classList.add('d-none'); } // Helper: validate email format function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } if (loginForm) { loginForm.addEventListener('submit', function(e) { e.preventDefault(); const btn = document.getElementById('loginBtn'); const errorDiv = document.getElementById('loginError'); hideError(errorDiv); const email = document.getElementById('loginEmail').value.trim(); const password = document.getElementById('loginPassword').value; // Frontend validation let errors = []; if (!email) errors.push('Please enter your email address.'); else if (!isValidEmail(email)) errors.push('Please enter a valid email address.'); if (!password) errors.push('Please enter your password.'); if (errors.length > 0) { showError(errorDiv, errors.join('<br>')); return; } btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Signing in...'; fetch('https://www.operatetools.com/login-auth', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }, body: JSON.stringify({ email, password, remember: document.getElementById('rememberMe').checked }) }) .then(r => r.json().then(data => ({status: r.status, data}))) .then(({status, data}) => { if (data.success) { btn.innerHTML = '<i class="fas fa-check me-1"></i> Success!'; setTimeout(() => window.location.reload(), 500); } else { let msg = data.message || 'The email or password you entered is incorrect.'; if (data.errors) msg = Object.values(data.errors).flat().join('<br>'); showError(errorDiv, msg); btn.disabled = false; btn.innerHTML = '<i class="fas fa-sign-in-alt me-1"></i> Sign In'; } }) .catch(() => { showError(errorDiv, 'Unable to connect. Please check your internet and try again.'); btn.disabled = false; btn.innerHTML = '<i class="fas fa-sign-in-alt me-1"></i> Sign In'; }); }); } if (registerForm) { registerForm.addEventListener('submit', function(e) { e.preventDefault(); const btn = document.getElementById('registerBtn'); const errorDiv = document.getElementById('registerError'); hideError(errorDiv); const name = document.getElementById('regName').value.trim(); const mobile = document.getElementById('regMobile').value.trim(); const country = document.getElementById('regCountry') ? document.getElementById('regCountry').value.trim() : ''; const email = document.getElementById('regEmail').value.trim(); const password = document.getElementById('regPassword').value; const confirmPw = document.getElementById('regPasswordConfirm').value; // Frontend validation with custom messages let errors = []; if (!name) errors.push('Please enter your full name.'); if (!mobile) errors.push('Please enter your mobile number.'); else if (!/^\d{10}$/.test(mobile)) errors.push('Mobile number must be exactly 10 digits.'); if (!email) errors.push('Please enter your email address.'); else if (!isValidEmail(email)) errors.push('Please enter a valid email address (e.g. name@example.com).'); if (!password) errors.push('Please create a password.'); else if (password.length < 6) errors.push('Password must be at least 6 characters long.'); if (!confirmPw) errors.push('Please confirm your password.'); else if (password !== confirmPw) errors.push('Passwords do not match. Please re-enter.'); if (errors.length > 0) { showError(errorDiv, errors.join('<br>')); return; } btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Creating account...'; fetch('https://www.operatetools.com/register', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }, body: JSON.stringify({ name, mobile, country, email, password, password_confirmation: confirmPw, state_id: document.getElementById('regStateId').value, city_id: document.getElementById('regCityId').value, state_name: document.getElementById('regStateName').value, city_name: document.getElementById('regCityName').value }) }) .then(r => r.json().then(data => ({status: r.status, data}))) .then(({status, data}) => { if (data.success) { btn.innerHTML = '<i class="fas fa-check me-1"></i> Success!'; setTimeout(() => window.location.reload(), 500); } else { let msg = data.message || 'Registration failed. Please check your details.'; if (data.errors) msg = Object.values(data.errors).flat().join('<br>'); showError(errorDiv, msg); btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus me-1"></i> Create Account'; } }) .catch(() => { showError(errorDiv, 'Unable to connect. Please check your internet and try again.'); btn.disabled = false; btn.innerHTML = '<i class="fas fa-user-plus me-1"></i> Create Account'; }); }); } const forgotForm = document.getElementById('forgotPasswordForm'); if (forgotForm) { forgotForm.addEventListener('submit', function(e) { e.preventDefault(); const btn = document.getElementById('forgotBtn'); const errorDiv = document.getElementById('forgotError'); const successDiv = document.getElementById('forgotSuccess'); hideError(errorDiv); hideError(successDiv); const email = document.getElementById('forgotEmail').value.trim(); if (!email) { showError(errorDiv, 'Please enter your email address.'); return; } else if (!isValidEmail(email)) { showError(errorDiv, 'Please enter a valid email address.'); return; } btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Sending...'; fetch('https://www.operatetools.com/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }, body: JSON.stringify({ email }) }) .then(r => r.json().then(data => ({status: r.status, data}))) .then(({status, data}) => { if (data.success) { btn.innerHTML = '<i class="fas fa-check me-1"></i> Sent!'; showSuccess(successDiv, data.message || 'Password reset link sent to your email!'); document.getElementById('forgotEmail').value = ''; setTimeout(() => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> Send Reset Link'; }, 4000); } else { btn.disabled = false; btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> Send Reset Link'; let msg = data.message || 'Unable to send password reset link. Please try again.'; if (data.errors) msg = Object.values(data.errors).flat().join('<br>'); showError(errorDiv, msg); } }) .catch(() => { btn.disabled = false; btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> Send Reset Link'; showError(errorDiv, 'Unable to connect. Please check your network and try again.'); }); }); } }); // Front-End Notification Center & Service Worker Script document.addEventListener('DOMContentLoaded', function() { // 1. Load recent notifications into header bell dropdown fetchRecentNotifications(); }); function getHeaderClickedPushIds() { try { const stored = localStorage.getItem('clicked_push_notification_ids'); return stored ? JSON.parse(stored) : []; } catch(e) { return []; } } function markHeaderPushClicked(id) { if (!id) return; try { const ids = getHeaderClickedPushIds(); const idStr = String(id); if (!ids.includes(idStr)) { ids.push(idStr); localStorage.setItem('clicked_push_notification_ids', JSON.stringify(ids)); } } catch(e) {} } function fetchRecentNotifications() { fetch('/api/notifications/recent') .then(res => res.json()) .then(data => { const badge = document.getElementById('notifBadgeCount'); const totalBadge = document.getElementById('notifTotalBadge'); const container = document.getElementById('notifListContainer'); if (!container) return; if (data.success && data.notifications && data.notifications.length > 0) { const clickedIds = getHeaderClickedPushIds(); const unread = data.notifications.filter(item => !clickedIds.includes(String(item.id))); if (unread.length > 0) { if (badge) { badge.innerText = unread.length; badge.style.display = 'inline-block'; } if (totalBadge) { totalBadge.innerText = unread.length + ' Recent'; } let html = ''; unread.forEach(item => { const trackUrl = item.id ? ('/push/track/click/' + item.id) : '#'; const validIcon = item.icon_url && String(item.icon_url).trim() !== '' && String(item.icon_url).trim().toLowerCase() !== 'null' && (String(item.icon_url).startsWith('/') || String(item.icon_url).startsWith('http')); html += ` <a href="${trackUrl}" class="list-group-item list-group-item-action p-3 d-flex align-items-start gap-2 text-decoration-none" onclick="markHeaderPushClicked('${item.id}')"> <div class="bg-light p-2 rounded text-primary" style="flex-shrink:0;color:#8b5cf6 !important"> ${validIcon ? `<img src="${item.icon_url}" alt="${item.title ? item.title.replace(/"/g, '"') : 'Notification icon'}" style="width:24px;height:24px;border-radius:4px">` : `<i class="fas fa-bell"></i>`} </div> <div class="flex-grow-1 overflow-hidden"> <div class="fw-bold text-dark text-truncate" style="font-size:0.83rem">${item.title}</div> <div class="text-secondary text-truncate" style="font-size:0.75rem">${item.message}</div> </div> </a> `; }); container.innerHTML = html; } else { if (badge) badge.style.display = 'none'; if (totalBadge) totalBadge.innerText = '0 New'; container.innerHTML = ` <div class="p-4 text-center text-muted small"> <i class="fas fa-bell-slash d-block mb-1 text-secondary fs-5"></i> No new notifications right now. </div> `; } } else { if (badge) badge.style.display = 'none'; if (totalBadge) totalBadge.innerText = '0 New'; container.innerHTML = ` <div class="p-4 text-center text-muted small"> <i class="fas fa-bell-slash d-block mb-1 text-secondary fs-5"></i> No new notifications right now. </div> `; } }) .catch(err => { const container = document.getElementById('notifListContainer'); if (container) { container.innerHTML = ` <div class="p-3 text-center text-muted small">No notifications available</div> `; } }); } // Live Header Tool Search & Similarity Matching Engine </script><script> (function() { function initHeaderSearch() { const searchInput = document.getElementById('headerToolsSearch'); const resultsDropdown = document.getElementById('headerSearchResults'); const searchForm = searchInput ? searchInput.closest('form') : null; const mobileSearchBtn = document.getElementById('headerMobileSearchBtn'); if (!searchInput || !resultsDropdown) return; let toolsCatalog = []; let catalogLoading = null; // Try reading fast from localStorage cache try { const cached = localStorage.getItem('op_tools_catalog_v2'); if (cached) { const parsed = JSON.parse(cached); if (Array.isArray(parsed) && parsed.length > 0) { toolsCatalog = parsed; } } } catch(e) {} function loadToolsCatalog() { if (toolsCatalog.length > 0) return Promise.resolve(toolsCatalog); if (catalogLoading) return catalogLoading; catalogLoading = fetch('https://www.operatetools.com/data/tools-catalog.json') .then(res => { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(data => { if (Array.isArray(data) && data.length > 0) { toolsCatalog = data; try { localStorage.setItem('op_tools_catalog_v2', JSON.stringify(data)); } catch(e) {} } return toolsCatalog; }) .catch(() => { return fetch('/data/tools-catalog.json') .then(r => r.json()) .then(d => { if (Array.isArray(d) && d.length > 0) { toolsCatalog = d; try { localStorage.setItem('op_tools_catalog_v2', JSON.stringify(d)); } catch(e) {} } return toolsCatalog; }) .catch(() => { return toolsCatalog; }); }); return catalogLoading; } // Preload catalog when search input is focused or hovered, or slightly after DOM load searchInput.addEventListener('focus', loadToolsCatalog, { once: true }); searchInput.addEventListener('mouseenter', loadToolsCatalog, { once: true }); if ('requestIdleCallback' in window) { window.requestIdleCallback(loadToolsCatalog, { timeout: 3000 }); } else { setTimeout(loadToolsCatalog, 1200); } // Mobile Search Toggle button if (mobileSearchBtn) { mobileSearchBtn.addEventListener('click', function(e) { e.preventDefault(); const navCollapse = document.getElementById('navbarNav'); if (navCollapse) { if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) { bootstrap.Collapse.getOrCreateInstance(navCollapse).show(); } else { navCollapse.classList.add('show'); } } setTimeout(() => { searchInput.focus(); }, 150); }); } let activeIndex = -1; let debounceTimer = null; // Comprehensive Synonyms and aliases for similarity matching const synonymMap = { 'curl': ['curl to python', 'curl to javascript', 'curl', 'requests', 'fetch', 'axios', 'terminal'], 'python': ['curl to python', 'python compiler', 'python tutorial', 'requests', 'py'], 'javascript': ['curl to javascript', 'javascript compiler', 'javascript tutorial', 'js', 'fetch', 'axios'], 'js': ['javascript', 'curl to javascript', 'javascript compiler'], 'py': ['python', 'curl to python', 'python compiler'], 'ai': ['ai agent roi calculator', 'ai image tools', 'text to image', 'ai document analyzer', 'ai email writer', 'ai content detector', 'ai'], 'roi': ['ai agent roi calculator', 'calculator', 'efficiency', 'savings', 'roi'], 'agent': ['ai agent roi calculator', 'ai agent', 'autonomous agent'], 'txt': ['csv to txt', 'plain text', 'text', 'convert csv to plain text columns', 'text editor', 'txt'], 'csv': ['csv to txt', 'csv to json', 'json to csv', 'csv to xml', 'xml to csv', 'csv to sql', 'csv to html', 'csv viewer', 'csv splitter', 'excel', 'spreadsheet', 'tsv'], 'xml': ['xml to json', 'json to xml', 'xml to csv', 'csv to xml', 'minify xml', 'xml editor', 'xml to image'], 'columns': ['csv to txt', 'plain text columns', 'csv viewer'], 'requests': ['curl to python', 'python requests', 'curl'], 'fetch': ['curl to javascript', 'fetch', 'axios'], 'axios': ['curl to javascript', 'axios', 'fetch'], 'bg': ['background', 'remove background', 'eraser'], 'background': ['bg', 'remove background', 'photo editor'], 'pic': ['image', 'photo', 'picture'], 'photo': ['image', 'picture', 'pic', 'editor', 'photo editor', 'passport'], 'picture': ['image', 'photo'], 'jpg': ['jpeg', 'image', 'photo', 'png', 'converter', 'compress'], 'jpeg': ['jpg', 'image', 'photo'], 'png': ['image', 'photo', 'jpg', 'svg'], 'webp': ['image', 'convert', 'jpg'], 'doc': ['word', 'document', 'pdf to word', 'word to pdf'], 'docx': ['word', 'document'], 'word': ['doc', 'pdf to word', 'word to pdf', 'jpg to word'], 'excel': ['xls', 'xlsx', 'spreadsheet', 'pdf to excel', 'excel to pdf'], 'xls': ['excel', 'spreadsheet'], 'xlsx': ['excel', 'spreadsheet'], 'calc': ['calculator', 'salary', 'gpa', 'currency', 'sip', 'emi', 'loan', 'ai agent roi calculator'], 'calculater': ['calculator', 'calc'], 'calculator': ['calc', 'salary', 'gpa', 'currency', 'ai agent roi calculator', 'sip', 'emi'], 'crop': ['cropper', 'image cropper', 'crop pdf'], 'croper': ['crop', 'cropper', 'image cropper'], 'cropper': ['crop', 'image cropper'], 'compress': ['compression', 'compressor', 'reduce size', 'mb to kb', 'compress pdf', 'compress image', 'compress jpg', 'compress png'], 'compres': ['compress', 'compressor', 'compression'], 'compressor': ['compress', 'reduce size'], 'resize': ['resizer', 'dimensions', 'whatsapp dp', 'image resizer'], 'resiz': ['resize', 'resizer'], 'resizer': ['resize', 'image resizer'], 'convert': ['converter', 'universal converter', 'unit converter'], 'converter': ['convert', 'universal converter', 'currency', 'unit converter'], 'qr': ['qrcode', 'qr code', 'barcode'], 'qrcode': ['qr', 'qr code', 'barcode'], 'barcode': ['barcode scanner', 'scanner', 'barcode reader', 'scan barcode', 'ean', 'upc', 'code 128', 'qr'], 'barcodes': ['barcode scanner', 'scanner', 'barcode', 'barcode reader'], 'scanner': ['barcode scanner', 'barcode', 'qr code', 'ocr', 'scan'], 'scan': ['barcode scanner', 'barcode', 'qr code', 'scanner'], 'separator': ['comma separator', 'comma', 'delimited list', 'delimiter'], 'duplicate': ['remove duplicate text', 'delete duplicate text', 'dedupe'], 'uuid': ['uuid generator', 'guid', 'guid generator', 'v4', 'random uuid', 'unique identifier'], 'guid': ['uuid generator', 'uuid', 'guid generator', 'v4'], 'v4': ['uuid generator', 'uuid v4', 'guid'], 'identifier': ['uuid generator', 'guid', 'uuid'], 'color': ['color converter online', 'color converter hex to rgb', 'hex to color converter', 'color converter rgb to cmyk', 'hex to css color converter'], 'hex': ['color converter online', 'color converter hex to rgb', 'hex to css color converter', 'hex to color converter'], 'rgb': ['color converter online', 'color converter hex to rgb', 'color converter rgb to cmyk'], 'hsl': ['color converter online', 'hex to color converter'], 'cmyk': ['color converter rgb to cmyk', 'color converter hex to cmyk'], 'pantone': ['color converter to pantone', 'color converter hex to pantone'], 'ncs': ['color converter ncs', 'color converter online'], 'frequency': ['word frequency counter', 'word frequency counter online', 'word count', 'keyword density'], 'density': ['word frequency counter', 'keyword density', 'word count'], 'words': ['word frequency counter', 'word frequency counter online free', 'word counter', 'text analyzer'], 'lottery': ['sambad', 'dear', 'kerala', 'sikkim', 'bumper', 'lottery results'], 'sambad': ['lottery', 'dear', 'lottery sambad'], 'dear': ['lottery', 'dear 1 pm', 'dear 6 pm', 'dear 8 pm'], 'tts': ['text to speech', 'voice', 'audio', 'speech'], 'voice': ['text to speech', 'tts', 'audio'], 'speech': ['text to speech', 'voice'], 'ocr': ['image to text', 'extract text', 'ocr translator'], 'watermark': ['add watermark', 'watermark remover', 'stamp'], 'watrmark': ['watermark', 'add watermark'], 'code': ['compiler', 'editor', 'python', 'javascript', 'java', 'c++', 'c#', 'php', 'typescript'], 'compiler': ['code', 'editor', 'online compiler'], 'editor': ['code', 'compiler', 'pdf editor', 'photo editor'], 'program': ['programming', 'tutorial', 'code', 'compiler'], 'programming': ['tutorial', 'code', 'examples'], 'yt': ['youtube', 'video', 'youtube description'], 'youtube': ['yt', 'video', 'youtube title', 'youtube description'], 'salary': ['salary after tax', 'tax calculator', 'income', 'take home pay'], 'tax': ['salary after tax', 'income tax'], 'col': ['cost of living', 'cost of living comparison'], 'living': ['cost of living'], 'domain': ['domain name search', 'whois', 'dns'], 'reverse': ['reverse image search', 'search by image'], 'pdf': ['merge pdf', 'split pdf', 'compress pdf', 'pdf editor', 'pdf to word', 'word to pdf', 'unlock pdf', 'sign pdf', 'crop pdf'], 'svg': ['svg converter', 'vector'], 'sql': ['mysql editor', 'sqlite memory', 'database', 'csv to sql'], 'json': ['csv to json', 'json to csv', 'json to excel', 'json to xml', 'json formatter', 'json beautifier', 'json editor', 'json viewer', 'json compare', 'ndjson'], 'tsv': ['csv to json', 'tsv to json', 'tab delimited', 'csv'], 'ndjson': ['csv to json', 'json to csv', 'jsonlines'] }; function levenshtein(a, b) { if (a.length === 0) return b.length; if (b.length === 0) return a.length; const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[b.length][a.length]; } function calculateScore(tool, rawQuery) { let q = (rawQuery || '').toLowerCase().trim(); if (!q) return 0; // Strip protocol, host, port, and slashes for URL matching const cleanUrlQ = q.replace(/^https?:\/\/[^\/]+/i, '').replace(/^\/+|\/+$/g, ''); const name = (tool.name || tool.title || '').toLowerCase(); const cat = (tool.category || '').toLowerCase(); const desc = (tool.description || '').toLowerCase(); const url = (tool.url || '').toLowerCase(); const cleanToolUrl = url.replace(/^\/+|\/+$/g, ''); const keywords = (tool.keywords || (name + ' ' + cat + ' ' + desc)).toLowerCase(); let score = 0; // 1. Exact match on URL path or clean slug if (cleanToolUrl === cleanUrlQ || url === q || url === '/' + q || '/' + cleanToolUrl === q) { return 300; } // 2. Exact match on Name & Prefix if (name === q) score += 160; else if (name.startsWith(q)) score += 110; else if (name.includes(q)) score += 80; else if (cleanToolUrl.includes(cleanUrlQ) && cleanUrlQ.length >= 3) score += 75; else if (url.includes(q)) score += 65; else if (cat.includes(q)) score += 40; else if (desc.includes(q) || keywords.includes(q)) score += 30; // 3. Word Token Matching & Synonyms const ignoredWords = ['http', 'https', 'www', 'com', 'org', 'net', '127', '0', '1', '8000', '8001', 'localhost']; const qWords = q.split(/[\s\-_\/]+/).filter(w => w.length > 0 && !ignoredWords.includes(w)); let allWordsMatch = true; for (const w of qWords) { let wordMatched = false; if (name.includes(w)) { score += 35; wordMatched = true; } else if (cleanToolUrl.includes(w)) { score += 30; wordMatched = true; } else if (cat.includes(w) || desc.includes(w) || keywords.includes(w)) { score += 20; wordMatched = true; } else { const syns = synonymMap[w] || []; for (const syn of syns) { if (name.includes(syn) || cleanToolUrl.includes(syn) || cat.includes(syn) || keywords.includes(syn)) { score += 28; wordMatched = true; break; } } } if (!wordMatched) allWordsMatch = false; } if (allWordsMatch && qWords.length > 1) { score += 55; } // 4. Fuzzy / Typo Tolerance (Levenshtein) if (score === 0 && q.length >= 3 && !q.includes('/')) { const nameWords = name.split(/[\s\-_\/]+/); for (const nw of nameWords) { if (nw.length >= 3) { const dist = levenshtein(q, nw); if (dist <= 1) { score += 40; } else if (dist === 2 && q.length >= 5) { score += 25; } } } const wholeDist = levenshtein(q, name); if (wholeDist <= 2) { score += 35; } } return score; } function highlightMatch(text, query) { if (!text || !query) return text || ''; const words = query.trim().split(/[\s\-_\/]+/).filter(w => w.length > 0 && w.length >= 2); if (words.length === 0) return text; const pattern = new RegExp('(' + words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'gi'); return text.replace(pattern, '<mark>$1</mark>'); } function renderResults(query, results) { if (!query.trim()) { resultsDropdown.style.display = 'none'; resultsDropdown.innerHTML = ''; return; } activeIndex = -1; const total = results.length; const searchUrl = 'https://www.operatetools.com/tools/search?q=' + encodeURIComponent(query); if (total === 0) { resultsDropdown.innerHTML = ` <div class="header-search-header"> <span>Search Tools</span> <span class="badge bg-secondary">0 Found</span> </div> <div class="header-search-empty"> <i class="fas fa-search mb-2 d-block text-muted" style="font-size:1.4rem"></i> <div>No exact tool for "<strong>${escapeHtml(query)}</strong>"</div> <div class="small text-muted mt-1">Try keywords like <em>curl, python, js, ai, roi, txt, csv, xml, pdf</em></div> </div> <div class="header-search-footer"> <a href="${searchUrl}">Search all 300+ tools on Directory <i class="fas fa-arrow-right"></i></a> </div> `; resultsDropdown.style.display = 'block'; return; } let itemsHtml = ''; results.slice(0, 8).forEach((item, idx) => { const iconClass = item.icon || 'fas fa-wrench'; const cat = item.category || 'Tool'; const itemName = item.name || item.title || 'Tool'; const titleHtml = highlightMatch(escapeHtml(itemName), query); const desc = item.description ? escapeHtml(item.description) : ''; const descHtml = desc ? `<div class="header-search-item-desc">${desc}</div>` : ''; const rawUrl = item.url ? String(item.url).trim() : ''; const itemUrl = (rawUrl && rawUrl.toLowerCase() !== 'null' && rawUrl.toLowerCase() !== 'undefined') ? rawUrl : '#'; itemsHtml += ` <li> <a href="${itemUrl}" class="header-search-item" data-index="${idx}"> <div class="header-search-item-icon"> <i class="${iconClass}"></i> </div> <div class="header-search-item-body"> <div class="header-search-item-title"> <span>${titleHtml}</span> <span class="header-search-item-cat">${escapeHtml(cat)}</span> </div> <div class="header-search-item-url">${escapeHtml(itemUrl)}</div> ${descHtml} </div> </a> </li> `; }); resultsDropdown.innerHTML = ` <div class="header-search-header"> <span><i class="fas fa-tools text-primary me-1"></i> Matching Tools</span> <span class="badge bg-primary">${total} ${total === 1 ? 'Match' : 'Matches'}</span> </div> <ul class="header-search-list"> ${itemsHtml} </ul> <div class="header-search-footer"> <a href="${searchUrl}">View all ${total} results on Directory <i class="fas fa-arrow-right"></i></a> </div> `; resultsDropdown.style.display = 'block'; } function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } function performSearch(query) { if (!query || !query.trim()) { resultsDropdown.style.display = 'none'; return; } loadToolsCatalog().then(catalog => { if (catalog && catalog.length > 0) { const scored = []; for (const tool of catalog) { const score = calculateScore(tool, query); if (score > 0) { scored.push({ ...tool, _score: score }); } } scored.sort((a, b) => b._score - a._score); renderResults(query, scored); } else { // Async fallback to API fetch('/api/tools/quick-search?q=' + encodeURIComponent(query)) .then(res => res.json()) .then(data => { const results = (data && data.results) ? data.results : []; renderResults(query, results); }) .catch(() => { renderResults(query, []); }); } }); } searchInput.addEventListener('input', function() { const val = this.value; clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { performSearch(val); }, 60); }); searchInput.addEventListener('focus', function() { if (this.value.trim()) { performSearch(this.value); } }); searchInput.addEventListener('click', function() { if (this.value.trim() && resultsDropdown.style.display === 'none') { performSearch(this.value); } }); searchInput.addEventListener('keydown', function(e) { if (resultsDropdown.style.display === 'none') return; const items = resultsDropdown.querySelectorAll('.header-search-item'); if (items.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = (activeIndex + 1) % items.length; updateActiveItem(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = (activeIndex - 1 + items.length) % items.length; updateActiveItem(items); } else if (e.key === 'Enter') { if (activeIndex >= 0 && items[activeIndex]) { e.preventDefault(); items[activeIndex].click(); } } else if (e.key === 'Escape') { resultsDropdown.style.display = 'none'; } }); function updateActiveItem(items) { items.forEach((it, i) => { if (i === activeIndex) { it.classList.add('is-active'); it.scrollIntoView({ block: 'nearest' }); } else { it.classList.remove('is-active'); } }); } document.addEventListener('click', function(e) { if (!searchForm || !searchForm.contains(e.target)) { resultsDropdown.style.display = 'none'; } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initHeaderSearch); } else { initHeaderSearch(); } })(); /* ========================================================================= * OperateTools Live Website Translator Engine (Powered by Python Microservice) * ========================================================================= */ (function() { const DEFAULT_LANG = 'en'; // Fast client-side timezone & browser locale country-to-language detector function detectCountryLanguageFromClient() { try { const tz = (Intl.DateTimeFormat().resolvedOptions().timeZone || '').toLowerCase(); if (tz.includes('calcutta') || tz.includes('kolkata')) return 'hi'; if (tz.includes('dhaka')) return 'bn'; if (tz.includes('madrid') || tz.includes('canary') || tz.includes('mexico') || tz.includes('bogota') || tz.includes('buenos_aires') || tz.includes('santiago') || tz.includes('lima') || tz.includes('caracas')) return 'es'; if (tz.includes('paris')) return 'fr'; if (tz.includes('berlin') || tz.includes('vienna') || tz.includes('zurich')) return 'de'; if (tz.includes('rome')) return 'it'; if (tz.includes('sao_paulo') || tz.includes('lisbon')) return 'pt'; if (tz.includes('moscow')) return 'ru'; if (tz.includes('tokyo')) return 'ja'; if (tz.includes('seoul')) return 'ko'; if (tz.includes('shanghai') || tz.includes('hong_kong') || tz.includes('taipei')) return 'zh'; if (tz.includes('riyadh') || tz.includes('dubai') || tz.includes('cairo')) return 'ar'; if (tz.includes('istanbul')) return 'tr'; if (tz.includes('jakarta')) return 'id'; if (tz.includes('bangkok')) return 'th'; if (tz.includes('ho_chi_minh') || tz.includes('saigon')) return 'vi'; } catch(e) {} try { const navLangs = navigator.languages || [navigator.language || navigator.userLanguage || '']; for (let l of navLangs) { if (!l) continue; const code = l.toLowerCase().split('-')[0]; if (['es', 'fr', 'de', 'hi', 'bn', 'zh', 'ja', 'ar', 'ru', 'pt', 'it', 'ko', 'tr', 'id', 'nl', 'pl', 'vi', 'th'].includes(code)) { return code; } } } catch(e) {} return null; } let currentLang = localStorage.getItem('site_language'); if (!currentLang) { if (localStorage.getItem('site_language_manual') !== 'true') { const fastDetected = detectCountryLanguageFromClient(); if (fastDetected && fastDetected !== DEFAULT_LANG) { currentLang = fastDetected; localStorage.setItem('site_language', fastDetected); } else { currentLang = DEFAULT_LANG; } } else { currentLang = DEFAULT_LANG; } } let allLanguages = [ { code: 'en', name: 'English', native_name: 'English' }, { code: 'es', name: 'Spanish', native_name: 'Español' }, { code: 'fr', name: 'French', native_name: 'Français' }, { code: 'de', name: 'German', native_name: 'Deutsch' }, { code: 'hi', name: 'Hindi', native_name: 'हिन्दी' }, { code: 'bn', name: 'Bengali', native_name: 'বাংলা' }, { code: 'zh', name: 'Chinese', native_name: '简体中文' }, { code: 'ja', name: 'Japanese', native_name: '日本語' }, { code: 'ar', name: 'Arabic', native_name: 'العربية' }, { code: 'ru', name: 'Russian', native_name: 'Русский' }, { code: 'pt', name: 'Portuguese', native_name: 'Português' }, { code: 'it', name: 'Italian', native_name: 'Italiano' }, { code: 'ko', name: 'Korean', native_name: '한국어' }, { code: 'tr', name: 'Turkish', native_name: 'Türkçe' }, { code: 'id', name: 'Indonesian', native_name: 'Bahasa Indonesia' }, { code: 'nl', name: 'Dutch', native_name: 'Nederlands' }, { code: 'pl', name: 'Polish', native_name: 'Polski' }, { code: 'vi', name: 'Vietnamese', native_name: 'Tiếng Việt' }, { code: 'th', name: 'Thai', native_name: 'ไทย' }, { code: 'uk', name: 'Ukrainian', native_name: 'Українська' } ]; let isTranslating = false; let activeSessionId = 0; // Retrieve translation dictionary from all possible storage keys (sanitizing dummy/identical entries) function getStoredCache(lang) { let cache = {}; if (!lang || lang === DEFAULT_LANG) return cache; function mergeValid(raw) { if (!raw) return; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') { for (let k in parsed) { const v = parsed[k]; if (v && typeof v === 'string' && v.trim() !== '' && v.trim() !== k.trim()) { cache[k] = v; cache[k.trim()] = v; const clean = k.trim().replace(/\s+/g, ' '); cache[clean] = v; } } } } catch(e) {} } mergeValid(localStorage.getItem(`site_trans_dict_${lang}`)); mergeValid(localStorage.getItem(`trans_cache_${lang}`)); mergeValid(sessionStorage.getItem(`trans_cache_${lang}`)); return cache; } // Persist translation dictionary safely function saveStoredCache(lang, newCache) { if (!lang || lang === DEFAULT_LANG) return; try { const clean = {}; for (let k in newCache) { const v = newCache[k]; if (v && typeof v === 'string' && v.trim() !== '' && v.trim() !== k.trim()) { clean[k] = v; } } localStorage.setItem(`site_trans_dict_${lang}`, JSON.stringify(clean)); localStorage.setItem(`trans_cache_${lang}`, JSON.stringify(clean)); } catch(e) { console.warn('Storage quota exceeded, could not cache translations:', e); } } function isNodeAcceptable(node) { if (!node || !node.nodeValue) return false; const raw = node.nodeValue.trim(); if (raw.length <= 1) return false; const parent = node.parentElement; if (!parent) return false; const tag = (parent.tagName || '').toLowerCase(); if (['script', 'style', 'code', 'pre', 'noscript', 'textarea', 'input', 'select', 'svg', 'path'].includes(tag)) { return false; } try { if (typeof parent.closest === 'function') { if (parent.closest('#siteLanguageDropdownWrapper') || parent.closest('#siteTranslationToast')) { return false; } } if (['i', 'span'].includes(tag)) { const cName = typeof parent.className === 'string' ? parent.className : (parent.className?.baseVal || ''); if (/\bfa[srbld]?\b|\bfa-[a-z0-9-]+\b/i.test(cName)) { return false; } } } catch(e) { return false; } return true; } // Capture and permanently preserve original English DOM text on every node function recordOriginalEnglishDom() { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { return isNodeAcceptable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); let currentNode; while ((currentNode = walker.nextNode())) { if (currentNode.__origText === undefined) { currentNode.__origText = currentNode.nodeValue; } } document.querySelectorAll('input[placeholder], textarea[placeholder]').forEach(el => { if (el.__origPlaceholder === undefined) { el.__origPlaceholder = el.getAttribute('placeholder') || ''; } }); document.querySelectorAll('[title]').forEach(el => { if (el.__origTitle === undefined && !el.closest('#siteLanguageDropdownWrapper')) { el.__origTitle = el.getAttribute('title') || ''; } }); } // Instantly restores original English text across the entire DOM with 0ms delay function restoreOriginalEnglishDom() { recordOriginalEnglishDom(); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null ); let currentNode; while ((currentNode = walker.nextNode())) { if (currentNode.__origText !== undefined) { currentNode.nodeValue = currentNode.__origText; } } document.querySelectorAll('input[placeholder], textarea[placeholder]').forEach(el => { if (el.__origPlaceholder !== undefined) { el.setAttribute('placeholder', el.__origPlaceholder); } }); document.querySelectorAll('[title]').forEach(el => { if (el.__origTitle !== undefined) { el.setAttribute('title', el.__origTitle); } }); } // Apply translations in-place to the DOM using current cache dictionary function applyTranslationsToDom(cache) { if (!cache || Object.keys(cache).length === 0) return 0; recordOriginalEnglishDom(); let appliedCount = 0; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { return isNodeAcceptable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); let currentNode; while ((currentNode = walker.nextNode())) { const orig = currentNode.__origText; if (!orig) continue; const raw = orig.trim(); if (raw.length <= 1) continue; const clean = raw.replace(/\s+/g, ' '); const trans = cache[raw] || cache[clean]; if (trans && trans !== raw && trans !== clean) { const leadingWs = orig.match(/^\s*/)[0] || ''; const trailingWs = orig.match(/\s*$/)[0] || ''; currentNode.nodeValue = leadingWs + trans + trailingWs; appliedCount++; } } // Placeholders document.querySelectorAll('input[placeholder], textarea[placeholder]').forEach(el => { const ph = (el.__origPlaceholder || '').trim(); if (ph && ph.length > 1) { const cleanPh = ph.replace(/\s+/g, ' '); const trans = cache[ph] || cache[cleanPh]; if (trans && trans !== ph && trans !== cleanPh) { el.setAttribute('placeholder', trans); } } }); // Titles document.querySelectorAll('[title]').forEach(el => { const t = (el.__origTitle || '').trim(); if (t && t.length > 1) { const cleanT = t.replace(/\s+/g, ' '); const trans = cache[t] || cache[cleanT]; if (trans && trans !== t && trans !== cleanT) { el.setAttribute('title', trans); } } }); return appliedCount; } // Fast, silent DOM translation strictly using cached storage data (0 API calls) function applyCachedTranslations(targetLang) { if (!targetLang || targetLang === DEFAULT_LANG) { restoreOriginalEnglishDom(); return 0; } const cache = getStoredCache(targetLang); return applyTranslationsToDom(cache); } function showToast(html, duration = 0) { let toast = document.getElementById('siteTranslationToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'siteTranslationToast'; document.body.appendChild(toast); } toast.innerHTML = html; toast.style.display = 'flex'; if (duration > 0) { setTimeout(() => { if (toast) toast.style.display = 'none'; }, duration); } } function hideToast() { const toast = document.getElementById('siteTranslationToast'); if (toast) toast.style.display = 'none'; } function renderLanguageList(filterText = '') { const container = document.getElementById('headerLangList'); if (!container) return; const term = filterText.toLowerCase().trim(); const filtered = allLanguages.filter(l => l.name.toLowerCase().includes(term) || (l.native_name && l.native_name.toLowerCase().includes(term)) || l.code.toLowerCase().includes(term) ); if (filtered.length === 0) { container.innerHTML = '<div class="p-3 text-center text-muted small">No language found</div>'; return; } let html = ''; filtered.forEach(lang => { const isActive = lang.code === currentLang; html += ` <button type="button" class="lang-item-btn ${isActive ? 'is-active' : ''}" data-lang="${lang.code}"> <span> <strong>${lang.name}</strong> ${lang.code === 'en' ? '<span class="badge bg-secondary ms-1" style="font-size:0.6rem">Original</span>' : ''} </span> <span class="lang-native">${lang.native_name || lang.code.toUpperCase()}</span> </button> `; }); container.innerHTML = html; container.querySelectorAll('.lang-item-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.preventDefault(); const code = this.getAttribute('data-lang'); localStorage.setItem('site_language_manual', 'true'); changeLanguage(code, true); const trigger = document.getElementById('siteLanguageBtn'); if (trigger && window.bootstrap && window.bootstrap.Dropdown) { const inst = bootstrap.Dropdown.getInstance(trigger); if (inst) inst.hide(); } }); }); } async function loadSupportedLanguages() { try { const res = await fetch('/api/language/supported-languages'); if (res.ok) { const data = await res.json(); if (data && data.languages && data.languages.length > 0) { allLanguages = data.languages; renderLanguageList(); } } } catch (e) { console.warn('Could not load full language list:', e); } } // Change Language async function changeLanguage(targetLang, isUserAction = false, isAutoDetect = false) { const sessionId = ++activeSessionId; if (isUserAction) { localStorage.setItem('site_language_manual', 'true'); } const resetBtn = document.getElementById('resetLangBtn'); const activeLabel = document.getElementById('activeLangLabel'); if (targetLang === DEFAULT_LANG) { localStorage.setItem('site_language', DEFAULT_LANG); currentLang = DEFAULT_LANG; if (activeLabel) activeLabel.textContent = 'EN'; if (resetBtn) resetBtn.style.display = 'none'; restoreOriginalEnglishDom(); renderLanguageList(); hideToast(); if (isUserAction) { fetch('/api/language/set-locale', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' }, body: JSON.stringify({ lang: 'en' }) }).catch(() => {}); } return; } const langMeta = allLanguages.find(l => l.code === targetLang) || { name: targetLang.toUpperCase(), code: targetLang }; if (activeLabel) activeLabel.textContent = targetLang.toUpperCase(); if (resetBtn) resetBtn.style.display = 'inline'; // 1. Immediately apply any translations already present in cache (0ms delay) applyCachedTranslations(targetLang); // Toast feedback if (isUserAction) { showToast(` <i class="fas fa-spinner fa-spin text-primary"></i> <span>Translating page to <strong>${langMeta.name}</strong>...</span> `); } else if (isAutoDetect) { showToast(` <i class="fas fa-globe text-primary"></i> <span>Language automatically selected (<strong>${langMeta.name}</strong>) based on your country.</span> <button type="button" id="toastRevertBtn" class="btn btn-sm btn-outline-warning py-0 px-2 ms-2" style="font-size:0.75rem">Switch to English</button> <button type="button" class="btn-close btn-close-white ms-1" style="font-size:0.65rem" onclick="document.getElementById('siteTranslationToast').style.display='none'"></button> `, 8000); const toastRevert = document.getElementById('toastRevertBtn'); if (toastRevert) { toastRevert.addEventListener('click', (e) => { e.preventDefault(); localStorage.setItem('site_language_manual', 'true'); changeLanguage(DEFAULT_LANG, true); }); } } isTranslating = true; try { await translatePageDOM(targetLang, sessionId); if (sessionId !== activeSessionId) return; currentLang = targetLang; localStorage.setItem('site_language', targetLang); renderLanguageList(); if (isUserAction) { fetch('/api/language/set-locale', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' }, body: JSON.stringify({ lang: targetLang }) }).catch(() => {}); showToast(` <i class="fas fa-check-circle text-success"></i> <span>Translated to <strong>${langMeta.name}</strong></span> <button type="button" id="toastRevertBtn" class="btn btn-sm btn-outline-warning py-0 px-2 ms-2" style="font-size:0.75rem">Show Original</button> <button type="button" class="btn-close btn-close-white ms-1" style="font-size:0.65rem" onclick="document.getElementById('siteTranslationToast').style.display='none'"></button> `, 5000); const toastRevert = document.getElementById('toastRevertBtn'); if (toastRevert) { toastRevert.addEventListener('click', (e) => { e.preventDefault(); localStorage.setItem('site_language_manual', 'true'); changeLanguage(DEFAULT_LANG, true); }); } } } catch (err) { console.error('Translation error:', err); if (isUserAction && sessionId === activeSessionId) { showToast(`<i class="fas fa-exclamation-triangle text-warning"></i> <span>Translation error: ${err.message || 'Service unavailable'}</span>`, 4000); } } finally { if (sessionId === activeSessionId) { isTranslating = false; } } } // Collect DOM texts and translate uncached elements via Python microservice async function translatePageDOM(targetLang, sessionId = null) { if (!targetLang || targetLang === DEFAULT_LANG) return; sessionId = sessionId || ++activeSessionId; recordOriginalEnglishDom(); const cache = getStoredCache(targetLang); const seenClean = new Set(); const mainTexts = []; const otherTexts = []; const mainContainer = document.querySelector('main, .main-content, #content, .content, article'); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { return isNodeAcceptable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); let currentNode; while ((currentNode = walker.nextNode())) { const orig = currentNode.__origText; if (!orig) continue; const raw = orig.trim(); if (raw.length <= 1) continue; if (/^[0-9\s.,:%$#@!*()_\-+=/\\\[\]{}|;'"<>?]+$/.test(raw)) continue; const clean = raw.replace(/\s+/g, ' '); const isCached = (cache[raw] && cache[raw] !== raw) || (cache[clean] && cache[clean] !== clean); if (!isCached && !seenClean.has(clean)) { seenClean.add(clean); if (mainContainer && mainContainer.contains(currentNode)) { mainTexts.push(clean); } else { otherTexts.push(clean); } } } // Placeholders & Titles document.querySelectorAll('input[placeholder], textarea[placeholder]').forEach(el => { const ph = (el.__origPlaceholder || '').trim(); if (ph && ph.length > 1 && !/^[0-9\s.,:%$#@!*()_\-+=/\\\[\]{}|;'"<>?]+$/.test(ph)) { const clean = ph.replace(/\s+/g, ' '); const isCached = (cache[ph] && cache[ph] !== ph) || (cache[clean] && cache[clean] !== clean); if (!isCached && !seenClean.has(clean)) { seenClean.add(clean); mainTexts.push(clean); } } }); document.querySelectorAll('[title]').forEach(el => { const t = (el.__origTitle || '').trim(); if (t && t.length > 1 && !/^[0-9\s.,:%$#@!*()_\-+=/\\\[\]{}|;'"<>?]+$/.test(t)) { const clean = t.replace(/\s+/g, ' '); const isCached = (cache[t] && cache[t] !== t) || (cache[clean] && cache[clean] !== clean); if (!isCached && !seenClean.has(clean)) { seenClean.add(clean); mainTexts.push(clean); } } }); // Apply any translations already present in cache applyTranslationsToDom(cache); // If all texts on page are already cached, return immediately (0 API calls) const totalPending = mainTexts.length + otherTexts.length; if (totalPending === 0) { return; } // Helper to fetch and apply a single batch async function fetchBatch(batchTexts) { if (!batchTexts || batchTexts.length === 0) return; if (sessionId && sessionId !== activeSessionId) return; try { const resp = await fetch('/api/language/translate-content', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' }, body: JSON.stringify({ texts: batchTexts, target_lang: targetLang }) }); if (sessionId && sessionId !== activeSessionId) return; if (resp.ok) { const data = await resp.json(); if (data && data.translations && Array.isArray(data.translations)) { data.translations.forEach(item => { if (item.original && item.translated) { if (targetLang !== 'en' && item.translated.trim() === item.original.trim()) { return; // skip unchanged } cache[item.original] = item.translated; cache[item.original.trim()] = item.translated; const cleanOrig = item.original.trim().replace(/\s+/g, ' '); cache[cleanOrig] = item.translated; } }); saveStoredCache(targetLang, cache); applyTranslationsToDom(cache); } } } catch (netErr) { console.warn('Batch translation error:', netErr); } } // BATCHING: // Priority 1: Main content on the screen translated first const batchSize = 60; const mainBatches = []; for (let i = 0; i < mainTexts.length; i += batchSize) { mainBatches.push(mainTexts.slice(i, i + batchSize)); } // Priority 2: Header dropdowns, mega-menus & footer const otherBatches = []; for (let i = 0; i < otherTexts.length; i += batchSize) { otherBatches.push(otherTexts.slice(i, i + batchSize)); } // 1. Process first main batch immediately so visible screen changes live! if (mainBatches.length > 0) { await fetchBatch(mainBatches[0]); } // 2. Process remaining main batches in parallel pairs const remainingMain = mainBatches.slice(1); for (let i = 0; i < remainingMain.length; i += 2) { if (sessionId && sessionId !== activeSessionId) return; await Promise.all(remainingMain.slice(i, i + 2).map(b => fetchBatch(b))); } // 3. Process secondary background batches in parallel pairs for (let i = 0; i < otherBatches.length; i += 2) { if (sessionId && sessionId !== activeSessionId) return; await Promise.all(otherBatches.slice(i, i + 2).map(b => fetchBatch(b))); } saveStoredCache(targetLang, cache); applyTranslationsToDom(cache); } async function checkAutoLanguageDetection() { // If user already manually selected a language, never override it if (localStorage.getItem('site_language_manual') === 'true') return; // If auto-detection already ran, do not interrupt if (localStorage.getItem('site_country_detected') === 'true') return; let detected = detectCountryLanguageFromClient(); try { const res = await fetch('/api/language/detect-country'); if (res.ok) { const data = await res.json(); if (data && data.detected_language && data.detected_language !== 'en') { detected = data.detected_language; } } } catch(e) {} localStorage.setItem('site_country_detected', 'true'); if (detected && detected !== DEFAULT_LANG && detected !== currentLang) { await changeLanguage(detected, false, true); } } function initLanguageSelector() { renderLanguageList(); loadSupportedLanguages(); const filterInput = document.getElementById('langSearchFilter'); if (filterInput) { filterInput.addEventListener('input', function() { renderLanguageList(this.value); }); } const resetBtn = document.getElementById('resetLangBtn'); if (resetBtn) { resetBtn.addEventListener('click', function(e) { e.preventDefault(); localStorage.setItem('site_language_manual', 'true'); changeLanguage(DEFAULT_LANG, true); }); } // On Page Load / New Page Navigation: if (currentLang && currentLang !== DEFAULT_LANG) { const activeLabel = document.getElementById('activeLangLabel'); if (activeLabel) activeLabel.textContent = currentLang.toUpperCase(); if (resetBtn) resetBtn.style.display = 'inline'; // 1. Immediately apply any already-cached translations from storage (0ms delay) applyCachedTranslations(currentLang); // 2. If this is a new page and storage does not have translations for it yet, // fetch only the new uncached texts, save to storage, and convert the page! translatePageDOM(currentLang); } // Check country auto-detection if user has not manually chosen a language checkAutoLanguageDetection(); } // Run as early as possible if (currentLang && currentLang !== DEFAULT_LANG) { applyCachedTranslations(currentLang); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initLanguageSelector); } else { initLanguageSelector(); } })(); </script></body></html>