HTML Editor
HTML 0 chars
Preview auto-refresh
HTML error — check your markup
`; // --- Init CodeMirror (syntax-highlighted input) --- const editor = CodeMirror.fromTextArea(editorTextarea, { mode: 'htmlmixed', theme: 'default', lineNumbers: true, tabSize: 2, indentUnit: 2, lineWrapping: false, extraKeys: { "Tab": function(cm) { cm.replaceSelection(" ", "end"); } } }); editor.setValue(DEFAULT_HTML); updateCharCount(); // --- State --- let updateTimeout = null; let errorVisible = false; let isResizing = false; let startX = 0, startY = 0; let startWidth = 0, startHeight = 0; // --- Theme --- function getPreferredTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); const isDark = theme === 'dark'; themeCheckbox.checked = isDark; themeToggleWrap.classList.toggle('active', isDark); localStorage.setItem('endroid-theme', theme); editor.refresh(); // re-render CodeMirror } function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); setTheme(current === 'dark' ? 'light' : 'dark'); } // Load saved theme or system default const savedTheme = localStorage.getItem('endroid-theme'); setTheme(savedTheme || getPreferredTheme()); themeToggleWrap.addEventListener('click', toggleTheme); themeToggleWrap.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleTheme(); } }); // Listen for system theme changes (if user never manually set) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('endroid-theme')) { setTheme(e.matches ? 'dark' : 'light'); } }); // --- Update Preview --- function updatePreview() { const html = editor.getValue(); try { preview.srcdoc = html; hideError(); } catch (e) { preview.srcdoc = `⚠️ Invalid HTML`; showError(); } updateCharCount(); } function scheduleUpdate() { clearTimeout(updateTimeout); updateTimeout = setTimeout(updatePreview, 300); } editor.on('change', scheduleUpdate); // --- Character counter --- function updateCharCount() { const len = editor.getValue().length; charCount.textContent = len + ' chars'; } // --- Error toast --- function showError() { if (!errorVisible) { errorVisible = true; errorToast.classList.add('show'); clearTimeout(errorToast._hide); } } function hideError() { if (errorVisible) { errorVisible = false; errorToast.classList.remove('show'); } } // --- Download --- downloadBtn.addEventListener('click', function() { const content = editor.getValue(); const blob = new Blob([content], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'index.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); // --- Resize divider --- function initResize(e) { isResizing = true; const isMobile = window.innerWidth <= 768; if (isMobile) { startY = e.clientY || e.touches?.[0]?.clientY || 0; startHeight = editorPanel.offsetHeight; } else { startX = e.clientX || e.touches?.[0]?.clientX || 0; startWidth = editorPanel.offsetWidth; } divider.classList.add('active'); document.addEventListener('mousemove', onResize); document.addEventListener('touchmove', onResize, { passive: false }); document.addEventListener('mouseup', stopResize); document.addEventListener('touchend', stopResize); e.preventDefault?.(); } function onResize(e) { if (!isResizing) return; const isMobile = window.innerWidth <= 768; const containerRect = mainContainer.getBoundingClientRect(); if (isMobile) { const currentY = e.clientY || e.touches?.[0]?.clientY || 0; const deltaY = currentY - startY; const newHeight = startHeight + deltaY; const totalHeight = containerRect.height; const minHeight = 80; const clamped = Math.max(minHeight, Math.min(totalHeight - minHeight, newHeight)); const percentage = (clamped / totalHeight) * 100; editorPanel.style.flex = `0 0 ${percentage}%`; previewPanel.style.flex = `1 1 ${100 - percentage}%`; } else { const currentX = e.clientX || e.touches?.[0]?.clientX || 0; const deltaX = currentX - startX; const newWidth = startWidth + deltaX; const totalWidth = containerRect.width; const minWidth = 180; const clamped = Math.max(minWidth, Math.min(totalWidth - minWidth, newWidth)); const percentage = (clamped / totalWidth) * 100; editorPanel.style.flex = `0 0 ${percentage}%`; previewPanel.style.flex = `1 1 ${100 - percentage}%`; } e.preventDefault?.(); editor.refresh(); } function stopResize() { isResizing = false; divider.classList.remove('active'); document.removeEventListener('mousemove', onResize); document.removeEventListener('touchmove', onResize); document.removeEventListener('mouseup', stopResize); document.removeEventListener('touchend', stopResize); editor.refresh(); } divider.addEventListener('mousedown', initResize); divider.addEventListener('touchstart', initResize, { passive: false }); // Reset flex when crossing mobile breakpoint let lastWidth = window.innerWidth; window.addEventListener('resize', () => { const currentWidth = window.innerWidth; if ((lastWidth <= 768 && currentWidth > 768) || (lastWidth > 768 && currentWidth <= 768)) { editorPanel.style.flex = ''; previewPanel.style.flex = ''; } lastWidth = currentWidth; editor.refresh(); }); // --- Initial render --- updatePreview(); setTimeout(() => editor.refresh(), 100); })();