2025-08-25 09:44:14 +01:00
{{define "create"}}
{{template "base" .}}
{{end}}
2025-08-25 08:48:52 +01:00
2025-08-25 17:26:27 +01:00
{{define "create_content"}}
2025-08-25 08:48:52 +01:00
< div class = "max-w-4xl mx-auto p-6" >
<!-- Header -->
< div class = "mb-6" >
2025-08-25 17:54:27 +01:00
< div class = "flex items-center justify-between mb-4" >
< h1 class = "text-3xl font-bold text-white" > Create New Note< / 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 = "create-form" class = "btn-primary" >
< i class = "fas fa-save mr-2" > < / i > Create
< / button >
< / div >
< / div >
2025-08-25 08:48:52 +01:00
{{if .folder_path}}
< p class = "text-gray-400" >
< i class = "fas fa-folder mr-2" > < / i >
Creating in: < span class = "text-blue-400" > {{.folder_path}}< / span >
< / p >
{{end}}
< / div >
<!-- Create Form -->
< form id = "create-form" class = "space-y-6" >
< div class = "bg-gray-800 rounded-lg p-6" >
<!-- Title Input -->
< div class = "mb-6" >
< label for = "title" class = "block text-sm font-medium text-gray-300 mb-2" >
Note Title
< / label >
< input type = "text" id = "title" name = "title" required
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter note title (e.g., My New Note)">
< p class = "text-xs text-gray-500 mt-1" > The .md extension will be added automatically< / p >
< / div >
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 = "20"
2025-08-25 08:48:52 +01:00
class="editor-textarea"
placeholder="# Your Note Title
Start writing your note here using Markdown syntax...
## Examples
- **Bold text**
- *Italic text*
- [Links](https://example.com)
- 
- `Code snippets`
```javascript
// Code blocks
console.log('Hello, World!');
```
> Blockquotes
| Tables | Work | Too |
|--------|------|-----|
| Cell 1 | Cell 2 | Cell 3 |
">< / textarea >
2025-08-25 17:54:27 +01:00
< 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 >
2025-08-28 07:29:51 +01:00
<!-- Editor Toolbar -->
< div class = "flex items-center justify-between border-t border-gray-700 pt-4" >
< div class = "flex items-center flex-wrap gap-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 = "Strikethrough" >
< i class = "fas fa-strikethrough" > < / i >
< / button >
< button type = "button" class = "btn-secondary text-sm" onclick = "insertMarkdown('`', '`')" title = "Inline code" >
< i class = "fas fa-code" > < / i >
< / button >
< 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 >
< 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 >
<!-- 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" >
< i class = "fas fa-list" > < / i >
< / button >
< 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 >
< / div >
< div class = "text-xs text-gray-500" > Press Ctrl+S to create< / div >
< / div >
2025-08-25 08:48:52 +01:00
<!-- Actions -->
2025-08-25 18:15:51 +01:00
2025-08-25 08:48:52 +01:00
< / div >
< / form >
< / div >
{{end}}
2025-08-25 17:26:27 +01:00
{{define "create_scripts"}}
2025-08-25 08:48:52 +01:00
< script >
const createForm = document.getElementById('create-form');
const titleInput = document.getElementById('title');
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}}";
// Preview state (persist for create page)
const PREVIEW_STATE_KEY = 'previewEnabled:create';
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) {
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) {
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
// Render preview
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);
// Highlight code blocks
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
createForm.addEventListener('submit', function(e) {
e.preventDefault();
const title = titleInput.value.trim();
const content = contentTextarea.value;
const folderPath = '{{.folder_path}}';
if (!title) {
showNotification('Please enter a note title', 'error');
return;
}
const formData = new FormData();
formData.append('title', title);
formData.append('content', content);
formData.append('folder_path', folderPath);
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/create'), {
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 created successfully', 'success');
if (data.redirect) {
window.location.href = data.redirect;
}
} else {
throw new Error(data.error || 'Failed to create note');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Auto-resize textarea
contentTextarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
2025-08-25 17:54:27 +01:00
renderPreview();
2025-08-25 08:48:52 +01:00
});
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();
createForm.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-28 07:29:51 +01:00
// Markdown insertion functions (parity with edit.html)
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;
const newPos = start + before.length + selectedText.length;
contentTextarea.focus();
contentTextarea.setSelectionRange(newPos, newPos);
renderPreview();
}
function toggleHeadingMenu(e) {
const menu = document.getElementById('heading-menu');
if (!menu) return;
const wasHidden = menu.classList.contains('hidden');
menu.classList.remove('hidden');
menu.classList.remove('top-full','mt-2','bottom-full','mb-2');
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;
if (spaceBelow < needed & & spaceAbove > = needed) {
menu.classList.add('bottom-full','mb-2','right-0');
} else {
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) {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const hashes = '#'.repeat(Math.min(Math.max(level,1),6)) + ' ';
const newText = text.substring(0, lineStart) + hashes + text.substring(lineStart);
contentTextarea.value = newText;
const newPos = start + hashes.length;
contentTextarea.focus();
contentTextarea.setSelectionRange(newPos, newPos);
const menu = document.getElementById('heading-menu');
if (menu) menu.classList.add('hidden');
renderPreview();
}
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);
renderPreview();
}
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);
renderPreview();
}
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);
renderPreview();
}
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);
renderPreview();
}
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);
renderPreview();
}
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;
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
renderPreview();
}
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;
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
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;
contentTextarea.focus();
contentTextarea.setSelectionRange(pos, pos);
renderPreview();
}
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 = '';
if (imageStorageMode === 3 || imageStorageMode === 4) {
// Use folder path with a dummy file to hint the directory
if (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 draft
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;
}
2025-08-25 08:48:52 +01:00
}
});
2025-08-25 17:54:27 +01:00
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}}