Files
PozzettiAndrea-ComfyUI-Geom…/javascript/js/load_mesh_batch_path.js
T
PozzettiAndrea feb273ad0a Isolation-clean frontend: delete dead JS, guard listeners, leave shared DOM
- Delete 7 dead-squat widgets (registered UI only for other packs' nodes:
  UniRig/SAM3D/Mocap/FBXPreview/BVH/pointcloud) + their 6 orphaned viewer
  HTMLs + the entire unused js/viewer tree (21 files, imported by nothing;
  only ui/styles.css survives) + unused utils barrel.
- Rename namespace geompack -> geometrypack (= DisplayName, lowercased);
  all 17 surviving extensions now geometrypack.*.
- Add event.source provenance guards to every message listener (2 inline,
  2 factory handlers via new iframe param).
- Stop touching document.body: upload file input stays detached (click()
  works detached in modern browsers); folder autocomplete dropdown becomes
  a manual popover inside the widget's own DOM (top layer escapes canvas
  transforms without shared-DOM appends).

Passes comfy-test's javascript isolation lint with 0 errors.
2026-08-13 14:31:51 +02:00

118 lines
5.8 KiB
JavaScript

// Load Mesh Batch: folder-path autocomplete (type to navigate directories),
// inspired by VHS "Load Video (Path)" but using GeometryPack's own /gpack/getpath
// route and restricted to FOLDERS. Uses a CUSTOM floating dropdown (native
// <datalist> is unreliable inside ComfyUI's transformed DOM widgets).
import { app } from "../../../scripts/app.js";
const TAG = "[MeshBatchPath]";
console.log(`${TAG} script loaded`);
// "output/meshes/fo" -> ["output/meshes/", "fo"] (dir, remainder)
function pathStem(value) {
const v = String(value || "");
const i = v.lastIndexOf("/");
return i < 0 ? ["", v] : [v.slice(0, i + 1), v.slice(i + 1)];
}
app.registerExtension({
name: "geometrypack.loadmeshbatch.path",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackLoadMeshBatch") return;
console.log(`${TAG} registering for GeomPackLoadMeshBatch`);
const onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onCreated?.apply(this, arguments);
const node = this;
// Replace the plain folder_path string widget with a DOM input of the
// same name (so it still serializes as folder_path).
const idx = (node.widgets || []).findIndex((w) => w.name === "folder_path");
const initial = idx >= 0 ? (node.widgets[idx].value ?? "3d") : "3d";
if (idx >= 0) node.widgets.splice(idx, 1);
const wrap = document.createElement("div");
wrap.style.cssText = "width:100%;box-sizing:border-box;padding:0 6px;";
const input = document.createElement("input");
input.type = "text";
input.value = initial;
input.placeholder = "folder path — type to autocomplete (input/… output/… or absolute)";
input.style.cssText = "width:100%;box-sizing:border-box;background:#222;color:#ddd;" +
"border:1px solid #444;border-radius:4px;padding:3px 6px;font:12px monospace;";
wrap.appendChild(input);
const widget = node.addDOMWidget("folder_path", "gpack_path", wrap, {
getValue() { return input.value; },
setValue(v) { input.value = v ?? ""; },
serialize: true,
});
widget.computeSize = (w) => [w, 30];
// Floating dropdown as a manual POPOVER inside our own widget DOM:
// showPopover() renders it in the browser's top layer, which
// escapes the canvas transform/clipping WITHOUT appending anything
// to the shared document.body (isolation: we never touch DOM we
// don't own). UA popover styles are reset (inset/margin/padding).
const menu = document.createElement("div");
menu.popover = "manual";
menu.style.cssText = "position:fixed;inset:auto;margin:0;padding:0;max-height:240px;overflow-y:auto;" +
"background:#1e1e1e;border:1px solid #555;border-radius:4px;" +
"box-shadow:0 4px 14px rgba(0,0,0,0.55);font:12px monospace;";
wrap.appendChild(menu);
let items = [];
let lastDir = null;
const hide = () => { if (menu.matches(":popover-open")) menu.hidePopover(); };
const place = () => {
const b = input.getBoundingClientRect();
menu.style.left = b.left + "px";
menu.style.top = (b.bottom + 2) + "px";
menu.style.minWidth = b.width + "px";
};
const render = (rem) => {
const remL = rem.toLowerCase();
const [dir] = pathStem(input.value);
const matches = items.filter(
(it) => it.endsWith("/") && it.toLowerCase().startsWith(remL));
menu.innerHTML = "";
if (!matches.length) { hide(); return; }
for (const it of matches) {
const row = document.createElement("div");
row.textContent = it;
row.style.cssText = "padding:4px 9px;color:#ddd;cursor:pointer;white-space:nowrap;";
row.addEventListener("mouseenter", () => { row.style.background = "#37506b"; });
row.addEventListener("mouseleave", () => { row.style.background = ""; });
row.addEventListener("mousedown", (e) => {
e.preventDefault(); // keep input focused
input.value = dir + it; // descend into the folder
fetchDir(true); // list its contents
});
menu.appendChild(row);
}
if (!menu.matches(":popover-open")) menu.showPopover();
place();
};
const fetchDir = async (force) => {
const [dir, rem] = pathStem(input.value);
if (!force && dir === lastDir) { render(rem); return; }
lastDir = dir;
try {
const resp = await fetch(`/gpack/getpath?path=${encodeURIComponent(dir || ".")}`);
items = resp.ok ? await resp.json() : [];
} catch (e) { items = []; }
render(rem);
};
input.addEventListener("input", () => fetchDir(false));
input.addEventListener("focus", () => { lastDir = null; fetchDir(true); });
input.addEventListener("blur", () => setTimeout(hide, 200));
input.addEventListener("keydown", (e) => { if (e.key === "Escape") hide(); });
const origRemoved = node.onRemoved;
node.onRemoved = function () { menu.remove(); return origRemoved?.apply(this, arguments); };
return r;
};
},
});