- Document view: Added hamburger menu button and breadcrumb navigation - Editor view: Added hamburger menu, breadcrumb nav, fixed handleCancel/handleSave - Editor now navigates to correct parent (project) instead of dashboard - Mobile menu works on all views (projects, projectView, document, editor) - Document view back button goes to project (not dashboard) - Editor cancel/save buttons now go to project or projects list
310 lines
12 KiB
JavaScript
310 lines
12 KiB
JavaScript
// Editor View
|
|
|
|
import { api } from '../api.js';
|
|
|
|
export async function renderEditor(app) {
|
|
const { id, projectId, libraryId } = app.state.params;
|
|
let doc = null;
|
|
let libraries = [];
|
|
|
|
if (id) {
|
|
try {
|
|
doc = await api.getDocument(id);
|
|
} catch (e) {
|
|
app.showToast('Failed to load document', 'error');
|
|
app.navigate(projectId ? 'project' : 'projects', { id: projectId });
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const libResult = await api.getLibraries();
|
|
libraries = libResult.libraries || [];
|
|
} catch (e) {}
|
|
|
|
const isNew = !id;
|
|
const appEl = document.getElementById('app');
|
|
|
|
// Determine back navigation target
|
|
const backTarget = projectId
|
|
? { view: 'project', params: { id: projectId } }
|
|
: { view: 'projects', params: {} };
|
|
|
|
const backToParent = () => {
|
|
if (projectId) {
|
|
app.navigate('project', { id: projectId });
|
|
} else {
|
|
app.navigate('projects');
|
|
}
|
|
};
|
|
|
|
// Mobile menu functions
|
|
window.toggleEditorMenu = function() {
|
|
const menu = document.getElementById('editor-menu');
|
|
if (menu) menu.classList.toggle('open');
|
|
};
|
|
|
|
window.closeEditorMenu = function() {
|
|
const menu = document.getElementById('editor-menu');
|
|
if (menu) menu.classList.remove('open');
|
|
};
|
|
|
|
let formData = {
|
|
title: doc?.title || '',
|
|
content: doc?.content || '',
|
|
tags: doc?.tags?.join(', ') || '',
|
|
type: doc?.type || 'general',
|
|
priority: doc?.priority || 'medium',
|
|
status: doc?.status || 'draft',
|
|
libraryId: doc?.libraryId || libraryId || ''
|
|
};
|
|
|
|
let showPreview = false;
|
|
let hasChanges = false;
|
|
|
|
function render() {
|
|
appEl.innerHTML = `
|
|
<header class="app-header">
|
|
<button type="button" class="mobile-nav-btn" onclick="window.toggleEditorMenu()" title="Menu">☰</button>
|
|
<button type="button" class="btn btn-ghost" onclick="handleCancel()">Cancel</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">${isNew ? 'New Document' : escapeHtml(formData.title)}</span>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" onclick="handleSave()">Save</button>
|
|
</header>
|
|
<div class="mobile-menu" id="editor-menu">
|
|
<div class="mobile-menu-header">
|
|
<span>Menu</span>
|
|
<button onclick="window.closeEditorMenu()">✕</button>
|
|
</div>
|
|
<div class="mobile-menu-content">
|
|
<a href="#" onclick="handleCancel(); return false;">← Cancel & Go Back</a>
|
|
<a href="#" onclick="handleSave(); window.closeEditorMenu(); return false;">💾 Save Document</a>
|
|
<a href="#" onclick="window.app.navigate('projects'); window.closeEditorMenu(); return false;">📋 All Projects</a>
|
|
</div>
|
|
</div>
|
|
<main class="main-content">
|
|
<div class="editor-container">
|
|
<form class="editor-form" id="editor-form">
|
|
<div class="form-row">
|
|
<div class="form-group" style="flex:2">
|
|
<label for="title">Title</label>
|
|
<input type="text" id="title" value="${escapeHtml(formData.title)}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="libraryId">Library</label>
|
|
<select id="libraryId">
|
|
<option value="">None</option>
|
|
${libraries.map(l => `<option value="${l.id}" ${formData.libraryId === l.id ? 'selected' : ''}>${escapeHtml(l.name)}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="type">Type</label>
|
|
<select id="type">
|
|
<option value="requirement" ${formData.type === 'requirement' ? 'selected' : ''}>Requirement</option>
|
|
<option value="note" ${formData.type === 'note' ? 'selected' : ''}>Note</option>
|
|
<option value="spec" ${formData.type === 'spec' ? 'selected' : ''}>Specification</option>
|
|
<option value="general" ${formData.type === 'general' ? 'selected' : ''}>General</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="status">Status</label>
|
|
<select id="status">
|
|
<option value="draft" ${formData.status === 'draft' ? 'selected' : ''}>Draft</option>
|
|
<option value="approved" ${formData.status === 'approved' ? 'selected' : ''}>Approved</option>
|
|
<option value="implemented" ${formData.status === 'implemented' ? 'selected' : ''}>Implemented</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="priority">Priority</label>
|
|
<select id="priority">
|
|
<option value="high" ${formData.priority === 'high' ? 'selected' : ''}>🔴 High</option>
|
|
<option value="medium" ${formData.priority === 'medium' ? 'selected' : ''}>🟡 Medium</option>
|
|
<option value="low" ${formData.priority === 'low' ? 'selected' : ''}>🟢 Low</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tags">Tags (comma-separated)</label>
|
|
<input type="text" id="tags" value="${escapeHtml(formData.tags)}" placeholder="backend, api, auth">
|
|
</div>
|
|
<div class="form-group full-width">
|
|
<div class="editor-toolbar">
|
|
<button type="button" class="toolbar-btn" onclick="insertFormat('**', '**')" title="Bold">B</button>
|
|
<button type="button" class="toolbar-btn" onclick="insertFormat('*', '*')" title="Italic"><em>I</em></button>
|
|
<span class="toolbar-separator"></span>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('# ')" title="Heading 1">H1</button>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('## ')" title="Heading 2">H2</button>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('### ')" title="Heading 3">H3</button>
|
|
<span class="toolbar-separator"></span>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('- ')" title="List">•</button>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('1. ')" title="Numbered List">1.</button>
|
|
<button type="button" class="toolbar-btn" onclick="insertLine('- [ ] ')" title="Task">☐</button>
|
|
<span class="toolbar-separator"></span>
|
|
<button type="button" class="toolbar-btn" onclick="insertFormat('\`', '\`')" title="Code"></></button>
|
|
<div class="toolbar-tabs">
|
|
<button type="button" class="tab-btn ${!showPreview ? 'active' : ''}" onclick="togglePreview(false)">Write</button>
|
|
<button type="button" class="tab-btn ${showPreview ? 'active' : ''}" onclick="togglePreview(true)">Preview</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-content ${showPreview ? 'show-preview' : ''}" id="editor-content">
|
|
<div class="editor-pane">
|
|
<textarea id="content" placeholder="Write your content in Markdown...">${escapeHtml(formData.content)}</textarea>
|
|
</div>
|
|
<div class="preview-pane prose">${renderMarkdown(formData.content)}</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
`;
|
|
|
|
// Event listeners
|
|
const titleInput = document.getElementById('title');
|
|
const contentInput = document.getElementById('content');
|
|
const tagsInput = document.getElementById('tags');
|
|
const typeInput = document.getElementById('type');
|
|
const statusInput = document.getElementById('status');
|
|
const priorityInput = document.getElementById('priority');
|
|
const libraryInput = document.getElementById('libraryId');
|
|
|
|
const inputs = [titleInput, contentInput, tagsInput, typeInput, statusInput, priorityInput, libraryInput];
|
|
inputs.forEach(input => {
|
|
if (input) {
|
|
input.addEventListener('input', () => {
|
|
hasChanges = true;
|
|
updateFormData();
|
|
if (showPreview) {
|
|
document.querySelector('.preview-pane').innerHTML = renderMarkdown(formData.content);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
window.insertFormat = (before, after) => {
|
|
const textarea = document.getElementById('content');
|
|
const start = textarea.selectionStart;
|
|
const end = textarea.selectionEnd;
|
|
const text = textarea.value;
|
|
const selected = text.substring(start, end);
|
|
textarea.value = text.substring(0, start) + before + selected + after + text.substring(end);
|
|
textarea.focus();
|
|
textarea.selectionStart = start + before.length;
|
|
textarea.selectionEnd = end + before.length;
|
|
hasChanges = true;
|
|
};
|
|
|
|
window.insertLine = (prefix) => {
|
|
const textarea = document.getElementById('content');
|
|
const start = textarea.selectionStart;
|
|
const text = textarea.value;
|
|
// Find start of current line
|
|
let lineStart = start;
|
|
while (lineStart > 0 && text[lineStart - 1] !== '\n') lineStart--;
|
|
textarea.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
|
textarea.focus();
|
|
textarea.selectionStart = textarea.selectionEnd = lineStart + prefix.length;
|
|
hasChanges = true;
|
|
};
|
|
|
|
window.togglePreview = (show) => {
|
|
showPreview = show;
|
|
const content = document.getElementById('editor-content');
|
|
if (show) {
|
|
content.classList.add('show-preview');
|
|
} else {
|
|
content.classList.remove('show-preview');
|
|
}
|
|
};
|
|
|
|
window.handleCancel = async () => {
|
|
if (hasChanges) {
|
|
const confirmed = await app.confirmDelete('You have unsaved changes. Discard?');
|
|
if (!confirmed) return;
|
|
}
|
|
if (projectId) {
|
|
app.navigate('project', { id: projectId });
|
|
} else {
|
|
app.navigate('projects');
|
|
}
|
|
};
|
|
|
|
window.handleSave = async () => {
|
|
updateFormData();
|
|
|
|
const data = {
|
|
title: formData.title,
|
|
content: formData.content,
|
|
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
|
|
type: formData.type,
|
|
priority: formData.priority,
|
|
status: formData.status,
|
|
libraryId: formData.libraryId || null
|
|
};
|
|
|
|
try {
|
|
if (isNew) {
|
|
await api.createDocument({...data, projectId});
|
|
} else {
|
|
await api.updateDocument(id, data);
|
|
}
|
|
app.showToast('Document saved', 'success');
|
|
if (projectId) {
|
|
app.navigate('project', { id: projectId });
|
|
} else {
|
|
app.navigate('projects');
|
|
}
|
|
} catch (e) {
|
|
app.showToast('Failed to save: ' + e.message, 'error');
|
|
}
|
|
};
|
|
|
|
function updateFormData() {
|
|
formData = {
|
|
title: titleInput.value,
|
|
content: contentInput.value,
|
|
tags: tagsInput.value,
|
|
type: typeInput.value,
|
|
priority: priorityInput.value,
|
|
status: statusInput.value,
|
|
libraryId: libraryInput.value
|
|
};
|
|
}
|
|
}
|
|
|
|
render();
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function renderMarkdown(content) {
|
|
if (!content) return '<p style="color:var(--color-text-muted)">Nothing to preview</p>';
|
|
|
|
if (typeof marked !== 'undefined') {
|
|
return marked.parse(content);
|
|
}
|
|
|
|
// Fallback
|
|
return content
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.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(/\n\n/g, '</p><p>');
|
|
}
|