|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|