Elysia-Suite's picture
Upload 25 files
0b194e5 verified
/*
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;