/* ELYSIA MARKDOWN STUDIO v1.0 - Editor Module Markdown editor with toolbar actions */ import Utils from "./utils.js"; const Editor = { textarea: null, currentDoc: null, autoSaveInterval: null, autoSaveInProgress: false, // Prevent race conditions init() { this.textarea = document.getElementById("markdown-editor"); this.setupEventListeners(); this.setupToolbar(); // Don't start auto-save yet - wait for app to be fully initialized }, setupEventListeners() { // Input event for stats update this.textarea.addEventListener( "input", Utils.debounce(() => { this.updateStats(); // Check if live preview is enabled const livePreview = Utils.storage.get("livePreview", true); if (livePreview && window.app?.preview) { window.app.preview.update(); } // Mark unsaved changes if (window.app) { window.app.unsavedChanges = true; } }, 300) ); // Drag & drop for images this.textarea.addEventListener("dragover", e => { e.preventDefault(); this.textarea.classList.add("drag-over"); }); this.textarea.addEventListener("dragleave", e => { e.preventDefault(); this.textarea.classList.remove("drag-over"); }); this.textarea.addEventListener("drop", e => { e.preventDefault(); this.textarea.classList.remove("drag-over"); this.handleImageDrop(e); }); // Paste images from clipboard this.textarea.addEventListener("paste", e => { const items = e.clipboardData?.items; if (!items) return; for (const item of items) { if (item.type.startsWith("image/")) { e.preventDefault(); this.handleImagePaste(item); break; } } }); // Keyboard shortcuts this.textarea.addEventListener("keydown", e => { if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case "s": e.preventDefault(); window.app?.saveDocument(); break; case "b": e.preventDefault(); this.wrapSelection("**", "**"); break; case "i": e.preventDefault(); this.wrapSelection("*", "*"); break; } } }); }, setupToolbar() { document.querySelectorAll(".toolbar-btn").forEach(btn => { btn.addEventListener("click", () => { const action = btn.getAttribute("data-action"); this.handleToolbarAction(action); }); }); }, handleToolbarAction(action) { switch (action) { case "bold": this.wrapSelection("**", "**"); break; case "italic": this.wrapSelection("*", "*"); break; case "strikethrough": this.wrapSelection("~~", "~~"); break; case "heading1": this.insertAtLineStart("# "); break; case "heading2": this.insertAtLineStart("## "); break; case "heading3": this.insertAtLineStart("### "); break; case "link": this.insertLink(); break; case "image": this.insertImage(); break; case "code": this.wrapSelection("`", "`"); break; case "quote": this.insertAtLineStart("> "); break; case "ul": this.insertAtLineStart("- "); break; case "ol": this.insertAtLineStart("1. "); break; case "task": this.insertAtLineStart("- [ ] "); break; case "table": this.insertTable(); break; case "hr": this.insertLine("\n---\n"); break; } this.textarea.focus(); }, wrapSelection(before, after) { const start = this.textarea.selectionStart; const end = this.textarea.selectionEnd; const text = this.textarea.value; const selected = text.substring(start, end); const wrapped = before + (selected || "text") + after; this.textarea.setRangeText(wrapped, start, end, "select"); this.textarea.dispatchEvent(new Event("input")); }, insertAtLineStart(prefix) { const start = this.textarea.selectionStart; const text = this.textarea.value; // Find line start let lineStart = start; while (lineStart > 0 && text[lineStart - 1] !== "\n") { lineStart--; } this.textarea.setRangeText(prefix, lineStart, lineStart, "end"); this.textarea.dispatchEvent(new Event("input")); }, insertLine(text) { const start = this.textarea.selectionStart; this.textarea.setRangeText(text, start, start, "end"); this.textarea.dispatchEvent(new Event("input")); }, insertLink() { const url = prompt("Enter URL:"); if (!url) return; const text = prompt("Link text (optional):") || url; this.wrapSelection(`[${text}](`, `)`); }, insertImage() { const url = prompt("Enter image URL:"); if (!url) return; const alt = prompt("Alt text (optional):") || "image"; const markdown = `![${alt}](${url})`; const start = this.textarea.selectionStart; this.textarea.setRangeText(markdown, start, start, "end"); this.textarea.dispatchEvent(new Event("input")); }, insertTable() { 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`; this.insertLine(table); }, updateStats() { const content = this.textarea.value; const wordCount = Utils.countWords(content); const charCount = Utils.countChars(content); const lineCount = Utils.countLines(content); const readingTime = Utils.readingTime(wordCount); document.getElementById("word-count").textContent = `${wordCount} words`; document.getElementById("char-count").textContent = `${charCount} chars`; document.getElementById("line-count").textContent = `${lineCount} lines`; // Add reading time if element exists const readingTimeEl = document.getElementById("reading-time"); if (readingTimeEl) { readingTimeEl.textContent = readingTime; } // Update current doc stats if exists if (this.currentDoc) { this.currentDoc.wordCount = wordCount; this.currentDoc.charCount = charCount; } }, getContent() { return this.textarea.value; }, setContent(content) { this.textarea.value = content || ""; this.updateStats(); window.app?.preview.update(); }, clear() { this.setContent(""); }, startAutoSave() { // Stop any existing interval this.stopAutoSave(); const autoSaveEnabled = Utils.storage.get("autoSave", true); if (!autoSaveEnabled) return; // Only start if app is fully initialized if (!window.app) { console.warn("Auto-save deferred - app not initialized yet"); return; } this.autoSaveInterval = setInterval(async () => { // Prevent concurrent auto-saves if (this.autoSaveInProgress) { console.log("⏭️ Skipping auto-save - already in progress"); return; } if (window.app?.unsavedChanges && this.textarea.value) { try { this.autoSaveInProgress = true; await window.app.saveDocument(true); // Silent save console.log("💾 Auto-saved"); } catch (err) { console.error("Auto-save failed:", err); } finally { this.autoSaveInProgress = false; } } }, 30000); // 30 seconds console.log("✅ Auto-save enabled (every 30s)"); }, stopAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = null; } }, // Handle image drop handleImageDrop(e) { const files = e.dataTransfer?.files; if (!files || files.length === 0) return; for (const file of files) { if (file.type.startsWith("image/")) { this.insertImageFromFile(file); } } }, // Handle image paste handleImagePaste(item) { const file = item.getAsFile(); if (file) { this.insertImageFromFile(file); } }, // Insert image from file (convert to base64 data URL) insertImageFromFile(file) { const reader = new FileReader(); reader.onload = e => { const dataUrl = e.target.result; const altText = file.name.replace(/\.[^/.]+$/, ""); // Remove extension const markdown = `\n![${altText}](${dataUrl})\n`; const start = this.textarea.selectionStart; this.textarea.setRangeText(markdown, start, start, "end"); this.textarea.dispatchEvent(new Event("input")); Utils.toast.success(`Image "${file.name}" inserted!`); }; reader.onerror = () => { Utils.toast.error("Failed to read image file"); }; reader.readAsDataURL(file); } }; export default Editor;