DocEditor / index.html
ThorAILabs's picture
Update index.html
1310834 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocClone - Google Docs Clone</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/docx/7.1.0/docx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.editor {
min-height: calc(100vh - 120px);
}
.document-item:hover {
background-color: #f3f4f6;
}
.document-item.active {
background-color: #e5e7eb;
}
#sidebar {
transition: all 0.3s ease;
}
@media (max-width: 768px) {
#sidebar {
position: fixed;
left: -100%;
top: 0;
z-index: 50;
height: 100vh;
width: 80%;
}
#sidebar.open {
left: 0;
}
#overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 40;
}
#overlay.open {
display: block;
}
}
</style>
</head>
<body class="bg-gray-50">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="flex items-center justify-between px-4 py-2">
<div class="flex items-center space-x-4">
<button id="menu-btn" class="md:hidden text-gray-600">
<i class="fas fa-bars text-xl"></i>
</button>
<div class="flex items-center">
<i class="fas fa-file-word text-blue-500 text-2xl mr-2"></i>
<h1 class="text-xl font-bold text-gray-800">DocClone</h1>
</div>
</div>
<div class="flex items-center space-x-4">
<button id="save-btn" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition">
<i class="fas fa-save mr-1"></i> Save
</button>
<button id="export-btn" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition">
<i class="fas fa-file-export mr-1"></i> Export DOCX
</button>
<div class="relative">
<button id="new-doc-btn" class="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 transition">
<i class="fas fa-plus mr-1"></i> New
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<div class="flex">
<!-- Sidebar -->
<div id="sidebar" class="bg-white w-64 h-screen shadow-md overflow-y-auto">
<div class="p-4 border-b">
<h2 class="text-lg font-semibold text-gray-700">My Documents</h2>
</div>
<div id="documents-list" class="p-2">
<!-- Documents will be loaded here -->
</div>
</div>
<!-- Overlay for mobile -->
<div id="overlay" class=""></div>
<!-- Editor -->
<div class="flex-1">
<div class="bg-white shadow-sm p-4">
<div class="flex items-center space-x-4 overflow-x-auto">
<select id="font-family" class="px-2 py-1 border rounded">
<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="font-size" class="px-2 py-1 border rounded">
<option value="1">8pt</option>
<option value="2">10pt</option>
<option value="3">12pt</option>
<option value="4">14pt</option>
<option value="5">18pt</option>
<option value="6">24pt</option>
<option value="7">36pt</option>
</select>
<button id="bold-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-bold"></i>
</button>
<button id="italic-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-italic"></i>
</button>
<button id="underline-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-underline"></i>
</button>
<div class="border-l h-6 mx-2"></div>
<button id="align-left-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-align-left"></i>
</button>
<button id="align-center-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-align-center"></i>
</button>
<button id="align-right-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-align-right"></i>
</button>
<div class="border-l h-6 mx-2"></div>
<button id="list-ul-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-list-ul"></i>
</button>
<button id="list-ol-btn" class="p-1 rounded hover:bg-gray-100">
<i class="fas fa-list-ol"></i>
</button>
</div>
</div>
<div class="p-4">
<div id="editor" class="editor bg-white border rounded p-6 shadow-inner focus:outline-none" contenteditable="true"></div>
</div>
</div>
</div>
<!-- Document Name Modal -->
<div id="doc-name-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-lg p-6 w-96">
<h3 class="text-lg font-semibold mb-4">Name your document</h3>
<input type="text" id="doc-name-input" class="w-full px-3 py-2 border rounded mb-4" placeholder="Document name">
<div class="flex justify-end space-x-2">
<button id="cancel-doc-btn" class="px-4 py-2 border rounded hover:bg-gray-100">Cancel</button>
<button id="confirm-doc-btn" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Create</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const editor = document.getElementById('editor');
const documentsList = document.getElementById('documents-list');
const saveBtn = document.getElementById('save-btn');
const exportBtn = document.getElementById('export-btn');
const newDocBtn = document.getElementById('new-doc-btn');
const menuBtn = document.getElementById('menu-btn');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
const docNameModal = document.getElementById('doc-name-modal');
const docNameInput = document.getElementById('doc-name-input');
const confirmDocBtn = document.getElementById('confirm-doc-btn');
const cancelDocBtn = document.getElementById('cancel-doc-btn');
// Formatting buttons
const boldBtn = document.getElementById('bold-btn');
const italicBtn = document.getElementById('italic-btn');
const underlineBtn = document.getElementById('underline-btn');
const alignLeftBtn = document.getElementById('align-left-btn');
const alignCenterBtn = document.getElementById('align-center-btn');
const alignRightBtn = document.getElementById('align-right-btn');
const listUlBtn = document.getElementById('list-ul-btn');
const listOlBtn = document.getElementById('list-ol-btn');
const fontFamily = document.getElementById('font-family');
const fontSize = document.getElementById('font-size');
// State
let currentDocId = null;
let documents = [];
// Initialize
loadDocuments();
if (documents.length > 0) {
loadDocument(documents[0].id);
} else {
createNewDocument();
}
// Event Listeners
menuBtn.addEventListener('click', toggleSidebar);
overlay.addEventListener('click', toggleSidebar);
saveBtn.addEventListener('click', saveCurrentDocument);
exportBtn.addEventListener('click', exportToDocx);
newDocBtn.addEventListener('click', showNewDocModal);
confirmDocBtn.addEventListener('click', createNewDocumentWithName);
cancelDocBtn.addEventListener('click', hideNewDocModal);
// Formatting event listeners
boldBtn.addEventListener('click', () => document.execCommand('bold', false, null));
italicBtn.addEventListener('click', () => document.execCommand('italic', false, null));
underlineBtn.addEventListener('click', () => document.execCommand('underline', false, null));
alignLeftBtn.addEventListener('click', () => document.execCommand('justifyLeft', false, null));
alignCenterBtn.addEventListener('click', () => document.execCommand('justifyCenter', false, null));
alignRightBtn.addEventListener('click', () => document.execCommand('justifyRight', false, null));
listUlBtn.addEventListener('click', () => document.execCommand('insertUnorderedList', false, null));
listOlBtn.addEventListener('click', () => document.execCommand('insertOrderedList', false, null));
fontFamily.addEventListener('change', () => {
document.execCommand('fontName', false, fontFamily.value);
});
fontSize.addEventListener('change', () => {
const sizes = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'];
document.execCommand('fontSize', false, fontSize.value);
// Fix the actual size (execCommand only sets the font size tag)
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const span = document.createElement('span');
span.style.fontSize = sizes[fontSize.value - 1];
range.surroundContents(span);
}
});
// Functions
function toggleSidebar() {
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
}
function showNewDocModal() {
docNameModal.classList.remove('hidden');
docNameInput.focus();
}
function hideNewDocModal() {
docNameModal.classList.add('hidden');
docNameInput.value = '';
}
function createNewDocumentWithName() {
const name = docNameInput.value.trim() || 'Untitled Document';
createNewDocument(name);
hideNewDocModal();
}
function createNewDocument(name = 'Untitled Document') {
const newDoc = {
id: Date.now().toString(),
name: name,
content: '<p><br></p>',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
documents.unshift(newDoc);
saveDocumentsToCookies();
renderDocumentsList();
loadDocument(newDoc.id);
}
function loadDocuments() {
const docsCookie = getCookie('documents');
if (docsCookie) {
documents = JSON.parse(docsCookie);
renderDocumentsList();
}
}
function saveDocumentsToCookies() {
setCookie('documents', JSON.stringify(documents), 365);
}
function renderDocumentsList() {
documentsList.innerHTML = '';
if (documents.length === 0) {
documentsList.innerHTML = '<p class="text-gray-500 p-2">No documents yet</p>';
return;
}
documents.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
documents.forEach(doc => {
const docElement = document.createElement('div');
docElement.className = `document-item p-3 cursor-pointer rounded flex items-center justify-between ${currentDocId === doc.id ? 'active' : ''}`;
docElement.innerHTML = `
<div class="flex items-center">
<i class="fas fa-file-word text-blue-500 mr-2"></i>
<span class="truncate">${doc.name}</span>
</div>
<button class="delete-doc text-red-500 hover:text-red-700 p-1" data-id="${doc.id}">
<i class="fas fa-trash"></i>
</button>
`;
docElement.addEventListener('click', () => loadDocument(doc.id));
const deleteBtn = docElement.querySelector('.delete-doc');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteDocument(doc.id);
});
documentsList.appendChild(docElement);
});
}
function loadDocument(docId) {
const doc = documents.find(d => d.id === docId);
if (doc) {
currentDocId = docId;
editor.innerHTML = doc.content;
renderDocumentsList();
// Set focus to editor
editor.focus();
// Move cursor to end
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
}
function saveCurrentDocument() {
if (!currentDocId) return;
const docIndex = documents.findIndex(d => d.id === currentDocId);
if (docIndex !== -1) {
documents[docIndex].content = editor.innerHTML;
documents[docIndex].updatedAt = new Date().toISOString();
saveDocumentsToCookies();
renderDocumentsList();
// Show save notification
showNotification('Document saved successfully');
}
}
function deleteDocument(docId) {
if (confirm('Are you sure you want to delete this document?')) {
documents = documents.filter(d => d.id !== docId);
saveDocumentsToCookies();
if (currentDocId === docId) {
if (documents.length > 0) {
loadDocument(documents[0].id);
} else {
createNewDocument();
}
} else {
renderDocumentsList();
}
}
}
function exportToDocx() {
if (!currentDocId) return;
const doc = documents.find(d => d.id === currentDocId);
if (!doc) return;
// Create a temporary div to parse the HTML content
const tempDiv = document.createElement('div');
tempDiv.innerHTML = doc.content;
// Initialize DOCX
const { Document, Paragraph, TextRun, HeadingLevel, AlignmentType } = docx;
const children = [];
// Process each node in the content
const nodes = tempDiv.childNodes;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.trim() !== '') {
children.push(
new Paragraph({
children: [new TextRun(node.textContent)],
})
);
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'P') {
const paragraphChildren = [];
const textRuns = [];
// Process child nodes of the paragraph
processNode(node, textRuns);
if (textRuns.length > 0) {
paragraphChildren.push(...textRuns);
}
children.push(
new Paragraph({
children: paragraphChildren,
})
);
} else if (node.tagName === 'H1') {
children.push(
new Paragraph({
text: node.textContent,
heading: HeadingLevel.HEADING_1,
})
);
} else if (node.tagName === 'H2') {
children.push(
new Paragraph({
text: node.textContent,
heading: HeadingLevel.HEADING_2,
})
);
} else if (node.tagName === 'UL' || node.tagName === 'OL') {
const listItems = node.querySelectorAll('li');
listItems.forEach(li => {
children.push(
new Paragraph({
text: li.textContent,
bullet: {
level: 0
},
})
);
});
} else if (node.tagName === 'DIV') {
// Handle divs (often used for line breaks)
if (node.textContent.trim() !== '') {
children.push(
new Paragraph({
children: [new TextRun(node.textContent)],
})
);
}
}
}
}
// Create the document
const docxDoc = new Document({
title: doc.name,
description: "Exported from DocClone",
creator: "DocClone",
children: children,
});
// Generate and download the DOCX file
docx.Packer.toBlob(docxDoc).then(blob => {
saveAs(blob, `${doc.name}.docx`);
});
function processNode(element, textRuns) {
for (let j = 0; j < element.childNodes.length; j++) {
const child = element.childNodes[j];
if (child.nodeType === Node.TEXT_NODE) {
if (child.textContent.trim() !== '') {
textRuns.push(
new TextRun({
text: child.textContent,
bold: element.style.fontWeight === 'bold' || element.tagName === 'STRONG' || element.tagName === 'B',
italics: element.style.fontStyle === 'italic' || element.tagName === 'EM' || element.tagName === 'I',
underline: element.style.textDecoration === 'underline' || element.tagName === 'U',
size: element.style.fontSize ? parseInt(element.style.fontSize) * 2 : undefined,
font: element.style.fontFamily || undefined,
})
);
}
} else if (child.nodeType === Node.ELEMENT_NODE) {
// Recursively process child elements
processNode(child, textRuns);
}
}
}
}
function showNotification(message) {
const notification = document.createElement('div');
notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('opacity-0', 'transition-opacity', 'duration-300');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Cookie helper functions
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Auto-save functionality
let saveTimeout;
editor.addEventListener('input', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveCurrentDocument();
}, 2000);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl+S or Cmd+S to save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveCurrentDocument();
}
// Ctrl+N or Cmd+N to create new document
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
showNewDocModal();
}
});
});
</script>
</body>
</html>