資料夾功能修正
This commit is contained in:
@@ -332,6 +332,116 @@
|
||||
window.addEventListener("keydown", keyHandler);
|
||||
}
|
||||
|
||||
// ── 三欄位頁面表單對話框 (頁面名稱 / 檔案名稱 / 資料夾) ────────────
|
||||
function showModalPageForm(actionTitle, defaultTitle, defaultFile, defaultFolder, 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)", 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.97)",
|
||||
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: "460px", 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.1rem"
|
||||
});
|
||||
|
||||
const fieldStyle = `
|
||||
width:100%; background:rgba(13,14,24,0.8);
|
||||
border:1px solid rgba(255,255,255,0.1); border-radius:8px;
|
||||
padding:0.6rem 0.85rem; color:#fff; font-size:0.88rem;
|
||||
outline:none; box-sizing:border-box; transition:border-color 0.2s, box-shadow 0.2s;`;
|
||||
const labelStyle = `display:block; font-size:0.78rem; color:#94a3b8; margin-bottom:0.3rem; font-weight:500;`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div style="font-size:1.1rem; font-weight:600; color:#fff; display:flex; align-items:center; gap:0.55rem;">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#818cf8" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 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>
|
||||
${actionTitle}
|
||||
</div>
|
||||
<div>
|
||||
<label style="${labelStyle}">頁面名稱(顯示用)</label>
|
||||
<input id="pf-title" type="text" style="${fieldStyle}" value="${defaultTitle}" placeholder="我的頁面" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="${labelStyle}">檔案名稱(.html 副檔名可省略)</label>
|
||||
<input id="pf-file" type="text" style="${fieldStyle}" value="${defaultFile}" placeholder="my-page" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="${labelStyle}">儲存至資料夾(留空則存放於根目錄)</label>
|
||||
<input id="pf-folder" type="text" style="${fieldStyle}" value="${defaultFolder}" placeholder="留空 = 根目錄" />
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:0.4rem;">
|
||||
<button id="pf-cancel" style="background:transparent; border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:0.5rem 1rem; color:#94a3b8; font-size:0.85rem; cursor:pointer; transition:background 0.2s;">取消</button>
|
||||
<button id="pf-confirm" style="background:#4f46e5; border:none; border-radius:8px; padding:0.5rem 1.2rem; color:#fff; font-size:0.85rem; font-weight:600; cursor:pointer; box-shadow:0 4px 12px rgba(79,70,229,0.3); transition:background 0.2s;">確定</button>
|
||||
</div>`;
|
||||
|
||||
overlay.appendChild(card);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const titleInput = card.querySelector("#pf-title");
|
||||
const fileInput = card.querySelector("#pf-file");
|
||||
const folderInput = card.querySelector("#pf-folder");
|
||||
const cancelBtn = card.querySelector("#pf-cancel");
|
||||
const confirmBtn = card.querySelector("#pf-confirm");
|
||||
|
||||
// Focus style
|
||||
[titleInput, fileInput, folderInput].forEach(inp => {
|
||||
inp.addEventListener("focus", () => { inp.style.borderColor = "#818cf8"; inp.style.boxShadow = "0 0 10px rgba(129,140,248,0.2)"; });
|
||||
inp.addEventListener("blur", () => { inp.style.borderColor = "rgba(255,255,255,0.1)"; inp.style.boxShadow = "none"; });
|
||||
});
|
||||
cancelBtn.addEventListener("mouseover", () => { cancelBtn.style.background = "rgba(255,255,255,0.06)"; });
|
||||
cancelBtn.addEventListener("mouseout", () => { cancelBtn.style.background = "transparent"; });
|
||||
confirmBtn.addEventListener("mouseover", () => { confirmBtn.style.background = "#4338ca"; });
|
||||
confirmBtn.addEventListener("mouseout", () => { confirmBtn.style.background = "#4f46e5"; });
|
||||
|
||||
// Auto-fill filename from title
|
||||
titleInput.addEventListener("input", () => {
|
||||
const slug = titleInput.value.trim().toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
if (slug) fileInput.value = slug;
|
||||
});
|
||||
|
||||
setTimeout(() => { overlay.style.opacity = "1"; card.style.transform = "scale(1)"; }, 10);
|
||||
titleInput.focus();
|
||||
titleInput.select();
|
||||
|
||||
function close(confirmed) {
|
||||
overlay.style.opacity = "0";
|
||||
card.style.transform = "scale(0.95)";
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
if (confirmed) {
|
||||
callback({ title: titleInput.value.trim(), filename: fileInput.value.trim(), folder: folderInput.value.trim() });
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
cancelBtn.onclick = () => close(false);
|
||||
confirmBtn.onclick = () => close(true);
|
||||
fileInput.onkeydown = titleInput.onkeydown = folderInput.onkeydown = (e) => {
|
||||
if (e.key === "Enter") close(true);
|
||||
if (e.key === "Escape") close(false);
|
||||
};
|
||||
}
|
||||
|
||||
// ── 覆蓋 Vvveb.Builder.saveAjax ──────────────────────────────────
|
||||
function patchSaveAjax() {
|
||||
if (typeof Vvveb === "undefined" || !Vvveb.Builder) {
|
||||
@@ -501,21 +611,25 @@
|
||||
});
|
||||
};
|
||||
|
||||
// 覆蓋重新命名 / 複製頁面
|
||||
// 覆蓋重新命名 / 複製頁面(三欄位對話框:頁面名稱、檔案名稱、儲存至資料夾)
|
||||
Vvveb.FileManager.renamePage = function (element, e, duplicate = false) {
|
||||
let page = element.dataset;
|
||||
showModalPrompt(`請輸入 "${page.file}" 的新檔名:`, page.file, function (newfile) {
|
||||
if (!newfile) return;
|
||||
const currentTitle = element.querySelector("label > span")?.textContent?.trim() || page.file.replace(".html","");
|
||||
const currentFile = page.file.replace(".html","");
|
||||
const action = duplicate ? "複製頁面" : "重新命名頁面";
|
||||
|
||||
// 確保副檔名為 .html
|
||||
if (!newfile.endsWith(".html")) {
|
||||
newfile += ".html";
|
||||
}
|
||||
showModalPageForm(action, currentTitle, currentFile, "", function (formData) {
|
||||
if (!formData) return;
|
||||
let { title, filename, folder } = formData;
|
||||
if (!filename) return;
|
||||
if (!filename.endsWith(".html")) filename += ".html";
|
||||
|
||||
const bodyData = {
|
||||
slug: SLUG,
|
||||
file: page.file,
|
||||
newfile: newfile,
|
||||
newfile: filename,
|
||||
title: title,
|
||||
folder: folder,
|
||||
duplicate: duplicate ? "true" : "false"
|
||||
};
|
||||
|
||||
@@ -534,40 +648,32 @@
|
||||
})
|
||||
.then((data) => {
|
||||
showToast(`✓ ${data.message}`);
|
||||
let baseName = data.newfile.replace('.html', '');
|
||||
let newName = friendlyName(data.newfile.replace(/.*[\/\\]+/, '')).replace('.html', '');
|
||||
const baseName = data.name || data.newfile.replace('.html', '');
|
||||
const newTitle = data.title || title || baseName;
|
||||
|
||||
if (duplicate) {
|
||||
// 複製頁面:在 FileManager 中加入新頁面
|
||||
let pageData = Object.assign({}, Vvveb.FileManager.pages[page.page]);
|
||||
pageData["file"] = data.newfile;
|
||||
pageData["title"] = newName;
|
||||
pageData["title"] = newTitle;
|
||||
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]["title"] = newTitle;
|
||||
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;
|
||||
}
|
||||
if (link) link.setAttribute("href", data.url);
|
||||
|
||||
let span = element.querySelector("label > span") || element.querySelector("span");
|
||||
if (span) span.textContent = newTitle;
|
||||
|
||||
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];
|
||||
@@ -666,6 +772,129 @@
|
||||
console.log("[my-editor] Vvveb.NewSection.insert patched to prevent layout crashes.");
|
||||
}
|
||||
|
||||
// ── 頁面清單樹狀結構 ────────────────────────────────────────────────
|
||||
function patchPageTree() {
|
||||
if (typeof Vvveb === "undefined" || !Vvveb.FileManager) {
|
||||
setTimeout(patchPageTree, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
// 攔截 addPages — 批次新增後重組樹狀結構
|
||||
const origAddPages = Vvveb.FileManager.addPages;
|
||||
if (origAddPages) {
|
||||
Vvveb.FileManager.addPages = function (pages) {
|
||||
origAddPages.call(this, pages);
|
||||
setTimeout(buildPageTree, 80);
|
||||
};
|
||||
}
|
||||
|
||||
// 攔截 addPage — 單筆新增(Rename/Duplicate)後重組
|
||||
const origAddPage = Vvveb.FileManager.addPage;
|
||||
if (origAddPage) {
|
||||
Vvveb.FileManager.addPage = function (name, page, ...rest) {
|
||||
const result = origAddPage.call(this, name, page, ...rest);
|
||||
setTimeout(buildPageTree, 80);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
console.log("[my-editor] Page tree patch ready.");
|
||||
}
|
||||
|
||||
function buildPageTree() {
|
||||
// 找到頁面清單容器(支援多種可能的 selector)
|
||||
const list = (
|
||||
document.querySelector("#file-manager .files") ||
|
||||
document.querySelector(".file-manager .files") ||
|
||||
document.querySelector("#file-manager ul") ||
|
||||
(() => {
|
||||
const li = document.querySelector("li[data-file]");
|
||||
return li ? li.closest("ul") : null;
|
||||
})()
|
||||
);
|
||||
if (!list) return;
|
||||
|
||||
// 取得所有頁面 li(不包含我們建立的 folder-node)
|
||||
const allPageItems = [...list.querySelectorAll("li[data-file]:not(.folder-node-item)")];
|
||||
if (allPageItems.length === 0) return;
|
||||
|
||||
// 移除舊的資料夾節點(避免重複)
|
||||
list.querySelectorAll(".folder-node").forEach(n => n.remove());
|
||||
|
||||
// 分組:根頁面 vs 子資料夾頁面
|
||||
const rootItems = [];
|
||||
const folderMap = {}; // folderName -> [li]
|
||||
|
||||
allPageItems.forEach(li => {
|
||||
const pageKey = li.dataset.page || "";
|
||||
if (pageKey.includes("/")) {
|
||||
const folderName = pageKey.split("/").slice(0, -1).join("/");
|
||||
if (!folderMap[folderName]) folderMap[folderName] = [];
|
||||
folderMap[folderName].push(li);
|
||||
li.remove(); // 先從扁平清單移除,稍後放進資料夾節點
|
||||
} else {
|
||||
rootItems.push(li);
|
||||
}
|
||||
});
|
||||
|
||||
// 每個資料夾建一個折疊節點並加到清單尾端
|
||||
for (const folderName in folderMap) {
|
||||
const folderNode = createFolderNode(folderName, folderMap[folderName]);
|
||||
list.appendChild(folderNode);
|
||||
}
|
||||
}
|
||||
|
||||
function createFolderNode(folderPath, childItems) {
|
||||
const displayName = folderPath.split("/").pop()
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.className = "folder-node";
|
||||
li.dataset.folder = folderPath;
|
||||
li.style.cssText = "list-style:none;";
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="folder-header" style="
|
||||
display:flex; align-items:center; gap:0.45rem;
|
||||
padding:0.35rem 0.6rem; cursor:pointer; border-radius:6px;
|
||||
user-select:none; color:inherit;
|
||||
transition:background 0.15s;">
|
||||
<svg class="chevron" width="12" height="12" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
style="transition:transform 0.2s; flex-shrink:0; opacity:0.6;">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="#6366f1" style="flex-shrink:0;">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
</svg>
|
||||
<span style="font-weight:500;">${displayName}</span>
|
||||
</div>
|
||||
<ul class="folder-children" style="padding-left:1.1rem; margin:0; list-style:none;"></ul>`;
|
||||
|
||||
const childrenUl = li.querySelector(".folder-children");
|
||||
childItems.forEach(child => {
|
||||
child.classList.add("folder-node-item");
|
||||
childrenUl.appendChild(child);
|
||||
});
|
||||
|
||||
// 折疊切換
|
||||
const header = li.querySelector(".folder-header");
|
||||
const chevron = li.querySelector(".chevron");
|
||||
let collapsed = false;
|
||||
|
||||
header.addEventListener("mouseenter", () => { header.style.background = "rgba(99,102,241,0.08)"; });
|
||||
header.addEventListener("mouseleave", () => { header.style.background = ""; });
|
||||
header.addEventListener("click", (e) => {
|
||||
if (e.target.closest("li[data-file]")) return;
|
||||
collapsed = !collapsed;
|
||||
childrenUl.style.display = collapsed ? "none" : "";
|
||||
chevron.style.transform = collapsed ? "rotate(-90deg)" : "";
|
||||
});
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
// ── 動態中文化 VvvebJS 元件與區塊 ─────────────────────────────────
|
||||
function patchI18n() {
|
||||
if (typeof Vvveb === "undefined" || !Vvveb.ComponentsGroup || Object.keys(Vvveb.ComponentsGroup).length === 0) {
|
||||
@@ -979,6 +1208,7 @@
|
||||
patchSaveAjax();
|
||||
patchFileManager();
|
||||
patchNewSection();
|
||||
patchPageTree();
|
||||
patchI18n();
|
||||
enableSaveBtn();
|
||||
});
|
||||
@@ -986,6 +1216,7 @@
|
||||
patchSaveAjax();
|
||||
patchFileManager();
|
||||
patchNewSection();
|
||||
patchPageTree();
|
||||
patchI18n();
|
||||
enableSaveBtn();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user