fix: flex-wrap grid + free resize (drop width floor)
The DOM-widget width doesn't reliably track the node width in frontend 1.45, so force a fixed column count clipped the grid and the width floor blocked resizing down. Use flex-wrap (cells wrap to fit, never clip), drop the computeSize width floor (resize freely), and re-sync the widget width + reflow on manual resize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-34
@@ -16,8 +16,7 @@ const PAD = 4; // .gip-grid padding
|
|||||||
const TOOLBAR_H = 26;
|
const TOOLBAR_H = 26;
|
||||||
const ROW_H = CELL + GAP;
|
const ROW_H = CELL + GAP;
|
||||||
const MAX_ROWS = 4; // beyond this the grid scrolls internally
|
const MAX_ROWS = 4; // beyond this the grid scrolls internally
|
||||||
const COLS = 4; // fixed column count
|
const MIN_W = 560; // default node width (the grey area); freely resizable
|
||||||
const MIN_W = 560; // minimum node/grid width (the grey area)
|
|
||||||
// ComfyUI insets DOM widgets by DEFAULT_MARGIN (10px) on every side and forces
|
// ComfyUI insets DOM widgets by DEFAULT_MARGIN (10px) on every side and forces
|
||||||
// our element to h-full/w-full of the (computedHeight - 2*MARGIN) box. Reserve
|
// our element to h-full/w-full of the (computedHeight - 2*MARGIN) box. Reserve
|
||||||
// that or the grid eats into the toolbar's space.
|
// that or the grid eats into the toolbar's space.
|
||||||
@@ -113,23 +112,32 @@ function viewUrl(poolId, name, bust) {
|
|||||||
// size the node to fit them exactly. An explicit column count avoids depending
|
// size the node to fit them exactly. An explicit column count avoids depending
|
||||||
// on ComfyUI's DOM-widget width tracking, which doesn't reliably follow the node
|
// on ComfyUI's DOM-widget width tracking, which doesn't reliably follow the node
|
||||||
// width in frontend 1.45 (columns would otherwise collapse on click).
|
// width in frontend 1.45 (columns would otherwise collapse on click).
|
||||||
|
// Height reservation only. Columns are handled by flex-wrap (the grid wraps to
|
||||||
|
// however many cells fit the actual width — so it never clips), so we estimate
|
||||||
|
// rows from the current node width for the height floor.
|
||||||
function recomputeSize(node, count) {
|
function recomputeSize(node, count) {
|
||||||
const rows = count > 0 ? Math.ceil(count / COLS) : 1;
|
const width = Math.max(node.size?.[0] || MIN_W, MIN_W);
|
||||||
|
const inner = width - 2 * MARGIN - 2 * PAD;
|
||||||
|
const perRow = Math.max(1, Math.floor((inner + GAP) / ROW_H));
|
||||||
|
const rows = count > 0 ? Math.ceil(count / perRow) : 1;
|
||||||
const cap = MAX_ROWS * ROW_H - GAP + 2 * PAD;
|
const cap = MAX_ROWS * ROW_H - GAP + 2 * PAD;
|
||||||
const full = count > 0 ? rows * ROW_H - GAP + 2 * PAD : 56;
|
const full = count > 0 ? rows * ROW_H - GAP + 2 * PAD : 56;
|
||||||
node._gridGridMax = cap;
|
|
||||||
node._gridWidgetH = 2 * MARGIN + TOOLBAR_H + 6 + Math.min(full, cap);
|
node._gridWidgetH = 2 * MARGIN + TOOLBAR_H + 6 + Math.min(full, cap);
|
||||||
// node width that fits exactly COLS cells: cells + gaps + grid padding + margins
|
|
||||||
const content = COLS * CELL + (COLS - 1) * GAP + 2 * PAD;
|
|
||||||
node._gridWidthWanted = Math.max(MIN_W, content + 2 * MARGIN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize to the fixed-column width (exact) and content height (grow-only so a
|
// DomWidgets.vue sizes the grid container from `posWidget.width ?? posNode.width`;
|
||||||
// manual taller resize is respected between content changes).
|
// on this version that can lag node.size[0], leaving the grey area narrow. Pin the
|
||||||
|
// widget's own width to the node width so the grid (flex-wrap) reflows to fill it.
|
||||||
|
function syncWidgetWidth(node) {
|
||||||
|
if (node._gridWidget) node._gridWidget.width = node.size?.[0] || MIN_W;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow the node height to fit content; keep the user's width (no width floor, so
|
||||||
|
// the node resizes down freely). Width is the user's to control.
|
||||||
function resizeToContent(node) {
|
function resizeToContent(node) {
|
||||||
const w = node._gridWidthWanted || MIN_W;
|
|
||||||
const h = node.computeSize()[1];
|
const h = node.computeSize()[1];
|
||||||
node.setSize([w, Math.max(node.size?.[1] || 0, h)]);
|
node.setSize([node.size?.[0] || MIN_W, Math.max(node.size?.[1] || 0, h)]);
|
||||||
|
syncWidgetWidth(node);
|
||||||
node.setDirtyCanvas(true, true);
|
node.setDirtyCanvas(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,14 +472,13 @@ function injectStyles() {
|
|||||||
min-height:${TOOLBAR_H - 2}px; }
|
min-height:${TOOLBAR_H - 2}px; }
|
||||||
.gip-toolbar button { font-size:11px; padding:2px 8px; cursor:pointer; }
|
.gip-toolbar button { font-size:11px; padding:2px 8px; cursor:pointer; }
|
||||||
.gip-count { font-size:11px; opacity:0.7; margin-left:auto; }
|
.gip-count { font-size:11px; opacity:0.7; margin-left:auto; }
|
||||||
/* scrolling body — explicit N columns (set via --gip-cols / inline style) so
|
/* scrolling body — flex-wrap so cells wrap to fit the actual width (never
|
||||||
the layout never depends on the DOM-widget width tracking */
|
clips); columns = however many ${CELL}px cells fit */
|
||||||
.gip-grid { display:grid; grid-template-columns:repeat(${COLS}, ${CELL}px); gap:${GAP}px;
|
.gip-grid { display:flex; flex-wrap:wrap; gap:${GAP}px; align-content:flex-start;
|
||||||
justify-content:start; align-content:start; overflow-y:auto;
|
overflow-y:auto; padding:${PAD}px; background:rgba(0,0,0,0.15);
|
||||||
padding:${PAD}px; background:rgba(0,0,0,0.15); border-radius:4px;
|
border-radius:4px; flex:1 1 auto; min-height:0; }
|
||||||
flex:1 1 auto; min-height:0; }
|
|
||||||
.gip-grid.gip-dragover { outline:2px dashed #6cf; outline-offset:-2px; }
|
.gip-grid.gip-dragover { outline:2px dashed #6cf; outline-offset:-2px; }
|
||||||
.gip-empty { font-size:12px; opacity:0.6; padding:12px; grid-column:1 / -1; text-align:center; }
|
.gip-empty { font-size:12px; opacity:0.6; padding:12px; width:100%; text-align:center; }
|
||||||
.gip-cell { position:relative; width:${CELL}px; height:${CELL}px; border:2px solid transparent;
|
.gip-cell { position:relative; width:${CELL}px; height:${CELL}px; border:2px solid transparent;
|
||||||
border-radius:4px; overflow:hidden; background:#222; transition:border-color .1s; }
|
border-radius:4px; overflow:hidden; background:#222; transition:border-color .1s; }
|
||||||
.gip-cell:hover { border-color:#555; }
|
.gip-cell:hover { border-color:#555; }
|
||||||
@@ -563,11 +570,21 @@ function setupGridNode(node) {
|
|||||||
// Pinning a max (or overriding widget.computeSize) locks the widget to a fixed
|
// Pinning a max (or overriding widget.computeSize) locks the widget to a fixed
|
||||||
// size while the node frame keeps resizing — they diverge and the grid appears
|
// size while the node frame keeps resizing — they diverge and the grid appears
|
||||||
// to detach / stop expanding on click.
|
// to detach / stop expanding on click.
|
||||||
node.addDOMWidget("grid", "div", wrap, {
|
node._gridWidget = node.addDOMWidget("grid", "div", wrap, {
|
||||||
serialize: false,
|
serialize: false,
|
||||||
getMinHeight: () => node._gridWidgetH || 120,
|
getMinHeight: () => node._gridWidgetH || 120,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// on manual resize: keep the DOM widget width synced (so the flex-wrap grid
|
||||||
|
// reflows to fill) and re-estimate the height floor for the new width
|
||||||
|
const onResize = node.onResize;
|
||||||
|
node.onResize = function () {
|
||||||
|
const r = onResize?.apply(this, arguments);
|
||||||
|
recomputeSize(node, node._lastCount || 0);
|
||||||
|
syncWidgetWidth(node);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
wireIngest(node, grid, uploadBtn, fileInput);
|
wireIngest(node, grid, uploadBtn, fileInput);
|
||||||
|
|
||||||
// a refresh that also updates the count label
|
// a refresh that also updates the count label
|
||||||
@@ -577,23 +594,12 @@ function setupGridNode(node) {
|
|||||||
if (node._countEl) node._countEl.textContent = `${n} image${n === 1 ? "" : "s"}`;
|
if (node._countEl) node._countEl.textContent = `${n} image${n === 1 ? "" : "s"}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ComfyUI snaps the node to computeSize() on selection, and computeSize's WIDTH
|
// initial width (a sensible wide default) + content-driven height; the node
|
||||||
// ignores DOM widgets — it collapses to ~the title/index width, clipping our
|
// stays freely resizable (no width floor) and the grid flex-wraps to fit.
|
||||||
// grid on click. Floor the computed width to the column-fit width so the snap
|
|
||||||
// keeps the node wide enough. (Only width is touched; the height path through
|
|
||||||
// computeLayoutSize is untouched, so the DOM overlay stays in sync.)
|
|
||||||
const origComputeSize = node.computeSize;
|
|
||||||
node.computeSize = function (out) {
|
|
||||||
const sz = origComputeSize ? origComputeSize.call(this, out) : [MIN_W, 120];
|
|
||||||
sz[0] = Math.max(sz[0], node._gridWidthWanted || MIN_W);
|
|
||||||
return sz;
|
|
||||||
};
|
|
||||||
|
|
||||||
// initial width (fits COLS columns) + content-driven height; the first refresh
|
|
||||||
// resizes once if the pool already has images
|
|
||||||
node._lastCount = 0;
|
node._lastCount = 0;
|
||||||
recomputeSize(node, 0);
|
recomputeSize(node, 0);
|
||||||
node.setSize([node._gridWidthWanted || MIN_W, node.computeSize()[1]]);
|
node.setSize([Math.max(node.size?.[0] || 0, MIN_W), node.computeSize()[1]]);
|
||||||
|
syncWidgetWidth(node);
|
||||||
|
|
||||||
node._gridRefresh();
|
node._gridRefresh();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user