Folder Breadcrumb Rendering with Drag-and-Drop support
This commit is contained in:
148
folderManager.js
148
folderManager.js
@@ -60,7 +60,111 @@ function getParentFolder(folder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
// DOM Building Functions
|
// Breadcrumb Functions
|
||||||
|
// ----------------------
|
||||||
|
// Render breadcrumb for a normalized folder path.
|
||||||
|
// For example, if window.currentFolder is "Folder1/Folder1SubFolder2",
|
||||||
|
// this will return: Root / Folder1 / Folder1SubFolder2.
|
||||||
|
function renderBreadcrumb(normalizedFolder) {
|
||||||
|
if (normalizedFolder === "root") {
|
||||||
|
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||||
|
}
|
||||||
|
const parts = normalizedFolder.split("/");
|
||||||
|
let breadcrumbItems = [];
|
||||||
|
// Always start with "Root".
|
||||||
|
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="root">Root</span>`);
|
||||||
|
let cumulative = "";
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
cumulative = index === 0 ? part : cumulative + "/" + part;
|
||||||
|
breadcrumbItems.push(`<span class="breadcrumb-separator"> / </span>`);
|
||||||
|
breadcrumbItems.push(`<span class="breadcrumb-link" data-folder="${cumulative}">${escapeHTML(part)}</span>`);
|
||||||
|
});
|
||||||
|
return breadcrumbItems.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind click and drag-and-drop events to breadcrumb links.
|
||||||
|
function bindBreadcrumbEvents() {
|
||||||
|
const breadcrumbLinks = document.querySelectorAll(".breadcrumb-link");
|
||||||
|
breadcrumbLinks.forEach(link => {
|
||||||
|
// Click event for navigation.
|
||||||
|
link.addEventListener("click", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
let folder = this.getAttribute("data-folder");
|
||||||
|
window.currentFolder = folder;
|
||||||
|
localStorage.setItem("lastOpenedFolder", folder);
|
||||||
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
|
if (folder === "root") {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||||
|
} else {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(folder) + ")";
|
||||||
|
}
|
||||||
|
// Expand the folder tree to ensure the target is visible.
|
||||||
|
expandTreePath(folder);
|
||||||
|
// Update folder tree selection.
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
// Load the file list.
|
||||||
|
loadFileList(folder);
|
||||||
|
// Re-bind breadcrumb events to ensure all links remain active.
|
||||||
|
bindBreadcrumbEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag-and-drop events.
|
||||||
|
link.addEventListener("dragover", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.classList.add("drop-hover");
|
||||||
|
});
|
||||||
|
link.addEventListener("dragleave", function(e) {
|
||||||
|
this.classList.remove("drop-hover");
|
||||||
|
});
|
||||||
|
link.addEventListener("drop", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.classList.remove("drop-hover");
|
||||||
|
const dropFolder = this.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("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.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// DOM Building Functions for Folder Tree
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
// Recursively builds HTML for the folder tree as nested <ul> elements.
|
// Recursively builds HTML for the folder tree as nested <ul> elements.
|
||||||
@@ -72,7 +176,6 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
|||||||
if (folder.toLowerCase() === "trash") {
|
if (folder.toLowerCase() === "trash") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
||||||
const hasChildren = Object.keys(tree[folder]).length > 0;
|
const hasChildren = Object.keys(tree[folder]).length > 0;
|
||||||
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
||||||
@@ -83,7 +186,6 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
|||||||
} else {
|
} else {
|
||||||
html += `<span class="folder-indent-placeholder"></span>`;
|
html += `<span class="folder-indent-placeholder"></span>`;
|
||||||
}
|
}
|
||||||
// Use escapeHTML to safely render the folder name.
|
|
||||||
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
|
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
html += renderFolderTree(tree[folder], fullPath, displayState);
|
html += renderFolderTree(tree[folder], fullPath, displayState);
|
||||||
@@ -94,7 +196,7 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expands the folder tree along a given path.
|
// Expands the folder tree along a given normalized path.
|
||||||
function expandTreePath(path) {
|
function expandTreePath(path) {
|
||||||
const parts = path.split("/");
|
const parts = path.split("/");
|
||||||
let cumulative = "";
|
let cumulative = "";
|
||||||
@@ -122,19 +224,15 @@ function expandTreePath(path) {
|
|||||||
// ----------------------
|
// ----------------------
|
||||||
// Drag & Drop Support for Folder Tree Nodes
|
// Drag & Drop Support for Folder Tree Nodes
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
// When a draggable file is dragged over a folder node, allow the drop and add a visual cue.
|
|
||||||
function folderDragOverHandler(event) {
|
function folderDragOverHandler(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.currentTarget.classList.add("drop-hover");
|
event.currentTarget.classList.add("drop-hover");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the visual cue when the drag leaves.
|
|
||||||
function folderDragLeaveHandler(event) {
|
function folderDragLeaveHandler(event) {
|
||||||
event.currentTarget.classList.remove("drop-hover");
|
event.currentTarget.classList.remove("drop-hover");
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a file is dropped onto a folder node, send a move request.
|
|
||||||
function folderDropHandler(event) {
|
function folderDropHandler(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.currentTarget.classList.remove("drop-hover");
|
event.currentTarget.classList.remove("drop-hover");
|
||||||
@@ -143,10 +241,9 @@ function folderDropHandler(event) {
|
|||||||
try {
|
try {
|
||||||
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
|
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invalid drag data");
|
console.error("Invalid drag data", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use the files array if present, or fall back to a single file.
|
|
||||||
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
|
||||||
if (filesToMove.length === 0) return;
|
if (filesToMove.length === 0) return;
|
||||||
fetch("moveFiles.php", {
|
fetch("moveFiles.php", {
|
||||||
@@ -232,15 +329,25 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
el.addEventListener("drop", folderDropHandler);
|
el.addEventListener("drop", folderDropHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine current folder.
|
// Determine current folder (normalized).
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
window.currentFolder = selectedFolder;
|
window.currentFolder = selectedFolder;
|
||||||
} else {
|
} else {
|
||||||
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
window.currentFolder = localStorage.getItem("lastOpenedFolder") || "root";
|
||||||
}
|
}
|
||||||
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
localStorage.setItem("lastOpenedFolder", window.currentFolder);
|
||||||
document.getElementById("fileListTitle").textContent =
|
|
||||||
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
|
// Update file list title using breadcrumb.
|
||||||
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
|
if (window.currentFolder === "root") {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||||
|
} else {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(window.currentFolder) + ")";
|
||||||
|
}
|
||||||
|
// Bind breadcrumb events (click and drag/drop).
|
||||||
|
bindBreadcrumbEvents();
|
||||||
|
|
||||||
|
// Load file list.
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
|
|
||||||
// Expand tree to current folder.
|
// Expand tree to current folder.
|
||||||
@@ -249,13 +356,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
expandTreePath(window.currentFolder);
|
expandTreePath(window.currentFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight current folder.
|
// Highlight current folder in folder tree.
|
||||||
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
const selectedEl = container.querySelector(`.folder-option[data-folder="${window.currentFolder}"]`);
|
||||||
if (selectedEl) {
|
if (selectedEl) {
|
||||||
|
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
|
||||||
selectedEl.classList.add("selected");
|
selectedEl.classList.add("selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event binding for folder selection.
|
// Event binding for folder selection in folder tree.
|
||||||
container.querySelectorAll(".folder-option").forEach(el => {
|
container.querySelectorAll(".folder-option").forEach(el => {
|
||||||
el.addEventListener("click", function(e) {
|
el.addEventListener("click", function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -264,8 +372,14 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
const selected = this.getAttribute("data-folder");
|
const selected = this.getAttribute("data-folder");
|
||||||
window.currentFolder = selected;
|
window.currentFolder = selected;
|
||||||
localStorage.setItem("lastOpenedFolder", selected);
|
localStorage.setItem("lastOpenedFolder", selected);
|
||||||
document.getElementById("fileListTitle").textContent =
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
selected === "root" ? "Files in (Root)" : "Files in (" + selected + ")";
|
if (selected === "root") {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb("root") + ")";
|
||||||
|
} else {
|
||||||
|
titleEl.innerHTML = "Files in (" + renderBreadcrumb(selected) + ")";
|
||||||
|
}
|
||||||
|
// Re-bind breadcrumb events so the new breadcrumb is clickable.
|
||||||
|
bindBreadcrumbEvents();
|
||||||
loadFileList(selected);
|
loadFileList(selected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
18
styles.css
18
styles.css
@@ -1245,6 +1245,24 @@ body.dark-mode #fileListContainer {
|
|||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
FOLDER TREE STYLES
|
FOLDER TREE STYLES
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
/* Make breadcrumb links look clickable */
|
||||||
|
.breadcrumb-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link.selected {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.folder-tree {
|
.folder-tree {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user