// folderManager.js import { loadFileList } from './fileListView.js'; import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js'; import { t } from './i18n.js'; import { openFolderShareModal } from './folderShareModal.js'; import { fetchWithCsrf } from './auth.js'; import { loadCsrfToken } from './main.js'; /* ---------------------- Helper Functions (Data/State) ----------------------*/ // Formats a folder name for display (e.g. adding indentations). export function formatFolderName(folder) { if (typeof folder !== "string") return ""; if (folder.indexOf("/") !== -1) { let parts = folder.split("/"); let indent = ""; for (let i = 1; i < parts.length; i++) { indent += "\u00A0\u00A0\u00A0\u00A0"; // 4 non-breaking spaces per level } return indent + parts[parts.length - 1]; } else { return folder; } } // Build a tree structure from a flat array of folder paths. function buildFolderTree(folders) { const tree = {}; folders.forEach(folderPath => { if (typeof folderPath !== "string") return; const parts = folderPath.split('/'); let current = tree; parts.forEach(part => { if (!current[part]) { current[part] = {}; } current = current[part]; }); }); return tree; } /* ---------------------- Folder Tree State (Save/Load) ----------------------*/ function loadFolderTreeState() { const state = localStorage.getItem("folderTreeState"); return state ? JSON.parse(state) : {}; } function saveFolderTreeState(state) { localStorage.setItem("folderTreeState", JSON.stringify(state)); } // Helper for getting the parent folder. function getParentFolder(folder) { if (folder === "root") return "root"; const lastSlash = folder.lastIndexOf("/"); return lastSlash === -1 ? "root" : folder.substring(0, lastSlash); } /* ---------------------- Breadcrumb Functions ----------------------*/ function renderBreadcrumb(normalizedFolder) { if (!normalizedFolder || normalizedFolder === "") return ""; const parts = normalizedFolder.split("/"); let breadcrumbItems = []; // Use the first segment as the root. breadcrumbItems.push(`${escapeHTML(parts[0])}`); let cumulative = parts[0]; parts.slice(1).forEach(part => { cumulative += "/" + part; breadcrumbItems.push(` / `); breadcrumbItems.push(`${escapeHTML(part)}`); }); return breadcrumbItems.join(''); } // --- NEW: Breadcrumb Delegation Setup --- // bindBreadcrumbEvents(); removed in favor of delegation export function setupBreadcrumbDelegation() { const container = document.getElementById("fileListTitle"); if (!container) { console.error("Breadcrumb container (fileListTitle) not found."); return; } // Remove any existing event listeners to avoid duplicates. container.removeEventListener("click", breadcrumbClickHandler); container.removeEventListener("dragover", breadcrumbDragOverHandler); container.removeEventListener("dragleave", breadcrumbDragLeaveHandler); container.removeEventListener("drop", breadcrumbDropHandler); // Attach delegated listeners container.addEventListener("click", breadcrumbClickHandler); container.addEventListener("dragover", breadcrumbDragOverHandler); container.addEventListener("dragleave", breadcrumbDragLeaveHandler); container.addEventListener("drop", breadcrumbDropHandler); } // Click handler via delegation function breadcrumbClickHandler(e) { const link = e.target.closest(".breadcrumb-link"); if (!link) return; e.stopPropagation(); e.preventDefault(); const folder = link.getAttribute("data-folder"); window.currentFolder = folder; localStorage.setItem("lastOpenedFolder", folder); // Update the container with sanitized breadcrumbs. const container = document.getElementById("fileListTitle"); const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder)); container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")"; expandTreePath(folder); document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected")); const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`); if (targetOption) targetOption.classList.add("selected"); loadFileList(folder); } // Dragover handler via delegation function breadcrumbDragOverHandler(e) { const link = e.target.closest(".breadcrumb-link"); if (!link) return; e.preventDefault(); link.classList.add("drop-hover"); } // Dragleave handler via delegation function breadcrumbDragLeaveHandler(e) { const link = e.target.closest(".breadcrumb-link"); if (!link) return; link.classList.remove("drop-hover"); } // Drop handler via delegation function breadcrumbDropHandler(e) { const link = e.target.closest(".breadcrumb-link"); if (!link) return; e.preventDefault(); link.classList.remove("drop-hover"); const dropFolder = link.getAttribute("data-folder"); let dragData; try { dragData = JSON.parse(e.dataTransfer.getData("application/json")); } catch (err) { console.error("Invalid drag data on breadcrumb:", err); return; } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") }, body: JSON.stringify({ source: dragData.sourceFolder, files: filesToMove, destination: dropFolder }) }) .then(response => response.json()) .then(data => { if (data.success) { showToast(`File(s) moved successfully to ${dropFolder}!`); loadFileList(dragData.sourceFolder); } else { showToast("Error moving files: " + (data.error || "Unknown error")); } }) .catch(error => { console.error("Error moving files via drop on breadcrumb:", error); showToast("Error moving files."); }); } /* ---------------------- Check Current User's Folder-Only Permission ----------------------*/ // This function uses localStorage values (set during login) to determine if the current user is restricted. // If folderOnly is "true", then the personal folder (i.e. username) is forced as the effective root. function checkUserFolderPermission() { const username = localStorage.getItem("username"); console.log("checkUserFolderPermission: username =", username); if (!username) { console.warn("No username in localStorage; skipping getUserPermissions fetch."); return Promise.resolve(false); } if (localStorage.getItem("folderOnly") === "true") { window.userFolderOnly = true; console.log("checkUserFolderPermission: using localStorage.folderOnly = true"); localStorage.setItem("lastOpenedFolder", username); window.currentFolder = username; return Promise.resolve(true); } return fetch("/api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { console.log("checkUserFolderPermission: permissionsData =", permissionsData); if (permissionsData && permissionsData[username] && permissionsData[username].folderOnly) { window.userFolderOnly = true; localStorage.setItem("folderOnly", "true"); localStorage.setItem("lastOpenedFolder", username); window.currentFolder = username; return true; } else { window.userFolderOnly = false; localStorage.setItem("folderOnly", "false"); return false; } }) .catch(err => { console.error("Error fetching user permissions:", err); window.userFolderOnly = false; return false; }); } /* ---------------------- DOM Building Functions for Folder Tree ----------------------*/ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") { const state = loadFolderTreeState(); let html = ``; return html; } function expandTreePath(path) { const parts = path.split("/"); let cumulative = ""; parts.forEach((part, index) => { cumulative = index === 0 ? part : cumulative + "/" + part; const option = document.querySelector(`.folder-option[data-folder="${cumulative}"]`); if (option) { const li = option.parentNode; const nestedUl = li.querySelector("ul"); if (nestedUl && (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded"))) { nestedUl.classList.remove("collapsed"); nestedUl.classList.add("expanded"); const toggle = li.querySelector(".folder-toggle"); if (toggle) { toggle.innerHTML = "[" + '-' + "]"; let state = loadFolderTreeState(); state[cumulative] = "block"; saveFolderTreeState(state); } } } }); } /* ---------------------- Drag & Drop Support for Folder Tree Nodes ----------------------*/ function folderDragOverHandler(event) { event.preventDefault(); event.currentTarget.classList.add("drop-hover"); } function folderDragLeaveHandler(event) { event.currentTarget.classList.remove("drop-hover"); } function folderDropHandler(event) { event.preventDefault(); event.currentTarget.classList.remove("drop-hover"); const dropFolder = event.currentTarget.getAttribute("data-folder"); let dragData; try { dragData = JSON.parse(event.dataTransfer.getData("application/json")); } catch (e) { console.error("Invalid drag data", e); return; } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") }, body: JSON.stringify({ source: dragData.sourceFolder, files: filesToMove, destination: dropFolder }) }) .then(response => response.json()) .then(data => { if (data.success) { showToast(`File(s) moved successfully to ${dropFolder}!`); loadFileList(dragData.sourceFolder); } else { showToast("Error moving files: " + (data.error || "Unknown error")); } }) .catch(error => { console.error("Error moving files via drop:", error); showToast("Error moving files."); }); } /* ---------------------- Main Folder Tree Rendering and Event Binding ----------------------*/ export async function loadFolderTree(selectedFolder) { try { // Check if the user has folder-only permission. await checkUserFolderPermission(); // Determine effective root folder. const username = localStorage.getItem("username") || "root"; let effectiveRoot = "root"; let effectiveLabel = "(Root)"; if (window.userFolderOnly) { effectiveRoot = username; // Use the username as the personal root. effectiveLabel = `(Root)`; // Force override of any saved folder. localStorage.setItem("lastOpenedFolder", username); window.currentFolder = username; } else { window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root"; } // Build fetch URL. let fetchUrl = '/api/folder/getFolderList.php'; if (window.userFolderOnly) { fetchUrl += '?restricted=1'; } console.log("Fetching folder list from:", fetchUrl); // Fetch folder list from the server. const response = await fetch(fetchUrl); if (response.status === 401) { console.error("Unauthorized: Please log in to view folders."); showToast("Session expired. Please log in again."); window.location.href = "/api/auth/logout.php"; return; } let folderData = await response.json(); console.log("Folder data received:", folderData); let folders = []; if (Array.isArray(folderData) && folderData.length && typeof folderData[0] === "object" && folderData[0].folder) { folders = folderData.map(item => item.folder); } else if (Array.isArray(folderData)) { folders = folderData; } // Remove any global "root" entry. folders = folders.filter(folder => folder.toLowerCase() !== "root"); // If restricted, filter folders: keep only those that start with effectiveRoot + "/" (do not include effectiveRoot itself). if (window.userFolderOnly && effectiveRoot !== "root") { folders = folders.filter(folder => folder.startsWith(effectiveRoot + "/")); // Force current folder to be the effective root. localStorage.setItem("lastOpenedFolder", effectiveRoot); window.currentFolder = effectiveRoot; } localStorage.setItem("lastOpenedFolder", window.currentFolder); // Render the folder tree. const container = document.getElementById("folderTreeContainer"); if (!container) { console.error("Folder tree container not found."); return; } let html = `
[-] ${effectiveLabel}
`; if (folders.length > 0) { const tree = buildFolderTree(folders); html += renderFolderTree(tree, "", "block"); } container.innerHTML = html; // Attach drag/drop event listeners. container.querySelectorAll(".folder-option").forEach(el => { el.addEventListener("dragover", folderDragOverHandler); el.addEventListener("dragleave", folderDragLeaveHandler); el.addEventListener("drop", folderDropHandler); }); if (selectedFolder) { window.currentFolder = selectedFolder; } localStorage.setItem("lastOpenedFolder", window.currentFolder); const titleEl = document.getElementById("fileListTitle"); titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")"; setupBreadcrumbDelegation(); loadFileList(window.currentFolder); const folderState = loadFolderTreeState(); if (window.currentFolder !== effectiveRoot && folderState[window.currentFolder] !== "none") { expandTreePath(window.currentFolder); } const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`); if (selectedEl) { container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected")); selectedEl.classList.add("selected"); } container.querySelectorAll(".folder-option").forEach(el => { el.addEventListener("click", function (e) { e.stopPropagation(); container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected")); this.classList.add("selected"); const selected = this.getAttribute("data-folder"); window.currentFolder = selected; localStorage.setItem("lastOpenedFolder", selected); const titleEl = document.getElementById("fileListTitle"); titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")"; setupBreadcrumbDelegation(); loadFileList(selected); }); }); const rootToggle = container.querySelector("#rootRow .folder-toggle"); if (rootToggle) { rootToggle.addEventListener("click", function (e) { e.stopPropagation(); const nestedUl = container.querySelector("#rootRow + ul"); if (nestedUl) { let state = loadFolderTreeState(); if (nestedUl.classList.contains("collapsed") || !nestedUl.classList.contains("expanded")) { nestedUl.classList.remove("collapsed"); nestedUl.classList.add("expanded"); this.innerHTML = "[" + '-' + "]"; state[effectiveRoot] = "block"; } else { nestedUl.classList.remove("expanded"); nestedUl.classList.add("collapsed"); this.textContent = "[+]"; state[effectiveRoot] = "none"; } saveFolderTreeState(state); } }); } container.querySelectorAll(".folder-toggle").forEach(toggle => { toggle.addEventListener("click", function (e) { e.stopPropagation(); const siblingUl = this.parentNode.querySelector("ul"); const folderPath = this.getAttribute("data-folder"); let state = loadFolderTreeState(); if (siblingUl) { if (siblingUl.classList.contains("collapsed") || !siblingUl.classList.contains("expanded")) { siblingUl.classList.remove("collapsed"); siblingUl.classList.add("expanded"); this.innerHTML = "[" + '-' + "]"; state[folderPath] = "block"; } else { siblingUl.classList.remove("expanded"); siblingUl.classList.add("collapsed"); this.textContent = "[+]"; state[folderPath] = "none"; } saveFolderTreeState(state); } }); }); } catch (error) { console.error("Error loading folder tree:", error); } } // For backward compatibility. export function loadFolderList(selectedFolder) { loadFolderTree(selectedFolder); } /* ---------------------- Folder Management (Rename, Delete, Create) ----------------------*/ document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal); document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal); function openRenameFolderModal() { const selectedFolder = window.currentFolder || "root"; if (!selectedFolder || selectedFolder === "root") { showToast("Please select a valid folder to rename."); return; } const parts = selectedFolder.split("/"); document.getElementById("newRenameFolderName").value = parts[parts.length - 1]; document.getElementById("renameFolderModal").style.display = "block"; setTimeout(() => { const input = document.getElementById("newRenameFolderName"); input.focus(); input.select(); }, 100); } document.getElementById("cancelRenameFolder").addEventListener("click", function () { document.getElementById("renameFolderModal").style.display = "none"; document.getElementById("newRenameFolderName").value = ""; }); attachEnterKeyListener("renameFolderModal", "submitRenameFolder"); document.getElementById("submitRenameFolder").addEventListener("click", function (event) { event.preventDefault(); const selectedFolder = window.currentFolder || "root"; const newNameBasename = document.getElementById("newRenameFolderName").value.trim(); if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) { showToast("Please enter a valid new folder name."); return; } const parentPath = getParentFolder(selectedFolder); const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename; const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); if (!csrfToken) { showToast("CSRF token not loaded yet! Please try again."); return; } fetch("/api/folder/renameFolder.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify({ oldFolder: window.currentFolder, newFolder: newFolderFull }) }) .then(response => response.json()) .then(data => { if (data.success) { showToast("Folder renamed successfully!"); window.currentFolder = newFolderFull; localStorage.setItem("lastOpenedFolder", newFolderFull); loadFolderList(newFolderFull); } else { showToast("Error: " + (data.error || "Could not rename folder")); } }) .catch(error => console.error("Error renaming folder:", error)) .finally(() => { document.getElementById("renameFolderModal").style.display = "none"; document.getElementById("newRenameFolderName").value = ""; }); }); function openDeleteFolderModal() { const selectedFolder = window.currentFolder || "root"; if (!selectedFolder || selectedFolder === "root") { showToast("Please select a valid folder to delete."); return; } document.getElementById("deleteFolderMessage").textContent = "Are you sure you want to delete folder " + selectedFolder + "?"; document.getElementById("deleteFolderModal").style.display = "block"; } document.getElementById("cancelDeleteFolder").addEventListener("click", function () { document.getElementById("deleteFolderModal").style.display = "none"; }); attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder"); document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); fetch("/api/folder/deleteFolder.php", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify({ folder: selectedFolder }) }) .then(response => response.json()) .then(data => { if (data.success) { showToast("Folder deleted successfully!"); window.currentFolder = getParentFolder(selectedFolder); localStorage.setItem("lastOpenedFolder", window.currentFolder); loadFolderList(window.currentFolder); } else { showToast("Error: " + (data.error || "Could not delete folder")); } }) .catch(error => console.error("Error deleting folder:", error)) .finally(() => { document.getElementById("deleteFolderModal").style.display = "none"; }); }); document.getElementById("createFolderBtn").addEventListener("click", function () { document.getElementById("createFolderModal").style.display = "block"; document.getElementById("newFolderName").focus(); }); document.getElementById("cancelCreateFolder").addEventListener("click", function () { document.getElementById("createFolderModal").style.display = "none"; document.getElementById("newFolderName").value = ""; }); attachEnterKeyListener("createFolderModal", "submitCreateFolder"); document.getElementById("submitCreateFolder").addEventListener("click", async () => { const folderInput = document.getElementById("newFolderName").value.trim(); if (!folderInput) return showToast("Please enter a folder name."); const selectedFolder = window.currentFolder || "root"; const parent = selectedFolder === "root" ? "" : selectedFolder; // 1) Guarantee fresh CSRF try { await loadCsrfToken(); } catch { return showToast("Could not refresh CSRF token. Please reload."); } // 2) Call with fetchWithCsrf fetchWithCsrf("/api/folder/createFolder.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folderName: folderInput, parent }) }) .then(async res => { if (!res.ok) { // pull out a JSON error, or fallback to status text let err; try { const j = await res.json(); err = j.error || j.message || res.statusText; } catch { err = res.statusText; } throw new Error(err); } return res.json(); }) .then(data => { showToast("Folder created!"); const full = parent ? `${parent}/${folderInput}` : folderInput; window.currentFolder = full; localStorage.setItem("lastOpenedFolder", full); loadFolderList(full); }) .catch(e => { showToast("Error creating folder: " + e.message); }) .finally(() => { document.getElementById("createFolderModal").style.display = "none"; document.getElementById("newFolderName").value = ""; }); }); // ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ---------- function showFolderManagerContextMenu(x, y, menuItems) { let menu = document.getElementById("folderManagerContextMenu"); if (!menu) { menu = document.createElement("div"); menu.id = "folderManagerContextMenu"; menu.style.position = "absolute"; menu.style.padding = "5px 0"; menu.style.minWidth = "150px"; menu.style.zIndex = "9999"; document.body.appendChild(menu); } if (document.body.classList.contains("dark-mode")) { menu.style.backgroundColor = "#2c2c2c"; menu.style.border = "1px solid #555"; menu.style.color = "#e0e0e0"; } else { menu.style.backgroundColor = "#fff"; menu.style.border = "1px solid #ccc"; menu.style.color = "#000"; } menu.innerHTML = ""; menuItems.forEach(item => { const menuItem = document.createElement("div"); menuItem.textContent = item.label; menuItem.style.padding = "5px 15px"; menuItem.style.cursor = "pointer"; menuItem.addEventListener("mouseover", () => { if (document.body.classList.contains("dark-mode")) { menuItem.style.backgroundColor = "#444"; } else { menuItem.style.backgroundColor = "#f0f0f0"; } }); menuItem.addEventListener("mouseout", () => { menuItem.style.backgroundColor = ""; }); menuItem.addEventListener("click", () => { item.action(); hideFolderManagerContextMenu(); }); menu.appendChild(menuItem); }); menu.style.left = x + "px"; menu.style.top = y + "px"; menu.style.display = "block"; } function hideFolderManagerContextMenu() { const menu = document.getElementById("folderManagerContextMenu"); if (menu) { menu.style.display = "none"; } } function folderManagerContextMenuHandler(e) { e.preventDefault(); e.stopPropagation(); const target = e.target.closest(".folder-option, .breadcrumb-link"); if (!target) return; const folder = target.getAttribute("data-folder"); if (!folder) return; window.currentFolder = folder; document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected")); target.classList.add("selected"); const menuItems = [ { label: t("create_folder"), action: () => { document.getElementById("createFolderModal").style.display = "block"; document.getElementById("newFolderName").focus(); } }, { label: t("rename_folder"), action: () => { openRenameFolderModal(); } }, { label: t("folder_share"), action: () => { openFolderShareModal(); } }, { label: t("delete_folder"), action: () => { openDeleteFolderModal(); } } ]; showFolderManagerContextMenu(e.pageX, e.pageY, menuItems); } function bindFolderManagerContextMenu() { const container = document.getElementById("folderTreeContainer"); if (container) { container.removeEventListener("contextmenu", folderManagerContextMenuHandler); container.addEventListener("contextmenu", folderManagerContextMenuHandler, false); } const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link"); breadcrumbNodes.forEach(node => { node.removeEventListener("contextmenu", folderManagerContextMenuHandler); node.addEventListener("contextmenu", folderManagerContextMenuHandler, false); }); } document.addEventListener("click", function () { hideFolderManagerContextMenu(); }); document.addEventListener("DOMContentLoaded", function () { document.addEventListener("keydown", function (e) { const tag = e.target.tagName.toLowerCase(); if (tag === "input" || tag === "textarea" || e.target.isContentEditable) { return; } if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) { if (window.currentFolder && window.currentFolder !== "root") { e.preventDefault(); openDeleteFolderModal(); } } }); }); document.addEventListener("DOMContentLoaded", function () { const shareFolderBtn = document.getElementById("shareFolderBtn"); if (shareFolderBtn) { shareFolderBtn.addEventListener("click", () => { const selectedFolder = window.currentFolder || "root"; if (!selectedFolder || selectedFolder === "root") { showToast("Please select a valid folder to share."); return; } // Call the folder share modal from the module. openFolderShareModal(selectedFolder); }); } else { console.warn("shareFolderBtn element not found in the DOM."); } }); bindFolderManagerContextMenu();