// Markdown Editor Application (function() { 'use strict'; // State management let currentFile = null; let editor = null; let isScrollingSynced = true; let scrollTimeout = null; let isDarkMode = false; // Dark mode management function initDarkMode() { // Check for saved preference const savedMode = localStorage.getItem('darkMode'); if (savedMode === 'true') { enableDarkMode(); } } function enableDarkMode() { isDarkMode = true; document.body.classList.add('dark-mode'); document.getElementById('darkModeIcon').textContent = '☀️'; localStorage.setItem('darkMode', 'true'); // Update mermaid theme mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' }); // Re-render preview if there's content if (editor && editor.getValue()) { updatePreview(); } } function disableDarkMode() { isDarkMode = false; document.body.classList.remove('dark-mode'); document.getElementById('darkModeIcon').textContent = '🌙'; localStorage.setItem('darkMode', 'false'); // Update mermaid theme mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' }); // Re-render preview if there's content if (editor && editor.getValue()) { updatePreview(); } } function toggleDarkMode() { if (isDarkMode) { disableDarkMode(); } else { enableDarkMode(); } } // Initialize Mermaid mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' }); // Configure marked.js for markdown parsing marked.setOptions({ breaks: true, gfm: true, headerIds: true, mangle: false, sanitize: false, // Allow HTML in markdown smartLists: true, smartypants: true, xhtml: false }); // Handle image upload async function uploadImage(file) { const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload-image', { method: 'POST', body: formData }); if (!response.ok) throw new Error('Upload failed'); const result = await response.json(); return result.url; } catch (error) { console.error('Error uploading image:', error); showNotification('Error uploading image', 'danger'); return null; } } // Handle drag and drop function setupDragAndDrop() { const editorElement = document.querySelector('.CodeMirror'); // Prevent default drag behavior ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { editorElement.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } // Highlight drop zone ['dragenter', 'dragover'].forEach(eventName => { editorElement.addEventListener(eventName, () => { editorElement.classList.add('drag-over'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { editorElement.addEventListener(eventName, () => { editorElement.classList.remove('drag-over'); }, false); }); // Handle drop editorElement.addEventListener('drop', async (e) => { const files = e.dataTransfer.files; if (files.length === 0) return; // Filter for images only const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/') ); if (imageFiles.length === 0) { showNotification('Please drop image files only', 'warning'); return; } showNotification(`Uploading ${imageFiles.length} image(s)...`, 'info'); // Upload images for (const file of imageFiles) { const url = await uploadImage(file); if (url) { // Insert markdown image syntax at cursor const cursor = editor.getCursor(); const imageMarkdown = ``; editor.replaceRange(imageMarkdown, cursor); editor.setCursor(cursor.line, cursor.ch + imageMarkdown.length); showNotification(`Image uploaded: ${file.name}`, 'success'); } } }, false); // Also handle paste events for images editorElement.addEventListener('paste', async (e) => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault(); const file = item.getAsFile(); if (file) { showNotification('Uploading pasted image...', 'info'); const url = await uploadImage(file); if (url) { const cursor = editor.getCursor(); const imageMarkdown = ``; editor.replaceRange(imageMarkdown, cursor); showNotification('Image uploaded successfully', 'success'); } } } } }); } // Initialize CodeMirror editor function initEditor() { const textarea = document.getElementById('editor'); editor = CodeMirror.fromTextArea(textarea, { mode: 'markdown', theme: 'monokai', lineNumbers: true, lineWrapping: true, autofocus: true, extraKeys: { 'Ctrl-S': function() { saveFile(); }, 'Cmd-S': function() { saveFile(); } } }); // Update preview on change editor.on('change', debounce(updatePreview, 300)); // Setup drag and drop after editor is ready setTimeout(setupDragAndDrop, 100); // Sync scroll editor.on('scroll', handleEditorScroll); } // Debounce function to limit update frequency function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Update preview with markdown content async function updatePreview() { const content = editor.getValue(); const previewDiv = document.getElementById('preview'); if (!content.trim()) { previewDiv.innerHTML = `
Start typing in the editor to see the preview
([\s\S]*?)<\/code><\/pre>/g,
'$1'
);
previewDiv.innerHTML = html;
// Apply syntax highlighting to code blocks
const codeBlocks = previewDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => {
// Detect language from class name
const languageClass = Array.from(block.classList).find(cls => cls.startsWith('language-'));
if (languageClass && languageClass !== 'language-mermaid') {
Prism.highlightElement(block);
}
});
// Render mermaid diagrams
const mermaidElements = previewDiv.querySelectorAll('.mermaid');
if (mermaidElements.length > 0) {
try {
await mermaid.run({
nodes: mermaidElements,
suppressErrors: false
});
} catch (error) {
console.error('Mermaid rendering error:', error);
}
}
} catch (error) {
console.error('Preview rendering error:', error);
previewDiv.innerHTML = `
Error rendering preview: ${error.message}
`;
}
}
// Handle editor scroll for synchronized scrolling
function handleEditorScroll() {
if (!isScrollingSynced) return;
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const editorScrollInfo = editor.getScrollInfo();
const editorScrollPercentage = editorScrollInfo.top / (editorScrollInfo.height - editorScrollInfo.clientHeight);
const previewPane = document.querySelector('.preview-pane');
const previewScrollHeight = previewPane.scrollHeight - previewPane.clientHeight;
if (previewScrollHeight > 0) {
previewPane.scrollTop = editorScrollPercentage * previewScrollHeight;
}
}, 10);
}
// Load file list from server
async function loadFileList() {
try {
const response = await fetch('/api/files');
if (!response.ok) throw new Error('Failed to load file list');
const files = await response.json();
const fileListDiv = document.getElementById('fileList');
if (files.length === 0) {
fileListDiv.innerHTML = 'No files yet';
return;
}
fileListDiv.innerHTML = files.map(file => `
${file.filename}
${formatFileSize(file.size)}
`).join('');
// Add click handlers
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const filename = item.dataset.filename;
loadFile(filename);
});
});
} catch (error) {
console.error('Error loading file list:', error);
showNotification('Error loading file list', 'danger');
}
}
// Load a specific file
async function loadFile(filename) {
try {
const response = await fetch(`/api/files/${filename}`);
if (!response.ok) throw new Error('Failed to load file');
const data = await response.json();
currentFile = data.filename;
// Update UI
document.getElementById('filenameInput').value = data.filename;
editor.setValue(data.content);
// Update active state in file list
document.querySelectorAll('.file-item').forEach(item => {
item.classList.toggle('active', item.dataset.filename === filename);
});
updatePreview();
showNotification(`Loaded ${filename}`, 'success');
} catch (error) {
console.error('Error loading file:', error);
showNotification('Error loading file', 'danger');
}
}
// Save current file
async function saveFile() {
const filename = document.getElementById('filenameInput').value.trim();
if (!filename) {
showNotification('Please enter a filename', 'warning');
return;
}
const content = editor.getValue();
try {
const response = await fetch('/api/files', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename, content })
});
if (!response.ok) throw new Error('Failed to save file');
const result = await response.json();
currentFile = result.filename;
showNotification(`Saved ${result.filename}`, 'success');
loadFileList();
} catch (error) {
console.error('Error saving file:', error);
showNotification('Error saving file', 'danger');
}
}
// Delete current file
async function deleteFile() {
const filename = document.getElementById('filenameInput').value.trim();
if (!filename) {
showNotification('No file selected', 'warning');
return;
}
if (!confirm(`Are you sure you want to delete ${filename}?`)) {
return;
}
try {
const response = await fetch(`/api/files/${filename}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete file');
showNotification(`Deleted ${filename}`, 'success');
// Clear editor
currentFile = null;
document.getElementById('filenameInput').value = '';
editor.setValue('');
updatePreview();
loadFileList();
} catch (error) {
console.error('Error deleting file:', error);
showNotification('Error deleting file', 'danger');
}
}
// Create new file
function newFile() {
currentFile = null;
document.getElementById('filenameInput').value = '';
editor.setValue('');
updatePreview();
// Remove active state from all file items
document.querySelectorAll('.file-item').forEach(item => {
item.classList.remove('active');
});
showNotification('New file created', 'info');
}
// Format file size for display
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// Show notification
function showNotification(message, type = 'info') {
// Create toast notification
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
${message}
`;
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
// Create toast container if it doesn't exist
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '9999';
document.body.appendChild(container);
return container;
}
// Initialize application
function init() {
initDarkMode();
initEditor();
loadFileList();
// Set up event listeners
document.getElementById('saveBtn').addEventListener('click', saveFile);
document.getElementById('deleteBtn').addEventListener('click', deleteFile);
document.getElementById('newFileBtn').addEventListener('click', newFile);
document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveFile();
}
});
console.log('Markdown Editor initialized');
}
// Start application when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();