/* =========================== Modern Java — app.js Vanilla JS for search, filters, syntax highlighting, and interactions =========================== */ (() => { 'use strict'; /* ---------- Locale Detection ---------- */ const detectLocale = () => { const path = location.pathname; const match = path.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//); return match ? match[1] : 'en'; }; const locale = detectLocale(); const localePrefix = locale === 'en' ? '' : '/' + locale; /* ---------- Browser Locale Auto-Redirect ---------- */ const autoRedirectLocale = () => { if (locale !== 'en') return; // already on a non-English locale const available = (window.i18n && window.i18n.availableLocales) || []; if (available.length <= 1) return; // Respect explicit user choice (set when using locale picker) const preferred = localStorage.getItem('preferred-locale'); if (preferred === 'en') return; // user explicitly chose English if (preferred && available.includes(preferred)) { window.location.replace('/' + preferred + location.pathname + location.search + location.hash); return; } // Match browser language to available locales const langs = navigator.languages || [navigator.language]; for (const lang of langs) { // Exact match (e.g. pt-BR) if (lang !== 'en' && available.includes(lang)) { window.location.replace('/' + lang + location.pathname + location.search + location.hash); return; } // Prefix match (e.g. pt matches pt-BR) const prefix = lang.split('-')[0]; if (prefix !== 'en') { const match = available.find(l => l.startsWith(prefix + '-') || l === prefix); if (match) { window.location.replace('/' + match + location.pathname + location.search + location.hash); return; } } } }; autoRedirectLocale(); /* ---------- Snippets Data ---------- */ let snippets = []; const loadSnippets = async () => { try { const indexPath = locale === 'en' ? '/data/snippets.json' : '/' + locale + '/data/snippets.json'; const res = await fetch(indexPath); snippets = await res.json(); } catch (e) { console.warn('Could not load snippets.json:', e); } }; /* ========================================================== 1. Search Overlay (⌘K / Ctrl+K) ========================================================== */ const initSearch = () => { const overlay = document.querySelector('.search-overlay'); const cmdBar = document.querySelector('.cmd-bar'); if (!overlay) return; const input = overlay.querySelector('.search-input'); const resultsContainer = overlay.querySelector('.search-results'); let selectedIndex = -1; let visibleResults = []; const openSearch = () => { overlay.classList.add('active'); if (input) { input.value = ''; // Double requestAnimationFrame ensures focus after visibility transition requestAnimationFrame(() => { requestAnimationFrame(() => { input.focus(); }); }); } renderResults(''); }; const closeSearch = () => { overlay.classList.remove('active'); selectedIndex = -1; }; // Fuzzy match: check if query words appear in target string const fuzzyMatch = (query, text) => { const lower = text.toLowerCase(); return query.toLowerCase().split(/\s+/).filter(Boolean) .every(word => lower.includes(word)); }; const renderResults = (query) => { if (!resultsContainer) return; if (!query.trim()) { visibleResults = snippets.slice(0, 12); } else { visibleResults = snippets.filter(s => fuzzyMatch(query, s.title) || fuzzyMatch(query, s.category) || fuzzyMatch(query, s.summary) ); } selectedIndex = visibleResults.length > 0 ? 0 : -1; resultsContainer.innerHTML = visibleResults.map((s, i) => `
${escapeHtml(s.title)}
${escapeHtml(s.summary)}
${s.category}
`).join(''); // Click handlers on results resultsContainer.querySelectorAll('.search-result').forEach(el => { el.addEventListener('click', () => { window.location.href = localePrefix + '/' + el.dataset.category + '/' + el.dataset.slug + '.html'; }); }); }; const updateSelection = () => { const items = resultsContainer.querySelectorAll('.search-result'); items.forEach((el, i) => { el.classList.toggle('selected', i === selectedIndex); }); // Scroll selected into view if (items[selectedIndex]) { items[selectedIndex].scrollIntoView({ block: 'nearest' }); } }; // Keyboard shortcut: ⌘K / Ctrl+K document.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } if (e.key === 'Escape') { closeSearch(); } }); // Cmd-bar click if (cmdBar) { cmdBar.addEventListener('click', openSearch); } // Click backdrop to close overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSearch(); }); // Search input events if (input) { input.addEventListener('input', () => { renderResults(input.value); }); input.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); if (visibleResults.length > 0) { selectedIndex = (selectedIndex + 1) % visibleResults.length; updateSelection(); } } else if (e.key === 'ArrowUp') { e.preventDefault(); if (visibleResults.length > 0) { selectedIndex = (selectedIndex - 1 + visibleResults.length) % visibleResults.length; updateSelection(); } } else if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && visibleResults[selectedIndex]) { window.location.href = localePrefix + '/' + visibleResults[selectedIndex].category + '/' + visibleResults[selectedIndex].slug + '.html'; } } }); } }; /* ========================================================== 2. Category + JDK Dropdown Filters (homepage) ========================================================== */ const initFilters = () => { const cards = document.querySelectorAll('.tip-card'); if (!cards.length) return; let activeCategory = null; let activeJdk = null; // LTS cycle ranges: each entry covers all versions introduced since the previous LTS const LTS_RANGES = { '11': [9, 11], '17': [12, 17], '21': [18, 21], '25': [22, 25] }; const noResultsMsg = document.getElementById('noResultsMessage'); const applyFilters = () => { let visibleCount = 0; cards.forEach(card => { const matchesCategory = !activeCategory || card.dataset.category === activeCategory; let matchesJdk = true; if (activeJdk) { const version = parseInt(card.dataset.jdk, 10); const range = LTS_RANGES[activeJdk]; matchesJdk = range && version >= range[0] && version <= range[1]; } const visible = matchesCategory && matchesJdk; card.classList.toggle('filter-hidden', !visible); if (visible) visibleCount++; }); if (noResultsMsg) { noResultsMsg.style.display = visibleCount === 0 ? '' : 'none'; } if (window.updateViewToggleState) { window.updateViewToggleState(); } }; // Generic helper to wire up a dropdown const initDropdown = (dropdownEl, onSelect) => { if (!dropdownEl) return; const toggleBtn = dropdownEl.querySelector('.jdk-dropdown-toggle'); const labelEl = dropdownEl.querySelector('.jdk-label'); const list = dropdownEl.querySelector('ul'); const openDropdown = () => { list.style.display = 'block'; // Flip dropdown below button if not enough space above const rect = toggleBtn.getBoundingClientRect(); const listHeight = list.offsetHeight; if (rect.top < listHeight + 12) { list.style.bottom = 'auto'; list.style.top = 'calc(100% + 6px)'; } else { list.style.top = 'auto'; list.style.bottom = 'calc(100% + 6px)'; } toggleBtn.setAttribute('aria-expanded', 'true'); }; const closeDropdown = () => { list.style.display = 'none'; toggleBtn.setAttribute('aria-expanded', 'false'); }; const selectItem = (li) => { list.querySelectorAll('li').forEach(l => l.classList.remove('active')); li.classList.add('active'); if (labelEl) labelEl.textContent = li.textContent.trim(); }; toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); list.style.display === 'block' ? closeDropdown() : openDropdown(); }); document.addEventListener('click', closeDropdown); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeDropdown(); }); list.querySelectorAll('li').forEach(li => { li.addEventListener('click', (e) => { e.stopPropagation(); closeDropdown(); selectItem(li); onSelect(li, toggleBtn); }); }); return { closeDropdown, setActive: (value) => { const target = list.querySelector(`li[data-filter="${value}"]`); if (target) { selectItem(target); toggleBtn.classList.toggle('has-filter', value !== 'all'); } }}; }; // Category dropdown const categoryDropdown = document.getElementById('categoryDropdown'); const catDropdownCtrl = initDropdown(categoryDropdown, (li, toggleBtn) => { const category = li.dataset.filter; activeCategory = category !== 'all' ? category : null; toggleBtn.classList.toggle('has-filter', !!activeCategory); if (activeCategory) { history.replaceState(null, '', '#' + activeCategory); } else { history.replaceState(null, '', window.location.pathname + window.location.search); } applyFilters(); }); // JDK dropdown const jdkDropdown = document.getElementById('jdkDropdown'); initDropdown(jdkDropdown, (li, toggleBtn) => { const version = li.dataset.jdkFilter; activeJdk = version !== 'all' ? version : null; toggleBtn.classList.toggle('has-filter', !!activeJdk); applyFilters(); }); // Apply filter from a given category string (or "all" / empty for no filter) const applyHashFilter = (category) => { if (category && catDropdownCtrl) { catDropdownCtrl.setActive(category); activeCategory = category; applyFilters(); const section = document.getElementById('all-comparisons'); if (section) section.scrollIntoView({ behavior: 'smooth' }); } else if (catDropdownCtrl) { catDropdownCtrl.setActive('all'); activeCategory = null; applyFilters(); } }; // On load, apply filter from URL hash or default to "All" applyHashFilter(window.location.hash.slice(1)); // Also react to browser back/forward hash changes window.addEventListener('hashchange', () => { applyHashFilter(window.location.hash.slice(1)); }); }; /* ========================================================== 3. Card Hover / Touch Toggle (homepage) ========================================================== */ const initCardToggle = () => { const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; if (!isTouchDevice) return; // Update hover hints for touch devices document.querySelectorAll('.hover-hint').forEach(hint => { hint.textContent = (window.i18n && window.i18n.touchHint) || '👆 tap or swipe →'; }); document.querySelectorAll('.tip-card').forEach(card => { let touchStartX = 0; let touchStartY = 0; let touchEndX = 0; let touchEndY = 0; // Track touch start card.addEventListener('touchstart', (e) => { // Only track touches on the card-code area if (!e.target.closest('.card-code')) return; touchStartX = e.changedTouches[0].clientX; touchStartY = e.changedTouches[0].clientY; }, { passive: true }); // Handle touch end for swipe or tap // Note: passive:false allows us to preventDefault on tap/swipe while still allowing vertical scrolling card.addEventListener('touchend', (e) => { // Only handle touches on the card-code area if (!e.target.closest('.card-code')) return; // Don't handle touch events when in expanded mode const tipsGrid = document.getElementById('tipsGrid'); if (tipsGrid && tipsGrid.classList.contains('expanded')) { return; } touchEndX = e.changedTouches[0].clientX; touchEndY = e.changedTouches[0].clientY; const deltaX = touchEndX - touchStartX; const deltaY = touchEndY - touchStartY; const absDeltaX = Math.abs(deltaX); const absDeltaY = Math.abs(deltaY); // Determine if it's a swipe (horizontal movement > 50px and more horizontal than vertical) const isHorizontalSwipe = absDeltaX > 50 && absDeltaX > absDeltaY; if (isHorizontalSwipe) { // Prevent default navigation for horizontal swipes e.preventDefault(); // Swipe left = show modern, swipe right = show old if (deltaX < 0) { // Swipe left - show modern card.classList.add('toggled'); } else { // Swipe right - show old card.classList.remove('toggled'); } } else if (absDeltaX < 10 && absDeltaY < 10) { // It's a tap (movement under 10px threshold) e.preventDefault(); card.classList.toggle('toggled'); } // Note: Vertical scrolling (large deltaY, small deltaX) doesn't call preventDefault }, { passive: false }); // Prevent click events on card-code from navigating (touch devices only) // This is a safety net in case touch events trigger click as fallback card.addEventListener('click', (e) => { if (e.target.closest('.card-code')) { // Don't prevent navigation when in expanded mode const tipsGrid = document.getElementById('tipsGrid'); if (tipsGrid && tipsGrid.classList.contains('expanded')) { return; } e.preventDefault(); e.stopPropagation(); } }); }); }; /* ========================================================== 4. Copy-to-Clipboard (article pages) ========================================================== */ const initCopyButtons = () => { document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => { // Find adjacent code block const codeBlock = btn.closest('.code-header')?.nextElementSibling || btn.closest('.compare-panel-header')?.nextElementSibling?.querySelector('pre, code, .code-text') || btn.parentElement?.querySelector('pre, code, .code-text'); if (!codeBlock) return; const text = codeBlock.textContent; navigator.clipboard.writeText(text).then(() => { btn.classList.add('copied'); const original = btn.textContent; btn.textContent = (window.i18n && window.i18n.copied) || 'Copied!'; setTimeout(() => { btn.classList.remove('copied'); btn.textContent = original; }, 2000); }).catch(() => { // Fallback for older browsers const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); btn.classList.add('copied'); const original = btn.textContent; btn.textContent = (window.i18n && window.i18n.copied) || 'Copied!'; setTimeout(() => { btn.classList.remove('copied'); btn.textContent = original; }, 2000); }); }); }); }; /* ========================================================== 5. Syntax Highlighting (Java) ========================================================== */ const JAVA_KEYWORDS = new Set([ 'abstract', 'assert', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extends', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements', 'import', 'instanceof', 'int', 'interface', 'long', 'module', 'native', 'new', 'null', 'package', 'permits', 'private', 'protected', 'public', 'record', 'return', 'sealed', 'short', 'static', 'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'try', 'var', 'void', 'volatile', 'when', 'while', 'yield' ]); const highlightJava = (code) => { const tokens = []; let i = 0; const len = code.length; while (i < len) { // Block comments: /* ... */ if (code[i] === '/' && code[i + 1] === '*') { let end = code.indexOf('*/', i + 2); if (end === -1) end = len - 2; const text = code.slice(i, end + 2); tokens.push(`${escapeHtml(text)}`); i = end + 2; continue; } // Line comments: // ... if (code[i] === '/' && code[i + 1] === '/') { let end = code.indexOf('\n', i); if (end === -1) end = len; const text = code.slice(i, end); tokens.push(`${escapeHtml(text)}`); i = end; continue; } // Text blocks: """ ... """ if (code[i] === '"' && code[i + 1] === '"' && code[i + 2] === '"') { let end = code.indexOf('"""', i + 3); if (end === -1) end = len - 3; const text = code.slice(i, end + 3); tokens.push(`${escapeHtml(text)}`); i = end + 3; continue; } // String literals: "..." if (code[i] === '"') { let j = i + 1; while (j < len && code[j] !== '"') { if (code[j] === '\\') j++; // skip escaped char j++; } const text = code.slice(i, j + 1); tokens.push(`${escapeHtml(text)}`); i = j + 1; continue; } // Char literals: '...' if (code[i] === "'") { let j = i + 1; while (j < len && code[j] !== "'") { if (code[j] === '\\') j++; j++; } const text = code.slice(i, j + 1); tokens.push(`${escapeHtml(text)}`); i = j + 1; continue; } // Annotations: @Word if (code[i] === '@' && i + 1 < len && /[A-Za-z_]/.test(code[i + 1])) { let j = i + 1; while (j < len && /[\w]/.test(code[j])) j++; const text = code.slice(i, j); tokens.push(`${escapeHtml(text)}`); i = j; continue; } // Numbers: digits (including hex, binary, underscores, suffixes) if (/[0-9]/.test(code[i]) && (i === 0 || !/[\w]/.test(code[i - 1]))) { let j = i; // Hex/binary prefix if (code[j] === '0' && (code[j + 1] === 'x' || code[j + 1] === 'X' || code[j + 1] === 'b' || code[j + 1] === 'B')) { j += 2; } while (j < len && /[0-9a-fA-F_]/.test(code[j])) j++; // Decimal part if (code[j] === '.' && /[0-9]/.test(code[j + 1])) { j++; while (j < len && /[0-9_]/.test(code[j])) j++; } // Exponent if (code[j] === 'e' || code[j] === 'E') { j++; if (code[j] === '+' || code[j] === '-') j++; while (j < len && /[0-9_]/.test(code[j])) j++; } // Type suffix (L, f, d) if (/[LlFfDd]/.test(code[j])) j++; const text = code.slice(i, j); tokens.push(`${escapeHtml(text)}`); i = j; continue; } // Words: keywords, types, method calls if (/[A-Za-z_$]/.test(code[i])) { let j = i; while (j < len && /[\w$]/.test(code[j])) j++; const word = code.slice(i, j); // Look ahead for method call: word( let k = j; while (k < len && code[k] === ' ') k++; if (JAVA_KEYWORDS.has(word)) { tokens.push(`${escapeHtml(word)}`); } else if (code[k] === '(' && !/^[A-Z]/.test(word)) { tokens.push(`${escapeHtml(word)}`); } else if (/^[A-Z]/.test(word)) { tokens.push(`${escapeHtml(word)}`); } else { tokens.push(escapeHtml(word)); } i = j; continue; } // Everything else: operators, punctuation, whitespace tokens.push(escapeHtml(code[i])); i++; } return tokens.join(''); }; const initSyntaxHighlighting = () => { document.querySelectorAll('.code-text').forEach(el => { // Skip if already highlighted if (el.dataset.highlighted) return; el.dataset.highlighted = 'true'; const raw = el.textContent; el.innerHTML = highlightJava(raw); }); }; /* ========================================================== 6. Newsletter Form ========================================================== */ const initNewsletter = () => { const form = document.querySelector('.newsletter-form'); if (!form) return; form.addEventListener('submit', (e) => { e.preventDefault(); const box = form.closest('.newsletter-box'); if (box) { box.innerHTML = '

Thanks! 🎉 You\'re on the list.

'; } else { form.innerHTML = '

Thanks!

'; } }); }; /* ========================================================== 6. View Toggle (Expand/Collapse All Cards) ========================================================== */ const initViewToggle = () => { const toggleBtn = document.getElementById('viewToggle'); const tipsGrid = document.getElementById('tipsGrid'); if (!toggleBtn || !tipsGrid) return; let isExpanded = false; const updateButtonState = () => { const visibleCards = document.querySelectorAll('.tip-card:not(.filter-hidden)'); const hasVisibleCards = visibleCards.length > 0; toggleBtn.disabled = !hasVisibleCards; if (!hasVisibleCards) { toggleBtn.style.opacity = '0.5'; toggleBtn.style.cursor = 'not-allowed'; } else { toggleBtn.style.opacity = '1'; toggleBtn.style.cursor = 'pointer'; } }; toggleBtn.addEventListener('click', () => { isExpanded = !isExpanded; if (isExpanded) { tipsGrid.classList.add('expanded'); toggleBtn.querySelector('.view-toggle-icon').textContent = '⊟'; toggleBtn.querySelector('.view-toggle-text').textContent = (window.i18n && window.i18n.collapseAll) || 'Collapse All'; // Remove toggled class from all cards when expanding document.querySelectorAll('.tip-card').forEach(card => { card.classList.remove('toggled'); }); } else { tipsGrid.classList.remove('expanded'); toggleBtn.querySelector('.view-toggle-icon').textContent = '⊞'; toggleBtn.querySelector('.view-toggle-text').textContent = (window.i18n && window.i18n.expandAll) || 'Expand All'; } }); // Check initial state updateButtonState(); // Make updateButtonState available for filter to call window.updateViewToggleState = updateButtonState; }; /* ========================================================== 7. Theme Toggle ========================================================== */ const initThemeToggle = () => { const btn = document.getElementById('themeToggle'); if (!btn) return; const updateButton = (theme) => { btn.textContent = theme === 'dark' ? '☀️' : '🌙'; btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'); }; // The anti-FOUC inline script already applied the theme; just sync the button state updateButton(document.documentElement.getAttribute('data-theme') || 'dark'); btn.addEventListener('click', () => { const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('theme', next); updateButton(next); }); }; /* ========================================================== 8. Locale Picker ========================================================== */ const initLocalePicker = () => { const picker = document.getElementById('localePicker'); if (!picker) return; const toggleBtn = picker.querySelector('.locale-toggle'); const list = picker.querySelector('ul'); const open = () => { list.style.display = 'block'; toggleBtn.setAttribute('aria-expanded', 'true'); }; const close = () => { list.style.display = 'none'; toggleBtn.setAttribute('aria-expanded', 'false'); }; toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); list.style.display === 'block' ? close() : open(); }); document.addEventListener('click', close); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); list.querySelectorAll('li').forEach(li => { li.addEventListener('click', (e) => { e.stopPropagation(); const targetLocale = li.dataset.locale; if (targetLocale === locale) { close(); return; } // Rewrite current path for the target locale let path = location.pathname; // Strip current locale prefix if present if (locale !== 'en') { path = path.replace(new RegExp('^/' + locale.replace('-', '\\-')), ''); } // Add target locale prefix if (targetLocale !== 'en') { path = '/' + targetLocale + path; } localStorage.setItem('preferred-locale', targetLocale); window.location.href = path; }); }); }; /* ========================================================== 9. Contribute Dropdown ========================================================== */ const initContributeDropdown = () => { const dropdown = document.getElementById('contributeDropdown'); if (!dropdown) return; const toggleBtn = dropdown.querySelector('.contribute-toggle'); const list = dropdown.querySelector('ul'); const open = () => { list.style.display = 'block'; toggleBtn.setAttribute('aria-expanded', 'true'); }; const close = () => { list.style.display = 'none'; toggleBtn.setAttribute('aria-expanded', 'false'); }; toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); list.style.display === 'block' ? close() : open(); }); document.addEventListener('click', close); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); }; /* ========================================================== Utilities ========================================================== */ const escapeHtml = (str) => { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }; /* ========================================================== Init ========================================================== */ document.addEventListener('DOMContentLoaded', () => { loadSnippets().then(() => { initSearch(); }); initFilters(); initCardToggle(); initViewToggle(); initCopyButtons(); initSyntaxHighlighting(); initNewsletter(); initThemeToggle(); initLocalePicker(); initContributeDropdown(); }); })();