add local TailwindCSS, fix side bar folder view, add more function to Markdown editor and allow .markdown files

This commit is contained in:
nahakubuilde
2025-08-28 07:29:51 +01:00
parent 090d491dd6
commit f364a4b6db
15 changed files with 714 additions and 79 deletions

2
web/static/tailwind.css Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -5,30 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.app_name}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
'obsidian': {
'50': '#f8fafc',
'100': '#f1f5f9',
'200': '#e2e8f0',
'300': '#cbd5e1',
'400': '#94a3b8',
'500': '#64748b',
'600': '#475569',
'700': '#334155',
'800': '#1e293b',
'900': '#0f172a',
}
}
}
}
}
</script>
<link rel="stylesheet" href="{{.prefix}}/static/tailwind.css">
<link rel="stylesheet" href="{{.prefix}}/static/styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
@@ -41,6 +19,8 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
<!-- Mermaid for diagrams -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<style>
/* Custom scrollbar for dark theme */
@@ -66,6 +46,21 @@
.prose-dark h1, .prose-dark h2, .prose-dark h3, .prose-dark h4, .prose-dark h5, .prose-dark h6 {
color: white;
}
/* Headings sizing and spacing */
.prose-dark h1 { font-size: 1.875rem; line-height: 2.25rem; margin-top: 1.5rem; margin-bottom: 0.75rem; }
.prose-dark h2 { font-size: 1.5rem; line-height: 2rem; margin-top: 1.25rem; margin-bottom: 0.5rem; }
.prose-dark h3 { font-size: 1.25rem; line-height: 1.75rem; margin-top: 1rem; margin-bottom: 0.5rem; }
.prose-dark h4 { font-size: 1.125rem; line-height: 1.5rem; margin-top: 0.75rem; margin-bottom: 0.5rem; }
.prose-dark p { margin: 0.75rem 0; }
.prose-dark hr { border-color: #374151; margin: 1.25rem 0; }
/* Lists */
.prose-dark ul { list-style-type: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
.prose-dark ol { list-style-type: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
.prose-dark li { margin: 0.25rem 0; }
.prose-dark li > ul { list-style-type: circle; }
.prose-dark li > ol { list-style-type: lower-alpha; }
.prose-dark a {
color: #60a5fa;
}
@@ -81,7 +76,9 @@
.prose-dark pre {
background-color: #111827;
border: 1px solid #374151;
overflow: auto;
}
.prose-dark pre code { background: transparent; color: inherit; padding: 0; }
.prose-dark blockquote {
border-left: 4px solid #3b82f6;
background-color: #1f2937;
@@ -255,6 +252,20 @@
transform: rotate(90deg);
}
</style>
<script>
// Initialize Mermaid (dark theme) and run on page load
document.addEventListener('DOMContentLoaded', function() {
if (window.mermaid) {
try {
window.mermaid.initialize({ startOnLoad: false, theme: 'dark' });
const containers = document.querySelectorAll('.mermaid');
if (containers.length) {
window.mermaid.run({ querySelector: '.mermaid' });
}
} catch (e) {}
}
});
</script>
</head>
<body class="bg-slate-900 text-gray-300 min-h-screen">
<div class="flex h-screen">
@@ -264,14 +275,21 @@
<!-- Header -->
<div class="p-4 border-b border-gray-700">
<div class="flex items-center justify-between">
<a href="{{url "/"}}" class="sidebar-title text-xl font-bold text-white hover:text-blue-300" title="Home">
{{.app_name}}
<a href="{{url "/"}}" class="sidebar-title text-xl font-bold text-white hover:text-blue-300" title="Home" aria-label="Home">
<i class="fas fa-house"></i>
<span class="sr-only">{{.app_name}}</span>
</a>
<div class="flex items-center space-x-2 items-center">
<div class="sidebar-actions flex items-center space-x-3">
<button id="open-search" class="text-gray-400 hover:text-white transition-colors" title="Search" aria-label="Search">
<i class="fas fa-magnifying-glass"></i>
</button>
<button id="expand-all" class="text-gray-400 hover:text-white transition-colors" title="Expand all" aria-label="Expand all">
<i class="fas fa-folder-open"></i>
</button>
<button id="collapse-all" class="text-gray-400 hover:text-white transition-colors" title="Collapse all" aria-label="Collapse all">
<i class="fas fa-folder"></i>
</button>
{{if .Authenticated}}
{{if .IsAdmin}}
<a href="{{url "/editor/admin"}}" class="text-gray-400 hover:text-white transition-colors" title="Admin">
@@ -404,7 +422,19 @@
<!-- Scripts -->
<script>
// Initialize syntax highlighting
hljs.highlightAll();
// Avoid warnings and double-highlighting: skip elements already highlighted by Chroma
if (window.hljs) {
try {
// Suppress unescaped HTML warnings from hljs
window.hljs.configure({ ignoreUnescapedHTML: true });
// Only highlight code blocks that are not inside a .chroma container
document.querySelectorAll('pre code').forEach(function (el) {
if (!el.closest('.chroma')) {
window.hljs.highlightElement(el);
}
});
} catch (e) { /* ignore */ }
}
// Base URL prefix from server
window.BASE = '{{base}}';
@@ -418,18 +448,48 @@
return p[0] === '/' ? (b + p) : (b + '/' + p);
};
// Tree functionality
// Tree functionality with persisted expanded state
const LS_KEY_EXPANDED = 'tree:expanded';
function getExpandedSet() {
try {
const raw = localStorage.getItem(LS_KEY_EXPANDED);
const arr = raw ? JSON.parse(raw) : [];
return new Set(Array.isArray(arr) ? arr : []);
} catch { return new Set(); }
}
function saveExpandedSet(set) {
try { localStorage.setItem(LS_KEY_EXPANDED, JSON.stringify(Array.from(set))); } catch {}
}
function setExpanded(toggleEl, expand) {
const children = toggleEl.nextElementSibling;
const chevron = toggleEl.querySelector('.tree-chevron');
if (!children || !children.classList.contains('tree-children')) return;
const isHidden = children.classList.contains('hidden');
if (expand && isHidden) children.classList.remove('hidden');
if (!expand && !isHidden) children.classList.add('hidden');
if (chevron) chevron.classList.toggle('rotate-90', expand);
}
function applyExpandedState() {
const expanded = getExpandedSet();
document.querySelectorAll('.tree-toggle').forEach(t => {
const path = t.getAttribute('data-path') || '';
const shouldExpand = expanded.has(path);
setExpanded(t, shouldExpand);
});
}
document.addEventListener('click', function(e) {
if (e.target.closest('.tree-toggle')) {
const toggle = e.target.closest('.tree-toggle');
const children = toggle.nextElementSibling;
const chevron = toggle.querySelector('.tree-chevron');
if (children && children.classList.contains('tree-children')) {
const expanded = getExpandedSet();
const path = toggle.getAttribute('data-path') || '';
const willExpand = children.classList.contains('hidden');
children.classList.toggle('hidden');
if (chevron) {
chevron.classList.toggle('rotate-90');
}
if (chevron) chevron.classList.toggle('rotate-90');
if (willExpand) expanded.add(path); else expanded.delete(path);
saveExpandedSet(expanded);
}
}
});
@@ -458,6 +518,11 @@
if (chevron) {
chevron.classList.add('rotate-90');
}
// persist this expanded state
const expanded = getExpandedSet();
const path = toggle.getAttribute('data-path') || '';
expanded.add(path);
saveExpandedSet(expanded);
}
}
parent = parent.parentElement;
@@ -528,9 +593,28 @@
});
}
// Expand active path in tree
// Apply persisted expanded folders, then ensure active path is expanded
applyExpandedState();
expandActivePath();
// Wire expand/collapse all
const expandAllBtn = document.getElementById('expand-all');
const collapseAllBtn = document.getElementById('collapse-all');
if (expandAllBtn) expandAllBtn.addEventListener('click', function() {
const expanded = getExpandedSet();
document.querySelectorAll('.tree-toggle').forEach(t => {
const path = t.getAttribute('data-path') || '';
setExpanded(t, true);
expanded.add(path);
});
saveExpandedSet(expanded);
});
if (collapseAllBtn) collapseAllBtn.addEventListener('click', function() {
const expanded = new Set();
document.querySelectorAll('.tree-toggle').forEach(t => setExpanded(t, false));
saveExpandedSet(expanded);
});
// Sidebar tree fallback: if server didn't render any tree nodes (e.g., error pages), fetch and render via API
(function ensureSidebarTree() {
const container = document.getElementById('sidebar-tree');
@@ -549,6 +633,7 @@
container.appendChild(renderTreeNode(child));
});
// Re-apply expanded state and active path if any
applyExpandedState();
expandActivePath();
})
.catch(() => {/* ignore */});

View File

@@ -77,6 +77,69 @@ console.log('Hello, World!');
</div>
</div>
<!-- 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('![', '](image.png)')" 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>
<!-- Actions -->
</div>
@@ -145,16 +208,38 @@ console.log('Hello, World!');
}
}
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');
}
// Render preview
function renderPreview() {
if (!previewEnabled) return;
if (!livePreview || !window.marked) return;
const transformed = transformObsidianEmbeds(contentTextarea.value || '');
let transformed = transformObsidianEmbeds(contentTextarea.value || '');
transformed = transformMermaidFences(transformed);
transformed = transformMediaEmbeds(transformed);
livePreview.innerHTML = marked.parse(transformed);
// Highlight code blocks
livePreview.querySelectorAll('pre code').forEach(block => {
try { hljs.highlightElement(block); } catch (e) {}
});
if (window.mermaid) {
try { window.mermaid.run({ querySelector: '#live-preview .mermaid' }); } catch (e) {}
}
}
createForm.addEventListener('submit', function(e) {
@@ -238,6 +323,154 @@ console.log('Hello, World!');
}
});
// 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();
}
// Paste-to-upload image
contentTextarea.addEventListener('paste', async function(e) {
const items = (e.clipboardData || window.clipboardData).items || [];

View File

@@ -43,28 +43,63 @@
<!-- Editor Toolbar -->
<div class="flex items-center justify-between border-t border-gray-700 pt-4">
<div class="flex items-center space-x-2">
<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="Code">
<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('![', '](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">
<!-- 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">
@@ -139,14 +174,36 @@
}
}
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');
}
function renderPreview() {
if (!previewEnabled) return;
if (!livePreview || !window.marked) return;
const transformed = transformObsidianEmbeds(contentTextarea.value || '');
let transformed = transformObsidianEmbeds(contentTextarea.value || '');
transformed = transformMermaidFences(transformed);
transformed = transformMediaEmbeds(transformed);
livePreview.innerHTML = marked.parse(transformed);
livePreview.querySelectorAll('pre code').forEach(block => {
try { hljs.highlightElement(block); } catch (e) {}
});
if (window.mermaid) {
try { window.mermaid.run({ querySelector: '#live-preview .mermaid' }); } catch (e) {}
}
}
editForm.addEventListener('submit', function(e) {
@@ -247,22 +304,52 @@
autoResize();
}
function insertHeading() {
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) {
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);
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(start + 2, start + 2);
contentTextarea.setSelectionRange(newPos, newPos);
autoResize();
const menu = document.getElementById('heading-menu');
if (menu) menu.classList.add('hidden');
}
function insertList() {
@@ -278,6 +365,88 @@
autoResize();
}
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();
}
// Paste-to-upload image
contentTextarea.addEventListener('paste', async function(e) {
const items = (e.clipboardData || window.clipboardData).items || [];