| |
| |
| |
|
|
|
|
| import Utils from "./utils.js";
|
|
|
| const Editor = {
|
| textarea: null,
|
| currentDoc: null,
|
| autoSaveInterval: null,
|
| autoSaveInProgress: false,
|
|
|
| init() {
|
| this.textarea = document.getElementById("markdown-editor");
|
| this.setupEventListeners();
|
| this.setupToolbar();
|
|
|
| },
|
|
|
| setupEventListeners() {
|
|
|
| this.textarea.addEventListener(
|
| "input",
|
| Utils.debounce(() => {
|
| this.updateStats();
|
|
|
|
|
| const livePreview = Utils.storage.get("livePreview", true);
|
| if (livePreview && window.app?.preview) {
|
| window.app.preview.update();
|
| }
|
|
|
|
|
| if (window.app) {
|
| window.app.unsavedChanges = true;
|
| }
|
| }, 300)
|
| );
|
|
|
|
|
| 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);
|
| });
|
|
|
|
|
| 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;
|
| }
|
| }
|
| });
|
|
|
|
|
| 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;
|
|
|
|
|
| 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 = ``;
|
|
|
| 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`;
|
|
|
|
|
| const readingTimeEl = document.getElementById("reading-time");
|
| if (readingTimeEl) {
|
| readingTimeEl.textContent = readingTime;
|
| }
|
|
|
|
|
| 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() {
|
|
|
| this.stopAutoSave();
|
|
|
| const autoSaveEnabled = Utils.storage.get("autoSave", true);
|
| if (!autoSaveEnabled) return;
|
|
|
|
|
| if (!window.app) {
|
| console.warn("Auto-save deferred - app not initialized yet");
|
| return;
|
| }
|
|
|
| this.autoSaveInterval = setInterval(async () => {
|
|
|
| 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);
|
| console.log("💾 Auto-saved");
|
| } catch (err) {
|
| console.error("Auto-save failed:", err);
|
| } finally {
|
| this.autoSaveInProgress = false;
|
| }
|
| }
|
| }, 30000);
|
|
|
| console.log("✅ Auto-save enabled (every 30s)");
|
| },
|
|
|
| stopAutoSave() {
|
| if (this.autoSaveInterval) {
|
| clearInterval(this.autoSaveInterval);
|
| this.autoSaveInterval = null;
|
| }
|
| },
|
|
|
|
|
| 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);
|
| }
|
| }
|
| },
|
|
|
|
|
| handleImagePaste(item) {
|
| const file = item.getAsFile();
|
| if (file) {
|
| this.insertImageFromFile(file);
|
| }
|
| },
|
|
|
|
|
| insertImageFromFile(file) {
|
| const reader = new FileReader();
|
|
|
| reader.onload = e => {
|
| const dataUrl = e.target.result;
|
| const altText = file.name.replace(/\.[^/.]+$/, "");
|
| const markdown = `\n\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;
|
|
|