Files
vvveb-cms/static/js/my-editor.js

837 lines
28 KiB
JavaScript
Raw Normal View History

2026-05-17 22:11:46 +08:00
/**
2026-05-17 22:44:11 +08:00
* my-editor.js VvvebJS 儲存與新增頁面橋接腳本
2026-05-18 12:45:57 +08:00
* 覆蓋 Vvveb.Builder.saveAjax Vvveb.FileManager 行為無縫串接到 Flask 後端並引入精美網頁型暗黑系模態框
2026-05-17 22:11:46 +08:00
*/
(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);
}
2026-05-18 12:45:57 +08:00
// ── 頂級網頁型模態框 (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);
}
2026-05-17 22:44:11 +08:00
// ── 覆蓋 Vvveb.Builder.saveAjax ──────────────────────────────────
2026-05-17 22:11:46 +08:00
function patchSaveAjax() {
if (typeof Vvveb === "undefined" || !Vvveb.Builder) {
setTimeout(patchSaveAjax, 200);
return;
}
2026-05-17 22:44:11 +08:00
/**
* VvvebJS 原始簽名: saveAjax(data, saveUrl, callback, error)
*/
Vvveb.Builder.saveAjax = function (data, saveUrl, callback, error) {
2026-05-17 22:11:46 +08:00
if (!SLUG) {
showToast("錯誤:找不到專案識別碼 (slug)", "error");
2026-05-17 22:44:11 +08:00
if (error) error(new Error("Missing slug"));
2026-05-17 22:11:46 +08:00
return;
}
2026-05-17 22:44:11 +08:00
// 確保 data 為物件並填入專案識別碼
if (typeof data !== "object" || data === null) {
data = {};
2026-05-17 22:11:46 +08:00
}
2026-05-17 22:44:11 +08:00
data.slug = SLUG;
2026-05-17 22:11:46 +08:00
2026-05-18 12:45:57 +08:00
// 扁平化處理新檔案檔名
2026-05-17 22:44:11 +08:00
if (data.file) {
const parts = data.file.split("/");
data.file = parts[parts.length - 1];
2026-05-17 22:11:46 +08:00
}
2026-05-17 22:44:11 +08:00
// 如果不是新增頁面(即一般的儲存頁面),且未帶有 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);
}
2026-05-17 22:11:46 +08:00
}
2026-05-18 12:45:57 +08:00
// 顯示儲存中狀態
2026-05-17 22:11:46 +08:00
const saveBtn = document.querySelector(".save-btn");
2026-05-17 22:44:11 +08:00
if (saveBtn && !isNewPage) {
2026-05-17 22:11:46 +08:00
saveBtn.querySelector(".loading")?.classList.remove("d-none");
saveBtn.querySelector(".button-text")?.classList.add("d-none");
}
2026-05-17 22:44:11 +08:00
// 發送 POST 請求至 Flask API
2026-05-17 22:11:46 +08:00
fetch("/api/save", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
2026-05-17 22:44:11 +08:00
body: new URLSearchParams(data).toString(),
2026-05-17 22:11:46 +08:00
})
2026-05-17 22:44:11 +08:00
.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
});
}
2026-05-17 22:11:46 +08:00
} else {
2026-05-17 22:44:11 +08:00
// 一般儲存成功
showToast("✓ 已儲存:" + (resData.saved || data.file));
if (callback) callback(resData);
// 停用儲存按鈕(直至下一次變更)
document.querySelectorAll("#top-panel .save-btn").forEach((e) =>
e.setAttribute("disabled", "true")
);
2026-05-17 22:11:46 +08:00
}
})
.catch((err) => {
2026-05-17 22:44:11 +08:00
showToast("儲存失敗:" + err.message, "error");
if (error) error(err);
2026-05-17 22:11:46 +08:00
})
.finally(() => {
2026-05-17 22:44:11 +08:00
if (saveBtn && !isNewPage) {
2026-05-17 22:11:46 +08:00
saveBtn.querySelector(".loading")?.classList.add("d-none");
saveBtn.querySelector(".button-text")?.classList.remove("d-none");
}
});
};
2026-05-17 22:44:11 +08:00
console.log("[my-editor] Robust Vvveb.Builder.saveAjax patched for slug:", SLUG);
2026-05-17 22:11:46 +08:00
}
2026-05-18 12:45:57 +08:00
// ── 覆蓋 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.");
}
2026-05-18 13:40:19 +08:00
// ── 動態中文化 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.");
}
2026-05-17 22:11:46 +08:00
// ── 啟用 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) {
2026-05-17 22:44:11 +08:00
// 觸發原生儲存按鈕的 click
const btn = document.querySelector(".save-btn");
if (btn) {
btn.click();
} else {
Vvveb.Builder.saveAjax({});
}
2026-05-17 22:11:46 +08:00
}
}
});
// ── 初始化 ────────────────────────────────────────────────────────
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
patchSaveAjax();
2026-05-18 12:45:57 +08:00
patchFileManager();
patchNewSection();
2026-05-18 13:40:19 +08:00
patchI18n();
2026-05-17 22:11:46 +08:00
enableSaveBtn();
});
} else {
patchSaveAjax();
2026-05-18 12:45:57 +08:00
patchFileManager();
patchNewSection();
2026-05-18 13:40:19 +08:00
patchI18n();
2026-05-17 22:11:46 +08:00
enableSaveBtn();
}
})();