302 lines
11 KiB
HTML
302 lines
11 KiB
HTML
{{define "create"}}
|
|
{{template "base" .}}
|
|
{{end}}
|
|
|
|
{{define "create_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">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>
|
|
{{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>
|
|
|
|
<!-- 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="20"
|
|
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>
|
|
<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>
|
|
|
|
<!-- Actions -->
|
|
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "create_scripts"}}
|
|
<script>
|
|
const createForm = document.getElementById('create-form');
|
|
const titleInput = document.getElementById('title');
|
|
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 = parseInt('{{.image_storage_mode}}', 10) || 1;
|
|
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) {
|
|
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) {
|
|
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');
|
|
}
|
|
}
|
|
|
|
// Render preview
|
|
function renderPreview() {
|
|
if (!previewEnabled) return;
|
|
if (!livePreview || !window.marked) return;
|
|
const transformed = transformObsidianEmbeds(contentTextarea.value || '');
|
|
livePreview.innerHTML = marked.parse(transformed);
|
|
// Highlight code blocks
|
|
livePreview.querySelectorAll('pre code').forEach(block => {
|
|
try { hljs.highlightElement(block); } catch (e) {}
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
// CSRF token from cookie
|
|
const csrf = (document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1] ? decodeURIComponent((document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)||[])[1]) : '';
|
|
|
|
fetch('/editor/create', {
|
|
method: 'POST',
|
|
headers: csrf ? { 'X-CSRF-Token': csrf } : {},
|
|
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';
|
|
renderPreview();
|
|
});
|
|
|
|
// 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();
|
|
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;
|
|
renderPreview();
|
|
}
|
|
});
|
|
|
|
// 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 {
|
|
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 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;
|
|
}
|
|
}
|
|
});
|
|
|
|
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}}
|