Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/panel/controllers/browserNavigationController.js
T
Ewald 00d141ef8c Add tests and enhancements for browser navigation and asset management
- Implemented size filters in the filesystem browser and added tests to ensure correct application of size constraints.
- Updated write access guard tests to reflect changes in handling forwarded remote requests.
- Introduced a new browser navigation controller to manage folder navigation and breadcrumb rendering.
- Added regression tests to ensure proper binding order for asset manager and back navigation functionality.
- Created tests to validate the absence of double reloads on folder context opening and the use of single folder keys in browser state.
- Added various regression tests to ensure database integrity, asset metadata handling, and viewer functionality.
- Enhanced tests for media filters and search scope to ensure correct behavior and data handling.
2026-02-18 08:11:14 +01:00

246 lines
9.2 KiB
JavaScript

export function createBrowserNavigationController({
state,
gridContainer,
folderBreadcrumb,
customSelect,
reloadGrid,
onContextChanged = null,
lifecycleSignal = null,
} = {}) {
const folderBackStack = [];
const normPath = (value) => String(value || "").trim().replaceAll("\\", "/");
const isWindowsDrive = (part) => /^[a-zA-Z]:$/.test(String(part || "").trim());
const currentFolderPath = () => normPath(state.currentFolderRelativePath || "");
const pushUniqueHistory = (stack, value) => {
const v = normPath(value);
if (!Array.isArray(stack)) return;
if (!v && v !== "") return;
const top = stack.length ? normPath(stack[stack.length - 1]) : null;
if (top === v) return;
stack.push(v);
};
const popDistinctHistory = (stack, current) => {
const cur = normPath(current);
if (!Array.isArray(stack)) return null;
while (stack.length) {
const candidate = normPath(stack.pop());
if (candidate !== cur) return candidate;
}
return null;
};
const setFolderPath = (next) => {
const v = normPath(next);
state.currentFolderRelativePath = v;
try {
if (gridContainer?.dataset) gridContainer.dataset.mjrSubfolder = v;
} catch {}
};
const parentFolderPath = (pathValue) => {
const cur = normPath(pathValue);
if (!cur) return "";
const parts = cur.split("/").filter(Boolean);
if (!parts.length) return "";
if (parts.length === 1) return isWindowsDrive(parts[0]) ? `${parts[0]}/` : "";
parts.pop();
if (parts.length === 1 && isWindowsDrive(parts[0])) return `${parts[0]}/`;
return parts.join("/");
};
const notifyContext = () => {
try {
onContextChanged?.();
} catch {}
try {
renderBreadcrumb();
} catch {}
};
const navigateFolder = async (target, { pushHistory = true } = {}) => {
const prev = currentFolderPath();
const next = normPath(target);
if (prev === next) return;
if (pushHistory) pushUniqueHistory(folderBackStack, prev);
setFolderPath(next);
notifyContext();
await reloadGrid?.();
};
const resetToBrowserRoot = async () => {
state.customRootId = "";
setFolderPath("");
notifyContext();
await reloadGrid?.();
};
const renderBreadcrumb = () => {
const isCustom = String(state.scope || "") === "custom";
const rel = String(state.currentFolderRelativePath || "").trim().replaceAll("\\", "/");
const folderBrowsingActive = isCustom || !!rel;
const selectedRootId = String(state.customRootId || "").trim();
const selectedRootLabel = (() => {
if (!selectedRootId) return "";
try {
const opt = customSelect?.selectedOptions?.[0] || null;
return String(opt?.textContent || "").trim();
} catch {
return "";
}
})();
if (!folderBrowsingActive) {
folderBackStack.length = 0;
folderBreadcrumb.style.display = "none";
folderBreadcrumb.replaceChildren();
return;
}
folderBreadcrumb.style.display = "flex";
folderBreadcrumb.replaceChildren();
const backBtn = document.createElement("button");
backBtn.type = "button";
backBtn.textContent = "Back";
backBtn.className = "mjr-btn-link";
const hasSelectedRoot = !!String(state.customRootId || "").trim();
const canBackToBrowserRoot = hasSelectedRoot && !rel;
const canBackByPath = !!rel;
backBtn.disabled = folderBackStack.length === 0 && !canBackToBrowserRoot && !canBackByPath;
backBtn.style.cssText = "background:rgba(122,162,255,0.12);border:1px solid rgba(122,162,255,0.35);border-radius:6px;padding:2px 8px;font:inherit;color:var(--mjr-accent, #7aa2ff);cursor:pointer;";
backBtn.addEventListener(
"click",
async () => {
const prev = popDistinctHistory(folderBackStack, currentFolderPath());
if (prev != null) {
await navigateFolder(prev, { pushHistory: false });
return;
}
const cur = currentFolderPath();
if (cur) {
const parent = parentFolderPath(cur);
if (parent === cur) {
if (hasSelectedRoot) await resetToBrowserRoot();
return;
}
await navigateFolder(parent, { pushHistory: false });
return;
}
if (canBackToBrowserRoot) await resetToBrowserRoot();
},
{ signal: lifecycleSignal || undefined }
);
folderBreadcrumb.appendChild(backBtn);
const upBtn = document.createElement("button");
upBtn.type = "button";
upBtn.textContent = "Up";
upBtn.className = "mjr-btn-link";
upBtn.disabled = !rel || parentFolderPath(currentFolderPath()) === currentFolderPath();
upBtn.style.cssText = "background:rgba(122,162,255,0.10);border:1px solid rgba(122,162,255,0.30);border-radius:6px;padding:2px 8px;font:inherit;color:var(--mjr-accent, #7aa2ff);cursor:pointer;";
upBtn.addEventListener(
"click",
async () => {
const parent = parentFolderPath(currentFolderPath());
if (parent === currentFolderPath()) {
if (hasSelectedRoot) await resetToBrowserRoot();
return;
}
await navigateFolder(parent);
},
{ signal: lifecycleSignal || undefined }
);
folderBreadcrumb.appendChild(upBtn);
const sep0 = document.createElement("span");
sep0.textContent = "|";
sep0.style.opacity = "0.5";
folderBreadcrumb.appendChild(sep0);
const mk = (label, target, isCurrent = false) => {
const el = document.createElement("button");
el.type = "button";
el.textContent = label;
el.className = "mjr-btn-link";
el.style.cssText = `background:none;border:none;padding:0;color:${isCurrent ? "var(--mjr-text, inherit)" : "var(--mjr-accent, #7aa2ff)"};cursor:${isCurrent ? "default" : "pointer"};font:inherit;`;
if (!isCurrent) {
el.addEventListener(
"click",
async () => {
if (label === "Computer" && String(state.customRootId || "").trim()) {
await resetToBrowserRoot();
return;
}
await navigateFolder(target);
},
{ signal: lifecycleSignal || undefined }
);
} else {
el.disabled = true;
}
return el;
};
folderBreadcrumb.appendChild(mk("Computer", "", !rel));
if (selectedRootId) {
const sepRoot = document.createElement("span");
sepRoot.textContent = "/";
sepRoot.style.opacity = "0.6";
folderBreadcrumb.appendChild(sepRoot);
folderBreadcrumb.appendChild(mk(selectedRootLabel || selectedRootId, "", !rel));
}
if (!rel) return;
const parts = rel.split("/").filter(Boolean);
let acc = "";
for (let i = 0; i < parts.length; i++) {
const sep = document.createElement("span");
sep.textContent = "/";
sep.style.opacity = "0.6";
folderBreadcrumb.appendChild(sep);
if (!acc) acc = isWindowsDrive(parts[i]) ? `${parts[i]}/` : parts[i];
else acc = `${acc}/${parts[i]}`.replace(/\/{2,}/g, "/");
folderBreadcrumb.appendChild(mk(parts[i], acc, i === parts.length - 1));
}
};
const bindGridFolderNavigation = () => {
const onCustomSubfolderChanged = async (e) => {
const next = String(e?.detail?.subfolder || "").trim().replaceAll("\\", "/");
const prev = currentFolderPath();
if (next !== prev) {
pushUniqueHistory(folderBackStack, prev);
setFolderPath(next);
notifyContext();
try {
await reloadGrid?.();
} catch {}
}
};
gridContainer?.addEventListener?.("mjr:custom-subfolder-changed", onCustomSubfolderChanged, {
signal: lifecycleSignal || undefined,
});
try {
gridContainer._mjrHasCustomSubfolderHandler = true;
} catch {}
return () => {
try {
gridContainer?.removeEventListener?.("mjr:custom-subfolder-changed", onCustomSubfolderChanged);
} catch {}
try {
gridContainer._mjrHasCustomSubfolderHandler = false;
} catch {}
};
};
return {
renderBreadcrumb,
resetHistory: () => {
folderBackStack.length = 0;
},
bindGridFolderNavigation,
notifyContext,
};
}