Files
simplenote-web/public/js/views/document.js

200 lines
7.4 KiB
JavaScript

// Document View
import { api } from '../api.js';
export async function renderDocument(app) {
const { id, projectId } = app.state.params;
let doc;
try {
doc = await api.getDocument(id);
} catch (e) {
app.showToast('Failed to load document', 'error');
app.navigate('projects');
return;
}
window.backToProject = () => {
if (projectId) {
app.navigate('project', { id: projectId });
} else {
app.navigate('projects');
}
};
// Mobile menu functions
window.toggleDocumentMenu = function() {
const menu = document.getElementById('document-menu');
if (menu) menu.classList.toggle('open');
};
window.closeDocumentMenu = function() {
const menu = document.getElementById('document-menu');
if (menu) menu.classList.remove('open');
};
const appEl = document.getElementById('app');
function render() {
const priorityEmoji = { high: '🔴', medium: '🟡', low: '🟢' };
const priority = doc.priority || 'medium';
const renderedContent = renderMarkdown(doc.content || '');
appEl.innerHTML = `
<header class="app-header">
<button type="button" class="mobile-nav-btn" onclick="window.toggleDocumentMenu()" title="Menu">☰</button>
<button type="button" class="btn btn-ghost" onclick="backToProject()">← Back</button>
<div class="breadcrumb-nav">
<span class="breadcrumb-link" onclick="window.app.navigate('projects')">Projects</span>
<span class="breadcrumb-sep">/</span>
${projectId ? `<span class="breadcrumb-link" onclick="window.app.navigate('project', {id: '${projectId}'})">Project</span><span class="breadcrumb-sep">/</span>` : ''}
<span class="breadcrumb-current">${escapeHtml(doc.title || 'Document')}</span>
</div>
<div class="header-actions">
<button type="button" class="btn btn-ghost" onclick="window.app.navigate('editor', {id: '${doc.id}', projectId: '${projectId || ''}'})">✏️ Edit</button>
<button type="button" class="btn btn-ghost" onclick="exportDoc()">📥 Export</button>
<button type="button" class="btn btn-ghost danger" onclick="deleteDoc()">🗑️ Delete</button>
</div>
</header>
<div class="mobile-menu" id="document-menu">
<div class="mobile-menu-header">
<span>Menu</span>
<button onclick="window.closeDocumentMenu()">✕</button>
</div>
<div class="mobile-menu-content">
<a href="#" onclick="backToProject(); return false;">← Back to ${projectId ? 'Project' : 'Projects'}</a>
<a href="#" onclick="window.app.navigate('editor', {id: '${doc.id}', projectId: '${projectId || ''}'}); window.closeDocumentMenu(); return false;">✏️ Edit Document</a>
<a href="#" onclick="window.app.navigate('projects'); window.closeDocumentMenu(); return false;">📋 All Projects</a>
</div>
</div>
<main class="main-content">
<div class="content-body">
<div class="doc-viewer">
<div class="doc-content">
<div class="doc-viewer-header">
<span class="doc-id">${doc.id}</span>
<span class="type-badge ${doc.type || 'general'}">${doc.type || 'general'}</span>
</div>
<div class="prose">${renderedContent}</div>
</div>
<aside class="doc-sidebar">
<div class="meta-section">
<div class="meta-header">Details</div>
<div class="meta-body">
<div class="meta-row">
<span class="meta-label">Status</span>
<span class="status-badge ${doc.status || 'draft'}">${doc.status || 'draft'}</span>
</div>
<div class="meta-row">
<span class="meta-label">Priority</span>
<span class="meta-value">${priorityEmoji[priority]} ${priority}</span>
</div>
<div class="meta-row">
<span class="meta-label">Author</span>
<span class="meta-value">${escapeHtml(doc.author || 'unknown')}</span>
</div>
<div class="meta-row">
<span class="meta-label">Created</span>
<span class="meta-value">${formatDate(doc.createdAt)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Updated</span>
<span class="meta-value">${formatDate(doc.updatedAt)}</span>
</div>
</div>
</div>
${doc.tags && doc.tags.length ? `
<div class="meta-section">
<div class="meta-header">Tags</div>
<div class="meta-body doc-tags">
${doc.tags.map(t => `<span class="tag-pill" onclick="filterByTag('${escapeHtml(t)}'); event.stopPropagation();">${escapeHtml(t)}</span>`).join('')}
</div>
</div>
` : ''}
</aside>
</div>
</div>
</main>
`;
window.filterByTag = (tag) => {
// Store the tag to filter by in app state so dashboard can pick it up
app.state.selectedTag = tag;
app.state.selectedLibrary = null;
backToProject();
};
}
render();
async function exportDoc() {
try {
const markdown = await api.exportDocument(id);
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Sanitize filename to prevent path traversal
const safeFilename = (doc.title || 'untitled')
.replace(/[^a-zA-Z0-9_\-\s]/g, '')
.replace(/\s+/g, '-')
.substring(0, 100);
a.download = `${doc.id}-${safeFilename}.md`;
a.click();
URL.revokeObjectURL(url);
app.showToast('Document exported', 'success');
} catch (e) {
app.showToast('Failed to export', 'error');
}
}
async function deleteDoc() {
const confirmed = await app.confirmDelete(`Delete "${doc.title}"? This cannot be undone.`);
if (confirmed) {
try {
await api.deleteDocument(id);
app.showToast('Document deleted', 'success');
backToProject();
} catch (e) {
app.showToast('Failed to delete', 'error');
}
}
}
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function renderMarkdown(content) {
// Simple markdown rendering using marked library if available
if (typeof marked !== 'undefined') {
return marked.parse(content);
}
// Fallback simple rendering
return content
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n\n/g, '</p><p>')
.replace(/^(.+)$/gm, (match) => {
if (match.startsWith('<')) return match;
return `<p>${match}</p>`;
});
}