diff --git a/l4d2web/l4d2web/static/js/files-overlay.js b/l4d2web/l4d2web/static/js/files-overlay.js
index 26ece4e..e3e0032 100644
--- a/l4d2web/l4d2web/static/js/files-overlay.js
+++ b/l4d2web/l4d2web/static/js/files-overlay.js
@@ -29,9 +29,6 @@
const csrfToken =
document.querySelector("meta[name='csrf-token']")?.getAttribute("content") || "";
- const newFolderDialog = document.getElementById("files-new-folder-modal");
- const conflictDialog = document.getElementById("files-conflict-modal");
- const deleteDialog = document.getElementById("files-delete-modal");
const uploadsPanel = document.querySelector(".files-uploads");
const uploadsList = uploadsPanel?.querySelector(".files-uploads-list");
const uploadsClearBtn = uploadsPanel?.querySelector(".files-uploads-clear");
@@ -194,23 +191,11 @@
}
// ---------- conflict modal ----------------------------------------------
-
- function askConflict(path) {
- return new Promise((resolve) => {
- conflictDialog.querySelector(".files-conflict-path").textContent = path;
- const handler = (event) => {
- const action = event.target?.dataset?.filesConflictAction;
- if (!action) return;
- conflictDialog.removeEventListener("click", handler);
- conflictDialog.close();
- resolve(action);
- };
- conflictDialog.addEventListener("click", handler);
- conflictDialog.showModal();
- });
- }
- // Exposed for editor.js (Phase A). Moves to dialogs.js in Step 3.
- if (window.__filesOverlay) window.__filesOverlay.askConflict = askConflict;
+ //
+ // Migrated to static/js/files-overlay/dialogs.js (Phase A, Step 3).
+ // askConflict is now exposed there on window.__filesOverlay. The
+ // legacy upload + drag-drop code below reads it via __filesOverlay
+ // at call time.
// Attach a path-collision suffix: foo.txt → foo (1).txt
function withCollisionSuffix(path) {
@@ -225,31 +210,10 @@
if (window.__filesOverlay) window.__filesOverlay.withCollisionSuffix = withCollisionSuffix;
// ---------- delete modal ------------------------------------------------
-
- function openDelete(targetPath, kind, name) {
- deleteDialog.querySelector(".files-delete-name").textContent = name;
- const errEl = deleteDialog.querySelector(".files-delete-error");
- errEl.hidden = true;
- errEl.textContent = "";
-
- const confirmBtn = deleteDialog.querySelector(".files-delete-confirm");
- const onConfirm = async () => {
- const fd = new FormData();
- fd.append("path", targetPath);
- fd.append("csrf_token", csrfToken);
- const r = await postForm(`${baseUrl}/files/delete`, fd);
- if (r.ok) {
- deleteDialog.close();
- scheduleRefresh(parentOf(targetPath));
- } else {
- errEl.hidden = false;
- errEl.textContent = (r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`;
- }
- };
- confirmBtn.replaceWith(confirmBtn.cloneNode(true));
- deleteDialog.querySelector(".files-delete-confirm").addEventListener("click", onConfirm);
- deleteDialog.showModal();
- }
+ //
+ // Migrated to static/js/files-overlay/dialogs.js (Phase A, Step 3).
+ // Now dispatched via the "delete" action-registry handler that
+ // dialogs.js registers.
// ---------- editor modal ------------------------------------------------
//
@@ -259,53 +223,10 @@
// window.__filesOverlay and de-duplicate in Steps 3 and 4.
// ---------- new-folder modal --------------------------------------------
-
- function openNewFolder(targetFolder) {
- const folder = targetFolder || "";
- newFolderDialog.querySelector(".files-new-folder-target").textContent =
- folder ? folder + "/" : "/";
- const input = newFolderDialog.querySelector(".files-new-folder-name");
- const errEl = newFolderDialog.querySelector(".files-new-folder-error");
- input.value = "";
- errEl.hidden = true;
- errEl.textContent = "";
-
- const createBtn = newFolderDialog.querySelector(".files-new-folder-create");
- const fresh = createBtn.cloneNode(true);
- createBtn.replaceWith(fresh);
- fresh.addEventListener("click", async () => {
- const name = input.value.trim().replace(/^\/+|\/+$/g, "");
- if (!name) return;
- const fullPath = joinPath(folder, name);
- const r = await postJson(`${baseUrl}/files/mkdir`, { path: fullPath });
- if (r.ok) {
- newFolderDialog.close();
- scheduleRefresh(folder);
- } else {
- errEl.hidden = false;
- errEl.textContent =
- (r.body && r.body.error) ||
- (r.status === 409
- ? "A file or folder with that name already exists."
- : `Failed (HTTP ${r.status}).`);
- }
- });
-
- newFolderDialog.showModal();
- setTimeout(() => input.focus(), 0);
- }
-
- // Enter on the new-folder input submits — bound once at module init so
- // it survives multiple openings of the dialog. (A previous version used
- // `{once: true}` inside openNewFolder, which was consumed by the first
- // letter the user typed and never saw Enter.)
- newFolderDialog
- .querySelector(".files-new-folder-name")
- .addEventListener("keydown", (event) => {
- if (event.key !== "Enter") return;
- event.preventDefault();
- newFolderDialog.querySelector(".files-new-folder-create")?.click();
- });
+ //
+ // Migrated to static/js/files-overlay/dialogs.js (Phase A, Step 3).
+ // Now dispatched via the "new-folder" action-registry handler that
+ // dialogs.js registers.
// ---------- upload queue + progress panel -------------------------------
@@ -449,7 +370,9 @@
} else if (xhr.status === 409 && !overwriteFlag) {
setRowState(item, "conflict");
const collidingPath = joinPath(item.targetFolder || "", item.relative);
- askConflict(collidingPath).then((action) => {
+ // askConflict moved to dialogs.js in Step 3 — reach it via the
+ // shared registry rather than a closure-scope reference.
+ window.__filesOverlay.askConflict(collidingPath).then((action) => {
if (action === "overwrite") {
runUpload(item, true);
} else if (action === "keep-both") {
@@ -566,7 +489,8 @@
scheduleRefresh(parentOf(src));
if (parentOf(src) !== dropFolder) scheduleRefresh(dropFolder);
} else if (r.status === 409) {
- const action = await askConflict(dst);
+ // askConflict moved to dialogs.js in Step 3 — see Edit above.
+ const action = await window.__filesOverlay.askConflict(dst);
if (action === "overwrite") {
const r2 = await postJson(`${baseUrl}/files/move`, { src, dst, overwrite: true });
if (r2.ok) {
@@ -654,16 +578,12 @@
if (!manager.contains(action)) return;
const op = action.dataset.action;
const path = action.dataset.targetPath || "";
- // new-file + edit: dispatched via __filesOverlay registry (editor.js).
- if (op === "new-folder") {
- openNewFolder(path);
- } else if (op === "zip") {
+ // new-file + edit dispatched via registry (editor.js); new-folder +
+ // delete dispatched via registry (dialogs.js). Only "zip" remains
+ // here — it's pure URL navigation with no module dependency.
+ if (op === "zip") {
const url = `${baseUrl}/files/download_zip?path=${encodeURIComponent(path)}`;
window.location.href = url;
- } else if (op === "delete") {
- const kind = action.dataset.rowKind;
- const name = action.dataset.rowName || basename(path);
- openDelete(path, kind, name);
}
});
})();
diff --git a/l4d2web/l4d2web/static/js/files-overlay/dialogs.js b/l4d2web/l4d2web/static/js/files-overlay/dialogs.js
new file mode 100644
index 0000000..1f2673e
--- /dev/null
+++ b/l4d2web/l4d2web/static/js/files-overlay/dialogs.js
@@ -0,0 +1,212 @@
+// files-overlay/dialogs.js — Phase A, Step 3.
+//
+// Owns the three inline dialogs that surround the file manager:
+// * #files-new-folder-modal — "+ folder" / mkdir prompt
+// * #files-delete-modal — delete-confirm for files and folders
+// * #files-conflict-modal — overwrite / keep-both / cancel choice
+// shown when save / upload / move hits 409
+//
+// Dispatch:
+// * "new-folder" registered into __filesOverlay → openNewFolder(path)
+// * "delete" registered into __filesOverlay → openDelete(path,
+// kind, name) — kind + name read from actionEl
+// dataset.
+//
+// askConflict(path) → Promise<"overwrite" | "keep-both" | "cancel">
+// is exposed on __filesOverlay so legacy upload / drag-drop callers in
+// files-overlay.js (lines runUpload at 452 and the drop handler at
+// 569) can still reach it after the function moved out of their file.
+// editor.js's save 409-conflict path also goes through this same
+// exposure.
+//
+// All three dialogs use a single delegated click listener per dialog,
+// reading per-dialog state from module-scope. The clone-and-rebind
+// pattern from the legacy file (replaceWith(cloneNode(true)) to drop
+// stale listeners) is gone — the listener is attached once and reads
+// freshly-set state each time the dialog opens.
+//
+// Dialogs open through window.modals.openInline / closeInline (the
+// consolidated modal API at static/js/modals.js) rather than calling
+// dialog.showModal() / close() directly, completing the inline-modal
+// convention from commit c51089d.
+
+(function () {
+ "use strict";
+
+ const fo = window.__filesOverlay;
+ if (!fo) return;
+
+ const { baseUrl, csrfToken } = fo;
+ const { joinPath, parentOf, basename, postJson, postForm, scheduleRefresh } = fo.helpers;
+
+ const newFolderDialog = document.getElementById("files-new-folder-modal");
+ const deleteDialog = document.getElementById("files-delete-modal");
+ const conflictDialog = document.getElementById("files-conflict-modal");
+
+ function openModal(dialog) {
+ if (window.modals && typeof window.modals.openInline === "function") {
+ window.modals.openInline(dialog);
+ } else {
+ dialog.showModal();
+ }
+ }
+ function closeModal(dialog) {
+ if (window.modals && typeof window.modals.closeInline === "function") {
+ window.modals.closeInline(dialog);
+ } else {
+ dialog.close();
+ }
+ }
+
+ // ---------- conflict modal ----------------------------------------------
+
+ // While a conflict dialog is open, conflictState.resolve is the
+ // pending Promise resolver. The delegated click handler calls it and
+ // nulls it so subsequent clicks don't double-resolve.
+ let conflictState = null;
+
+ function askConflict(path) {
+ return new Promise((resolve) => {
+ if (!conflictDialog) {
+ resolve("cancel");
+ return;
+ }
+ conflictDialog.querySelector(".files-conflict-path").textContent = path;
+ conflictState = { resolve };
+ openModal(conflictDialog);
+ });
+ }
+ fo.askConflict = askConflict;
+
+ if (conflictDialog) {
+ conflictDialog.addEventListener("click", (event) => {
+ const action = event.target?.dataset?.filesConflictAction;
+ if (!action) return;
+ const state = conflictState;
+ conflictState = null;
+ closeModal(conflictDialog);
+ if (state && typeof state.resolve === "function") {
+ state.resolve(action);
+ }
+ });
+ // If the user dismisses the dialog any other way (Esc, backdrop,
+ // programmatic close), resolve as "cancel" so callers don't hang.
+ conflictDialog.addEventListener("close", () => {
+ const state = conflictState;
+ conflictState = null;
+ if (state && typeof state.resolve === "function") {
+ state.resolve("cancel");
+ }
+ });
+ }
+
+ // ---------- delete-confirm modal ----------------------------------------
+
+ let deleteState = null;
+
+ function openDelete(targetPath, kind, name) {
+ if (!deleteDialog) return;
+ deleteDialog.querySelector(".files-delete-name").textContent = name;
+ const errEl = deleteDialog.querySelector(".files-delete-error");
+ errEl.hidden = true;
+ errEl.textContent = "";
+ deleteState = { path: targetPath, kind, errEl };
+ openModal(deleteDialog);
+ }
+
+ if (deleteDialog) {
+ deleteDialog.addEventListener("click", async (event) => {
+ const btn = event.target?.closest?.(".files-delete-confirm");
+ if (!btn) return;
+ if (!deleteState) return;
+ const { path, errEl } = deleteState;
+ const fd = new FormData();
+ fd.append("path", path);
+ fd.append("csrf_token", csrfToken);
+ const r = await postForm(`${baseUrl}/files/delete`, fd);
+ if (r.ok) {
+ deleteState = null;
+ closeModal(deleteDialog);
+ scheduleRefresh(parentOf(path));
+ } else {
+ errEl.hidden = false;
+ errEl.textContent =
+ (r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`;
+ }
+ });
+ deleteDialog.addEventListener("close", () => {
+ deleteState = null;
+ });
+ }
+
+ // ---------- new-folder modal --------------------------------------------
+
+ let newFolderState = null;
+
+ function openNewFolder(targetFolder) {
+ if (!newFolderDialog) return;
+ const folder = targetFolder || "";
+ newFolderDialog.querySelector(".files-new-folder-target").textContent =
+ folder ? folder + "/" : "/";
+ const input = newFolderDialog.querySelector(".files-new-folder-name");
+ const errEl = newFolderDialog.querySelector(".files-new-folder-error");
+ input.value = "";
+ errEl.hidden = true;
+ errEl.textContent = "";
+ newFolderState = { folder, input, errEl };
+ openModal(newFolderDialog);
+ setTimeout(() => input.focus(), 0);
+ }
+
+ async function newFolderSubmit() {
+ if (!newFolderState) return;
+ const { folder, input, errEl } = newFolderState;
+ const name = input.value.trim().replace(/^\/+|\/+$/g, "");
+ if (!name) return;
+ const fullPath = joinPath(folder, name);
+ const r = await postJson(`${baseUrl}/files/mkdir`, { path: fullPath });
+ if (r.ok) {
+ newFolderState = null;
+ closeModal(newFolderDialog);
+ scheduleRefresh(folder);
+ } else {
+ errEl.hidden = false;
+ errEl.textContent =
+ (r.body && r.body.error) ||
+ (r.status === 409
+ ? "A file or folder with that name already exists."
+ : `Failed (HTTP ${r.status}).`);
+ }
+ }
+
+ if (newFolderDialog) {
+ newFolderDialog.addEventListener("click", (event) => {
+ if (event.target?.closest?.(".files-new-folder-create")) {
+ newFolderSubmit();
+ }
+ });
+ // Enter inside the name input submits. Direct-bound to the
+ // persistent input — high-frequency keydown, no benefit from
+ // delegation.
+ newFolderDialog
+ .querySelector(".files-new-folder-name")
+ .addEventListener("keydown", (event) => {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ newFolderSubmit();
+ });
+ newFolderDialog.addEventListener("close", () => {
+ newFolderState = null;
+ });
+ }
+
+ // ---------- register action-registry handlers ----------
+
+ fo.registerHandler("new-folder", (path) => openNewFolder(path));
+
+ fo.registerHandler("delete", (path, actionEl) => {
+ const kind = actionEl?.dataset?.rowKind;
+ const name = actionEl?.dataset?.rowName || basename(path);
+ openDelete(path, kind, name);
+ });
+})();
diff --git a/l4d2web/l4d2web/templates/overlay_detail.html b/l4d2web/l4d2web/templates/overlay_detail.html
index 5450102..fde0996 100644
--- a/l4d2web/l4d2web/templates/overlay_detail.html
+++ b/l4d2web/l4d2web/templates/overlay_detail.html
@@ -284,6 +284,7 @@
+
{% endif %}
{% endblock %}