1045 lines
36 KiB
HTML
1045 lines
36 KiB
HTML
{% extends "layout.html" %}
|
||
|
||
{% block content %}
|
||
<div class="note-editor-container">
|
||
<!-- Page Header -->
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<div class="header-left">
|
||
<h1 class="page-title">
|
||
<span class="page-icon">{% if note %}✏️{% else %}📝{% endif %}</span>
|
||
{% if note %}Edit Note{% else %}Create New Note{% endif %}
|
||
</h1>
|
||
<p class="page-subtitle">
|
||
{% if note %}
|
||
Editing: {{ note.title }}
|
||
{% else %}
|
||
Write markdown notes with live preview
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button type="button" class="btn btn-secondary" id="settings-toggle">
|
||
<span class="icon">⚙️</span>
|
||
Settings
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" id="preview-toggle">
|
||
<span class="icon">👁️</span>
|
||
<span class="toggle-text">Hide Preview</span>
|
||
</button>
|
||
<a href="{{ url_for('notes.notes_list') }}" class="btn btn-secondary">
|
||
<span class="icon">×</span>
|
||
Cancel
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Panel -->
|
||
<div class="settings-panel" id="settings-panel" style="display: none;">
|
||
<div class="settings-card">
|
||
<div class="settings-grid">
|
||
<div class="settings-item">
|
||
<label for="visibility" class="settings-label">
|
||
<span class="icon">👁️</span>
|
||
Visibility
|
||
</label>
|
||
<select id="visibility" name="visibility" class="form-control">
|
||
<option value="Private" {% if not note or note.visibility.value == 'Private' %}selected{% endif %}>
|
||
🔒 Private - Only you can see this
|
||
</option>
|
||
<option value="Team" {% if note and note.visibility.value == 'Team' %}selected{% endif %}>
|
||
👥 Team - Your team members can see this
|
||
</option>
|
||
<option value="Company" {% if note and note.visibility.value == 'Company' %}selected{% endif %}>
|
||
🏢 Company - Everyone in your company can see this
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="settings-item">
|
||
<label for="folder" class="settings-label">
|
||
<span class="icon">📁</span>
|
||
Folder
|
||
</label>
|
||
<input type="text" id="folder" name="folder" class="form-control"
|
||
placeholder="e.g., Work/Projects or Personal"
|
||
value="{{ note.folder if note and note.folder else '' }}"
|
||
list="folder-suggestions">
|
||
<datalist id="folder-suggestions">
|
||
{% for folder in folders %}
|
||
<option value="{{ folder.path }}">
|
||
{% endfor %}
|
||
</datalist>
|
||
</div>
|
||
|
||
<div class="settings-item">
|
||
<label for="tags" class="settings-label">
|
||
<span class="icon">🏷️</span>
|
||
Tags
|
||
</label>
|
||
<input type="text" id="tags" name="tags" class="form-control"
|
||
placeholder="documentation, meeting-notes, technical"
|
||
value="{{ note.tags if note and note.tags else '' }}">
|
||
<small class="form-text">Separate tags with commas</small>
|
||
</div>
|
||
|
||
<div class="settings-item">
|
||
<label for="project_id" class="settings-label">
|
||
<span class="icon">📋</span>
|
||
Project
|
||
</label>
|
||
<select id="project_id" name="project_id" class="form-control">
|
||
<option value="">No project</option>
|
||
{% for project in projects %}
|
||
<option value="{{ project.id }}"
|
||
{% if note and note.project_id == project.id %}selected{% endif %}
|
||
{% if task and task.project_id == project.id %}selected{% endif %}>
|
||
{{ project.code }} - {{ project.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="settings-item">
|
||
<label for="task_id" class="settings-label">
|
||
<span class="icon">✅</span>
|
||
Task
|
||
</label>
|
||
<select id="task_id" name="task_id" class="form-control">
|
||
<option value="">No task</option>
|
||
<!-- Tasks will be populated dynamically based on project selection -->
|
||
{% if task %}
|
||
<option value="{{ task.id }}" selected>
|
||
#{{ task.id }} - {{ task.title }}
|
||
</option>
|
||
{% endif %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="settings-item">
|
||
<label class="settings-label">
|
||
<span class="icon">📌</span>
|
||
Pin Note
|
||
</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="is_pinned" name="is_pinned"
|
||
{% if note and note.is_pinned %}checked{% endif %}>
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Editor Form -->
|
||
<form method="POST" id="note-form">
|
||
<input type="hidden" id="title" name="title" value="{{ note.title if note else '' }}" required>
|
||
<input type="hidden" id="hidden-folder" name="folder" value="{{ note.folder if note and note.folder else '' }}">
|
||
<input type="hidden" id="hidden-visibility" name="visibility" value="{{ note.visibility.value if note else 'Private' }}">
|
||
<input type="hidden" id="hidden-tags" name="tags" value="{{ note.tags if note and note.tags else '' }}">
|
||
<input type="hidden" id="hidden-project-id" name="project_id" value="{{ note.project_id if note and note.project_id else '' }}">
|
||
<input type="hidden" id="hidden-task-id" name="task_id" value="{{ note.task_id if note and note.task_id else '' }}">
|
||
<input type="hidden" id="hidden-is-pinned" name="is_pinned" value="{{ '1' if note and note.is_pinned else '0' }}">
|
||
|
||
<div class="editor-layout" id="editor-layout">
|
||
<!-- Editor Panel -->
|
||
<div class="editor-panel">
|
||
<div class="editor-card">
|
||
<!-- Toolbar -->
|
||
<div class="editor-toolbar">
|
||
<div class="toolbar-group">
|
||
<button type="button" class="toolbar-btn" onclick="toggleFrontmatter()" title="Toggle Frontmatter">
|
||
<span class="icon">📄</span>
|
||
<span class="btn-text">Frontmatter</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<div class="toolbar-group">
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('**', '**')" title="Bold (Ctrl+B)">
|
||
<strong>B</strong>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('*', '*')" title="Italic (Ctrl+I)">
|
||
<em>I</em>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('~~', '~~')" title="Strikethrough">
|
||
<s>S</s>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('`', '`')" title="Inline Code">
|
||
<code></></code>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<div class="toolbar-group">
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('# ', '')" title="Heading 1">
|
||
H1
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('## ', '')" title="Heading 2">
|
||
H2
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('### ', '')" title="Heading 3">
|
||
H3
|
||
</button>
|
||
</div>
|
||
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<div class="toolbar-group">
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('- ', '')" title="Bullet List">
|
||
<span class="icon">•</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('1. ', '')" title="Numbered List">
|
||
<span class="icon">1.</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('- [ ] ', '')" title="Checklist">
|
||
<span class="icon">☐</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('> ', '')" title="Quote">
|
||
<span class="icon">❝</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<div class="toolbar-group">
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('[', '](url)')" title="Link (Ctrl+K)">
|
||
<span class="icon">🔗</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('')" title="Image">
|
||
<span class="icon">🖼️</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertTable()" title="Table">
|
||
<span class="icon">⊞</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('```\n', '\n```')" title="Code Block">
|
||
<span class="icon">{ }</span>
|
||
</button>
|
||
<button type="button" class="toolbar-btn" onclick="insertMarkdown('\n---\n', '')" title="Horizontal Rule">
|
||
<span class="icon">—</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ace Editor Container -->
|
||
<div id="ace-editor" class="ace-editor-container">{{ note.content if note else '# New Note\n\nStart writing here...' }}</div>
|
||
<textarea id="content" name="content" style="display: none;" required>{{ note.content if note else '# New Note\n\nStart writing here...' }}</textarea>
|
||
|
||
<!-- Editor Footer -->
|
||
<div class="editor-footer">
|
||
<div class="editor-stats">
|
||
<span id="word-count">0 words</span>
|
||
<span class="stat-divider">•</span>
|
||
<span id="char-count">0 characters</span>
|
||
</div>
|
||
<div class="editor-actions">
|
||
<button type="submit" class="btn btn-primary">
|
||
<span class="icon">💾</span>
|
||
{% if note %}Update Note{% else %}Create Note{% endif %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preview Panel -->
|
||
<div class="preview-panel" id="preview-panel">
|
||
<div class="preview-card">
|
||
<div class="preview-header">
|
||
<h3 class="preview-title">
|
||
<span class="icon">👁️</span>
|
||
Preview
|
||
</h3>
|
||
</div>
|
||
<div id="preview-content" class="preview-content markdown-content">
|
||
<p class="preview-placeholder">Start typing to see the preview...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Include Ace Editor -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/ace.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/mode-markdown.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/theme-github.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.2/ext-language_tools.js"></script>
|
||
|
||
<style>
|
||
/* Note Editor Styles following the new design system */
|
||
.note-editor-container {
|
||
padding: 2rem;
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* Settings Panel */
|
||
.settings-panel {
|
||
margin-bottom: 2rem;
|
||
animation: slideDown 0.3s ease-out;
|
||
}
|
||
|
||
.settings-card {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 2rem;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.settings-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.settings-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.settings-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
font-size: 0.875rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.025em;
|
||
}
|
||
|
||
/* Toggle Switch */
|
||
.toggle-switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 50px;
|
||
height: 24px;
|
||
}
|
||
|
||
.toggle-switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.toggle-slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #ccc;
|
||
transition: .4s;
|
||
border-radius: 34px;
|
||
}
|
||
|
||
.toggle-slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 16px;
|
||
width: 16px;
|
||
left: 4px;
|
||
bottom: 4px;
|
||
background-color: white;
|
||
transition: .4s;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.toggle-switch input:checked + .toggle-slider {
|
||
background-color: var(--primary-color);
|
||
}
|
||
|
||
.toggle-switch input:checked + .toggle-slider:before {
|
||
transform: translateX(26px);
|
||
}
|
||
|
||
/* Editor Layout */
|
||
.editor-layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 2rem;
|
||
min-height: 600px;
|
||
}
|
||
|
||
.editor-layout.preview-hidden {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
/* Editor Panel */
|
||
.editor-card {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
/* Toolbar */
|
||
.editor-toolbar {
|
||
background: #f8f9fa;
|
||
border-bottom: 1px solid #e9ecef;
|
||
padding: 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-group {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.toolbar-divider {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: #dee2e6;
|
||
margin: 0 0.5rem;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 0.25rem 0.5rem;
|
||
min-width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-size: 0.875rem;
|
||
color: #495057;
|
||
}
|
||
|
||
.toolbar-btn:hover {
|
||
background: #e9ecef;
|
||
border-color: #adb5bd;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.toolbar-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.toolbar-btn code {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.btn-text {
|
||
margin-left: 0.25rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
/* Ace Editor */
|
||
.ace-editor-container {
|
||
flex: 1;
|
||
min-height: 500px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Editor Footer */
|
||
.editor-footer {
|
||
background: #f8f9fa;
|
||
border-top: 1px solid #e9ecef;
|
||
padding: 1rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.editor-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.875rem;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.stat-divider {
|
||
color: #dee2e6;
|
||
}
|
||
|
||
/* Preview Panel */
|
||
.preview-card {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.preview-header {
|
||
background: #f8f9fa;
|
||
border-bottom: 1px solid #e9ecef;
|
||
padding: 1rem 1.5rem;
|
||
}
|
||
|
||
.preview-title {
|
||
margin: 0;
|
||
font-size: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.preview-content {
|
||
flex: 1;
|
||
padding: 2rem;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.preview-placeholder {
|
||
color: #6c757d;
|
||
font-style: italic;
|
||
text-align: center;
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 1024px) {
|
||
.editor-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.preview-panel {
|
||
display: none;
|
||
}
|
||
|
||
.editor-layout:not(.preview-hidden) .preview-panel {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.note-editor-container {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.settings-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.page-header .header-content {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.header-actions {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.editor-toolbar {
|
||
overflow-x: auto;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.toolbar-group {
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
// Global variables
|
||
let aceEditor;
|
||
let previewTimer;
|
||
let frontmatterUpdateTimer;
|
||
|
||
// Function to toggle frontmatter visibility
|
||
function toggleFrontmatter() {
|
||
if (!aceEditor) return;
|
||
|
||
const content = aceEditor.getValue();
|
||
const frontmatterRegex = /^---\n[\s\S]*?\n---\n/;
|
||
const hasFrontmatter = frontmatterRegex.test(content);
|
||
|
||
if (hasFrontmatter) {
|
||
// Find the end of frontmatter
|
||
const match = content.match(frontmatterRegex);
|
||
if (match) {
|
||
const endPos = aceEditor.getSession().getDocument().indexToPosition(match[0].length);
|
||
|
||
// Check if frontmatter is folded
|
||
const foldLine = aceEditor.getSession().getFoldLine(0);
|
||
if (foldLine) {
|
||
// Unfold
|
||
aceEditor.getSession().unfold(foldLine.range);
|
||
} else {
|
||
// Fold
|
||
aceEditor.getSession().foldAll(0, endPos.row);
|
||
}
|
||
}
|
||
} else {
|
||
// Add frontmatter if it doesn't exist
|
||
const newContent = updateContentFrontmatter(content);
|
||
aceEditor.setValue(newContent, -1);
|
||
}
|
||
}
|
||
|
||
// Extract title from content
|
||
function extractTitleFromContent(content) {
|
||
// First try to get title from frontmatter
|
||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||
if (frontmatterMatch) {
|
||
const titleMatch = frontmatterMatch[1].match(/^title:\s*(.+)$/m);
|
||
if (titleMatch) {
|
||
return titleMatch[1].trim();
|
||
}
|
||
}
|
||
|
||
// Otherwise, get first heading
|
||
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
||
if (headingMatch) {
|
||
return headingMatch[1].trim();
|
||
}
|
||
|
||
// Default
|
||
return 'Untitled Note';
|
||
}
|
||
|
||
// Update content with frontmatter
|
||
function updateContentFrontmatter(content) {
|
||
const title = extractTitleFromContent(content);
|
||
const visibility = document.getElementById('visibility').value.toLowerCase();
|
||
const folder = document.getElementById('folder').value;
|
||
const tags = document.getElementById('tags').value;
|
||
const projectSelect = document.getElementById('project_id');
|
||
const taskSelect = document.getElementById('task_id');
|
||
const isPinned = document.getElementById('is_pinned').checked;
|
||
|
||
let frontmatter = `---\nvisibility: ${visibility}\n`;
|
||
|
||
if (projectSelect.value) {
|
||
const projectText = projectSelect.options[projectSelect.selectedIndex].text;
|
||
const projectCode = projectText.split(' - ')[0];
|
||
frontmatter += `project: ${projectCode}\n`;
|
||
} else {
|
||
frontmatter += `project: No project\n`;
|
||
}
|
||
|
||
if (taskSelect.value) {
|
||
frontmatter += `task_id: ${taskSelect.value}\n`;
|
||
}
|
||
|
||
if (folder) {
|
||
frontmatter += `folder: ${folder}\n`;
|
||
}
|
||
|
||
if (tags) {
|
||
const tagList = tags.split(',').map(t => t.trim()).filter(t => t);
|
||
if (tagList.length > 0) {
|
||
frontmatter += `tags: [${tagList.map(t => `"${t}"`).join(', ')}]\n`;
|
||
}
|
||
}
|
||
|
||
frontmatter += `pinned: ${isPinned}\n`;
|
||
frontmatter += `title: ${title}\n`;
|
||
frontmatter += `---\n\n`;
|
||
|
||
// Remove existing frontmatter if present
|
||
const existingFrontmatterRegex = /^---\n[\s\S]*?\n---\n\n?/;
|
||
const cleanContent = content.replace(existingFrontmatterRegex, '');
|
||
|
||
return frontmatter + cleanContent;
|
||
}
|
||
|
||
// Sync content and update preview
|
||
function syncContentAndUpdatePreview(updateFrontmatter = true) {
|
||
if (!aceEditor) return;
|
||
|
||
let content = aceEditor.getValue();
|
||
|
||
// Only update frontmatter when settings change, not on every keystroke
|
||
if (updateFrontmatter) {
|
||
clearTimeout(frontmatterUpdateTimer);
|
||
frontmatterUpdateTimer = setTimeout(() => {
|
||
const newContent = updateContentFrontmatter(aceEditor.getValue());
|
||
if (aceEditor.getValue() !== newContent) {
|
||
const currentPosition = aceEditor.getCursorPosition();
|
||
aceEditor.setValue(newContent, -1);
|
||
aceEditor.moveCursorToPosition(currentPosition);
|
||
}
|
||
}, 2000); // Wait 2 seconds after typing stops
|
||
}
|
||
|
||
// Update the hidden textarea first
|
||
document.getElementById('content').value = content;
|
||
|
||
// Update title from content
|
||
const title = extractTitleFromContent(content);
|
||
document.getElementById('title').value = title;
|
||
|
||
// Update the page header to show current title
|
||
const headerTitle = document.querySelector('.page-subtitle');
|
||
if (headerTitle) {
|
||
const isEdit = headerTitle.textContent.includes('Editing');
|
||
headerTitle.textContent = title ? (isEdit ? `Editing: ${title}` : title) : (isEdit ? 'Edit Note' : 'Create Note');
|
||
}
|
||
|
||
// Sync settings from frontmatter
|
||
syncSettingsFromFrontmatter(content);
|
||
|
||
// Update preview after everything else is synced
|
||
updatePreview();
|
||
}
|
||
|
||
// Sync settings UI from frontmatter
|
||
function syncSettingsFromFrontmatter(content) {
|
||
if (!content.trim().startsWith('---')) return;
|
||
|
||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||
if (!frontmatterMatch) return;
|
||
|
||
const frontmatter = frontmatterMatch[1];
|
||
const lines = frontmatter.split('\n');
|
||
|
||
lines.forEach(line => {
|
||
const [key, ...valueParts] = line.split(':');
|
||
if (!key) return;
|
||
|
||
const value = valueParts.join(':').trim();
|
||
|
||
switch (key.trim()) {
|
||
case 'visibility':
|
||
const visibilitySelect = document.getElementById('visibility');
|
||
const capitalizedValue = value.charAt(0).toUpperCase() + value.slice(1);
|
||
for (let option of visibilitySelect.options) {
|
||
if (option.value === capitalizedValue) {
|
||
visibilitySelect.value = capitalizedValue;
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'folder':
|
||
document.getElementById('folder').value = value;
|
||
break;
|
||
|
||
case 'tags':
|
||
// Handle both array format and comma-separated format
|
||
let tags = value;
|
||
if (tags.startsWith('[') && tags.endsWith(']')) {
|
||
// Parse array format
|
||
tags = tags.slice(1, -1).split(',').map(t => t.trim().replace(/^["']|["']$/g, '')).join(', ');
|
||
}
|
||
document.getElementById('tags').value = tags;
|
||
break;
|
||
|
||
case 'pinned':
|
||
document.getElementById('is_pinned').checked = value === 'true';
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update preview
|
||
function updatePreview() {
|
||
clearTimeout(previewTimer);
|
||
previewTimer = setTimeout(() => {
|
||
const content = aceEditor ? aceEditor.getValue() : document.getElementById('content').value;
|
||
const preview = document.getElementById('preview-content');
|
||
|
||
if (content.trim() === '') {
|
||
preview.innerHTML = '<p class="preview-placeholder">Start typing to see the preview...</p>';
|
||
return;
|
||
}
|
||
|
||
// Send content to server for markdown rendering
|
||
fetch('/api/render-markdown', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ content: content })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.html) {
|
||
preview.innerHTML = data.html;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error rendering markdown:', error);
|
||
});
|
||
}, 300);
|
||
}
|
||
|
||
// Insert markdown
|
||
function insertMarkdown(before, after) {
|
||
if (!aceEditor) return;
|
||
|
||
const session = aceEditor.getSession();
|
||
const selection = aceEditor.getSelection();
|
||
const selectedText = session.getTextRange(selection.getRange());
|
||
const newText = before + (selectedText || 'text') + after;
|
||
|
||
if (selectedText) {
|
||
// Replace selection
|
||
session.replace(selection.getRange(), newText);
|
||
} else {
|
||
// Insert at cursor
|
||
aceEditor.insert(newText);
|
||
// Move cursor between markers
|
||
const pos = aceEditor.getCursorPosition();
|
||
aceEditor.moveCursorTo(pos.row, pos.column - after.length);
|
||
}
|
||
|
||
aceEditor.focus();
|
||
}
|
||
|
||
// Insert table
|
||
function insertTable() {
|
||
if (!aceEditor) return;
|
||
|
||
const table = '\n| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |\n\n';
|
||
aceEditor.insert(table);
|
||
aceEditor.focus();
|
||
}
|
||
|
||
// Update word and character count
|
||
function updateStats() {
|
||
if (!aceEditor) return;
|
||
|
||
const content = aceEditor.getValue();
|
||
const words = content.trim().split(/\s+/).filter(w => w.length > 0).length;
|
||
const chars = content.length;
|
||
|
||
document.getElementById('word-count').textContent = `${words} words`;
|
||
document.getElementById('char-count').textContent = `${chars} characters`;
|
||
}
|
||
|
||
// Initialize Ace Editor
|
||
function initializeAceEditor() {
|
||
// Create Ace Editor instance
|
||
aceEditor = ace.edit("ace-editor");
|
||
|
||
// Set theme (use github theme for light mode)
|
||
aceEditor.setTheme("ace/theme/github");
|
||
|
||
// Set markdown mode (which includes YAML frontmatter highlighting)
|
||
aceEditor.session.setMode("ace/mode/markdown");
|
||
|
||
// Configure editor options
|
||
aceEditor.setOptions({
|
||
fontSize: "14px",
|
||
showPrintMargin: false,
|
||
showGutter: true,
|
||
highlightActiveLine: true,
|
||
enableBasicAutocompletion: true,
|
||
enableLiveAutocompletion: true,
|
||
enableSnippets: true,
|
||
tabSize: 2,
|
||
useSoftTabs: true,
|
||
wrap: true,
|
||
showInvisibles: false,
|
||
scrollPastEnd: 0.5
|
||
});
|
||
|
||
// Set initial content from hidden textarea
|
||
const initialContent = document.getElementById('content').value;
|
||
aceEditor.setValue(initialContent, -1); // -1 moves cursor to start
|
||
|
||
// If editing and has content, sync from frontmatter
|
||
if (initialContent) {
|
||
// If editing existing note without frontmatter, add it
|
||
if (!initialContent.trim().startsWith('---')) {
|
||
const newContent = updateContentFrontmatter(initialContent);
|
||
aceEditor.setValue(newContent, -1);
|
||
}
|
||
syncSettingsFromFrontmatter(aceEditor.getValue());
|
||
const title = extractTitleFromContent(aceEditor.getValue());
|
||
document.getElementById('title').value = title;
|
||
}
|
||
|
||
// Listen for changes in Ace Editor
|
||
aceEditor.on('change', function() {
|
||
syncContentAndUpdatePreview(false); // Don't update frontmatter on every keystroke
|
||
updateStats();
|
||
});
|
||
|
||
// If this is a new note, add initial frontmatter
|
||
if (!initialContent || initialContent.trim() === '') {
|
||
const newContent = updateContentFrontmatter('# New Note\n\nStart writing here...');
|
||
aceEditor.setValue(newContent, -1);
|
||
syncContentAndUpdatePreview(false);
|
||
}
|
||
|
||
// Function to sync settings to hidden fields
|
||
function syncSettingsToHiddenFields() {
|
||
document.getElementById('hidden-folder').value = document.getElementById('folder').value;
|
||
document.getElementById('hidden-visibility').value = document.getElementById('visibility').value;
|
||
document.getElementById('hidden-tags').value = document.getElementById('tags').value;
|
||
document.getElementById('hidden-project-id').value = document.getElementById('project_id').value;
|
||
document.getElementById('hidden-task-id').value = document.getElementById('task_id').value;
|
||
document.getElementById('hidden-is-pinned').value = document.getElementById('is_pinned').checked ? '1' : '0';
|
||
}
|
||
|
||
// Listen for changes in settings to update frontmatter
|
||
['visibility', 'folder', 'tags', 'project_id', 'task_id'].forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.addEventListener('change', function() {
|
||
// Force immediate frontmatter update when settings change
|
||
const content = updateContentFrontmatter(aceEditor.getValue());
|
||
const currentPosition = aceEditor.getCursorPosition();
|
||
aceEditor.setValue(content, -1);
|
||
aceEditor.moveCursorToPosition(currentPosition);
|
||
syncContentAndUpdatePreview(false);
|
||
// Sync to hidden fields
|
||
syncSettingsToHiddenFields();
|
||
});
|
||
}
|
||
});
|
||
|
||
// Handle form submission - ensure content is synced
|
||
document.getElementById('note-form').addEventListener('submit', function(e) {
|
||
syncContentAndUpdatePreview();
|
||
syncSettingsToHiddenFields(); // Sync all settings to hidden fields
|
||
const submitBtn = this.querySelector('button[type="submit"]');
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = 'Saving...';
|
||
});
|
||
|
||
// Set focus to ace editor
|
||
aceEditor.focus();
|
||
}
|
||
|
||
// Initialize when DOM is ready
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Initialize Ace Editor
|
||
initializeAceEditor();
|
||
|
||
// Initial preview
|
||
updatePreview();
|
||
updateStats();
|
||
|
||
// Add keyboard shortcuts
|
||
if (aceEditor) {
|
||
aceEditor.commands.addCommand({
|
||
name: 'bold',
|
||
bindKey: {win: 'Ctrl-B', mac: 'Command-B'},
|
||
exec: function() { insertMarkdown('**', '**'); }
|
||
});
|
||
|
||
aceEditor.commands.addCommand({
|
||
name: 'italic',
|
||
bindKey: {win: 'Ctrl-I', mac: 'Command-I'},
|
||
exec: function() { insertMarkdown('*', '*'); }
|
||
});
|
||
|
||
aceEditor.commands.addCommand({
|
||
name: 'link',
|
||
bindKey: {win: 'Ctrl-K', mac: 'Command-K'},
|
||
exec: function() { insertMarkdown('[', '](url)'); }
|
||
});
|
||
}
|
||
|
||
// Settings toggle
|
||
const settingsBtn = document.getElementById('settings-toggle');
|
||
const settingsPanel = document.getElementById('settings-panel');
|
||
|
||
settingsBtn.addEventListener('click', function() {
|
||
if (settingsPanel.style.display === 'none' || !settingsPanel.style.display) {
|
||
settingsPanel.style.display = 'block';
|
||
this.classList.add('active');
|
||
} else {
|
||
settingsPanel.style.display = 'none';
|
||
this.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Preview toggle
|
||
const previewToggle = document.getElementById('preview-toggle');
|
||
const previewPanel = document.getElementById('preview-panel');
|
||
const editorLayout = document.getElementById('editor-layout');
|
||
|
||
previewToggle.addEventListener('click', function() {
|
||
if (editorLayout.classList.contains('preview-hidden')) {
|
||
editorLayout.classList.remove('preview-hidden');
|
||
previewPanel.style.display = 'block';
|
||
this.querySelector('.toggle-text').textContent = 'Hide Preview';
|
||
} else {
|
||
editorLayout.classList.add('preview-hidden');
|
||
previewPanel.style.display = 'none';
|
||
this.querySelector('.toggle-text').textContent = 'Show Preview';
|
||
}
|
||
|
||
// Resize Ace Editor after layout change
|
||
setTimeout(function() {
|
||
if (aceEditor) {
|
||
aceEditor.resize();
|
||
}
|
||
}, 300);
|
||
});
|
||
|
||
// Project change handler - load tasks
|
||
document.getElementById('project_id').addEventListener('change', function() {
|
||
const projectId = this.value;
|
||
const taskSelect = document.getElementById('task_id');
|
||
|
||
// Clear current tasks
|
||
taskSelect.innerHTML = '<option value="">No task</option>';
|
||
|
||
if (projectId) {
|
||
// Fetch tasks for the selected project
|
||
fetch(`/api/projects/${projectId}/tasks`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.tasks) {
|
||
data.tasks.forEach(task => {
|
||
const option = document.createElement('option');
|
||
option.value = task.id;
|
||
option.textContent = `#${task.id} - ${task.title}`;
|
||
taskSelect.appendChild(option);
|
||
});
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading tasks:', error);
|
||
});
|
||
}
|
||
});
|
||
|
||
// Pin toggle handler
|
||
document.getElementById('is_pinned').addEventListener('change', function() {
|
||
// Update frontmatter when pin status changes
|
||
const content = updateContentFrontmatter(aceEditor.getValue());
|
||
const currentPosition = aceEditor.getCursorPosition();
|
||
aceEditor.setValue(content, -1);
|
||
aceEditor.moveCursorToPosition(currentPosition);
|
||
syncContentAndUpdatePreview(false);
|
||
syncSettingsToHiddenFields(); // Sync to hidden fields
|
||
});
|
||
});
|
||
</script>
|
||
|
||
{% endblock %} |