Make a Microsoft Word like web app with the possibility to write the document, customise it with colors, font, size and simple format, and save documents or export pdf to computer locally
ca946d2 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WordWizard - Online Document Editor</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
| <style> | |
| #editor { | |
| min-height: calc(100vh - 120px); | |
| padding: 2rem; | |
| outline: none; | |
| } | |
| .tooltip { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .tooltip .tooltiptext { | |
| visibility: hidden; | |
| width: 120px; | |
| background-color: #333; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -60px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltiptext { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| @media (max-width: 768px) { | |
| #toolbar { | |
| flex-wrap: wrap; | |
| height: auto; | |
| padding: 0.5rem; | |
| } | |
| .tool-group { | |
| margin-bottom: 0.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="flex flex-col h-screen"> | |
| <!-- Header --> | |
| <header class="bg-blue-600 text-white shadow-md"> | |
| <div class="container mx-auto px-4 py-3 flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <i data-feather="file-text" class="w-6 h-6"></i> | |
| <h1 class="text-xl font-bold">WordWizard</h1> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <button id="saveBtn" class="flex items-center space-x-1 px-3 py-1 bg-blue-700 rounded hover:bg-blue-800 transition"> | |
| <i data-feather="save" class="w-4 h-4"></i> | |
| <span>Save</span> | |
| </button> | |
| <button id="exportPdfBtn" class="flex items-center space-x-1 px-3 py-1 bg-green-600 rounded hover:bg-green-700 transition"> | |
| <i data-feather="download" class="w-4 h-4"></i> | |
| <span>Export PDF</span> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Toolbar --> | |
| <div id="toolbar" class="bg-white border-b border-gray-200 px-4 py-2 flex items-center space-x-4 overflow-x-auto"> | |
| <!-- File group --> | |
| <div class="tool-group flex items-center space-x-2"> | |
| <button id="newDocBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="file" class="w-5 h-5"></i> | |
| <span class="tooltiptext">New Document</span> | |
| </button> | |
| </div> | |
| <!-- Format group --> | |
| <div class="tool-group flex items-center space-x-2 border-l border-gray-300 pl-4"> | |
| <select id="fontFamily" class="border border-gray-300 rounded px-2 py-1 text-sm bg-white"> | |
| <option value="Arial">Arial</option> | |
| <option value="Times New Roman">Times New Roman</option> | |
| <option value="Courier New">Courier New</option> | |
| <option value="Georgia">Georgia</option> | |
| <option value="Verdana">Verdana</option> | |
| </select> | |
| <select id="fontSize" class="border border-gray-300 rounded px-2 py-1 text-sm bg-white"> | |
| <option value="1">8</option> | |
| <option value="2">10</option> | |
| <option value="3">12</option> | |
| <option value="4">14</option> | |
| <option value="5">18</option> | |
| <option value="6">24</option> | |
| <option value="7">36</option> | |
| </select> | |
| <button id="boldBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="bold" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Bold</span> | |
| </button> | |
| <button id="italicBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="italic" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Italic</span> | |
| </button> | |
| <button id="underlineBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="underline" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Underline</span> | |
| </button> | |
| </div> | |
| <!-- Alignment group --> | |
| <div class="tool-group flex items-center space-x-2 border-l border-gray-300 pl-4"> | |
| <button id="alignLeftBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="align-left" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Align Left</span> | |
| </button> | |
| <button id="alignCenterBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="align-center" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Align Center</span> | |
| </button> | |
| <button id="alignRightBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="align-right" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Align Right</span> | |
| </button> | |
| <button id="justifyBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="align-justify" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Justify</span> | |
| </button> | |
| </div> | |
| <!-- Color group --> | |
| <div class="tool-group flex items-center space-x-2 border-l border-gray-300 pl-4"> | |
| <div class="relative"> | |
| <input type="color" id="textColor" class="w-8 h-8 border border-gray-300 rounded cursor-pointer" value="#000000"> | |
| <span class="tooltiptext">Text Color</span> | |
| </div> | |
| <div class="relative"> | |
| <input type="color" id="highlightColor" class="w-8 h-8 border border-gray-300 rounded cursor-pointer" value="#ffffff"> | |
| <span class="tooltiptext">Highlight Color</span> | |
| </div> | |
| </div> | |
| <!-- List group --> | |
| <div class="tool-group flex items-center space-x-2 border-l border-gray-300 pl-4"> | |
| <button id="bulletListBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="list" class="w-5 h-5"></i> | |
| <span class="tooltiptext">Bullet List</span> | |
| </button> | |
| <button id="numberedListBtn" class="tooltip p-2 rounded hover:bg-gray-100"> | |
| <i data-feather="list" class="w-5 h-5 rotate-90"></i> | |
| <span class="tooltiptext">Numbered List</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Editor --> | |
| <div id="editor" class="flex-1 bg-white shadow-inner" contenteditable="true"> | |
| <p style="font-size: 14pt; line-height: 1.5;">Start typing here...</p> | |
| </div> | |
| <!-- Status bar --> | |
| <footer class="bg-gray-100 border-t border-gray-200 px-4 py-2 text-sm text-gray-600"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <div>Word count: <span id="wordCount">0</span></div> | |
| <div>Characters: <span id="charCount">0</span></div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // Initialize feather icons | |
| feather.replace(); | |
| // Document properties | |
| let currentDocument = { | |
| id: Date.now().toString(), | |
| title: 'Untitled Document', | |
| content: '' | |
| }; | |
| // Editor functionality | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const editor = document.getElementById('editor'); | |
| const wordCount = document.getElementById('wordCount'); | |
| const charCount = document.getElementById('charCount'); | |
| // Formatting buttons | |
| document.getElementById('boldBtn').addEventListener('click', () => document.execCommand('bold', false, null)); | |
| document.getElementById('italicBtn').addEventListener('click', () => document.execCommand('italic', false, null)); | |
| document.getElementById('underlineBtn').addEventListener('click', () => document.execCommand('underline', false, null)); | |
| // Alignment buttons | |
| document.getElementById('alignLeftBtn').addEventListener('click', () => document.execCommand('justifyLeft', false, null)); | |
| document.getElementById('alignCenterBtn').addEventListener('click', () => document.execCommand('justifyCenter', false, null)); | |
| document.getElementById('alignRightBtn').addEventListener('click', () => document.execCommand('justifyRight', false, null)); | |
| document.getElementById('justifyBtn').addEventListener('click', () => document.execCommand('justifyFull', false, null)); | |
| // List buttons | |
| document.getElementById('bulletListBtn').addEventListener('click', () => document.execCommand('insertUnorderedList', false, null)); | |
| document.getElementById('numberedListBtn').addEventListener('click', () => document.execCommand('insertOrderedList', false, null)); | |
| // Font family | |
| document.getElementById('fontFamily').addEventListener('change', function() { | |
| document.execCommand('fontName', false, this.value); | |
| }); | |
| // Font size | |
| document.getElementById('fontSize').addEventListener('change', function() { | |
| const sizes = ['8px', '10px', '12px', '14px', '18px', '24px', '36px']; | |
| document.execCommand('fontSize', false, this.value); | |
| // Workaround for consistent font size | |
| const fontElements = editor.querySelectorAll('font[size="' + this.value + '"]'); | |
| fontElements.forEach(el => { | |
| el.removeAttribute('size'); | |
| el.style.fontSize = sizes[parseInt(this.value) - 1]; | |
| }); | |
| }); | |
| // Text color | |
| document.getElementById('textColor').addEventListener('input', function() { | |
| document.execCommand('foreColor', false, this.value); | |
| }); | |
| // Highlight color | |
| document.getElementById('highlightColor').addEventListener('input', function() { | |
| document.execCommand('hiliteColor', false, this.value); | |
| }); | |
| // New document | |
| document.getElementById('newDocBtn').addEventListener('click', () => { | |
| if (confirm('Are you sure you want to create a new document? All unsaved changes will be lost.')) { | |
| editor.innerHTML = '<p style="font-size: 14pt; line-height: 1.5;">Start typing here...</p>'; | |
| updateCounts(); | |
| } | |
| }); | |
| // Save document | |
| document.getElementById('saveBtn').addEventListener('click', () => { | |
| currentDocument.content = editor.innerHTML; | |
| const blob = new Blob([JSON.stringify(currentDocument)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `${currentDocument.title}.wordwizard`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| // Show success message | |
| alert('Document saved successfully!'); | |
| }); | |
| // Export PDF | |
| document.getElementById('exportPdfBtn').addEventListener('click', () => { | |
| const { jsPDF } = window.jspdf; | |
| const doc = new jsPDF(); | |
| html2canvas(editor).then(canvas => { | |
| const imgData = canvas.toDataURL('image/png'); | |
| const imgWidth = doc.internal.pageSize.getWidth() - 20; | |
| const imgHeight = (canvas.height * imgWidth) / canvas.width; | |
| doc.addImage(imgData, 'PNG', 10, 10, imgWidth, imgHeight); | |
| doc.save(`${currentDocument.title || 'document'}.pdf`); | |
| }); | |
| }); | |
| // Update word and character counts | |
| function updateCounts() { | |
| const text = editor.innerText || ''; | |
| const words = text.trim() ? text.trim().split(/\s+/).length : 0; | |
| const chars = text.length; | |
| wordCount.textContent = words; | |
| charCount.textContent = chars; | |
| } | |
| // Listen for changes in the editor | |
| editor.addEventListener('input', updateCounts); | |
| updateCounts(); | |
| // Load document if one exists in localStorage | |
| if (localStorage.getItem('lastDocument')) { | |
| try { | |
| const savedDoc = JSON.parse(localStorage.getItem('lastDocument')); | |
| editor.innerHTML = savedDoc.content; | |
| currentDocument = savedDoc; | |
| updateCounts(); | |
| } catch (e) { | |
| console.error('Failed to load last document:', e); | |
| } | |
| } | |
| // Auto-save every 30 seconds | |
| setInterval(() => { | |
| currentDocument.content = editor.innerHTML; | |
| localStorage.setItem('lastDocument', JSON.stringify(currentDocument)); | |
| }, 30000); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |