| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>TextCraft Pro - Ultimate</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.12/marked.min.js"></script> |
| |
| <script> |
| tailwind.config = { |
| darkMode: 'class', |
| theme: { |
| extend: { |
| colors: { |
| darkbg: '#121212', |
| lightbg: '#ffffff', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| |
| ::-webkit-scrollbar { width: 8px; height: 8px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } |
| .dark ::-webkit-scrollbar-thumb { background: #475569; } |
| |
| |
| @media print { |
| body { background: white; color: black; } |
| .no-print { display: none !important; } |
| #markdownPreview { display: block !important; width: 100% !important; border: none; } |
| } |
| </style> |
| </head> |
| <body class="bg-lightbg text-black dark:bg-darkbg dark:text-white transition-colors duration-300 h-screen flex flex-col overflow-hidden"> |
|
|
| <header class="flex-none px-6 py-3 border-b border-gray-300 dark:border-gray-700 flex justify-between items-center bg-white dark:bg-darkbg z-10"> |
| <div class="flex items-center gap-2"> |
| <h1 class="text-xl font-bold tracking-tight text-fuchsia-600 dark:text-fuchsia-400">TextCraft Pro</h1> |
| <span class="text-sm px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300">v3.0 (Fixed)</span> |
| </div> |
| |
| <div class="flex items-center gap-3"> |
| <button id="themeToggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"> |
| <i data-feather="moon" class="w-5 h-5"></i> |
| </button> |
| <button id="fullscreenBtn" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors hidden md:block"> |
| <i data-feather="maximize-2" class="w-5 h-5"></i> |
| </button> |
| </div> |
| </header> |
|
|
| <div class="flex-none px-4 py-2 bg-gray-50 dark:bg-[#1a1a1a] border-b border-gray-300 dark:border-gray-700 flex flex-wrap gap-2 items-center justify-between"> |
| |
| <div class="flex items-center gap-4"> |
| <div class="flex items-center cursor-pointer select-none"> |
| <input type="checkbox" id="modeToggle" class="sr-only peer"> |
| <label for="modeToggle" class="relative w-10 h-5 bg-gray-300 peer-focus:outline-none rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-fuchsia-600 cursor-pointer"></label> |
| <span id="modeLabel" class="ml-2 text-sm font-medium">Plain Text</span> |
| </div> |
| |
| <button id="previewToggle" class="hidden text-sm flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-fuchsia-500"> |
| <i data-feather="eye" class="w-4 h-4"></i> <span class="hidden sm:inline">Preview</span> |
| </button> |
| </div> |
|
|
| <div class="flex items-center gap-1 bg-gray-200 dark:bg-gray-800 rounded-lg p-1"> |
| <button id="undoBtn" class="p-1.5 rounded hover:bg-white dark:hover:bg-gray-700 disabled:opacity-30" title="Undo"> |
| <i data-feather="rotate-ccw" class="w-4 h-4"></i> |
| </button> |
| <button id="redoBtn" class="p-1.5 rounded hover:bg-white dark:hover:bg-gray-700 disabled:opacity-30" title="Redo"> |
| <i data-feather="rotate-cw" class="w-4 h-4"></i> |
| </button> |
| <div class="w-px h-4 bg-gray-400 dark:bg-gray-600 mx-1"></div> |
| <button id="copyBtn" class="p-1.5 rounded hover:bg-white dark:hover:bg-gray-700" title="Copy"> |
| <i data-feather="copy" class="w-4 h-4"></i> |
| </button> |
| <button id="pasteBtn" class="p-1.5 rounded hover:bg-white dark:hover:bg-gray-700" title="Paste"> |
| <i data-feather="clipboard" class="w-4 h-4"></i> |
| </button> |
| <button id="clearBtn" class="p-1.5 rounded hover:bg-white dark:hover:bg-gray-700 text-red-500" title="Clear All"> |
| <i data-feather="trash-2" class="w-4 h-4"></i> |
| </button> |
| </div> |
|
|
| <div class="flex items-center gap-3"> |
| <button id="newDocBtn" class="text-sm flex items-center gap-1 hover:text-fuchsia-500 transition-colors"> |
| <i data-feather="file-plus" class="w-4 h-4"></i> <span class="hidden sm:inline">New</span> |
| </button> |
| <div id="wordCountBtn" class="text-xs px-2 py-1 bg-white dark:bg-gray-800 border dark:border-gray-700 rounded text-gray-500 dark:text-gray-400 font-mono"> |
| <span id="wordCount">0</span> w | <span id="charCount">0</span> c |
| </div> |
| </div> |
| </div> |
|
|
| <main class="flex-1 flex overflow-hidden relative"> |
| <div class="flex-1 h-full relative"> |
| <div id="textEditor" |
| contenteditable="true" |
| spellcheck="false" |
| class="w-full h-full p-6 text-lg font-mono outline-none overflow-y-auto resize-none bg-lightbg text-black dark:bg-darkbg dark:text-white placeholder:text-gray-400" |
| placeholder="Start typing..."></div> |
| </div> |
|
|
| <div id="markdownPreview" class="hidden flex-1 h-full p-8 overflow-y-auto border-l border-gray-200 dark:border-gray-700 prose prose-slate dark:prose-invert max-w-none bg-gray-50 dark:bg-[#151515]"> |
| </div> |
| </main> |
|
|
| <footer class="flex-none p-2 bg-gray-50 dark:bg-[#1a1a1a] border-t border-gray-300 dark:border-gray-700 flex justify-center gap-2"> |
| <button id="exportTxtBtn" class="flex items-center gap-2 px-4 py-1.5 text-sm rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-fuchsia-50 dark:hover:bg-gray-700 transition-all shadow-sm"> |
| <span class="font-bold text-fuchsia-600 dark:text-fuchsia-400">TXT</span> <i data-feather="download" class="w-3 h-3"></i> |
| </button> |
| <button id="exportMdBtn" class="flex items-center gap-2 px-4 py-1.5 text-sm rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-fuchsia-50 dark:hover:bg-gray-700 transition-all shadow-sm"> |
| <span class="font-bold text-fuchsia-600 dark:text-fuchsia-400">MD</span> <i data-feather="download" class="w-3 h-3"></i> |
| </button> |
| <button id="exportPdfBtn" class="flex items-center gap-2 px-4 py-1.5 text-sm rounded-md bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-fuchsia-50 dark:hover:bg-gray-700 transition-all shadow-sm"> |
| <span class="font-bold text-fuchsia-600 dark:text-fuchsia-400">PDF</span> <i data-feather="download" class="w-3 h-3"></i> |
| </button> |
| </footer> |
|
|
| <div id="pdf-container" style="display: none;"></div> |
|
|
| <script> |
| feather.replace(); |
| |
| |
| const textEditor = document.getElementById('textEditor'); |
| const markdownPreview = document.getElementById('markdownPreview'); |
| const modeToggle = document.getElementById('modeToggle'); |
| const modeLabel = document.getElementById('modeLabel'); |
| const previewToggle = document.getElementById('previewToggle'); |
| const wordCountBtn = document.getElementById('wordCountBtn'); |
| const wordCount = document.getElementById('wordCount'); |
| const charCount = document.getElementById('charCount'); |
| const themeToggle = document.getElementById('themeToggle'); |
| const fullscreenBtn = document.getElementById('fullscreenBtn'); |
| const pasteBtn = document.getElementById('pasteBtn'); |
| const clearBtn = document.getElementById('clearBtn'); |
| const copyBtn = document.getElementById('copyBtn'); |
| const undoBtn = document.getElementById('undoBtn'); |
| const redoBtn = document.getElementById('redoBtn'); |
| const newDocBtn = document.getElementById('newDocBtn'); |
| const exportTxtBtn = document.getElementById('exportTxtBtn'); |
| const exportMdBtn = document.getElementById('exportMdBtn'); |
| const exportPdfBtn = document.getElementById('exportPdfBtn'); |
| |
| |
| let isMarkdownMode = false; |
| let isPreviewVisible = true; |
| let editHistory = []; |
| let historyPointer = -1; |
| let isDarkMode = document.documentElement.classList.contains('dark'); |
| let debounceTimer; |
| |
| function initEditor() { |
| const savedContent = localStorage.getItem('textcraftContent'); |
| const savedMode = localStorage.getItem('textcraftMode'); |
| if (savedContent) textEditor.innerHTML = savedContent; |
| if (savedMode === 'markdown') { |
| modeToggle.checked = true; |
| toggleMarkdownMode(true); |
| } |
| updateCounts(); |
| |
| if(savedContent) updatePreview(true); |
| saveToHistory(); |
| } |
| |
| function toggleMarkdownMode(force = null) { |
| if (force !== null) { |
| isMarkdownMode = force; |
| modeToggle.checked = isMarkdownMode; |
| } else { |
| isMarkdownMode = modeToggle.checked; |
| } |
| |
| if (isMarkdownMode) { |
| modeLabel.textContent = 'Markdown'; |
| previewToggle.classList.remove('hidden'); |
| markdownPreview.classList.remove('hidden'); |
| textEditor.parentElement.classList.add('w-1/2'); |
| localStorage.setItem('textcraftMode', 'markdown'); |
| updatePreview(true); |
| } else { |
| modeLabel.textContent = 'Plain Text'; |
| previewToggle.classList.add('hidden'); |
| markdownPreview.classList.add('hidden'); |
| textEditor.parentElement.classList.remove('w-1/2'); |
| localStorage.setItem('textcraftMode', 'plaintext'); |
| } |
| } |
| |
| function togglePreview() { |
| isPreviewVisible = !isPreviewVisible; |
| if(isPreviewVisible) { |
| markdownPreview.classList.remove('hidden'); |
| textEditor.parentElement.classList.remove('w-full'); |
| updatePreview(true); |
| } else { |
| markdownPreview.classList.add('hidden'); |
| textEditor.parentElement.classList.add('w-full'); |
| } |
| previewToggle.innerHTML = isPreviewVisible ? |
| '<i data-feather="eye" class="w-4 h-4 mr-1 inline"></i> <span class="hidden sm:inline">Preview</span>' : |
| '<i data-feather="eye-off" class="w-4 h-4 mr-1 inline"></i> <span class="hidden sm:inline">Editor Only</span>'; |
| feather.replace(); |
| } |
| |
| function updateCounts() { |
| const text = textEditor.innerText; |
| const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; |
| const chars = text.length; |
| wordCount.textContent = words.toLocaleString(); |
| charCount.textContent = chars.toLocaleString(); |
| } |
| |
| |
| function handleInput() { |
| updateCounts(); |
| saveContent(); |
| |
| |
| clearTimeout(debounceTimer); |
| |
| |
| |
| debounceTimer = setTimeout(() => { |
| updatePreview(); |
| }, 300); |
| } |
| |
| function updatePreview(force = false) { |
| if (isMarkdownMode || force) { |
| |
| const rawText = textEditor.innerText; |
| const renderedHTML = marked.parse(rawText); |
| markdownPreview.innerHTML = renderedHTML; |
| } |
| } |
| |
| function saveContent() { |
| localStorage.setItem('textcraftContent', textEditor.innerHTML); |
| } |
| |
| function saveToHistory() { |
| if (historyPointer < editHistory.length - 1) { |
| editHistory = editHistory.slice(0, historyPointer + 1); |
| } |
| editHistory.push(textEditor.innerHTML); |
| historyPointer++; |
| if (editHistory.length > 50) { |
| editHistory.shift(); |
| historyPointer--; |
| } |
| updateUndoRedoButtons(); |
| } |
| |
| function undo() { |
| if (historyPointer > 0) { |
| historyPointer--; |
| textEditor.innerHTML = editHistory[historyPointer]; |
| updateCounts(); |
| updatePreview(true); |
| updateUndoRedoButtons(); |
| } |
| } |
| |
| function redo() { |
| if (historyPointer < editHistory.length - 1) { |
| historyPointer++; |
| textEditor.innerHTML = editHistory[historyPointer]; |
| updateCounts(); |
| updatePreview(true); |
| updateUndoRedoButtons(); |
| } |
| } |
| |
| function updateUndoRedoButtons() { |
| undoBtn.disabled = historyPointer <= 0; |
| redoBtn.disabled = historyPointer >= editHistory.length - 1; |
| undoBtn.style.opacity = undoBtn.disabled ? '0.3' : '1'; |
| redoBtn.style.opacity = redoBtn.disabled ? '0.3' : '1'; |
| } |
| |
| function toggleTheme() { |
| isDarkMode = !isDarkMode; |
| if (isDarkMode) { |
| document.documentElement.classList.add('dark'); |
| themeToggle.innerHTML = '<i data-feather="sun" class="w-5 h-5"></i>'; |
| } else { |
| document.documentElement.classList.remove('dark'); |
| themeToggle.innerHTML = '<i data-feather="moon" class="w-5 h-5"></i>'; |
| } |
| localStorage.setItem('textcraftTheme', isDarkMode ? 'dark' : 'light'); |
| feather.replace(); |
| } |
| |
| function toggleFullscreen() { |
| if (!document.fullscreenElement) { |
| document.documentElement.requestFullscreen().catch(err => { |
| console.error(`Error: ${err.message}`); |
| }); |
| fullscreenBtn.innerHTML = '<i data-feather="minimize-2" class="w-5 h-5"></i>'; |
| } else { |
| if (document.exitFullscreen) { |
| document.exitFullscreen(); |
| fullscreenBtn.innerHTML = '<i data-feather="maximize-2" class="w-5 h-5"></i>'; |
| } |
| } |
| feather.replace(); |
| } |
| |
| async function pasteText() { |
| try { |
| const text = await navigator.clipboard.readText(); |
| textEditor.focus(); |
| document.execCommand('insertText', false, text); |
| saveToHistory(); |
| handleInput(); |
| } catch (err) { |
| alert('Use Ctrl+V'); |
| } |
| } |
| |
| function clearEditor() { |
| if (confirm('Clear everything?')) { |
| textEditor.innerHTML = ''; |
| saveToHistory(); |
| handleInput(); |
| updatePreview(true); |
| } |
| } |
| |
| async function copyText() { |
| try { |
| await navigator.clipboard.writeText(textEditor.innerText); |
| const originalHTML = copyBtn.innerHTML; |
| copyBtn.innerHTML = '<i data-feather="check" class="w-4 h-4 text-green-500"></i>'; |
| setTimeout(() => { |
| copyBtn.innerHTML = originalHTML; |
| feather.replace(); |
| }, 2000); |
| feather.replace(); |
| } catch (err) { |
| alert('Use Ctrl+C'); |
| } |
| } |
| |
| function newDocument() { |
| if (textEditor.innerText.trim() === '' || confirm('Start new document? Unsaved changes will be lost.')) { |
| textEditor.innerHTML = ''; |
| saveToHistory(); |
| handleInput(); |
| updatePreview(true); |
| } |
| } |
| |
| function exportAsTxt() { |
| const text = textEditor.innerText; |
| const blob = new Blob([text], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `textcraft-export.txt`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
| |
| function exportAsMd() { |
| const text = textEditor.innerText; |
| const blob = new Blob([text], { type: 'text/markdown' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `textcraft-export.md`; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
| |
| |
| function exportAsPdf() { |
| const originalBtnText = exportPdfBtn.innerHTML; |
| exportPdfBtn.innerHTML = '<span class="animate-pulse">Saving...</span>'; |
| |
| |
| |
| let contentToPrint; |
| |
| |
| const rawText = textEditor.innerText; |
| const renderedHTML = marked.parse(rawText); |
| |
| |
| const element = document.createElement('div'); |
| element.innerHTML = renderedHTML; |
| |
| |
| |
| element.className = 'prose prose-slate max-w-none p-8 bg-white text-black'; |
| element.style.fontFamily = 'ui-sans-serif, system-ui, sans-serif'; |
| element.style.fontSize = '12pt'; |
| element.style.lineHeight = '1.5'; |
| |
| |
| const opt = { |
| margin: [10, 15, 10, 15], |
| filename: 'textcraft-document.pdf', |
| image: { type: 'jpeg', quality: 0.98 }, |
| html2canvas: { scale: 2, useCORS: true }, |
| jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, |
| pagebreak: { mode: ['avoid-all', 'css', 'legacy'] } |
| }; |
| |
| |
| html2pdf().set(opt).from(element).save().then(() => { |
| exportPdfBtn.innerHTML = originalBtnText; |
| feather.replace(); |
| }).catch(err => { |
| console.error(err); |
| alert('Error generating PDF. For very large files, try using Ctrl+P and "Save as PDF".'); |
| exportPdfBtn.innerHTML = originalBtnText; |
| feather.replace(); |
| }); |
| } |
| |
| |
| textEditor.addEventListener('input', handleInput); |
| |
| textEditor.addEventListener('keydown', (e) => { |
| if (e.key === ' ' || e.key === 'Enter') setTimeout(saveToHistory, 500); |
| }); |
| modeToggle.addEventListener('change', () => toggleMarkdownMode()); |
| previewToggle.addEventListener('click', togglePreview); |
| themeToggle.addEventListener('click', toggleTheme); |
| fullscreenBtn.addEventListener('click', toggleFullscreen); |
| pasteBtn.addEventListener('click', pasteText); |
| clearBtn.addEventListener('click', clearEditor); |
| copyBtn.addEventListener('click', copyText); |
| undoBtn.addEventListener('click', undo); |
| redoBtn.addEventListener('click', redo); |
| newDocBtn.addEventListener('click', newDocument); |
| exportTxtBtn.addEventListener('click', exportAsTxt); |
| exportMdBtn.addEventListener('click', exportAsMd); |
| exportPdfBtn.addEventListener('click', exportAsPdf); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undo(); } |
| if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) { e.preventDefault(); redo(); } |
| if (e.ctrlKey && e.key === 's') { e.preventDefault(); saveContent(); } |
| |
| if (e.ctrlKey && e.key === 'p') { |
| e.preventDefault(); |
| |
| |
| window.print(); |
| } |
| }); |
| |
| |
| const savedTheme = localStorage.getItem('textcraftTheme'); |
| if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { |
| document.documentElement.classList.add('dark'); |
| themeToggle.innerHTML = '<i data-feather="sun" class="w-5 h-5"></i>'; |
| } else { |
| document.documentElement.classList.remove('dark'); |
| themeToggle.innerHTML = '<i data-feather="moon" class="w-5 h-5"></i>'; |
| } |
| |
| initEditor(); |
| </script> |
| </body> |
| </html> |