(function () { "use strict"; const CONFIG = { AUTOSAVE_DELAY: 3000, MAX_HISTORY: 50, WS_RECONNECT_DELAY: 5000, }; const state = { docId: null, docTitle: "Untitled Document", content: "", history: [], historyIndex: -1, isDirty: false, autoSaveTimer: null, ws: null, collaborators: [], chatPanelOpen: true, driveSource: null, zoom: 100, }; const elements = {}; function init() { cacheElements(); bindEvents(); loadFromUrlParams(); setupToolbar(); setupKeyboardShortcuts(); updateWordCount(); connectWebSocket(); } function cacheElements() { elements.app = document.getElementById("docs-app"); elements.docName = document.getElementById("docName"); elements.editorContent = document.getElementById("editorContent"); elements.editorPage = document.getElementById("editorPage"); elements.collaborators = document.getElementById("collaborators"); elements.pageInfo = document.getElementById("pageInfo"); elements.wordCount = document.getElementById("wordCount"); elements.charCount = document.getElementById("charCount"); elements.saveStatus = document.getElementById("saveStatus"); elements.zoomLevel = document.getElementById("zoomLevel"); elements.chatPanel = document.getElementById("chatPanel"); elements.chatMessages = document.getElementById("chatMessages"); elements.chatInput = document.getElementById("chatInput"); elements.chatForm = document.getElementById("chatForm"); elements.shareModal = document.getElementById("shareModal"); elements.linkModal = document.getElementById("linkModal"); elements.imageModal = document.getElementById("imageModal"); elements.tableModal = document.getElementById("tableModal"); elements.exportModal = document.getElementById("exportModal"); } function bindEvents() { if (elements.editorContent) { elements.editorContent.addEventListener("input", handleEditorInput); elements.editorContent.addEventListener("keydown", handleEditorKeydown); elements.editorContent.addEventListener("paste", handlePaste); } if (elements.docName) { elements.docName.addEventListener("change", handleDocNameChange); elements.docName.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); elements.editorContent?.focus(); } }); } document.getElementById("undoBtn")?.addEventListener("click", undo); document.getElementById("redoBtn")?.addEventListener("click", redo); document .getElementById("boldBtn") ?.addEventListener("click", () => execCommand("bold")); document .getElementById("italicBtn") ?.addEventListener("click", () => execCommand("italic")); document .getElementById("underlineBtn") ?.addEventListener("click", () => execCommand("underline")); document .getElementById("strikeBtn") ?.addEventListener("click", () => execCommand("strikeThrough")); document .getElementById("alignLeftBtn") ?.addEventListener("click", () => execCommand("justifyLeft")); document .getElementById("alignCenterBtn") ?.addEventListener("click", () => execCommand("justifyCenter")); document .getElementById("alignRightBtn") ?.addEventListener("click", () => execCommand("justifyRight")); document .getElementById("alignJustifyBtn") ?.addEventListener("click", () => execCommand("justifyFull")); document .getElementById("bulletListBtn") ?.addEventListener("click", () => execCommand("insertUnorderedList")); document .getElementById("numberListBtn") ?.addEventListener("click", () => execCommand("insertOrderedList")); document .getElementById("indentBtn") ?.addEventListener("click", () => execCommand("indent")); document .getElementById("outdentBtn") ?.addEventListener("click", () => execCommand("outdent")); document .getElementById("linkBtn") ?.addEventListener("click", () => showModal("linkModal")); document .getElementById("imageBtn") ?.addEventListener("click", () => showModal("imageModal")); document .getElementById("tableBtn") ?.addEventListener("click", () => showModal("tableModal")); document .getElementById("shareBtn") ?.addEventListener("click", () => showModal("shareModal")); document .getElementById("headingSelect") ?.addEventListener("change", handleHeadingChange); document .getElementById("fontFamily") ?.addEventListener("change", handleFontFamilyChange); document .getElementById("fontSize") ?.addEventListener("change", handleFontSizeChange); document.getElementById("textColorBtn")?.addEventListener("click", () => { document.getElementById("textColorPicker")?.click(); }); document .getElementById("textColorPicker") ?.addEventListener("input", handleTextColorChange); document.getElementById("highlightBtn")?.addEventListener("click", () => { document.getElementById("highlightPicker")?.click(); }); document .getElementById("highlightPicker") ?.addEventListener("input", handleHighlightChange); document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); document .getElementById("chatToggle") ?.addEventListener("click", toggleChatPanel); document .getElementById("chatClose") ?.addEventListener("click", toggleChatPanel); elements.chatForm?.addEventListener("submit", handleChatSubmit); document.querySelectorAll(".suggestion-btn").forEach((btn) => { btn.addEventListener("click", () => handleSuggestionClick(btn.dataset.action), ); }); document.querySelectorAll(".btn-close, .modal").forEach((el) => { el.addEventListener("click", (e) => { if (e.target === el) closeModals(); }); }); document .getElementById("closeShareModal") ?.addEventListener("click", () => hideModal("shareModal")); document .getElementById("closeLinkModal") ?.addEventListener("click", () => hideModal("linkModal")); document .getElementById("closeImageModal") ?.addEventListener("click", () => hideModal("imageModal")); document .getElementById("closeTableModal") ?.addEventListener("click", () => hideModal("tableModal")); document .getElementById("closeExportModal") ?.addEventListener("click", () => hideModal("exportModal")); document .getElementById("insertLinkBtn") ?.addEventListener("click", insertLink); document .getElementById("insertImageBtn") ?.addEventListener("click", insertImage); document .getElementById("insertTableBtn") ?.addEventListener("click", insertTable); document .getElementById("copyLinkBtn") ?.addEventListener("click", copyShareLink); document.querySelectorAll(".export-option").forEach((btn) => { btn.addEventListener("click", () => exportDocument(btn.dataset.format)); }); window.addEventListener("beforeunload", handleBeforeUnload); } function handleEditorInput() { saveToHistory(); state.isDirty = true; updateWordCount(); scheduleAutoSave(); broadcastChange(); } function handleDocNameChange() { state.docTitle = elements.docName.value || "Untitled Document"; state.isDirty = true; scheduleAutoSave(); } function handleEditorKeydown(e) { if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case "b": e.preventDefault(); execCommand("bold"); break; case "i": e.preventDefault(); execCommand("italic"); break; case "u": e.preventDefault(); execCommand("underline"); break; case "z": e.preventDefault(); if (e.shiftKey) { redo(); } else { undo(); } break; case "y": e.preventDefault(); redo(); break; case "s": e.preventDefault(); saveDocument(); break; } } } function handlePaste(e) { e.preventDefault(); const text = e.clipboardData.getData("text/plain"); document.execCommand("insertText", false, text); } function handleBeforeUnload(e) { if (state.isDirty) { e.preventDefault(); e.returnValue = ""; } } function setupToolbar() { updateToolbarState(); if (elements.editorContent) { elements.editorContent.addEventListener("mouseup", updateToolbarState); elements.editorContent.addEventListener("keyup", updateToolbarState); } } function updateToolbarState() { document .getElementById("boldBtn") ?.classList.toggle("active", document.queryCommandState("bold")); document .getElementById("italicBtn") ?.classList.toggle("active", document.queryCommandState("italic")); document .getElementById("underlineBtn") ?.classList.toggle("active", document.queryCommandState("underline")); document .getElementById("strikeBtn") ?.classList.toggle("active", document.queryCommandState("strikeThrough")); } function setupKeyboardShortcuts() { document.addEventListener("keydown", (e) => { if (e.target.closest(".chat-input, .modal input")) return; if (e.key === "Escape") { closeModals(); } }); } function execCommand(command, value = null) { elements.editorContent?.focus(); document.execCommand(command, false, value); saveToHistory(); state.isDirty = true; scheduleAutoSave(); updateToolbarState(); } function handleHeadingChange(e) { const value = e.target.value; execCommand("formatBlock", value); } function handleFontFamilyChange(e) { execCommand("fontName", e.target.value); } function handleFontSizeChange(e) { execCommand("fontSize", e.target.value); } function handleTextColorChange(e) { execCommand("foreColor", e.target.value); const indicator = document.querySelector("#textColorBtn .color-indicator"); if (indicator) indicator.style.background = e.target.value; } function handleHighlightChange(e) { execCommand("hiliteColor", e.target.value); const indicator = document.querySelector("#highlightBtn .color-indicator"); if (indicator) indicator.style.background = e.target.value; } function saveToHistory() { if (!elements.editorContent) return; const content = elements.editorContent.innerHTML; if (state.history[state.historyIndex] === content) return; state.history = state.history.slice(0, state.historyIndex + 1); state.history.push(content); if (state.history.length > CONFIG.MAX_HISTORY) { state.history.shift(); } else { state.historyIndex++; } } function undo() { if (state.historyIndex > 0) { state.historyIndex--; if (elements.editorContent) { elements.editorContent.innerHTML = state.history[state.historyIndex]; } state.isDirty = true; updateWordCount(); } } function redo() { if (state.historyIndex < state.history.length - 1) { state.historyIndex++; if (elements.editorContent) { elements.editorContent.innerHTML = state.history[state.historyIndex]; } state.isDirty = true; updateWordCount(); } } function updateWordCount() { if (!elements.editorContent) return; const text = elements.editorContent.innerText || ""; const words = text .trim() .split(/\s+/) .filter((w) => w.length > 0); const chars = text.length; if (elements.wordCount) { elements.wordCount.textContent = `${words.length} word${words.length !== 1 ? "s" : ""}`; } if (elements.charCount) { elements.charCount.textContent = `${chars} character${chars !== 1 ? "s" : ""}`; } const pageHeight = 1056; const contentHeight = elements.editorContent.scrollHeight || pageHeight; const pages = Math.max(1, Math.ceil(contentHeight / pageHeight)); if (elements.pageInfo) { elements.pageInfo.textContent = `Page 1 of ${pages}`; } } function zoomIn() { if (state.zoom < 200) { state.zoom += 10; applyZoom(); } } function zoomOut() { if (state.zoom > 50) { state.zoom -= 10; applyZoom(); } } function applyZoom() { if (elements.editorPage) { elements.editorPage.style.transform = `scale(${state.zoom / 100})`; elements.editorPage.style.transformOrigin = "top center"; } if (elements.zoomLevel) { elements.zoomLevel.textContent = `${state.zoom}%`; } } function scheduleAutoSave() { if (state.autoSaveTimer) { clearTimeout(state.autoSaveTimer); } state.autoSaveTimer = setTimeout(saveDocument, CONFIG.AUTOSAVE_DELAY); if (elements.saveStatus) { elements.saveStatus.textContent = "Saving..."; } } async function saveDocument() { if (!state.isDirty) return; const content = elements.editorContent?.innerHTML || ""; const title = state.docTitle; try { const response = await fetch("/api/docs/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: state.docId, title, content, driveSource: state.driveSource, }), }); if (response.ok) { const result = await response.json(); if (result.id) { state.docId = result.id; window.history.replaceState({}, "", `#id=${state.docId}`); } state.isDirty = false; if (elements.saveStatus) { elements.saveStatus.textContent = "Saved"; } } else { if (elements.saveStatus) { elements.saveStatus.textContent = "Save failed"; } } } catch (e) { console.error("Save error:", e); if (elements.saveStatus) { elements.saveStatus.textContent = "Save failed"; } } } async function loadFromUrlParams() { const urlParams = new URLSearchParams(window.location.search); const hash = window.location.hash; let docId = urlParams.get("id"); let bucket = urlParams.get("bucket"); let path = urlParams.get("path"); if (hash) { const hashQueryIndex = hash.indexOf("?"); if (hashQueryIndex > -1) { const hashParams = new URLSearchParams(hash.slice(hashQueryIndex + 1)); docId = docId || hashParams.get("id"); bucket = bucket || hashParams.get("bucket"); path = path || hashParams.get("path"); } else if (hash.startsWith("#id=")) { docId = hash.slice(4); } } if (bucket && path) { state.driveSource = { bucket, path }; await loadFromDrive(bucket, path); } else if (docId) { try { const response = await fetch(`/api/docs/${docId}`); if (response.ok) { const data = await response.json(); state.docId = docId; state.docTitle = data.title || "Untitled Document"; if (elements.docName) elements.docName.value = state.docTitle; if (elements.editorContent) elements.editorContent.innerHTML = data.content || ""; saveToHistory(); updateWordCount(); } } catch (e) { console.error("Load failed:", e); } } else { saveToHistory(); } } async function loadFromDrive(bucket, path) { const fileName = path.split("/").pop() || "Document"; const ext = fileName.split(".").pop()?.toLowerCase(); try { const response = await fetch("/api/drive/content", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ bucket, path }), }); if (response.ok) { const data = await response.json(); const content = data.content || ""; state.docTitle = fileName.replace(/\.[^.]+$/, ""); if (elements.docName) elements.docName.value = state.docTitle; if (ext === "md") { if (elements.editorContent) { elements.editorContent.innerHTML = markdownToHtml(content); } } else if (ext === "txt") { if (elements.editorContent) { elements.editorContent.innerHTML = `
${escapeHtml(content).replace(/\n/g, "
")}
`; } } else { if (elements.editorContent) { elements.editorContent.innerHTML = content; } } saveToHistory(); updateWordCount(); } } catch (e) { console.error("Drive load failed:", e); } } function markdownToHtml(md) { return md .replace(/^### (.+)$/gm, "$1")
.replace(/\n/g, "