Files
gobsidian/web/templates/edit.html
2025-08-25 17:54:27 +01:00

335 lines
13 KiB
HTML

{{define "edit"}}
{{template "base" .}}
{{end}}
{{define "edit_content"}}
<div class="max-w-4xl mx-auto p-6">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold text-white">Edit: {{.title}}</h1>
<div class="flex items-center space-x-3">
<button id="toggle-preview" type="button" class="btn-secondary">
<i class="fas fa-eye mr-2"></i>Preview
</button>
<button type="submit" form="edit-form" class="btn-primary">
<i class="fas fa-save mr-2"></i>Save
</button>
</div>
</div>
{{if .folder_path}}
<p class="text-gray-400">
<i class="fas fa-folder mr-2"></i>
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
</p>
{{end}}
</div>
<!-- Edit Form -->
<form id="edit-form" class="space-y-6">
<div class="bg-gray-800 rounded-lg p-6">
<!-- Content Editor with Live Preview -->
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<label for="content" class="block text-sm font-medium text-gray-300">Content</label>
<div class="text-xs text-gray-400">Live Preview</div>
</div>
<div id="editor-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<textarea id="content" name="content" rows="25" class="editor-textarea">{{.content}}</textarea>
<div id="live-preview" class="prose prose-invert prose-dark bg-slate-800 border border-gray-700 rounded-lg p-4 overflow-y-auto hidden" style="min-height: 20rem;"></div>
</div>
</div>
<!-- Editor Toolbar -->
<div class="flex items-center justify-between border-t border-gray-700 pt-4">
<div class="flex items-center space-x-2">
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('**', '**')" title="Bold">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('*', '*')" title="Italic">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('`', '`')" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('[', '](url)')" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('![', '](image.png)')" title="Image">
<i class="fas fa-image"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertHeading()" title="Heading">
<i class="fas fa-heading"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertList()" title="List">
<i class="fas fa-list"></i>
</button>
</div>
<div class="text-xs text-gray-500">
Press Ctrl+S to save
</div>
</div>
</div>
</form>
</div>
{{end}}
{{define "edit_scripts"}}
<script>
const editForm = document.getElementById('edit-form');
const contentTextarea = document.getElementById('content');
const livePreview = document.getElementById('live-preview');
const editorGrid = document.getElementById('editor-grid');
const togglePreviewBtn = document.getElementById('toggle-preview');
const imageStorageMode = {{.image_storage_mode}};
const imageSubfolderName = "{{.image_subfolder_name}}";
const currentFolderPath = "{{.folder_path}}";
const currentNotePath = "{{.note_path}}";
// Preview state (persist per-note)
const PREVIEW_STATE_KEY = `previewEnabled:${currentNotePath}`;
let previewEnabled = localStorage.getItem(PREVIEW_STATE_KEY) === 'true';
// Load Marked for client-side markdown rendering
(function ensureMarked() {
if (window.marked) return;
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
document.head.appendChild(s);
})();
function buildImageURL(filename) {
// Map Obsidian embed to server URL
if (imageStorageMode === 2) {
return `/serve_stored_image/${filename}`;
}
let path = filename;
if (imageStorageMode === 3 && currentFolderPath) {
path = `${currentFolderPath}/${filename}`;
} else if (imageStorageMode === 4 && currentFolderPath) {
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
}
return `/serve_attached_image/${path}`;
}
function transformObsidianEmbeds(md) {
// Convert ![[name.png]] or ![[sub/name.png|alt]] to ![alt](url)
return (md || '').replace(/!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, (m, file, alt) => {
const filename = file.trim();
const url = buildImageURL(filename);
const altText = (alt || filename).trim();
return `![${altText}](${url})`;
});
}
function applyPreviewState() {
if (!livePreview || !editorGrid) return;
if (previewEnabled) {
livePreview.classList.remove('hidden');
editorGrid.classList.add('lg:grid-cols-2');
renderPreview();
togglePreviewBtn && (togglePreviewBtn.innerHTML = '<i class="fas fa-eye-slash mr-2"></i>Hide Preview');
} else {
livePreview.classList.add('hidden');
editorGrid.classList.remove('lg:grid-cols-2');
togglePreviewBtn && (togglePreviewBtn.innerHTML = '<i class="fas fa-eye mr-2"></i>Preview');
}
}
function renderPreview() {
if (!previewEnabled) return;
if (!livePreview || !window.marked) return;
const transformed = transformObsidianEmbeds(contentTextarea.value || '');
livePreview.innerHTML = marked.parse(transformed);
livePreview.querySelectorAll('pre code').forEach(block => {
try { hljs.highlightElement(block); } catch (e) {}
});
}
editForm.addEventListener('submit', function(e) {
e.preventDefault();
const content = contentTextarea.value;
const notePath = '{{.note_path}}';
const formData = new FormData();
formData.append('content', content);
fetch('/edit/' + notePath, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Note saved successfully', 'success');
if (data.redirect) {
setTimeout(() => {
window.location.href = data.redirect;
}, 1000);
}
} else {
throw new Error(data.error || 'Failed to save note');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Auto-resize textarea
function autoResize() {
contentTextarea.style.height = 'auto';
contentTextarea.style.height = (contentTextarea.scrollHeight) + 'px';
renderPreview();
}
contentTextarea.addEventListener('input', autoResize);
// Initial resize
autoResize();
// Initial preview state
document.addEventListener('DOMContentLoaded', applyPreviewState);
// Toggle preview
if (togglePreviewBtn) {
togglePreviewBtn.addEventListener('click', function() {
previewEnabled = !previewEnabled;
localStorage.setItem(PREVIEW_STATE_KEY, String(previewEnabled));
applyPreviewState();
});
}
// Keyboard shortcuts
contentTextarea.addEventListener('keydown', function(e) {
// Ctrl+S or Cmd+S to save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
editForm.dispatchEvent(new Event('submit'));
}
// Tab for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
const value = this.value;
this.value = value.substring(0, start) + '\t' + value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
renderPreview();
}
});
// Markdown insertion functions
function insertMarkdown(before, after) {
const start = contentTextarea.selectionStart;
const end = contentTextarea.selectionEnd;
const text = contentTextarea.value;
const selectedText = text.substring(start, end);
const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
contentTextarea.value = newText;
// Position cursor
const newPos = start + before.length + selectedText.length;
contentTextarea.focus();
contentTextarea.setSelectionRange(newPos, newPos);
autoResize();
}
function insertHeading() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const lineText = text.substring(lineStart, start);
if (lineText.startsWith('# ')) {
return; // Already a heading
}
const newText = text.substring(0, lineStart) + '# ' + text.substring(lineStart);
contentTextarea.value = newText;
contentTextarea.focus();
contentTextarea.setSelectionRange(start + 2, start + 2);
autoResize();
}
function insertList() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const newText = text.substring(0, lineStart) + '- ' + text.substring(lineStart);
contentTextarea.value = newText;
contentTextarea.focus();
contentTextarea.setSelectionRange(start + 2, start + 2);
autoResize();
}
// Paste-to-upload image
contentTextarea.addEventListener('paste', async function(e) {
const items = (e.clipboardData || window.clipboardData).items || [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type && item.type.indexOf('image') === 0) {
e.preventDefault();
const blob = item.getAsFile();
const ext = blob.type.split('/')[1] || 'png';
const filename = `pasted-${Date.now()}.${ext}`;
// Compute upload path hint
let uploadPath = currentNotePath || '';
if (!uploadPath && (imageStorageMode === 3 || imageStorageMode === 4) && currentFolderPath) {
uploadPath = currentFolderPath + '/_new_.md';
}
const formData = new FormData();
formData.append('file', blob, filename);
formData.append('path', uploadPath);
try {
const resp = await fetch('/upload', { method: 'POST', body: formData });
const data = await resp.json();
if (!resp.ok || !data.success) throw new Error(data.error || 'Upload failed');
// Build Obsidian link path for current note
let obsidianPath = filename;
if (imageStorageMode === 4 && currentFolderPath) {
obsidianPath = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
} else if (imageStorageMode === 3 && currentFolderPath) {
obsidianPath = `${currentFolderPath}/${filename}`;
} else if (imageStorageMode === 1) {
obsidianPath = filename;
} // mode 2 uses filename only
insertAtCursor(contentTextarea, `![[${obsidianPath}]]`);
renderPreview();
showNotification('Image pasted and uploaded', 'success');
} catch (err) {
showNotification('Paste upload failed: ' + err.message, 'error');
}
break;
}
}
});
function insertAtCursor(textarea, text) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
textarea.value = value.substring(0, start) + text + value.substring(end);
const pos = start + text.length;
textarea.focus();
textarea.setSelectionRange(pos, pos);
}
</script>
{{end}}