2025-08-25 09:44:14 +01:00
{{define "edit"}}
{{template "base" .}}
{{end}}
2025-08-25 08:48:52 +01:00
2025-08-25 17:26:27 +01:00
{{define "edit_content"}}
2025-08-25 08:48:52 +01:00
< 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" >
2025-08-25 17:54:27 +01:00
< button id = "toggle-preview" type = "button" class = "btn-secondary" >
2025-08-25 08:48:52 +01:00
< i class = "fas fa-eye mr-2" > < / i > Preview
2025-08-25 17:54:27 +01:00
< / button >
2025-08-25 08:48:52 +01:00
< 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 >
2025-08-26 20:55:08 +01:00
< a href = "{{url " / folder / " } } { { . folder_path } } " class = "text-blue-400 hover:text-blue-300" > {{.folder_path}}< / a >
2025-08-25 08:48:52 +01:00
< / p >
{{end}}
< / div >
<!-- Edit Form -->
< form id = "edit-form" class = "space-y-6" >
< div class = "bg-gray-800 rounded-lg p-6" >
2025-08-25 17:54:27 +01:00
<!-- Content Editor with Live Preview -->
2025-08-25 08:48:52 +01:00
< div class = "mb-6" >
2025-08-25 17:54:27 +01:00
< 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 >
2025-08-25 08:48:52 +01:00
< / div >
<!-- Editor Toolbar -->
< div class = "flex items-center justify-between border-t border-gray-700 pt-4" >
2025-08-28 07:29:51 +01:00
< div class = "flex items-center flex-wrap gap-2" >
2025-08-25 08:48:52 +01:00
< 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 >
2025-08-28 07:29:51 +01:00
< button type = "button" class = "btn-secondary text-sm" onclick = "insertMarkdown('~~', '~~')" title = "Strikethrough" >
< i class = "fas fa-strikethrough" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertMarkdown('`', '`')" title = "Inline code" >
2025-08-25 08:48:52 +01:00
< i class = "fas fa-code" > < / i >
< / button >
2025-08-28 07:29:51 +01:00
< button type = "button" class = "btn-secondary text-sm" onclick = "insertCodeBlock()" title = "Code block" >
< i class = "fas fa-file-code" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertMermaid()" title = "Mermaid diagram" >
< i class = "fas fa-project-diagram" > < / i >
< / button >
2025-08-25 08:48:52 +01:00
< 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('')" title = "Image" >
< i class = "fas fa-image" > < / i >
< / button >
2025-08-28 07:29:51 +01:00
<!-- Headings dropdown -->
< div class = "relative" >
< button type = "button" class = "btn-secondary text-sm" onclick = "toggleHeadingMenu(event)" title = "Headings" >
< i class = "fas fa-heading" > < / i >
< / button >
< div id = "heading-menu" class = "absolute z-10 hidden bg-slate-800 border border-gray-700 rounded shadow-lg right-0 bottom-full mb-2" >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(1)" > H1< / button >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(2)" > H2< / button >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(3)" > H3< / button >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(4)" > H4< / button >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(5)" > H5< / button >
< button type = "button" class = "block w-full text-left px-3 py-2 hover:bg-slate-700" onclick = "insertHeadingLevel(6)" > H6< / button >
< / div >
< / div >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertList()" title = "Bulleted list" >
2025-08-25 08:48:52 +01:00
< i class = "fas fa-list" > < / i >
< / button >
2025-08-28 07:29:51 +01:00
< button type = "button" class = "btn-secondary text-sm" onclick = "insertNumberedList()" title = "Numbered list" >
< i class = "fas fa-list-ol" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertChecklist()" title = "Task list" >
< i class = "fas fa-square-check" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertBlockquote()" title = "Blockquote" >
< i class = "fas fa-quote-left" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertHr()" title = "Horizontal rule" >
< i class = "fas fa-minus" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertTable()" title = "Table" >
< i class = "fas fa-table" > < / i >
< / button >
2025-08-25 08:48:52 +01:00
< / div >
< div class = "text-xs text-gray-500" >
Press Ctrl+S to save
< / div >
< / div >
< / div >
< / form >
< / div >
{{end}}
2025-08-25 17:26:27 +01:00
{{define "edit_scripts"}}
2025-08-25 08:48:52 +01:00
< script >
const editForm = document.getElementById('edit-form');
const contentTextarea = document.getElementById('content');
2025-08-25 17:54:27 +01:00
const livePreview = document.getElementById('live-preview');
const editorGrid = document.getElementById('editor-grid');
const togglePreviewBtn = document.getElementById('toggle-preview');
2025-08-25 21:19:15 +01:00
const imageStorageMode = parseInt('{{.image_storage_mode}}', 10) || 1;
2025-08-25 17:54:27 +01:00
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) {
2025-08-26 20:55:08 +01:00
return window.prefix(`/serve_stored_image/${filename}`);
2025-08-25 17:54:27 +01:00
}
let path = filename;
if (imageStorageMode === 3 & & currentFolderPath) {
path = `${currentFolderPath}/${filename}`;
} else if (imageStorageMode === 4 & & currentFolderPath) {
path = `${currentFolderPath}/${imageSubfolderName}/${filename}`;
}
2025-08-26 20:55:08 +01:00
return window.prefix(`/serve_attached_image/${path}`);
2025-08-25 17:54:27 +01:00
}
function transformObsidianEmbeds(md) {
// Convert ![[name.png]] or ![[sub/name.png|alt]] to 
return (md || '').replace(/!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, (m, file, alt) => {
const filename = file.trim();
const url = buildImageURL(filename);
const altText = (alt || filename).trim();
return ``;
});
}
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');
}
}
2025-08-28 07:29:51 +01:00
function transformMermaidFences(md) {
return (md || '').replace(/```mermaid\n([\s\S]*?)```/g, function(_, inner){
return `< div class = \"mermaid\" > ${inner}< / div > `;
});
}
function transformMediaEmbeds(md) {
const lines = (md || '').split(/\n/);
return lines.map(l => {
const t = l.trim();
if (/^https?:\/\/\S+\.(?:mp3|wav|ogg)$/.test(t)) return `< audio controls preload = \"metadata\" src = \"${t}\" > < / audio > `;
if (/^https?:\/\/\S+\.(?:mp4|webm|ogg)$/.test(t)) return `< video controls preload = \"metadata\" style = \"max-width:100%\" src = \"${t}\" > < / video > `;
if (/^https?:\/\/\S+\.(?:pdf)$/.test(t)) return `< iframe src = \"${t}\" style = \"width:100%;height:70vh;border:1px solid # 374151 ; border-radius:8px \ " > < / iframe > `;
return l;
}).join('\n');
}
2025-08-25 17:54:27 +01:00
function renderPreview() {
if (!previewEnabled) return;
if (!livePreview || !window.marked) return;
2025-08-28 07:29:51 +01:00
let transformed = transformObsidianEmbeds(contentTextarea.value || '');
transformed = transformMermaidFences(transformed);
transformed = transformMediaEmbeds(transformed);
2025-08-25 17:54:27 +01:00
livePreview.innerHTML = marked.parse(transformed);
livePreview.querySelectorAll('pre code').forEach(block => {
try { hljs.highlightElement(block); } catch (e) {}
});
2025-08-28 07:29:51 +01:00
if (window.mermaid) {
try { window.mermaid.run({ querySelector: '#live-preview .mermaid' }); } catch (e) {}
}
2025-08-25 17:54:27 +01:00
}
2025-08-25 08:48:52 +01:00
editForm.addEventListener('submit', function(e) {
e.preventDefault();
const content = contentTextarea.value;
const notePath = '{{.note_path}}';
const formData = new FormData();
formData.append('content', content);
2025-08-25 21:19:15 +01:00
// CSRF token from cookie
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
2025-08-26 20:55:08 +01:00
fetch(window.prefix('/editor/edit/' + notePath), {
2025-08-25 08:48:52 +01:00
method: 'POST',
2025-08-25 21:19:15 +01:00
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
2025-08-25 08:48:52 +01:00
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';
2025-08-25 17:54:27 +01:00
renderPreview();
2025-08-25 08:48:52 +01:00
}
contentTextarea.addEventListener('input', autoResize);
// Initial resize
autoResize();
2025-08-25 17:54:27 +01:00
// 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();
});
}
2025-08-25 08:48:52 +01:00
// 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;
2025-08-25 17:54:27 +01:00
renderPreview();
2025-08-25 08:48:52 +01:00
}
});
// 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();
}
2025-08-28 07:29:51 +01:00
function toggleHeadingMenu(e) {
const menu = document.getElementById('heading-menu');
if (!menu) return;
// Show to measure, then decide direction
const wasHidden = menu.classList.contains('hidden');
menu.classList.remove('hidden');
// Reset placement classes
menu.classList.remove('top-full','mt-2','bottom-full','mb-2');
// Determine available space
const btn = e.currentTarget;
const rect = btn.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
const needed = Math.max(menu.offsetHeight || 192, 192) + 12; // fallback height
if (spaceBelow < needed & & spaceAbove > = needed) {
// Open upwards
menu.classList.add('bottom-full','mb-2','right-0');
} else {
// Open downwards
menu.classList.add('top-full','mt-2','right-0');
}
if (wasHidden) {
const close = (ev) => {
if (!menu.contains(ev.target) & & ev.target !== btn) {
menu.classList.add('hidden');
document.removeEventListener('click', close);
}
};
setTimeout(() => document.addEventListener('click', close), 0);
}
}
function insertHeadingLevel(level) {
2025-08-25 08:48:52 +01:00
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
2025-08-28 07:29:51 +01:00
const hashes = '#'.repeat(Math.min(Math.max(level,1),6)) + ' ';
const newText = text.substring(0, lineStart) + hashes + text.substring(lineStart);
2025-08-25 08:48:52 +01:00
contentTextarea.value = newText;
2025-08-28 07:29:51 +01:00
const newPos = start + hashes.length;
2025-08-25 08:48:52 +01:00
contentTextarea.focus();
2025-08-28 07:29:51 +01:00
contentTextarea.setSelectionRange(newPos, newPos);
2025-08-25 08:48:52 +01:00
autoResize();
2025-08-28 07:29:51 +01:00
const menu = document.getElementById('heading-menu');
if (menu) menu.classList.add('hidden');
2025-08-25 08:48:52 +01:00
}
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();
}
2025-08-25 17:54:27 +01:00
2025-08-28 07:29:51 +01:00
function insertNumberedList() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const newText = text.substring(0, lineStart) + '1. ' + text.substring(lineStart);
contentTextarea.value = newText;
contentTextarea.focus();
contentTextarea.setSelectionRange(start + 3, start + 3);
autoResize();
}
function insertChecklist() {
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 + 6, start + 6);
autoResize();
}
function insertBlockquote() {
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();
}
function insertHr() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const insert = '\n\n---\n\n';
const newText = text.substring(0, start) + insert + text.substring(start);
contentTextarea.value = newText;
const pos = start + insert.length;
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
autoResize();
}
function insertCodeBlock() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const insert = '\n```language\n// code\n```\n';
const newText = text.substring(0, start) + insert + text.substring(start);
contentTextarea.value = newText;
const pos = start + 4; // place cursor after first backticks
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
autoResize();
}
function insertMermaid() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const tpl = '\n```mermaid\nflowchart TD\n A[Start] --> B{Decision}\n B -- Yes --> C[Do thing]\n B -- No --> D[Something else]\n```\n';
const newText = text.substring(0, start) + tpl + text.substring(start);
contentTextarea.value = newText;
const pos = start + 4; // cursor after opening backticks
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
autoResize();
renderPreview();
}
function insertTable() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const tpl = '\n| Col 1 | Col 2 | Col 3 |\n|-------|-------|-------|\n| A1 | B1 | C1 |\n| A2 | B2 | C2 |\n';
const newText = text.substring(0, start) + tpl + text.substring(start);
contentTextarea.value = newText;
const pos = start + 3; // inside first cell
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
autoResize();
}
2025-08-25 17:54:27 +01:00
// 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 {
2025-08-26 21:43:47 +01:00
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
const resp = await fetch(window.prefix('/editor/upload'), {
method: 'POST',
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
body: formData
});
2025-08-25 17:54:27 +01:00
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);
}
2025-08-25 08:48:52 +01:00
< / script >
{{end}}