/** * my-editor.js — VvvebJS 儲存與新增頁面橋接腳本 * 覆蓋 Vvveb.Builder.saveAjax 與 Vvveb.FileManager 行為,無縫串接到 Flask 後端,並引入精美網頁型暗黑系模態框 */ (function () { "use strict"; const SLUG = window.VVVEB_PROJECT_SLUG || ""; // ── Toast 通知 ────────────────────────────────────────────────── function showToast(msg, type) { const existing = document.getElementById("vvveb-save-toast"); if (existing) existing.remove(); const toast = document.createElement("div"); toast.id = "vvveb-save-toast"; const bg = type === "error" ? "#7f1d1d" : "#14532d"; const border = type === "error" ? "#ef444460" : "#22c55e60"; Object.assign(toast.style, { position: "fixed", top: "1rem", right: "1rem", zIndex: "99999", background: bg, border: "1px solid " + border, color: "#fff", borderRadius: "8px", padding: "0.65rem 1.1rem", fontSize: "0.85rem", fontFamily: "Inter, system-ui, sans-serif", fontWeight: "500", boxShadow: "0 4px 20px rgba(0,0,0,0.5)", opacity: "0", transform: "translateY(-8px)", transition: "opacity 0.3s ease, transform 0.3s ease", pointerEvents: "none", }); toast.textContent = msg; document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = "1"; toast.style.transform = "translateY(0)"; }); setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateY(-8px)"; setTimeout(() => toast.remove(), 350); }, 3000); } // ── 頂級網頁型模態框 (Web-based Custom Dialogs) ────────────────────── function showModalPrompt(title, defaultValue, callback) { const existing = document.getElementById("vvveb-custom-modal"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "vvveb-custom-modal"; Object.assign(overlay.style, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", zIndex: "100000", background: "rgba(10, 11, 18, 0.65)", backdropFilter: "blur(12px)", webkitBackdropFilter: "blur(12px)", display: "flex", alignItems: "center", justifyContent: "center", opacity: "0", transition: "opacity 0.25s ease-out" }); const card = document.createElement("div"); Object.assign(card.style, { background: "rgba(22, 24, 38, 0.95)", border: "1px solid rgba(255, 255, 255, 0.08)", boxShadow: "0 20px 50px rgba(0, 0, 0, 0.6), 0 0 40px rgba(99, 102, 241, 0.1)", borderRadius: "16px", width: "420px", padding: "2rem", color: "#f8fafc", fontFamily: "Inter, system-ui, sans-serif", transform: "scale(0.95)", transition: "transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)", display: "flex", flexDirection: "column", gap: "1.25rem" }); card.innerHTML = `
${title}
`; overlay.appendChild(card); document.body.appendChild(overlay); const input = card.querySelector("#vvveb-modal-input"); input.value = defaultValue; input.addEventListener("focus", () => { input.style.borderColor = "#818cf8"; input.style.boxShadow = "0 0 12px rgba(129, 140, 248, 0.25)"; }); input.addEventListener("blur", () => { input.style.borderColor = "rgba(255, 255, 255, 0.1)"; input.style.boxShadow = "none"; }); const cancelBtn = card.querySelector("#vvveb-modal-cancel"); cancelBtn.addEventListener("mouseover", () => { cancelBtn.style.background = "rgba(255, 255, 255, 0.05)"; cancelBtn.style.color = "#fff"; }); cancelBtn.addEventListener("mouseout", () => { cancelBtn.style.background = "transparent"; cancelBtn.style.color = "#94a3b8"; }); const confirmBtn = card.querySelector("#vvveb-modal-confirm"); confirmBtn.addEventListener("mouseover", () => { confirmBtn.style.background = "#4338ca"; }); confirmBtn.addEventListener("mouseout", () => { confirmBtn.style.background = "#4f46e5"; }); // Fade in setTimeout(() => { overlay.style.opacity = "1"; card.style.transform = "scale(1)"; }, 10); input.focus(); input.select(); function close(value) { overlay.style.opacity = "0"; card.style.transform = "scale(0.95)"; setTimeout(() => { overlay.remove(); if (value !== undefined) { const finalVal = value.trim ? value.trim() : value; callback(finalVal); } }, 250); } cancelBtn.onclick = () => close(); confirmBtn.onclick = () => close(input.value); input.onkeydown = (e) => { if (e.key === "Enter") { close(input.value); } else if (e.key === "Escape") { close(); } }; } function showModalConfirm(title, message, callback) { const existing = document.getElementById("vvveb-custom-modal"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "vvveb-custom-modal"; Object.assign(overlay.style, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", zIndex: "100000", background: "rgba(10, 11, 18, 0.65)", backdropFilter: "blur(12px)", webkitBackdropFilter: "blur(12px)", display: "flex", alignItems: "center", justifyContent: "center", opacity: "0", transition: "opacity 0.25s ease-out" }); const card = document.createElement("div"); Object.assign(card.style, { background: "rgba(22, 24, 38, 0.95)", border: "1px solid rgba(255, 255, 255, 0.08)", boxShadow: "0 20px 50px rgba(0, 0, 0, 0.6), 0 0 40px rgba(239, 68, 68, 0.1)", borderRadius: "16px", width: "420px", padding: "2rem", color: "#f8fafc", fontFamily: "Inter, system-ui, sans-serif", transform: "scale(0.95)", transition: "transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)", display: "flex", flexDirection: "column", gap: "1.25rem" }); card.innerHTML = `
${title}
${message}
`; overlay.appendChild(card); document.body.appendChild(overlay); const cancelBtn = card.querySelector("#vvveb-modal-cancel"); cancelBtn.addEventListener("mouseover", () => { cancelBtn.style.background = "rgba(255, 255, 255, 0.05)"; cancelBtn.style.color = "#fff"; }); cancelBtn.addEventListener("mouseout", () => { cancelBtn.style.background = "transparent"; cancelBtn.style.color = "#94a3b8"; }); const confirmBtn = card.querySelector("#vvveb-modal-confirm"); confirmBtn.addEventListener("mouseover", () => { confirmBtn.style.background = "#dc2626"; }); confirmBtn.addEventListener("mouseout", () => { confirmBtn.style.background = "#ef4444"; }); // Fade in setTimeout(() => { overlay.style.opacity = "1"; card.style.transform = "scale(1)"; }, 10); confirmBtn.focus(); function close(confirmed) { overlay.style.opacity = "0"; card.style.transform = "scale(0.95)"; setTimeout(() => { overlay.remove(); if (confirmed) callback(); }, 250); } cancelBtn.onclick = () => close(false); confirmBtn.onclick = () => close(true); const keyHandler = (e) => { if (e.key === "Escape") { close(false); window.removeEventListener("keydown", keyHandler); } }; window.addEventListener("keydown", keyHandler); } // ── 覆蓋 Vvveb.Builder.saveAjax ────────────────────────────────── function patchSaveAjax() { if (typeof Vvveb === "undefined" || !Vvveb.Builder) { setTimeout(patchSaveAjax, 200); return; } /** * VvvebJS 原始簽名: saveAjax(data, saveUrl, callback, error) */ Vvveb.Builder.saveAjax = function (data, saveUrl, callback, error) { if (!SLUG) { showToast("錯誤:找不到專案識別碼 (slug)", "error"); if (error) error(new Error("Missing slug")); return; } // 確保 data 為物件並填入專案識別碼 if (typeof data !== "object" || data === null) { data = {}; } data.slug = SLUG; // 扁平化處理新檔案檔名 if (data.file) { const parts = data.file.split("/"); data.file = parts[parts.length - 1]; } // 如果不是新增頁面(即一般的儲存頁面),且未帶有 HTML 內容,則動態獲取目前 HTML const isNewPage = !!data.startTemplateUrl; if (!isNewPage && !data.html) { try { data.html = Vvveb.Builder.getHtml ? Vvveb.Builder.getHtml() : ""; } catch (e) { console.error("[my-editor] getHtml failed:", e); } } // 顯示儲存中狀態 const saveBtn = document.querySelector(".save-btn"); if (saveBtn && !isNewPage) { saveBtn.querySelector(".loading")?.classList.remove("d-none"); saveBtn.querySelector(".button-text")?.classList.add("d-none"); } // 發送 POST 請求至 Flask API fetch("/api/save", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(data).toString(), }) .then((res) => { if (!res.ok) { return res.json().then((errData) => { throw new Error(errData.error || "伺服器儲存錯誤"); }); } return res.json(); }) .then((resData) => { if (isNewPage) { // 新增頁面成功:秀出通知並執行 Vvveb 原生 callback 以載入新頁面 showToast("✓ 成功建立新頁面:" + resData.file); if (callback) { callback({ name: resData.name, title: resData.title, file: resData.file, url: resData.url }); } } else { // 一般儲存成功 showToast("✓ 已儲存:" + (resData.saved || data.file)); if (callback) callback(resData); // 停用儲存按鈕(直至下一次變更) document.querySelectorAll("#top-panel .save-btn").forEach((e) => e.setAttribute("disabled", "true") ); } }) .catch((err) => { showToast("儲存失敗:" + err.message, "error"); if (error) error(err); }) .finally(() => { if (saveBtn && !isNewPage) { saveBtn.querySelector(".loading")?.classList.add("d-none"); saveBtn.querySelector(".button-text")?.classList.remove("d-none"); } }); }; console.log("[my-editor] Robust Vvveb.Builder.saveAjax patched for slug:", SLUG); } // ── 覆蓋 Vvveb.FileManager 頁面操作 ────────────────────────────── function patchFileManager() { if (typeof Vvveb === "undefined" || !Vvveb.FileManager) { setTimeout(patchFileManager, 200); return; } // 覆蓋 init 方法以修正 VvvebJS 使用全球廢棄的 `event.target` 導致在某些瀏覽器/嚴格模式下點擊無效的 Bug const originalInit = Vvveb.FileManager.init; Vvveb.FileManager.init = function (allowedComponents = {}) { originalInit.call(this, allowedComponents); // 利用 Clone 移除舊有包含 Bug 的匿名字選區接聽器 const newTree = this.tree.cloneNode(true); this.tree.parentNode.replaceChild(newTree, this.tree); this.tree = newTree; // 綁定符合標準且高度相容 (e.target) 的新接聽器 this.tree.addEventListener("click", function (e) { let element = e.target.closest("a"); if (element) { e.stopImmediatePropagation(); if (element.classList.contains('view')) return; e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = e.target.closest(".delete"); if (element) { Vvveb.FileManager.deletePage(element.closest("li"), e); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = e.target.closest(".rename"); if (element) { Vvveb.FileManager.renamePage(element.closest("li"), e, false); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = e.target.closest(".duplicate"); if (element) { Vvveb.FileManager.renamePage(element.closest("li"), e, true); e.stopImmediatePropagation(); e.preventDefault(); return false; } }); this.tree.addEventListener("click", function (e) { let element = e.target.closest("li[data-page] label"); if (element) { let page = element.parentNode.dataset.page; if (page) { Vvveb.FileManager.loadPage(page); e.stopImmediatePropagation(); e.preventDefault(); return false; } } }); }; // 覆蓋重新命名 / 複製頁面 Vvveb.FileManager.renamePage = function (element, e, duplicate = false) { let page = element.dataset; showModalPrompt(`請輸入 "${page.file}" 的新檔名:`, page.file, function (newfile) { if (!newfile) return; // 確保副檔名為 .html if (!newfile.endsWith(".html")) { newfile += ".html"; } const bodyData = { slug: SLUG, file: page.file, newfile: newfile, duplicate: duplicate ? "true" : "false" }; fetch(`/api/save?action=rename`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(bodyData).toString() }) .then((res) => { if (!res.ok) { return res.json().then((err) => { throw new Error(err.error || "操作失敗"); }); } return res.json(); }) .then((data) => { showToast(`✓ ${data.message}`); let baseName = data.newfile.replace('.html', ''); let newName = friendlyName(data.newfile.replace(/.*[\/\\]+/, '')).replace('.html', ''); if (duplicate) { // 複製頁面:在 FileManager 中加入新頁面 let pageData = Object.assign({}, Vvveb.FileManager.pages[page.page]); pageData["file"] = data.newfile; pageData["title"] = newName; pageData["url"] = data.url; pageData["name"] = baseName; Vvveb.FileManager.addPage(baseName, pageData); } else { // 重新命名:更新現有節點資訊 const oldPageKey = page.page; Vvveb.FileManager.pages[oldPageKey]["file"] = data.newfile; Vvveb.FileManager.pages[oldPageKey]["title"] = newName; Vvveb.FileManager.pages[oldPageKey]["url"] = data.url; Vvveb.FileManager.pages[oldPageKey]["name"] = baseName; let link = element.querySelector("a.view"); if (link) { link.setAttribute("href", data.url); } let span = element.querySelector("label > span"); if (!span) { span = element.querySelector("span"); } if (span) { span.textContent = newName; } element.dataset.file = data.newfile; element.dataset.page = baseName; // 將 key 重新綁定 Vvveb.FileManager.pages[baseName] = Vvveb.FileManager.pages[oldPageKey]; if (baseName !== oldPageKey) { delete Vvveb.FileManager.pages[oldPageKey]; } } }) .catch((err) => { showToast(`操作失敗:${err.message}`, "error"); }); }); }; // 覆蓋刪除頁面 Vvveb.FileManager.deletePage = function (element, e) { let page = element.dataset; if (page.file.toLowerCase() === "index.html") { showToast("錯誤:無法刪除主頁 index.html", "error"); return; } showModalConfirm("確認刪除頁面", `確定要刪除 "${page.file}" 頁面嗎?此動作將無法復原。`, function () { const bodyData = { slug: SLUG, file: page.file }; fetch(`/api/save?action=delete`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(bodyData).toString() }) .then((res) => { if (!res.ok) { return res.json().then((err) => { throw new Error(err.error || "刪除失敗"); }); } return res.json(); }) .then((data) => { showToast(`✓ ${data.message}`); // 從內部 pages 列表中移除 delete Vvveb.FileManager.pages[page.page]; element.remove(); // 如果刪除的是目前正在編輯的頁面,切換回第一個頁面 if (Vvveb.FileManager.currentPage === page.page) { let firstPage = Object.keys(Vvveb.FileManager.pages)[0]; if (firstPage) { Vvveb.FileManager.loadPage(firstPage); } } }) .catch((err) => { showToast(`刪除失敗:${err.message}`, "error"); }); }); }; console.log("[my-editor] Vvveb.FileManager operations patched."); } // ── 覆蓋 Vvveb.NewSection 防止佈局崩潰 ───────────────────────────── function patchNewSection() { if (typeof Vvveb === "undefined" || !Vvveb.NewSection) { setTimeout(patchNewSection, 200); return; } Vvveb.NewSection.insert = function () { let position = 'before'; let lastSection = Vvveb.Builder.frameBody.querySelector(':scope > footer:last-of-type'); if (!lastSection) { position = 'after'; lastSection = Vvveb.Builder.frameBody.querySelector(':scope > main:last-of-type') || Vvveb.Builder.frameBody.querySelector(':scope > section:last-of-type') || Vvveb.Builder.frameBody.querySelector(':scope > div:last-of-type') || Vvveb.Builder.frameBody; // 最安全的回退點:整個 body 節點 } this.container = generateElements(this.template)[0]; if (lastSection === Vvveb.Builder.frameBody) { Vvveb.Builder.frameBody.appendChild(this.container); } else { if (position == 'after') { lastSection.after(this.container); } else { lastSection.before(this.container); } } return this.container; }; console.log("[my-editor] Vvveb.NewSection.insert patched to prevent layout crashes."); } // ── 動態中文化 VvvebJS 元件與區塊 ───────────────────────────────── function patchI18n() { if (typeof Vvveb === "undefined" || !Vvveb.ComponentsGroup || Object.keys(Vvveb.ComponentsGroup).length === 0) { setTimeout(patchI18n, 500); return; } const translateMap = { // 分類 (Groups) "Grid": "網格系統", "Components": "基礎元件", "Basic": "基本", "Typography": "排版", "Media": "媒體", "Forms": "表單", "Widgets": "小工具", "Advanced": "進階", "Content": "內容區塊", "Layout": "佈局", "Blocks": "區塊", "Features": "功能特色", "Headers": "頁首", "Footers": "頁尾", "Pricing": "價格表", "Testimonials": "客戶評價", "Cards": "卡片", "Services": "服務", "Team": "團隊", "About": "關於我們", // 元件 (Components) "Grid Row": "網格列 (Row)", "Grid Column": "網格欄 (Column)", "Container": "容器 (Container)", "Heading": "標題 (Heading)", "Link": "超連結 (Link)", "Image": "圖片 (Image)", "Video": "影片 (Video)", "Paragraph": "段落文字", "List": "列表", "List item": "列表項目", "Button": "按鈕", "Button Group": "按鈕群組", "Button Toolbar": "按鈕工具列", "Form": "表單", "Text Input": "文字輸入框", "Textarea": "多行文字框", "Checkbox": "核取方塊", "Radio": "單選按鈕", "Select": "下拉選單", "Label": "標籤文字", "Alert": "警告提示", "Badge": "徽章 (Badge)", "Card": "卡片 (Card)", "Table": "表格", "Progress": "進度條", "Navbar": "導覽列 (Navbar)", "Breadcrumb": "麵包屑 (Breadcrumb)", "Pagination": "分頁", "Jumbotron": "大型看板", "Panel": "面板", "Icon": "圖示", "Divider": "分隔線", "Map": "地圖", "Audio": "音訊", "File Upload": "檔案上傳", "Date Input": "日期輸入框", "Password Input": "密碼輸入框", "Email Input": "Email 輸入框", // 屬性面板 - 標題與一般屬性 "General": "一般", "Id": "ID", "Title": "標題 (Title)", "Class": "類別 (Class)", "Display": "顯示 (Display)", "Position": "定位 (Position)", "Top": "上", "Left": "左", "Bottom": "下", "Right": "右", "Float": "浮動 (Float)", "Opacity": "透明度 (Opacity)", "Background": "背景 (Background)", "Background Color": "背景顏色", "Text Color": "文字顏色", "Color": "顏色", // 排版與字體 "Font size": "字體大小", "Font weight": "字體粗細", "Font family": "字體家族", "Text align": "文字對齊", "Line height": "行高", "Letter spacing": "字距", "Text decoration": "文字裝飾", "Decoration Color": "裝飾顏色", "Decoration style": "裝飾樣式", // 尺寸與間距 "Size": "尺寸", "Width": "寬度", "Height": "高度", "Min Width": "最小寬度", "Min Height": "最小高度", "Max Width": "最大寬度", "Max Height": "最大高度", "Margin": "外邊距 (Margin)", "Padding": "內邊距 (Padding)", // 邊框與陰影 "Border": "邊框 (Border)", "Style": "樣式", "Radius": "圓角 (Radius)", "Radius Top Left": "左上圓角", "Radius Top Right": "右上圓角", "Radius Bottom Left": "左下圓角", "Radius Bottom Right": "右下圓角", "Background Image": "背景圖片", "Box Shadow": "盒子陰影", "Text Shadow": "文字陰影", "Filter": "濾鏡", "Transform": "變形", "Transition": "過渡效果", // 選項值 "Default": "預設", "Primary": "主要 (Primary)", "Secondary": "次要 (Secondary)", "Success": "成功 (Success)", "Danger": "危險 (Danger)", "Warning": "警告 (Warning)", "Info": "資訊 (Info)", "Light": "亮色 (Light)", "Dark": "暗色 (Dark)", "White": "白色", "Block": "區塊 (Block)", "Inline": "行內 (Inline)", "Inline Block": "行內區塊 (Inline Block)", "Flex": "彈性 (Flex)", "Inline Flex": "行內彈性 (Inline Flex)", "Grid": "網格 (Grid)", "Static": "靜態 (Static)", "Fixed": "固定 (Fixed)", "Relative": "相對 (Relative)", "Absolute": "絕對 (Absolute)", // 裝置響應式顯示 "Hide based on device screen width": "依裝置螢幕寬度隱藏", "Extra small devices": "超小螢幕 (xs)", "Small devices": "小螢幕 (sm)", "Medium devices": "中螢幕 (md)", "Large devices": "大螢幕 (lg)", "Xl devices": "超大螢幕 (xl)", "Xxl devices": "超超大螢幕 (xxl)", // AOS 動畫 "Animate on scroll": "滾動動畫 (AOS)", "Animation type": "動畫類型", "Duration": "持續時間", "Delay": "延遲", "Play animation": "播放動畫", "[none]": "[無]", "Fade animations": "淡入淡出動畫", "Fade": "淡入", "Fade Up": "向上淡入", "Fade down": "向下淡入", "Fade left": "向左淡入", "Fade right": "向右淡入", "Fade up right": "向右上淡入", "Fade up left": "向左上淡入", "Fade down right": "向右下淡入", "Fade down left": "向左下淡入", "Flip animations": "翻轉動畫", "Flip Up": "向上翻轉", "Flip Down": "向下翻轉", "Flip left": "向左翻轉", "Flip right": "向右翻轉", "Slide animations": "滑動動畫", "Slide up": "向上滑動", "Slide down": "向下滑動", "Slide left": "向左滑動", "Slide right": "向右滑動", "Zoom animations": "縮放動畫", "Zoom in": "放大", "Zoom in up": "向上放大", "Zoom in down": "向下放大", "Zoom in left": "向左放大", "Zoom in right": "向右放大", "Zoom out": "縮小", "Zoom out up": "向上縮小", "Zoom out down": "向下縮小", "Zoom out left": "向左縮小", "Zoom out right": "向右縮小", // 連結樣式提示 "Linked styles": "共用樣式提示" }; const translateText = (text) => { if (!text || typeof text !== "string") return text; const trimmed = text.trim(); return translateMap[trimmed] ? translateMap[trimmed] : text; }; // 1. 翻譯 Vvveb 物件內的資料 (供重新渲染時使用) if (Vvveb.ComponentsGroup) { const newGroups = {}; for (let group in Vvveb.ComponentsGroup) { let tGroup = translateText(group); newGroups[tGroup] = Vvveb.ComponentsGroup[group]; newGroups[tGroup].forEach(compName => { let comp = Vvveb.Components.get(compName); if (comp && comp.name) { comp.name = translateText(comp.name); } }); } Vvveb.ComponentsGroup = newGroups; } if (Vvveb.BlocksGroup) { const newBlockGroups = {}; for (let group in Vvveb.BlocksGroup) { let tGroup = translateText(group); newBlockGroups[tGroup] = Vvveb.BlocksGroup[group]; } Vvveb.BlocksGroup = newBlockGroups; } // 2. 翻譯所有元件的屬性面板 (Style, Advanced, etc) if (Vvveb.Components && Vvveb.Components._components) { Object.keys(Vvveb.Components._components).forEach(key => { let comp = Vvveb.Components._components[key]; if (comp.properties) { comp.properties.forEach(prop => { if (prop.name) prop.name = translateText(prop.name); if (prop.data) { if (prop.data.header) prop.data.header = translateText(prop.data.header); if (prop.data.text) prop.data.text = translateText(prop.data.text); if (prop.data.options && Array.isArray(prop.data.options)) { prop.data.options.forEach(opt => { if (opt.text) opt.text = translateText(opt.text); if (opt.title) opt.title = translateText(opt.title); if (opt.optgroup) opt.optgroup = translateText(opt.optgroup); }); } } }); } }); } // 3. 直接修改當前已渲染的 DOM 文字 document.querySelectorAll(".components-list .header span, .blocks-list .header span, .sections-list .header span").forEach(el => { const txt = el.textContent.trim(); if (translateMap[txt]) { el.textContent = translateMap[txt]; } }); document.querySelectorAll(".components-list li .name, .blocks-list li .name, .sections-list li .name").forEach(el => { const txt = el.textContent.trim(); if (translateMap[txt]) { // 如果有子元素,只替換文本節點 if (el.childNodes.length > 0) { for (let i = 0; i < el.childNodes.length; i++) { if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim() === txt) { el.childNodes[i].textContent = translateMap[txt]; break; } } } else { el.textContent = translateMap[txt]; } } }); console.log("[my-editor] VvvebJS components and blocks localized."); } // ── 啟用 Save 按鈕(vvvebjs 預設 disabled)────────────────────── function enableSaveBtn() { const btn = document.querySelector(".save-btn"); if (btn) { btn.removeAttribute("disabled"); } else { setTimeout(enableSaveBtn, 300); } } // ── Ctrl+S 快捷鍵 ──────────────────────────────────────────────── document.addEventListener("keydown", function (e) { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); if (typeof Vvveb !== "undefined" && Vvveb.Builder && Vvveb.Builder.saveAjax) { // 觸發原生儲存按鈕的 click const btn = document.querySelector(".save-btn"); if (btn) { btn.click(); } else { Vvveb.Builder.saveAjax({}); } } } }); // ── 初始化 ──────────────────────────────────────────────────────── if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { patchSaveAjax(); patchFileManager(); patchNewSection(); patchI18n(); enableSaveBtn(); }); } else { patchSaveAjax(); patchFileManager(); patchNewSection(); patchI18n(); enableSaveBtn(); } })();