feat(files): migrate editor handlers to files-overlay/editor.js
Step 2/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
editor.js is dual-purpose during Phase A: drives both the legacy
inline #files-editor-modal <dialog> (binary-replace + create-new flows)
and the URL-addressable modal swapped into #modal-content (editable
text files). Phase B migrates the legacy flows to URL-addressable too
and removes the legacy branches.
What moved:
* Editor state object, editorEls DOM refs, CM6 bridge (getEditorValue,
setEditorValue), UI helpers (setEditorTitle, updateByteCount,
updateRenameHint, updateSaveEnabled, setQueuedReplacement)
* openEditorTextNew (create-new file flow)
* openEditorForFile (legacy binary + editable-as-fallback flow)
* All save/delete/replace handlers — converted from direct-bound on
editorEls.{saveBtn,deleteBtn} to a single document-level click
listener that discriminates by ancestor (legacy editorDialog vs.
URL-addressable #modal-content)
* Replace-zone dragover/dragleave/drop — direct-bound on
editorEls.replaceZone → document-level delegation gated on the zone
being inside the legacy dialog
* Replace-input change, replace-clear / replace-browse clicks — also
delegated
* The previously-separate URL-addressable save/delete delegation
block (lines 593-664 of the legacy file) collapses into the same
delegated listeners
What stays direct-bound (per plan escape hatch):
* input on .files-editor-filename
* input + keydown on .files-editor-content (Ctrl+S handling)
* close on the persistent legacy <dialog>
These are high-frequency events on persistent inputs inside the
persistent legacy dialog; delegation would add per-keystroke
selector-matching overhead with no benefit.
Action dispatch: editor.js registers "new-file" and "edit" handlers
into __filesOverlay (set up by core.js). The legacy switch-case in
files-overlay.js's click delegation loses both cases — they're now
dispatched via the registry. The legacy switch still owns new-folder,
zip, and delete (those migrate in Step 3).
Cross-module exposure: askConflict and withCollisionSuffix stay in
files-overlay.js (the upload queue and drag-drop code at lines 857
and 974 still use them) and are exposed on __filesOverlay so editor.js
can call them. They migrate to dialogs.js (askConflict, Step 3) and
uploads.js (withCollisionSuffix, Step 4); the call sites in editor.js
don't change.
Numbers:
files-overlay.js: 1091 → 669 lines (-422)
files-overlay/editor.js: 550 lines (new)
Net: +128 lines; the growth is from the dual-editor delegation
scaffolding (separate handler functions for legacy vs. routed) and
module-header comments. The legacy file is now a stub editor section
comment plus the unmigrated dialogs/uploads/drag-drop blocks.
Verified live on /overlays/2 in Chromium:
* 3 script tags load in document order (core → editor → legacy)
* window.__filesOverlay registry now has 10 keys (added askConflict +
withCollisionSuffix); withCollisionSuffix('foo.txt') = 'foo (1).txt'
* No console errors on page load or after synthetic actions
* E2E dispatch check: clicking a "+ new file" action button opens the
legacy dialog with empty filename + Create save-button label
(proves core → handleAction → editor.js handler → openEditorTextNew
chain works)
* E2E dispatch check: clicking the filename button on an editable
file sets ?modal=%2Foverlays%2F2%2Ffiles%2Fedit%3Fpath%3D... in the
URL (proves editor.js's "edit" handler correctly routes editable
files through window.modals.openRouted)
* pytest still 573 passed, 1 skipped, 3 deselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
052ddcb4f0
commit
f094eca074
3 changed files with 562 additions and 433 deletions
|
|
@ -29,7 +29,6 @@
|
|||
const csrfToken =
|
||||
document.querySelector("meta[name='csrf-token']")?.getAttribute("content") || "";
|
||||
|
||||
const editorDialog = document.getElementById("files-editor-modal");
|
||||
const newFolderDialog = document.getElementById("files-new-folder-modal");
|
||||
const conflictDialog = document.getElementById("files-conflict-modal");
|
||||
const deleteDialog = document.getElementById("files-delete-modal");
|
||||
|
|
@ -210,6 +209,8 @@
|
|||
conflictDialog.showModal();
|
||||
});
|
||||
}
|
||||
// Exposed for editor.js (Phase A). Moves to dialogs.js in Step 3.
|
||||
if (window.__filesOverlay) window.__filesOverlay.askConflict = askConflict;
|
||||
|
||||
// Attach a path-collision suffix: foo.txt → foo (1).txt
|
||||
function withCollisionSuffix(path) {
|
||||
|
|
@ -220,6 +221,8 @@
|
|||
}
|
||||
return path + " (1)";
|
||||
}
|
||||
// Exposed for editor.js (Phase A). Moves to uploads.js in Step 4.
|
||||
if (window.__filesOverlay) window.__filesOverlay.withCollisionSuffix = withCollisionSuffix;
|
||||
|
||||
// ---------- delete modal ------------------------------------------------
|
||||
|
||||
|
|
@ -249,419 +252,11 @@
|
|||
}
|
||||
|
||||
// ---------- editor modal ------------------------------------------------
|
||||
|
||||
// Editor state. Only one editor is open at a time.
|
||||
const editor = {
|
||||
mode: null, // "text" | "binary"
|
||||
creating: false,
|
||||
originalPath: null,
|
||||
folder: null,
|
||||
queuedReplacement: null, // File object
|
||||
};
|
||||
|
||||
const editorEls = {
|
||||
title: editorDialog.querySelector(".files-editor-title-text"),
|
||||
filename: editorDialog.querySelector(".files-editor-filename"),
|
||||
renameHint: editorDialog.querySelector(".files-editor-rename-hint"),
|
||||
renameFrom: editorDialog.querySelector(".files-rename-from"),
|
||||
renameTo: editorDialog.querySelector(".files-rename-to"),
|
||||
textPanel: editorDialog.querySelector(".files-editor-text"),
|
||||
contentBox: editorDialog.querySelector(".files-editor-content"),
|
||||
byteCount: editorDialog.querySelector(".files-editor-byte-count"),
|
||||
binaryPanel: editorDialog.querySelector(".files-editor-binary"),
|
||||
binarySize: editorDialog.querySelector(".files-editor-binary-size"),
|
||||
replaceZone: editorDialog.querySelector(".files-editor-replace-zone"),
|
||||
replaceIdle: editorDialog.querySelector(".files-editor-replace-idle"),
|
||||
replaceQueued: editorDialog.querySelector(".files-editor-replace-queued"),
|
||||
replaceName: editorDialog.querySelector(".files-editor-replace-name"),
|
||||
replaceSize: editorDialog.querySelector(".files-editor-replace-size"),
|
||||
replaceClear: editorDialog.querySelector(".files-editor-replace-clear"),
|
||||
replaceBrowse: editorDialog.querySelector(".files-editor-replace-browse"),
|
||||
replaceInput: editorDialog.querySelector(".files-editor-replace-input"),
|
||||
deleteBtn: editorDialog.querySelector(".files-editor-delete"),
|
||||
downloadBtn: editorDialog.querySelector(".files-editor-download"),
|
||||
saveBtn: editorDialog.querySelector(".files-editor-save"),
|
||||
};
|
||||
|
||||
// Bridge to the CodeMirror 6 controller, set up by static/js/editor.js
|
||||
// on the .files-editor-content textarea. Falls back to the textarea
|
||||
// directly if the bundle didn't load (no-JS fallback / file open
|
||||
// before the controller has been mounted).
|
||||
function getEditorValue() {
|
||||
return (window.__filesEditor && window.__filesEditor.getValue)
|
||||
? window.__filesEditor.getValue()
|
||||
: editorEls.contentBox.value;
|
||||
}
|
||||
function setEditorValue(text) {
|
||||
if (window.__filesEditor && window.__filesEditor.setContent) {
|
||||
window.__filesEditor.setContent(text);
|
||||
} else {
|
||||
editorEls.contentBox.value = text;
|
||||
}
|
||||
}
|
||||
|
||||
function setEditorTitle(text) {
|
||||
editorEls.title.textContent = text;
|
||||
}
|
||||
|
||||
function updateByteCount() {
|
||||
const bytes = new TextEncoder().encode(getEditorValue()).length;
|
||||
editorEls.byteCount.textContent = `UTF-8 · ${bytes} bytes`;
|
||||
}
|
||||
|
||||
function updateRenameHint() {
|
||||
const current = editorEls.filename.value.trim();
|
||||
const original = basename(editor.originalPath || "");
|
||||
if (editor.creating || !current || current === original) {
|
||||
editorEls.renameHint.hidden = true;
|
||||
return;
|
||||
}
|
||||
editorEls.renameFrom.textContent = original;
|
||||
editorEls.renameTo.textContent = current;
|
||||
editorEls.renameHint.hidden = false;
|
||||
}
|
||||
|
||||
function updateSaveEnabled() {
|
||||
if (editor.mode === "binary" && !editor.creating) {
|
||||
const filenameChanged =
|
||||
editorEls.filename.value.trim() !== basename(editor.originalPath || "");
|
||||
const hasReplacement = !!editor.queuedReplacement;
|
||||
editorEls.saveBtn.disabled = !filenameChanged && !hasReplacement;
|
||||
editorEls.saveBtn.textContent = "Save";
|
||||
} else if (editor.creating) {
|
||||
editorEls.saveBtn.disabled = !editorEls.filename.value.trim();
|
||||
editorEls.saveBtn.textContent = "Create";
|
||||
} else {
|
||||
editorEls.saveBtn.disabled = false;
|
||||
editorEls.saveBtn.textContent = "Save";
|
||||
}
|
||||
}
|
||||
|
||||
function setQueuedReplacement(file) {
|
||||
editor.queuedReplacement = file;
|
||||
if (file) {
|
||||
editorEls.replaceIdle.hidden = true;
|
||||
editorEls.replaceQueued.hidden = false;
|
||||
editorEls.replaceName.textContent = file.name;
|
||||
editorEls.replaceSize.textContent = humanSize(file.size);
|
||||
} else {
|
||||
editorEls.replaceIdle.hidden = false;
|
||||
editorEls.replaceQueued.hidden = true;
|
||||
}
|
||||
updateSaveEnabled();
|
||||
}
|
||||
|
||||
function openEditorTextNew(folder) {
|
||||
editor.mode = "text";
|
||||
editor.creating = true;
|
||||
editor.originalPath = null;
|
||||
editor.folder = folder;
|
||||
editor.queuedReplacement = null;
|
||||
|
||||
setEditorTitle(`${folder ? folder + "/" : ""}…new file`);
|
||||
editorEls.filename.value = "";
|
||||
editorEls.filename.disabled = false;
|
||||
setEditorValue("");
|
||||
editorEls.contentBox.disabled = false;
|
||||
editorEls.renameHint.hidden = true;
|
||||
editorEls.textPanel.hidden = false;
|
||||
editorEls.binaryPanel.hidden = true;
|
||||
editorEls.deleteBtn.hidden = true;
|
||||
editorEls.downloadBtn.hidden = true;
|
||||
editorEls.saveBtn.textContent = "Create";
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.filename.focus(), 0);
|
||||
}
|
||||
|
||||
async function openEditorForFile(path, isEditable) {
|
||||
editor.creating = false;
|
||||
editor.originalPath = path;
|
||||
editor.folder = parentOf(path);
|
||||
editor.queuedReplacement = null;
|
||||
setQueuedReplacement(null);
|
||||
|
||||
editorEls.filename.value = basename(path);
|
||||
editorEls.filename.disabled = false;
|
||||
editorEls.renameHint.hidden = true;
|
||||
editorEls.deleteBtn.hidden = false;
|
||||
editorEls.downloadBtn.hidden = false;
|
||||
editorEls.downloadBtn.href = `${baseUrl}/files/download?path=${encodeURIComponent(path)}`;
|
||||
setEditorTitle(path);
|
||||
|
||||
if (isEditable) {
|
||||
editor.mode = "text";
|
||||
editorEls.textPanel.hidden = false;
|
||||
editorEls.binaryPanel.hidden = true;
|
||||
setEditorValue("Loading…");
|
||||
editorEls.contentBox.disabled = true;
|
||||
|
||||
const r = await fetchJson(
|
||||
`${baseUrl}/files/content?path=${encodeURIComponent(path)}`
|
||||
);
|
||||
if (r.ok && r.body) {
|
||||
setEditorValue(r.body.content);
|
||||
editorEls.contentBox.disabled = false;
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.contentBox.focus(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: server says not editable. Re-open as binary.
|
||||
editorEls.contentBox.disabled = false;
|
||||
editor.mode = "binary";
|
||||
} else {
|
||||
editor.mode = "binary";
|
||||
}
|
||||
|
||||
// Binary mode setup.
|
||||
editorEls.textPanel.hidden = true;
|
||||
editorEls.binaryPanel.hidden = false;
|
||||
editorEls.binarySize.textContent = "—"; // server gave us no size; cosmetic
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.filename.focus(), 0);
|
||||
}
|
||||
|
||||
editorEls.filename.addEventListener("input", () => {
|
||||
updateRenameHint();
|
||||
updateSaveEnabled();
|
||||
});
|
||||
editorEls.contentBox.addEventListener("input", () => {
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
});
|
||||
editorEls.contentBox.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
|
||||
event.preventDefault();
|
||||
editorEls.saveBtn.click();
|
||||
}
|
||||
});
|
||||
editorEls.replaceClear.addEventListener("click", () => setQueuedReplacement(null));
|
||||
editorEls.replaceBrowse.addEventListener("click", () => editorEls.replaceInput.click());
|
||||
editorEls.replaceInput.addEventListener("change", () => {
|
||||
const f = editorEls.replaceInput.files && editorEls.replaceInput.files[0];
|
||||
if (f) setQueuedReplacement(f);
|
||||
});
|
||||
editorEls.replaceZone.addEventListener("dragover", (event) => {
|
||||
if (Array.from(event.dataTransfer.types).includes("Files")) {
|
||||
event.preventDefault();
|
||||
editorEls.replaceZone.classList.add("is-drop-target");
|
||||
}
|
||||
});
|
||||
editorEls.replaceZone.addEventListener("dragleave", () => {
|
||||
editorEls.replaceZone.classList.remove("is-drop-target");
|
||||
});
|
||||
editorEls.replaceZone.addEventListener("drop", (event) => {
|
||||
if (!Array.from(event.dataTransfer.types).includes("Files")) return;
|
||||
event.preventDefault();
|
||||
editorEls.replaceZone.classList.remove("is-drop-target");
|
||||
const f = event.dataTransfer.files && event.dataTransfer.files[0];
|
||||
if (f) setQueuedReplacement(f);
|
||||
});
|
||||
editorDialog.addEventListener("close", () => {
|
||||
setQueuedReplacement(null);
|
||||
});
|
||||
|
||||
editorEls.saveBtn.addEventListener("click", async () => {
|
||||
const filename = editorEls.filename.value.trim();
|
||||
if (!filename) return;
|
||||
if (filename.includes("/")) {
|
||||
alert("Filename can't contain '/'. Use drag-to-move to relocate.");
|
||||
return;
|
||||
}
|
||||
const folder = editor.folder || "";
|
||||
const newRel = joinPath(folder, filename);
|
||||
|
||||
if (editor.creating) {
|
||||
// Text-flavor create → /save with no new_path.
|
||||
const r = await postJson(`${baseUrl}/files/save`, {
|
||||
path: newRel,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else if (r.status === 409) {
|
||||
const action = await askConflict(newRel);
|
||||
if (action === "overwrite") {
|
||||
// Re-call /save (no overwrite flag — /save just writes); skip
|
||||
// the conflict by writing in-place which is what users want.
|
||||
// First delete the colliding entry to avoid the implicit
|
||||
// "destination is not a file" branch when it's a directory.
|
||||
// For files, a plain /save overwrite is fine.
|
||||
const r2 = await postJson(`${baseUrl}/files/save`, {
|
||||
path: newRel,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r2.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r2.body && r2.body.error) ||
|
||||
`Save failed (HTTP ${r2.status}).`
|
||||
);
|
||||
}
|
||||
} else if (action === "keep-both") {
|
||||
const altered = withCollisionSuffix(newRel);
|
||||
const r2 = await postJson(`${baseUrl}/files/save`, {
|
||||
path: altered,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r2.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Save failed (HTTP ${r.status}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const renaming = newRel !== editor.originalPath;
|
||||
if (editor.mode === "text") {
|
||||
const payload = {
|
||||
path: editor.originalPath,
|
||||
content: getEditorValue(),
|
||||
};
|
||||
if (renaming) payload.new_path = newRel;
|
||||
const r = await postJson(`${baseUrl}/files/save`, payload);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Save failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Binary mode.
|
||||
const fd = new FormData();
|
||||
fd.append("path", editor.originalPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
if (renaming) fd.append("new_path", newRel);
|
||||
if (editor.queuedReplacement) {
|
||||
fd.append("file", editor.queuedReplacement);
|
||||
const r = await postForm(`${baseUrl}/files/replace`, fd);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Replace failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
} else if (renaming) {
|
||||
// Rename only via /move (no content change).
|
||||
const r = await postJson(`${baseUrl}/files/move`, {
|
||||
src: editor.originalPath,
|
||||
dst: newRel,
|
||||
});
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Rename failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editorEls.deleteBtn.addEventListener("click", async () => {
|
||||
if (!editor.originalPath) return;
|
||||
if (!confirm(`Delete ${editor.originalPath}?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append("path", editor.originalPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
const r = await postForm(`${baseUrl}/files/delete`, fd);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(parentOf(editor.originalPath));
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- modal-content save / delete (URL-addressable modal) ----------
|
||||
// The new server-rendered editor (loaded into #modal-content) has its own
|
||||
// .files-editor-save and .files-editor-delete buttons. Those are not the
|
||||
// same elements as editorEls.saveBtn / editorEls.deleteBtn (which live in
|
||||
// the old inline dialog still present for create-new-file / binary flows).
|
||||
// Use event delegation so the handlers fire on dynamically swapped content.
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const modalContent = document.getElementById("modal-content");
|
||||
if (!modalContent) return;
|
||||
|
||||
const saveBtn = event.target.closest(".files-editor-save");
|
||||
if (saveBtn && modalContent.contains(saveBtn)) {
|
||||
const ta = modalContent.querySelector("textarea[data-rel-path]");
|
||||
if (!ta) return;
|
||||
const relPath = ta.dataset.relPath;
|
||||
if (!relPath) return;
|
||||
const content = (window.__filesEditor && window.__filesEditor.getValue)
|
||||
? window.__filesEditor.getValue()
|
||||
: ta.value;
|
||||
|
||||
// Rename-on-save: if the user edited the filename input, compose the
|
||||
// new path (sibling rename only — joining parent of relPath with the
|
||||
// new filename). Send payload.new_path so the server moves and writes
|
||||
// atomically. Matches the legacy save handler's contract.
|
||||
const filenameInput = modalContent.querySelector("[data-editor-filename]");
|
||||
const editedFilename = filenameInput ? filenameInput.value.trim() : "";
|
||||
const originalLeaf = relPath.split("/").pop() || relPath;
|
||||
const parent = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
|
||||
let newPath = null;
|
||||
if (editedFilename && editedFilename !== originalLeaf) {
|
||||
newPath = parent ? `${parent}/${editedFilename}` : editedFilename;
|
||||
}
|
||||
|
||||
const payload = { path: relPath, content };
|
||||
if (newPath) payload.new_path = newPath;
|
||||
|
||||
const r = await postJson(`${baseUrl}/files/save`, payload);
|
||||
if (r.ok) {
|
||||
if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
|
||||
scheduleRefresh(parentOf(newPath || relPath));
|
||||
} else if (r.status === 409) {
|
||||
// Conflict (destination already exists) — show error and keep modal
|
||||
// open so the user can pick a different filename.
|
||||
alert(r.rawText || `Conflict: destination already exists.`);
|
||||
return;
|
||||
} else {
|
||||
alert((r.body && r.body.error) || r.rawText || `Save failed (HTTP ${r.status}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteBtn = event.target.closest(".files-editor-delete");
|
||||
if (deleteBtn && modalContent.contains(deleteBtn)) {
|
||||
const ta = modalContent.querySelector("textarea[data-rel-path]");
|
||||
if (!ta) return;
|
||||
const relPath = ta.dataset.relPath;
|
||||
if (!relPath) return;
|
||||
if (!confirm(`Delete ${relPath}?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append("path", relPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
const r = await postForm(`${baseUrl}/files/delete`, fd);
|
||||
if (r.ok) {
|
||||
if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
|
||||
scheduleRefresh(parentOf(relPath));
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
//
|
||||
// Migrated to static/js/files-overlay/editor.js (Phase A, Step 2).
|
||||
// This module no longer touches the editor. Helpers it still needs
|
||||
// (askConflict, withCollisionSuffix) are exposed above on
|
||||
// window.__filesOverlay and de-duplicate in Steps 3 and 4.
|
||||
|
||||
// ---------- new-folder modal --------------------------------------------
|
||||
|
||||
|
|
@ -1059,29 +654,12 @@
|
|||
if (!manager.contains(action)) return;
|
||||
const op = action.dataset.action;
|
||||
const path = action.dataset.targetPath || "";
|
||||
if (op === "new-file") {
|
||||
openEditorTextNew(path);
|
||||
} else if (op === "new-folder") {
|
||||
// new-file + edit: dispatched via __filesOverlay registry (editor.js).
|
||||
if (op === "new-folder") {
|
||||
openNewFolder(path);
|
||||
} else if (op === "zip") {
|
||||
const url = `${baseUrl}/files/download_zip?path=${encodeURIComponent(path)}`;
|
||||
window.location.href = url;
|
||||
} else if (op === "edit") {
|
||||
const editable = action.dataset.editable === "1";
|
||||
if (editable) {
|
||||
// Editable text files: open via URL-addressable modal.
|
||||
const editUrl = `/overlays/${overlayId}/files/edit?path=${encodeURIComponent(path)}`;
|
||||
if (typeof window.modals?.openRouted === "function") {
|
||||
window.modals.openRouted(editUrl);
|
||||
} else {
|
||||
// Graceful fallback if modals.js didn't load — full-page navigation
|
||||
// still hits the same route and renders the standalone editor page.
|
||||
window.location.href = editUrl;
|
||||
}
|
||||
} else {
|
||||
// Binary files: keep old inline dialog (binary replace deferred from pilot).
|
||||
openEditorForFile(path, false);
|
||||
}
|
||||
} else if (op === "delete") {
|
||||
const kind = action.dataset.rowKind;
|
||||
const name = action.dataset.rowName || basename(path);
|
||||
|
|
|
|||
550
l4d2web/l4d2web/static/js/files-overlay/editor.js
Normal file
550
l4d2web/l4d2web/static/js/files-overlay/editor.js
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// files-overlay/editor.js — Phase A, Step 2.
|
||||
//
|
||||
// Owns the editor flows during Phase A. Dual-purpose: drives both the
|
||||
// legacy inline #files-editor-modal <dialog> (binary-replace + create-
|
||||
// new-file) and the URL-addressable modal swapped into #modal-content
|
||||
// (editable text files). Phase B migrates the legacy flows to URL-
|
||||
// addressable too and removes the legacy branches here.
|
||||
//
|
||||
// Action-registry dispatch (registered into __filesOverlay):
|
||||
// * "new-file" → openEditorTextNew(folder) (legacy dialog)
|
||||
// * "edit" → URL-addressable modal for editable files;
|
||||
// openEditorForFile(path, false) for binary.
|
||||
//
|
||||
// Save / delete: a single document-level click listener handles both
|
||||
// the legacy dialog and the URL-addressable modal, discriminated by
|
||||
// which ancestor contains the click target.
|
||||
//
|
||||
// Direct-bound (per plan, escape hatch): filename `input`, content
|
||||
// textarea `input` + `keydown` — high-frequency events on persistent
|
||||
// inputs inside the persistent legacy dialog.
|
||||
//
|
||||
// Cross-module deps consumed via window.__filesOverlay (set by
|
||||
// core.js): manager, overlayId, baseUrl, csrfToken, helpers, plus
|
||||
// askConflict (set by the legacy files-overlay.js, used here for save
|
||||
// 409-conflict handling).
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const fo = window.__filesOverlay;
|
||||
if (!fo) return;
|
||||
|
||||
const { overlayId, baseUrl, csrfToken } = fo;
|
||||
const {
|
||||
joinPath, parentOf, basename, humanSize,
|
||||
fetchJson, postJson, postForm, scheduleRefresh,
|
||||
} = fo.helpers;
|
||||
|
||||
// Legacy inline dialog — present in Phase A. Phase B deletes it; the
|
||||
// legacy-branch handlers below short-circuit on its absence.
|
||||
const editorDialog = document.getElementById("files-editor-modal");
|
||||
|
||||
// ---------- legacy editor state + DOM refs ----------
|
||||
|
||||
const editor = {
|
||||
mode: null, // "text" | "binary"
|
||||
creating: false,
|
||||
originalPath: null,
|
||||
folder: null,
|
||||
queuedReplacement: null, // File object
|
||||
};
|
||||
|
||||
const editorEls = editorDialog ? {
|
||||
title: editorDialog.querySelector(".files-editor-title-text"),
|
||||
filename: editorDialog.querySelector(".files-editor-filename"),
|
||||
renameHint: editorDialog.querySelector(".files-editor-rename-hint"),
|
||||
renameFrom: editorDialog.querySelector(".files-rename-from"),
|
||||
renameTo: editorDialog.querySelector(".files-rename-to"),
|
||||
textPanel: editorDialog.querySelector(".files-editor-text"),
|
||||
contentBox: editorDialog.querySelector(".files-editor-content"),
|
||||
byteCount: editorDialog.querySelector(".files-editor-byte-count"),
|
||||
binaryPanel: editorDialog.querySelector(".files-editor-binary"),
|
||||
binarySize: editorDialog.querySelector(".files-editor-binary-size"),
|
||||
replaceZone: editorDialog.querySelector(".files-editor-replace-zone"),
|
||||
replaceIdle: editorDialog.querySelector(".files-editor-replace-idle"),
|
||||
replaceQueued: editorDialog.querySelector(".files-editor-replace-queued"),
|
||||
replaceName: editorDialog.querySelector(".files-editor-replace-name"),
|
||||
replaceSize: editorDialog.querySelector(".files-editor-replace-size"),
|
||||
replaceClear: editorDialog.querySelector(".files-editor-replace-clear"),
|
||||
replaceBrowse: editorDialog.querySelector(".files-editor-replace-browse"),
|
||||
replaceInput: editorDialog.querySelector(".files-editor-replace-input"),
|
||||
deleteBtn: editorDialog.querySelector(".files-editor-delete"),
|
||||
downloadBtn: editorDialog.querySelector(".files-editor-download"),
|
||||
saveBtn: editorDialog.querySelector(".files-editor-save"),
|
||||
} : null;
|
||||
|
||||
// ---------- CM6 bridge + UI updates (legacy dialog) ----------
|
||||
|
||||
// Bridge to the CodeMirror 6 controller, set up by static/js/editor.js
|
||||
// on the .files-editor-content textarea. Falls back to the textarea
|
||||
// directly if the bundle didn't load (no-JS fallback / file open
|
||||
// before the controller has been mounted).
|
||||
function getEditorValue() {
|
||||
return (window.__filesEditor && window.__filesEditor.getValue)
|
||||
? window.__filesEditor.getValue()
|
||||
: editorEls.contentBox.value;
|
||||
}
|
||||
function setEditorValue(text) {
|
||||
if (window.__filesEditor && window.__filesEditor.setContent) {
|
||||
window.__filesEditor.setContent(text);
|
||||
} else {
|
||||
editorEls.contentBox.value = text;
|
||||
}
|
||||
}
|
||||
|
||||
function setEditorTitle(text) {
|
||||
editorEls.title.textContent = text;
|
||||
}
|
||||
|
||||
function updateByteCount() {
|
||||
const bytes = new TextEncoder().encode(getEditorValue()).length;
|
||||
editorEls.byteCount.textContent = `UTF-8 · ${bytes} bytes`;
|
||||
}
|
||||
|
||||
function updateRenameHint() {
|
||||
const current = editorEls.filename.value.trim();
|
||||
const original = basename(editor.originalPath || "");
|
||||
if (editor.creating || !current || current === original) {
|
||||
editorEls.renameHint.hidden = true;
|
||||
return;
|
||||
}
|
||||
editorEls.renameFrom.textContent = original;
|
||||
editorEls.renameTo.textContent = current;
|
||||
editorEls.renameHint.hidden = false;
|
||||
}
|
||||
|
||||
function updateSaveEnabled() {
|
||||
if (editor.mode === "binary" && !editor.creating) {
|
||||
const filenameChanged =
|
||||
editorEls.filename.value.trim() !== basename(editor.originalPath || "");
|
||||
const hasReplacement = !!editor.queuedReplacement;
|
||||
editorEls.saveBtn.disabled = !filenameChanged && !hasReplacement;
|
||||
editorEls.saveBtn.textContent = "Save";
|
||||
} else if (editor.creating) {
|
||||
editorEls.saveBtn.disabled = !editorEls.filename.value.trim();
|
||||
editorEls.saveBtn.textContent = "Create";
|
||||
} else {
|
||||
editorEls.saveBtn.disabled = false;
|
||||
editorEls.saveBtn.textContent = "Save";
|
||||
}
|
||||
}
|
||||
|
||||
function setQueuedReplacement(file) {
|
||||
editor.queuedReplacement = file;
|
||||
if (file) {
|
||||
editorEls.replaceIdle.hidden = true;
|
||||
editorEls.replaceQueued.hidden = false;
|
||||
editorEls.replaceName.textContent = file.name;
|
||||
editorEls.replaceSize.textContent = humanSize(file.size);
|
||||
} else {
|
||||
editorEls.replaceIdle.hidden = false;
|
||||
editorEls.replaceQueued.hidden = true;
|
||||
}
|
||||
updateSaveEnabled();
|
||||
}
|
||||
|
||||
// ---------- legacy editor openers ----------
|
||||
|
||||
function openEditorTextNew(folder) {
|
||||
if (!editorDialog) return;
|
||||
editor.mode = "text";
|
||||
editor.creating = true;
|
||||
editor.originalPath = null;
|
||||
editor.folder = folder;
|
||||
editor.queuedReplacement = null;
|
||||
|
||||
setEditorTitle(`${folder ? folder + "/" : ""}…new file`);
|
||||
editorEls.filename.value = "";
|
||||
editorEls.filename.disabled = false;
|
||||
setEditorValue("");
|
||||
editorEls.contentBox.disabled = false;
|
||||
editorEls.renameHint.hidden = true;
|
||||
editorEls.textPanel.hidden = false;
|
||||
editorEls.binaryPanel.hidden = true;
|
||||
editorEls.deleteBtn.hidden = true;
|
||||
editorEls.downloadBtn.hidden = true;
|
||||
editorEls.saveBtn.textContent = "Create";
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.filename.focus(), 0);
|
||||
}
|
||||
|
||||
async function openEditorForFile(path, isEditable) {
|
||||
if (!editorDialog) return;
|
||||
editor.creating = false;
|
||||
editor.originalPath = path;
|
||||
editor.folder = parentOf(path);
|
||||
editor.queuedReplacement = null;
|
||||
setQueuedReplacement(null);
|
||||
|
||||
editorEls.filename.value = basename(path);
|
||||
editorEls.filename.disabled = false;
|
||||
editorEls.renameHint.hidden = true;
|
||||
editorEls.deleteBtn.hidden = false;
|
||||
editorEls.downloadBtn.hidden = false;
|
||||
editorEls.downloadBtn.href = `${baseUrl}/files/download?path=${encodeURIComponent(path)}`;
|
||||
setEditorTitle(path);
|
||||
|
||||
if (isEditable) {
|
||||
editor.mode = "text";
|
||||
editorEls.textPanel.hidden = false;
|
||||
editorEls.binaryPanel.hidden = true;
|
||||
setEditorValue("Loading…");
|
||||
editorEls.contentBox.disabled = true;
|
||||
|
||||
const r = await fetchJson(
|
||||
`${baseUrl}/files/content?path=${encodeURIComponent(path)}`
|
||||
);
|
||||
if (r.ok && r.body) {
|
||||
setEditorValue(r.body.content);
|
||||
editorEls.contentBox.disabled = false;
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.contentBox.focus(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: server says not editable. Re-open as binary.
|
||||
editorEls.contentBox.disabled = false;
|
||||
editor.mode = "binary";
|
||||
} else {
|
||||
editor.mode = "binary";
|
||||
}
|
||||
|
||||
// Binary mode setup.
|
||||
editorEls.textPanel.hidden = true;
|
||||
editorEls.binaryPanel.hidden = false;
|
||||
editorEls.binarySize.textContent = "—"; // server gave us no size; cosmetic
|
||||
updateSaveEnabled();
|
||||
editorDialog.showModal();
|
||||
setTimeout(() => editorEls.filename.focus(), 0);
|
||||
}
|
||||
|
||||
// ---------- direct-bound persistent-input listeners (legacy dialog) ----------
|
||||
|
||||
if (editorDialog) {
|
||||
// Per plan: filename + content textarea input/keydown stay direct-
|
||||
// bound. They're high-frequency events on persistent inputs inside
|
||||
// the persistent legacy dialog; delegation would add per-keystroke
|
||||
// selector-matching overhead.
|
||||
editorEls.filename.addEventListener("input", () => {
|
||||
updateRenameHint();
|
||||
updateSaveEnabled();
|
||||
});
|
||||
editorEls.contentBox.addEventListener("input", () => {
|
||||
updateByteCount();
|
||||
updateSaveEnabled();
|
||||
});
|
||||
editorEls.contentBox.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
|
||||
event.preventDefault();
|
||||
editorEls.saveBtn.click();
|
||||
}
|
||||
});
|
||||
editorDialog.addEventListener("close", () => {
|
||||
setQueuedReplacement(null);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- delegated handlers (legacy + URL-addressable) ----------
|
||||
|
||||
function inLegacyEditor(el) {
|
||||
return !!editorDialog && editorDialog.contains(el);
|
||||
}
|
||||
function inRoutedEditor(el) {
|
||||
const mc = document.getElementById("modal-content");
|
||||
return !!mc && mc.contains(el);
|
||||
}
|
||||
|
||||
// Replace-zone drag (legacy binary mode). Document-level delegation
|
||||
// gated on the zone being inside the legacy dialog.
|
||||
document.addEventListener("dragover", (event) => {
|
||||
const zone = event.target?.closest?.(".files-editor-replace-zone");
|
||||
if (!zone || !inLegacyEditor(zone)) return;
|
||||
if (Array.from(event.dataTransfer.types).includes("Files")) {
|
||||
event.preventDefault();
|
||||
zone.classList.add("is-drop-target");
|
||||
}
|
||||
});
|
||||
document.addEventListener("dragleave", (event) => {
|
||||
const zone = event.target?.closest?.(".files-editor-replace-zone");
|
||||
if (!zone || !inLegacyEditor(zone)) return;
|
||||
zone.classList.remove("is-drop-target");
|
||||
});
|
||||
document.addEventListener("drop", (event) => {
|
||||
const zone = event.target?.closest?.(".files-editor-replace-zone");
|
||||
if (!zone || !inLegacyEditor(zone)) return;
|
||||
if (!Array.from(event.dataTransfer.types).includes("Files")) return;
|
||||
event.preventDefault();
|
||||
zone.classList.remove("is-drop-target");
|
||||
const f = event.dataTransfer.files && event.dataTransfer.files[0];
|
||||
if (f) setQueuedReplacement(f);
|
||||
});
|
||||
|
||||
// Replace-input change (browse-fallback): low frequency, delegated.
|
||||
document.addEventListener("change", (event) => {
|
||||
const input = event.target?.closest?.(".files-editor-replace-input");
|
||||
if (!input || !inLegacyEditor(input)) return;
|
||||
const f = input.files && input.files[0];
|
||||
if (f) setQueuedReplacement(f);
|
||||
});
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const target = event.target;
|
||||
if (!target) return;
|
||||
|
||||
// Legacy: replace-clear button.
|
||||
const clearBtn = target.closest?.(".files-editor-replace-clear");
|
||||
if (clearBtn && inLegacyEditor(clearBtn)) {
|
||||
setQueuedReplacement(null);
|
||||
return;
|
||||
}
|
||||
// Legacy: replace-browse button → trigger hidden file input.
|
||||
const browseBtn = target.closest?.(".files-editor-replace-browse");
|
||||
if (browseBtn && inLegacyEditor(browseBtn)) {
|
||||
editorEls.replaceInput.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// Save (both editors).
|
||||
const saveBtn = target.closest?.(".files-editor-save");
|
||||
if (saveBtn) {
|
||||
if (inLegacyEditor(saveBtn)) {
|
||||
await legacySaveClicked();
|
||||
return;
|
||||
}
|
||||
if (inRoutedEditor(saveBtn)) {
|
||||
await routedSaveClicked();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete (both editors).
|
||||
const delBtn = target.closest?.(".files-editor-delete");
|
||||
if (delBtn) {
|
||||
if (inLegacyEditor(delBtn)) {
|
||||
await legacyDeleteClicked();
|
||||
return;
|
||||
}
|
||||
if (inRoutedEditor(delBtn)) {
|
||||
await routedDeleteClicked();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function legacySaveClicked() {
|
||||
const filename = editorEls.filename.value.trim();
|
||||
if (!filename) return;
|
||||
if (filename.includes("/")) {
|
||||
alert("Filename can't contain '/'. Use drag-to-move to relocate.");
|
||||
return;
|
||||
}
|
||||
const folder = editor.folder || "";
|
||||
const newRel = joinPath(folder, filename);
|
||||
|
||||
if (editor.creating) {
|
||||
// Text-flavor create → /save with no new_path.
|
||||
const r = await postJson(`${baseUrl}/files/save`, {
|
||||
path: newRel,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else if (r.status === 409) {
|
||||
// askConflict lives in the legacy files-overlay.js for Phase A;
|
||||
// exposed there on __filesOverlay. Phase A Step 3 moves it to
|
||||
// dialogs.js (still on __filesOverlay), so this call site
|
||||
// doesn't change in Step 3.
|
||||
const askConflict = fo.askConflict;
|
||||
const withCollisionSuffix = fo.withCollisionSuffix;
|
||||
if (typeof askConflict !== "function" || typeof withCollisionSuffix !== "function") {
|
||||
alert(`Save failed (HTTP ${r.status}).`);
|
||||
return;
|
||||
}
|
||||
const action = await askConflict(newRel);
|
||||
if (action === "overwrite") {
|
||||
const r2 = await postJson(`${baseUrl}/files/save`, {
|
||||
path: newRel,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r2.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r2.body && r2.body.error) ||
|
||||
`Save failed (HTTP ${r2.status}).`
|
||||
);
|
||||
}
|
||||
} else if (action === "keep-both") {
|
||||
const altered = withCollisionSuffix(newRel);
|
||||
const r2 = await postJson(`${baseUrl}/files/save`, {
|
||||
path: altered,
|
||||
content: getEditorValue(),
|
||||
});
|
||||
if (r2.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Save failed (HTTP ${r.status}).`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const renaming = newRel !== editor.originalPath;
|
||||
if (editor.mode === "text") {
|
||||
const payload = {
|
||||
path: editor.originalPath,
|
||||
content: getEditorValue(),
|
||||
};
|
||||
if (renaming) payload.new_path = newRel;
|
||||
const r = await postJson(`${baseUrl}/files/save`, payload);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Save failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Binary mode.
|
||||
const fd = new FormData();
|
||||
fd.append("path", editor.originalPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
if (renaming) fd.append("new_path", newRel);
|
||||
if (editor.queuedReplacement) {
|
||||
fd.append("file", editor.queuedReplacement);
|
||||
const r = await postForm(`${baseUrl}/files/replace`, fd);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Replace failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
} else if (renaming) {
|
||||
// Rename only via /move (no content change).
|
||||
const r = await postJson(`${baseUrl}/files/move`, {
|
||||
src: editor.originalPath,
|
||||
dst: newRel,
|
||||
});
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(folder);
|
||||
} else {
|
||||
alert(
|
||||
(r.body && r.body.error) || `Rename failed (HTTP ${r.status}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function legacyDeleteClicked() {
|
||||
if (!editor.originalPath) return;
|
||||
if (!confirm(`Delete ${editor.originalPath}?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append("path", editor.originalPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
const r = await postForm(`${baseUrl}/files/delete`, fd);
|
||||
if (r.ok) {
|
||||
editorDialog.close();
|
||||
scheduleRefresh(parentOf(editor.originalPath));
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- URL-addressable modal (#modal-content) save / delete ----------
|
||||
|
||||
async function routedSaveClicked() {
|
||||
const modalContent = document.getElementById("modal-content");
|
||||
if (!modalContent) return;
|
||||
const ta = modalContent.querySelector("textarea[data-rel-path]");
|
||||
if (!ta) return;
|
||||
const relPath = ta.dataset.relPath;
|
||||
if (!relPath) return;
|
||||
const content = (window.__filesEditor && window.__filesEditor.getValue)
|
||||
? window.__filesEditor.getValue()
|
||||
: ta.value;
|
||||
|
||||
// Rename-on-save: if the user edited the filename input, compose the
|
||||
// new path (sibling rename only — joining parent of relPath with the
|
||||
// new filename). Send payload.new_path so the server moves and writes
|
||||
// atomically. Matches the legacy save handler's contract.
|
||||
const filenameInput = modalContent.querySelector("[data-editor-filename]");
|
||||
const editedFilename = filenameInput ? filenameInput.value.trim() : "";
|
||||
const originalLeaf = relPath.split("/").pop() || relPath;
|
||||
const parent = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
|
||||
let newPath = null;
|
||||
if (editedFilename && editedFilename !== originalLeaf) {
|
||||
newPath = parent ? `${parent}/${editedFilename}` : editedFilename;
|
||||
}
|
||||
|
||||
const payload = { path: relPath, content };
|
||||
if (newPath) payload.new_path = newPath;
|
||||
|
||||
const r = await postJson(`${baseUrl}/files/save`, payload);
|
||||
if (r.ok) {
|
||||
if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
|
||||
scheduleRefresh(parentOf(newPath || relPath));
|
||||
} else if (r.status === 409) {
|
||||
alert(r.rawText || `Conflict: destination already exists.`);
|
||||
} else {
|
||||
alert((r.body && r.body.error) || r.rawText || `Save failed (HTTP ${r.status}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async function routedDeleteClicked() {
|
||||
const modalContent = document.getElementById("modal-content");
|
||||
if (!modalContent) return;
|
||||
const ta = modalContent.querySelector("textarea[data-rel-path]");
|
||||
if (!ta) return;
|
||||
const relPath = ta.dataset.relPath;
|
||||
if (!relPath) return;
|
||||
if (!confirm(`Delete ${relPath}?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append("path", relPath);
|
||||
fd.append("csrf_token", csrfToken);
|
||||
const r = await postForm(`${baseUrl}/files/delete`, fd);
|
||||
if (r.ok) {
|
||||
if (typeof window.modals?.closeRouted === "function") window.modals.closeRouted();
|
||||
scheduleRefresh(parentOf(relPath));
|
||||
} else {
|
||||
alert((r.body && r.body.error) || `Delete failed (HTTP ${r.status}).`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- register action-registry handlers ----------
|
||||
|
||||
fo.registerHandler("new-file", (path) => openEditorTextNew(path));
|
||||
|
||||
fo.registerHandler("edit", (path, actionEl) => {
|
||||
const editable = actionEl?.dataset?.editable === "1";
|
||||
if (editable) {
|
||||
// Editable text files: open via URL-addressable modal.
|
||||
const editUrl = `/overlays/${overlayId}/files/edit?path=${encodeURIComponent(path)}`;
|
||||
if (typeof window.modals?.openRouted === "function") {
|
||||
window.modals.openRouted(editUrl);
|
||||
} else {
|
||||
// Graceful fallback if modals.js didn't load — full-page nav
|
||||
// hits the same route and renders the standalone editor page.
|
||||
window.location.href = editUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Binary files: legacy inline dialog (Phase B migrates this to
|
||||
// URL-addressable too).
|
||||
openEditorForFile(path, false);
|
||||
});
|
||||
})();
|
||||
|
|
@ -283,6 +283,7 @@
|
|||
</aside>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/files-overlay/core.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/files-overlay/editor.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/files-overlay.js') }}" defer></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue