Spaces:
Build error
Build error
| // Enhanced Document Viewer with Editing and Commenting | |
| (function() { | |
| 'use strict'; | |
| class DocumentEditor { | |
| constructor() { | |
| this.comments = {}; | |
| this.editMode = false; | |
| this.currentDocId = null; | |
| this.annotations = {}; | |
| } | |
| enableEditMode(docId) { | |
| this.editMode = true; | |
| this.currentDocId = docId; | |
| const contentArea = document.getElementById('document-content'); | |
| const content = contentArea.querySelector('.document-content'); | |
| if (content) { | |
| content.contentEditable = true; | |
| content.style.border = '2px dashed #ff4757'; | |
| content.style.padding = '20px'; | |
| // Add edit toolbar | |
| this.createEditToolbar(contentArea); | |
| // Enable text selection for commenting | |
| this.enableTextSelection(content); | |
| } | |
| } | |
| createEditToolbar(container) { | |
| const toolbar = document.createElement('div'); | |
| toolbar.className = 'document-edit-toolbar'; | |
| toolbar.style.cssText = ` | |
| position: sticky; | |
| top: 0; | |
| background: #2c3e50; | |
| padding: 10px; | |
| display: flex; | |
| gap: 10px; | |
| z-index: 150; | |
| border-radius: 4px; | |
| margin-bottom: 10px; | |
| `; | |
| toolbar.innerHTML = ` | |
| <button class="edit-btn" data-action="bold" title="Bold"> | |
| <b>B</b> | |
| </button> | |
| <button class="edit-btn" data-action="italic" title="Italic"> | |
| <i>I</i> | |
| </button> | |
| <button class="edit-btn" data-action="underline" title="Underline"> | |
| <u>U</u> | |
| </button> | |
| <button class="edit-btn" data-action="highlight" title="Highlight"> | |
| ποΈ | |
| </button> | |
| <div style="border-left: 1px solid #666; margin: 0 5px;"></div> | |
| <button class="edit-btn" data-action="comment" title="Add Comment"> | |
| π¬ | |
| </button> | |
| <button class="edit-btn" data-action="save" title="Save Changes"> | |
| πΎ | |
| </button> | |
| <button class="edit-btn" data-action="export" title="Export PDF"> | |
| π | |
| </button> | |
| <div style="margin-left: auto;"> | |
| <button class="edit-btn" data-action="exit" title="Exit Edit Mode"> | |
| βοΈ | |
| </button> | |
| </div> | |
| `; | |
| // Style buttons | |
| toolbar.querySelectorAll('.edit-btn').forEach(btn => { | |
| btn.style.cssText = ` | |
| padding: 6px 12px; | |
| background: #34495e; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background 0.3s; | |
| `; | |
| btn.addEventListener('mouseover', () => { | |
| btn.style.background = '#ff4757'; | |
| }); | |
| btn.addEventListener('mouseout', () => { | |
| btn.style.background = '#34495e'; | |
| }); | |
| btn.addEventListener('click', (e) => { | |
| this.handleToolbarAction(e.currentTarget.dataset.action); | |
| }); | |
| }); | |
| container.insertBefore(toolbar, container.firstChild); | |
| } | |
| handleToolbarAction(action) { | |
| const content = document.querySelector('.document-content[contenteditable="true"]'); | |
| switch(action) { | |
| case 'bold': | |
| document.execCommand('bold', false, null); | |
| break; | |
| case 'italic': | |
| document.execCommand('italic', false, null); | |
| break; | |
| case 'underline': | |
| document.execCommand('underline', false, null); | |
| break; | |
| case 'highlight': | |
| this.highlightSelection(); | |
| break; | |
| case 'comment': | |
| this.addCommentToSelection(); | |
| break; | |
| case 'save': | |
| this.saveDocument(); | |
| break; | |
| case 'export': | |
| this.exportToPDF(); | |
| break; | |
| case 'exit': | |
| this.exitEditMode(); | |
| break; | |
| } | |
| } | |
| highlightSelection() { | |
| const selection = window.getSelection(); | |
| if (selection.toString()) { | |
| const range = selection.getRangeAt(0); | |
| const span = document.createElement('span'); | |
| span.style.backgroundColor = '#fff59d'; | |
| span.className = 'document-highlight'; | |
| try { | |
| range.surroundContents(span); | |
| } catch (e) { | |
| // Handle complex selections | |
| const contents = range.extractContents(); | |
| span.appendChild(contents); | |
| range.insertNode(span); | |
| } | |
| selection.removeAllRanges(); | |
| } | |
| } | |
| addCommentToSelection() { | |
| const selection = window.getSelection(); | |
| if (selection.toString()) { | |
| const commentText = prompt('Add your comment:'); | |
| if (commentText) { | |
| const commentId = 'comment_' + Date.now(); | |
| const range = selection.getRangeAt(0); | |
| // Create comment marker | |
| const marker = document.createElement('span'); | |
| marker.className = 'comment-marker'; | |
| marker.dataset.commentId = commentId; | |
| marker.style.cssText = ` | |
| background: #ffe082; | |
| border-bottom: 2px solid #ff9800; | |
| cursor: pointer; | |
| position: relative; | |
| `; | |
| try { | |
| range.surroundContents(marker); | |
| } catch (e) { | |
| const contents = range.extractContents(); | |
| marker.appendChild(contents); | |
| range.insertNode(marker); | |
| } | |
| // Store comment | |
| this.comments[commentId] = { | |
| text: commentText, | |
| author: 'Current User', | |
| timestamp: new Date().toISOString(), | |
| position: marker.getBoundingClientRect() | |
| }; | |
| // Add click handler to show comment | |
| marker.addEventListener('click', () => { | |
| this.showComment(commentId); | |
| }); | |
| // Show comment count badge | |
| this.updateCommentCount(); | |
| selection.removeAllRanges(); | |
| } | |
| } else { | |
| alert('Please select text to comment on'); | |
| } | |
| } | |
| showComment(commentId) { | |
| const comment = this.comments[commentId]; | |
| if (comment) { | |
| // Remove existing comment popup | |
| const existingPopup = document.querySelector('.comment-popup'); | |
| if (existingPopup) { | |
| existingPopup.remove(); | |
| } | |
| // Create comment popup | |
| const popup = document.createElement('div'); | |
| popup.className = 'comment-popup'; | |
| popup.style.cssText = ` | |
| position: absolute; | |
| background: white; | |
| border: 1px solid #ccc; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| z-index: 200; | |
| max-width: 300px; | |
| right: 20px; | |
| top: ${comment.position.top}px; | |
| `; | |
| popup.innerHTML = ` | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 10px;"> | |
| <strong style="color: #2c3e50;">${comment.author}</strong> | |
| <button onclick="this.parentElement.parentElement.remove()" | |
| style="border: none; background: none; cursor: pointer; color: #999;">β</button> | |
| </div> | |
| <p style="color: #333; margin: 10px 0;">${comment.text}</p> | |
| <small style="color: #999;">${new Date(comment.timestamp).toLocaleString()}</small> | |
| <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> | |
| <input type="text" placeholder="Reply..." | |
| style="width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 4px;"> | |
| </div> | |
| `; | |
| document.getElementById('document-viewer').appendChild(popup); | |
| } | |
| } | |
| updateCommentCount() { | |
| const count = Object.keys(this.comments).length; | |
| let badge = document.querySelector('.comment-count-badge'); | |
| if (!badge) { | |
| badge = document.createElement('div'); | |
| badge.className = 'comment-count-badge'; | |
| badge.style.cssText = ` | |
| position: fixed; | |
| top: 80px; | |
| right: 20px; | |
| background: #ff9800; | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| z-index: 150; | |
| `; | |
| document.getElementById('document-viewer').appendChild(badge); | |
| } | |
| badge.textContent = `π¬ ${count} Comments`; | |
| } | |
| saveDocument() { | |
| const content = document.querySelector('.document-content[contenteditable="true"]'); | |
| if (content) { | |
| const savedContent = content.innerHTML; | |
| const docId = this.currentDocId || 'doc_' + Date.now(); | |
| // Save to localStorage (in real app, would save to backend) | |
| const savedDoc = { | |
| id: docId, | |
| content: savedContent, | |
| comments: this.comments, | |
| annotations: this.annotations, | |
| lastModified: new Date().toISOString(), | |
| version: 1 | |
| }; | |
| localStorage.setItem(`navada_doc_${docId}`, JSON.stringify(savedDoc)); | |
| // Show success message | |
| this.showNotification('Document saved successfully!', 'success'); | |
| return savedDoc; | |
| } | |
| } | |
| exportToPDF() { | |
| const content = document.querySelector('.document-content[contenteditable="true"]'); | |
| if (content) { | |
| // In a real implementation, this would generate a proper PDF | |
| // For now, we'll trigger a print dialog which can save as PDF | |
| const printWindow = window.open('', '_blank'); | |
| printWindow.document.write(` | |
| <html> | |
| <head> | |
| <title>Document Export</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; padding: 20px; } | |
| .comment-marker { background: #ffe082; } | |
| .document-highlight { background: #fff59d; } | |
| </style> | |
| </head> | |
| <body> | |
| ${content.innerHTML} | |
| </body> | |
| </html> | |
| `); | |
| printWindow.document.close(); | |
| printWindow.print(); | |
| } | |
| } | |
| exitEditMode() { | |
| const content = document.querySelector('.document-content[contenteditable="true"]'); | |
| if (content) { | |
| content.contentEditable = false; | |
| content.style.border = 'none'; | |
| } | |
| // Remove toolbar | |
| const toolbar = document.querySelector('.document-edit-toolbar'); | |
| if (toolbar) { | |
| toolbar.remove(); | |
| } | |
| this.editMode = false; | |
| this.showNotification('Edit mode disabled', 'info'); | |
| } | |
| enableTextSelection(element) { | |
| element.addEventListener('mouseup', () => { | |
| const selection = window.getSelection(); | |
| if (selection.toString()) { | |
| this.showSelectionMenu(selection); | |
| } | |
| }); | |
| } | |
| showSelectionMenu(selection) { | |
| // Remove existing menu | |
| const existingMenu = document.querySelector('.selection-menu'); | |
| if (existingMenu) { | |
| existingMenu.remove(); | |
| } | |
| const range = selection.getRangeAt(0); | |
| const rect = range.getBoundingClientRect(); | |
| const menu = document.createElement('div'); | |
| menu.className = 'selection-menu'; | |
| menu.style.cssText = ` | |
| position: fixed; | |
| top: ${rect.top - 40}px; | |
| left: ${rect.left + (rect.width / 2) - 100}px; | |
| background: #2c3e50; | |
| border-radius: 4px; | |
| padding: 5px; | |
| display: flex; | |
| gap: 5px; | |
| z-index: 300; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| `; | |
| const actions = [ | |
| { icon: 'π¬', action: 'comment', title: 'Comment' }, | |
| { icon: 'ποΈ', action: 'highlight', title: 'Highlight' }, | |
| { icon: 'π', action: 'note', title: 'Add Note' } | |
| ]; | |
| actions.forEach(item => { | |
| const btn = document.createElement('button'); | |
| btn.textContent = item.icon; | |
| btn.title = item.title; | |
| btn.style.cssText = ` | |
| background: transparent; | |
| border: none; | |
| color: white; | |
| cursor: pointer; | |
| padding: 5px 10px; | |
| font-size: 16px; | |
| `; | |
| btn.addEventListener('click', () => { | |
| if (item.action === 'comment') { | |
| this.addCommentToSelection(); | |
| } else if (item.action === 'highlight') { | |
| this.highlightSelection(); | |
| } else if (item.action === 'note') { | |
| this.addNoteToSelection(); | |
| } | |
| menu.remove(); | |
| }); | |
| menu.appendChild(btn); | |
| }); | |
| document.body.appendChild(menu); | |
| // Remove menu when clicking elsewhere | |
| setTimeout(() => { | |
| document.addEventListener('click', function removeMenu(e) { | |
| if (!menu.contains(e.target)) { | |
| menu.remove(); | |
| document.removeEventListener('click', removeMenu); | |
| } | |
| }); | |
| }, 100); | |
| } | |
| addNoteToSelection() { | |
| const selection = window.getSelection(); | |
| if (selection.toString()) { | |
| const noteText = prompt('Add your note:'); | |
| if (noteText) { | |
| const noteId = 'note_' + Date.now(); | |
| const range = selection.getRangeAt(0); | |
| // Create note marker | |
| const marker = document.createElement('span'); | |
| marker.className = 'note-marker'; | |
| marker.dataset.noteId = noteId; | |
| marker.style.cssText = ` | |
| background: #e1f5fe; | |
| border-bottom: 2px solid #03a9f4; | |
| cursor: help; | |
| position: relative; | |
| `; | |
| marker.title = noteText; | |
| try { | |
| range.surroundContents(marker); | |
| } catch (e) { | |
| const contents = range.extractContents(); | |
| marker.appendChild(contents); | |
| range.insertNode(marker); | |
| } | |
| // Store note | |
| this.annotations[noteId] = { | |
| type: 'note', | |
| text: noteText, | |
| timestamp: new Date().toISOString() | |
| }; | |
| selection.removeAllRanges(); | |
| } | |
| } | |
| } | |
| showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.className = `document-notification ${type}`; | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| padding: 15px 20px; | |
| border-radius: 8px; | |
| color: white; | |
| font-weight: 500; | |
| z-index: 1000; | |
| animation: slideIn 0.3s ease; | |
| ${type === 'success' ? 'background: #27ae60;' : ''} | |
| ${type === 'error' ? 'background: #e74c3c;' : ''} | |
| ${type === 'info' ? 'background: #3498db;' : ''} | |
| `; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.style.animation = 'slideOut 0.3s ease'; | |
| setTimeout(() => notification.remove(), 300); | |
| }, 3000); | |
| } | |
| // Collaborative features (simulation) | |
| simulateCollaboration() { | |
| // In a real app, this would use WebSockets | |
| setInterval(() => { | |
| const collaborators = ['Alice', 'Bob', 'Charlie']; | |
| const randomUser = collaborators[Math.floor(Math.random() * collaborators.length)]; | |
| const cursor = document.createElement('div'); | |
| cursor.className = 'collaborator-cursor'; | |
| cursor.style.cssText = ` | |
| position: absolute; | |
| width: 2px; | |
| height: 20px; | |
| background: #ff4757; | |
| top: ${Math.random() * 400}px; | |
| left: ${Math.random() * 600}px; | |
| z-index: 100; | |
| `; | |
| const label = document.createElement('div'); | |
| label.textContent = randomUser; | |
| label.style.cssText = ` | |
| position: absolute; | |
| top: -20px; | |
| left: 0; | |
| background: #ff4757; | |
| color: white; | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| `; | |
| cursor.appendChild(label); | |
| const viewer = document.getElementById('document-viewer'); | |
| if (viewer) { | |
| viewer.appendChild(cursor); | |
| // Remove after animation | |
| setTimeout(() => cursor.remove(), 5000); | |
| } | |
| }, 10000); | |
| } | |
| } | |
| // Initialize editor | |
| window.DocumentEditor = new DocumentEditor(); | |
| // Add edit button to document viewer header | |
| function addEditButton() { | |
| const interval = setInterval(() => { | |
| const header = document.querySelector('.document-header'); | |
| if (header && !header.querySelector('.edit-document-btn')) { | |
| const editBtn = document.createElement('button'); | |
| editBtn.className = 'edit-document-btn'; | |
| editBtn.textContent = 'βοΈ Edit'; | |
| editBtn.style.cssText = ` | |
| padding: 6px 16px; | |
| background: #27ae60; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin-right: 10px; | |
| `; | |
| editBtn.addEventListener('click', () => { | |
| window.DocumentEditor.enableEditMode('current'); | |
| }); | |
| const btnContainer = header.querySelector('div'); | |
| if (btnContainer) { | |
| btnContainer.insertBefore(editBtn, btnContainer.firstChild); | |
| } | |
| clearInterval(interval); | |
| } | |
| }, 1000); | |
| } | |
| // Initialize when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', addEditButton); | |
| } else { | |
| setTimeout(addEditButton, 1000); | |
| } | |
| // Add styles for animations | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes slideOut { | |
| from { transform: translateX(0); opacity: 1; } | |
| to { transform: translateX(100%); opacity: 0; } | |
| } | |
| .collaborator-cursor { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| })(); |