Files
vvveb-cms/static/js/my-editor.js
2026-05-18 13:40:19 +08:00

837 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<div style="font-size: 1.15rem; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 0.6rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#818cf8" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
${title}
</div>
<div>
<input type="text" id="vvveb-modal-input" style="
width: 100%;
background: rgba(13, 14, 24, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.65rem 0.9rem;
color: #fff;
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
" />
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 0.5rem;">
<button id="vvveb-modal-cancel" style="
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.55rem 1.1rem;
color: #94a3b8;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s;
">取消</button>
<button id="vvveb-modal-confirm" style="
background: #4f46e5;
border: none;
border-radius: 8px;
padding: 0.55rem 1.25rem;
color: #fff;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
">確定</button>
</div>
`;
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 = `
<div style="font-size: 1.15rem; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 0.6rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
${title}
</div>
<div style="font-size: 0.9rem; color: #cbd5e1; line-height: 1.5;">
${message}
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 0.5rem;">
<button id="vvveb-modal-cancel" style="
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 0.55rem 1.1rem;
color: #94a3b8;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s;
">取消</button>
<button id="vvveb-modal-confirm" style="
background: #ef4444;
border: none;
border-radius: 8px;
padding: 0.55rem 1.25rem;
color: #fff;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
">確定</button>
</div>
`;
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 輸入框"
};
// 1. 翻譯 Vvveb 物件內的資料 (供重新渲染時使用)
if (Vvveb.ComponentsGroup) {
const newGroups = {};
for (let group in Vvveb.ComponentsGroup) {
let tGroup = translateMap[group] || group;
newGroups[tGroup] = Vvveb.ComponentsGroup[group];
newGroups[tGroup].forEach(compName => {
let comp = Vvveb.Components.get(compName);
if (comp && comp.name) {
comp.name = translateMap[comp.name] || comp.name;
}
});
}
Vvveb.ComponentsGroup = newGroups;
}
if (Vvveb.BlocksGroup) {
const newBlockGroups = {};
for (let group in Vvveb.BlocksGroup) {
let tGroup = translateMap[group] || group;
newBlockGroups[tGroup] = Vvveb.BlocksGroup[group];
}
Vvveb.BlocksGroup = newBlockGroups;
}
// 2. 直接修改當前已渲染的 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();
}
})();