diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs
index dd0c2b4..32bec14 100644
--- a/tests/test_downloads_tab_workflow_route.mjs
+++ b/tests/test_downloads_tab_workflow_route.mjs
@@ -413,7 +413,7 @@ test('missing browser splitter keeps a compositor preview between live layout fr
}
assert.match(missingBrowserMethodsSource, /dragThreshold:\s*4/);
- assert.match(missingBrowserMethodsSource, /layoutFrameStride:\s*3/);
+ assert.match(missingBrowserMethodsSource, /layoutFrameStride:\s*1/);
assert.doesNotMatch(missingBrowserMethodsSource, /translate3d\(/);
assert.match(
resolverMainCssSource,
@@ -507,7 +507,7 @@ test('docked ComfyUI splitter coalesces live layout while its gutter stays respo
globalThis.window = previousWindow;
}
- assert.match(dialogShellMethodsSource, /minLayoutInterval:\s*40/);
+ assert.match(dialogShellMethodsSource, /minLayoutInterval:\s*16/);
assert.match(dialogShellMethodsSource, /setPointerCapture\(event\.pointerId\)/);
assert.match(dialogShellMethodsSource, /translate3d\(/);
assert.match(dialogShellMethodsSource, /requestAnimationFrame\(/);
diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css
index 7246e43..7514882 100644
--- a/web/css/resolver-main.css
+++ b/web/css/resolver-main.css
@@ -706,6 +706,8 @@
}
.mr-missing-list {
+ display: flex;
+ flex-direction: column;
min-height: 0;
overflow-y: auto;
scrollbar-gutter: stable;
@@ -713,6 +715,23 @@
background: var(--mr-missing-bg);
}
+.mr-missing-list-virtual-scroll {
+ position: relative;
+ flex: 0 0 auto;
+ width: 100%;
+ min-width: 0;
+ contain: layout paint style;
+}
+
+.mr-missing-list-virtual-rows {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ min-width: 0;
+ will-change: transform;
+}
+
.mr-missing-browser.is-resizing::after {
content: "";
position: absolute;
diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css
index 48ae36c..1a34eaf 100644
--- a/web/css/resolver-shell.css
+++ b/web/css/resolver-shell.css
@@ -110,7 +110,7 @@
display: none;
flex-direction: column;
overflow: hidden;
- resize: both;
+ resize: none;
z-index: var(--mr-z-dialog);
color: var(--mr-text, #f3f3f3);
background: linear-gradient(180deg, var(--mr-bg-soft) 0%, var(--mr-bg) 100%);
@@ -120,6 +120,41 @@
box-shadow: var(--mr-shadow-dialog);
}
+.mr-modal-resize-handle {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+ width: 18px;
+ height: 18px;
+ cursor: nwse-resize;
+ touch-action: none;
+ user-select: none;
+}
+
+.mr-modal-resize-handle::after {
+ content: "";
+ position: absolute;
+ right: 4px;
+ bottom: 4px;
+ width: 8px;
+ height: 8px;
+ opacity: 0.58;
+ background: linear-gradient(135deg, transparent 42%, var(--mr-text-muted, #b1b1b1) 43% 50%, transparent 51% 64%, var(--mr-text-muted, #b1b1b1) 65% 72%, transparent 73%);
+}
+
+.model-resolver-modal.is-resizing {
+ user-select: none;
+}
+
+.model-resolver-modal.is-resizing .mr-modal-resize-handle::after {
+ opacity: 1;
+}
+
+#model-resolver-modal.mr-is-docked .mr-modal-resize-handle {
+ display: none;
+}
+
.mr-context-menu {
position: fixed;
display: none;
diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js
index 47e3e3e..646188c 100644
--- a/web/resolver/resolver_dialog.js
+++ b/web/resolver/resolver_dialog.js
@@ -266,6 +266,7 @@ export class ResolverManagerDialog extends ComfyDialog {
this.createContent(),
this.createFooter()
]);
+ this.installModalResizeHandle();
this.tooltipObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
diff --git a/web/resolver/shell/dialog_shell_methods.js b/web/resolver/shell/dialog_shell_methods.js
index f26f642..7cb3325 100644
--- a/web/resolver/shell/dialog_shell_methods.js
+++ b/web/resolver/shell/dialog_shell_methods.js
@@ -128,7 +128,7 @@ export const dialogShellMethods = {
el.style.maxHeight = '100vh';
el.style.minWidth = '640px';
el.style.minHeight = '420px';
- el.style.resize = 'both';
+ el.style.resize = 'none';
el.style.borderRadius = '7px';
const rect = this._floatingRectBeforeDock;
@@ -160,6 +160,125 @@ export const dialogShellMethods = {
el.style.transform = 'translate(-50%, -50%)';
},
+ installModalResizeHandle() {
+ if (!this.element || this._modalResizeHandle?.isConnected) return;
+
+ const handle = $el("div.mr-modal-resize-handle", {
+ id: "model-resolver-resize-handle",
+ ariaHidden: "true"
+ });
+ const eventName = typeof PointerEvent === 'function' ? 'pointerdown' : 'mousedown';
+ this._modalResizeStartHandler = (event) => this.startModalResize(event);
+ handle.addEventListener(eventName, this._modalResizeStartHandler, true);
+ this._modalResizeHandle = handle;
+ this._modalResizeStartEventName = eventName;
+ this.element.appendChild(handle);
+ },
+
+ startModalResize(event) {
+ if (!this.element || this.docked || this.fullscreen) return;
+ if (event.button !== undefined && event.button !== 0) return;
+ if (event.isPrimary === false) return;
+
+ this.finishModalResize({ flush: true });
+ event.preventDefault?.();
+ event.stopPropagation?.();
+
+ const rect = this.element.getBoundingClientRect();
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth || rect.right;
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight || rect.bottom;
+ const computed = typeof getComputedStyle === 'function'
+ ? getComputedStyle(this.element)
+ : null;
+ const minWidth = Math.max(1, Number.parseFloat(computed?.minWidth) || 640);
+ const minHeight = Math.max(1, Number.parseFloat(computed?.minHeight) || 420);
+ const startWidth = Math.max(minWidth, Number(rect.width) || this.element.offsetWidth || minWidth);
+ const startHeight = Math.max(minHeight, Number(rect.height) || this.element.offsetHeight || minHeight);
+ const state = {
+ startX: Number(event.clientX) || 0,
+ startY: Number(event.clientY) || 0,
+ startWidth,
+ startHeight,
+ minWidth,
+ minHeight,
+ maxWidth: Math.max(minWidth, startWidth, viewportWidth - rect.left),
+ maxHeight: Math.max(minHeight, startHeight, viewportHeight - rect.top),
+ pointerId: event.pointerId,
+ moveEventName: typeof PointerEvent === 'function' ? 'pointermove' : 'mousemove',
+ endEventName: typeof PointerEvent === 'function' ? 'pointerup' : 'mouseup',
+ cancelEventName: typeof PointerEvent === 'function' ? 'pointercancel' : null,
+ pendingWidth: Number(rect.width) || minWidth,
+ pendingHeight: Number(rect.height) || minHeight,
+ animationFrame: null,
+ moveHandler: null,
+ endHandler: null,
+ cancelHandler: null,
+ blurHandler: null
+ };
+
+ this.element.style.top = `${Math.round(rect.top)}px`;
+ this.element.style.left = `${Math.round(rect.left)}px`;
+ this.element.style.transform = 'none';
+ this.element.style.resize = 'none';
+ this.element.style.willChange = 'width, height';
+ this.element.classList.add('is-resizing');
+
+ state.moveHandler = (moveEvent) => {
+ if (state.pointerId !== undefined && moveEvent.pointerId !== state.pointerId) return;
+ const nextWidth = state.startWidth + ((Number(moveEvent.clientX) || state.startX) - state.startX);
+ const nextHeight = state.startHeight + ((Number(moveEvent.clientY) || state.startY) - state.startY);
+ state.pendingWidth = Math.min(state.maxWidth, Math.max(state.minWidth, nextWidth));
+ state.pendingHeight = Math.min(state.maxHeight, Math.max(state.minHeight, nextHeight));
+ if (state.animationFrame) return;
+ state.animationFrame = requestAnimationFrame(() => {
+ state.animationFrame = null;
+ this.applyModalResize(state);
+ });
+ };
+ state.endHandler = () => this.finishModalResize({ flush: true });
+ state.cancelHandler = state.endHandler;
+ state.blurHandler = state.endHandler;
+ this._modalResizeState = state;
+
+ document.addEventListener(state.moveEventName, state.moveHandler, true);
+ document.addEventListener(state.endEventName, state.endHandler, true);
+ if (state.cancelEventName) {
+ document.addEventListener(state.cancelEventName, state.cancelHandler, true);
+ }
+ window.addEventListener('blur', state.blurHandler, { once: true });
+ },
+
+ applyModalResize(state) {
+ if (!state || this._modalResizeState !== state || !this.element) return;
+ this.element.style.width = `${Math.round(state.pendingWidth)}px`;
+ this.element.style.height = `${Math.round(state.pendingHeight)}px`;
+ },
+
+ finishModalResize({ flush = false } = {}) {
+ const state = this._modalResizeState;
+ if (!state) return false;
+ if (state.animationFrame) {
+ cancelAnimationFrame(state.animationFrame);
+ state.animationFrame = null;
+ }
+ if (flush) this.applyModalResize(state);
+
+ document.removeEventListener(state.moveEventName, state.moveHandler, true);
+ document.removeEventListener(state.endEventName, state.endHandler, true);
+ if (state.cancelEventName) {
+ document.removeEventListener(state.cancelEventName, state.cancelHandler, true);
+ }
+ window.removeEventListener('blur', state.blurHandler);
+ this._modalResizeState = null;
+ this.element?.classList.remove('is-resizing');
+ if (this.element) {
+ this.element.style.willChange = '';
+ this.captureFloatingRect();
+ this.saveModalPosition?.();
+ }
+ return true;
+ },
+
rememberSidebarOpenMode(mode) {
safeStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked');
},
@@ -301,7 +420,7 @@ export const dialogShellMethods = {
animationFrame: null,
delayTimer: null,
lastLayoutAt: typeof performance === 'object' ? performance.now() : Date.now(),
- minLayoutInterval: 40,
+ minLayoutInterval: 16,
appliedPageX: Number(event.pageX ?? event.clientX) || 0,
appliedPageY: Number(event.pageY ?? event.clientY) || 0,
vertical: proxy?.horizontal === false,
@@ -641,6 +760,7 @@ export const dialogShellMethods = {
el.style.maxHeight = '100vh';
el.style.borderRadius = '0';
el.style.resize = 'none';
+ this._modalResizeHandle?.style.setProperty('display', 'none');
if (btn) {
btn.innerHTML = getSvgIcon('windowRestore', 'currentColor', 'mr-window-btn-icon');
btn.setAttribute('aria-label', 'Exit full screen');
@@ -653,7 +773,8 @@ export const dialogShellMethods = {
el.style.maxWidth = '100vw';
el.style.maxHeight = '100vh';
el.style.borderRadius = '8px';
- el.style.resize = 'both';
+ el.style.resize = 'none';
+ this._modalResizeHandle?.style.removeProperty('display');
// Restore saved pre-FS size if available
let wh = null;
try { wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (_e) {}
diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js
index e0e4864..6f712c2 100644
--- a/web/resolver/views/missing_browser_methods.js
+++ b/web/resolver/views/missing_browser_methods.js
@@ -5,6 +5,10 @@ import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
import { matchesWorkflowModelReference } from "../node_context_menu.js";
+const MISSING_ROW_FALLBACK_HEIGHT = 70;
+const MISSING_VIRTUAL_INITIAL_ROWS = 24;
+const MISSING_VIRTUAL_OVERSCAN_ROWS = 6;
+
export const missingBrowserMethods = {
getMissingFilename(missing = {}) {
return this.getFilenameFromPath(missing.original_path) || missing.name || 'Missing model';
@@ -434,9 +438,12 @@ export const missingBrowserMethods = {
}
requestAnimationFrame(() => {
- const row = Array.from(
+ let row = Array.from(
this.contentElement?.querySelectorAll?.('.mr-missing-list-row') || []
).find(item => item.dataset.missingKey === this.selectedMissingModelKey);
+ if (!row) {
+ row = this.scrollMissingModelIntoView(this.selectedMissingModelKey);
+ }
row?.scrollIntoView?.({ block: 'center', inline: 'nearest' });
});
return selected;
@@ -502,6 +509,177 @@ export const missingBrowserMethods = {
requestAnimationFrame(applyScroll);
},
+ refreshMissingModelsVirtualizer(container, force = false) {
+ const list = container?.querySelector?.('.mr-missing-list');
+ list?._mrMissingVirtualState?.update?.(force);
+ },
+
+ getMissingBrowserVirtualRowHeight(list = null) {
+ const target = list instanceof HTMLElement
+ ? list
+ : this.contentElement?.querySelector?.('.mr-missing-list');
+ if (target instanceof HTMLElement && typeof getComputedStyle === 'function') {
+ const rawHeight = getComputedStyle(target)
+ .getPropertyValue('--mr-missing-row-height')
+ .trim();
+ const parsedHeight = Number.parseFloat(rawHeight);
+ if (Number.isFinite(parsedHeight) && parsedHeight > 0) return parsedHeight;
+ }
+ return MISSING_ROW_FALLBACK_HEIGHT;
+ },
+
+ renderMissingModelListRow(missing, index, totalCount, selectedKey) {
+ const key = this.getMissingModelKey(missing);
+ const isSelected = key === selectedKey;
+ const isBatchSelected = this.batchSelectedMissingKeys?.has(key);
+ const isResolved = this.isMissingModelResolved(missing);
+ const filename = this.getMissingFilename(missing);
+ const { bestMatch, confidence, matchDisplay, matchClass } = this.getLocalMatchDisplayInfo(missing);
+ const typeLabel = missing.category ? this.getCategoryDisplayName(missing.category) : 'unknown';
+ const typeColorClass = this.getModelTypeColorClass(missing.category || typeLabel);
+ const nodeDisplay = this.getMissingNodeDisplay(missing);
+ const locateTarget = nodeDisplay.locateTarget || this.getMissingLocateTarget(missing);
+ const nodeId = locateTarget.nodeId ?? '';
+ const rowNodeHtml = nodeDisplay.canLocate
+ ? ``
+ : `${this.escapeHtml(nodeDisplay.text)}`;
+
+ return `
+
+
+
+
+ ${index + 1}
+
+ ${isResolved ? `${getSvgIcon('circleCheckBig')}` : ''}
+
+ ${this.escapeHtml(filename)}
+
+ ${rowNodeHtml}
+ ${this.renderMissingAutoDownloadBadge(missing, { compact: true })}
+
+
+
+ ${this.escapeHtml(typeLabel)}
+
+ ${bestMatch ? this.escapeHtml(matchDisplay) : '-- No local match'}
+
+
+ ${bestMatch ? `${confidence.toFixed(confidence % 1 ? 1 : 0)}%` : '--'}
+
+ ${this.renderMissingSourcesSummary(missing)}
+
+ `;
+ },
+
+ setupMissingModelsVirtualizer(container, data, sortedMissingModels) {
+ const list = container?.querySelector?.('.mr-missing-list');
+ const virtualScroll = list?.querySelector?.('.mr-missing-list-virtual-scroll');
+ const rowsHost = virtualScroll?.querySelector?.('.mr-missing-list-virtual-rows');
+ if (!(list instanceof HTMLElement) || !(virtualScroll instanceof HTMLElement) || !(rowsHost instanceof HTMLElement)) {
+ return;
+ }
+
+ const existingState = list._mrMissingVirtualState;
+ const state = existingState && existingState.rowsHost === rowsHost
+ ? existingState
+ : {
+ list,
+ virtualScroll,
+ rowsHost,
+ models: sortedMissingModels,
+ data,
+ rowHeight: this.getMissingBrowserVirtualRowHeight(list),
+ start: -1,
+ end: -1,
+ resizeObserver: null,
+ onScroll: null,
+ update: null
+ };
+
+ state.models = sortedMissingModels;
+ state.data = data;
+ state.rowHeight = this.getMissingBrowserVirtualRowHeight(list);
+ state.virtualScroll.style.height = `${Math.max(0, sortedMissingModels.length * state.rowHeight)}px`;
+
+ state.update = (force = false) => {
+ if (!list.isConnected || !rowsHost.isConnected) return;
+
+ const rowHeight = state.rowHeight || MISSING_ROW_FALLBACK_HEIGHT;
+ const header = list.querySelector('.mr-missing-list-head');
+ const headerHeight = Number(header?.offsetHeight) || 0;
+ const viewportHeight = Math.max(
+ rowHeight,
+ Number(list.clientHeight) || MISSING_VIRTUAL_INITIAL_ROWS * rowHeight
+ );
+ const viewportTop = Math.max(0, Number(list.scrollTop) - headerHeight);
+ const viewportBottom = Math.max(viewportTop, Number(list.scrollTop) + viewportHeight - headerHeight);
+ const nextStart = Math.max(
+ 0,
+ Math.floor(viewportTop / rowHeight) - MISSING_VIRTUAL_OVERSCAN_ROWS
+ );
+ const nextEnd = Math.min(
+ state.models.length,
+ Math.max(
+ nextStart,
+ Math.ceil(viewportBottom / rowHeight) + MISSING_VIRTUAL_OVERSCAN_ROWS
+ )
+ );
+
+ if (!force && nextStart === state.start && nextEnd === state.end) return;
+
+ state.start = nextStart;
+ state.end = nextEnd;
+ rowsHost.style.transform = `translateY(${Math.round(nextStart * rowHeight)}px)`;
+ rowsHost.innerHTML = state.models
+ .slice(nextStart, nextEnd)
+ .map((missing, offset) => this.renderMissingModelListRow(
+ missing,
+ nextStart + offset,
+ state.models.length,
+ this.selectedMissingModelKey
+ ))
+ .join('');
+ this.wireVisibleMissingModelRows(container, state.data, state.models);
+ };
+
+ if (!existingState) {
+ state.onScroll = () => state.update(false);
+ list.addEventListener('scroll', state.onScroll, { passive: true });
+ if (typeof ResizeObserver === 'function') {
+ state.resizeObserver = new ResizeObserver(() => state.update(false));
+ state.resizeObserver.observe(list);
+ }
+ list._mrMissingVirtualState = state;
+ }
+
+ state.update(true);
+ },
+
+ scrollMissingModelIntoView(missingKey) {
+ const list = this.contentElement?.querySelector?.('.mr-missing-list');
+ const state = list?._mrMissingVirtualState;
+ if (!(list instanceof HTMLElement) || !state) return null;
+
+ const index = state.models.findIndex(missing => this.getMissingModelKey(missing) === missingKey);
+ if (index < 0) return null;
+
+ const header = list.querySelector('.mr-missing-list-head');
+ const headerHeight = Number(header?.offsetHeight) || 0;
+ const rowHeight = state.rowHeight || MISSING_ROW_FALLBACK_HEIGHT;
+ const targetTop = headerHeight + index * rowHeight;
+ const targetScrollTop = targetTop - Math.max(0, (list.clientHeight - rowHeight) / 2);
+ list.scrollTop = Math.max(0, targetScrollTop);
+ state.update(true);
+ return Array.from(list.querySelectorAll('.mr-missing-list-row'))
+ .find(row => row.dataset.missingKey === missingKey) || null;
+ },
+
renderMissingModelsBrowser(missingModels, selectedKey, totalMissing, activeCount, hasAny100Match, options = {}) {
const hiddenResolvedCount = Number(options.hiddenResolvedCount || 0);
const hiddenAutoDownloadCount = Number(options.hiddenAutoDownloadCount || 0);
@@ -618,51 +796,23 @@ export const missingBrowserMethods = {
`;
- missingModels.forEach((missing, index) => {
- const key = this.getMissingModelKey(missing);
- const isSelected = key === selectedKey;
- const isBatchSelected = this.batchSelectedMissingKeys?.has(key);
- const isResolved = this.isMissingModelResolved(missing);
- const filename = this.getMissingFilename(missing);
- const { bestMatch, confidence, matchDisplay, matchClass } = this.getLocalMatchDisplayInfo(missing);
- const typeLabel = missing.category ? this.getCategoryDisplayName(missing.category) : 'unknown';
- const typeColorClass = this.getModelTypeColorClass(missing.category || typeLabel);
- const nodeDisplay = this.getMissingNodeDisplay(missing);
- const locateTarget = nodeDisplay.locateTarget || this.getMissingLocateTarget(missing);
- const nodeId = locateTarget.nodeId ?? '';
- const rowNodeHtml = nodeDisplay.canLocate
- ? ``
- : `${this.escapeHtml(nodeDisplay.text)}`;
-
- html += `
-
-
-
-
- ${index + 1}
-
- ${isResolved ? `${getSvgIcon('circleCheckBig')}` : ''}
-
- ${this.escapeHtml(filename)}
-
- ${rowNodeHtml}
- ${this.renderMissingAutoDownloadBadge(missing, { compact: true })}
-
-
-
- ${this.escapeHtml(typeLabel)}
-
- ${bestMatch ? this.escapeHtml(matchDisplay) : '-- No local match'}
-
-
- ${bestMatch ? `${confidence.toFixed(confidence % 1 ? 1 : 0)}%` : '--'}
-
- ${this.renderMissingSourcesSummary(missing)}
-
- `;
- });
+ const initialRowCount = Math.min(missingModels.length, MISSING_VIRTUAL_INITIAL_ROWS);
+ const initialRowsHtml = missingModels
+ .slice(0, initialRowCount)
+ .map((missing, index) => this.renderMissingModelListRow(
+ missing,
+ index,
+ missingModels.length,
+ selectedKey
+ ))
+ .join('');
+ html += `
+
+ `;
html += `
@@ -700,19 +850,30 @@ export const missingBrowserMethods = {
currentHead.replaceWith(nextHead);
}
+ const currentVirtualScroll = currentList.querySelector('.mr-missing-list-virtual-scroll');
+ const nextVirtualScroll = nextList.querySelector('.mr-missing-list-virtual-scroll');
+ if (currentVirtualScroll && nextVirtualScroll) {
+ currentVirtualScroll.style.height = nextVirtualScroll.style.height;
+ }
+ const currentRowsHost = currentVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || currentList;
+ const nextRowsHost = nextVirtualScroll?.querySelector('.mr-missing-list-virtual-rows') || nextList;
const currentRows = new Map(
- Array.from(currentList.querySelectorAll('.mr-missing-list-row'))
+ Array.from(currentRowsHost.querySelectorAll('.mr-missing-list-row'))
.map(row => [row.dataset.missingKey || '', row])
.filter(([key]) => key)
);
const retainedRows = new Set();
- nextList.querySelectorAll('.mr-missing-list-row').forEach((nextRow) => {
+ nextRowsHost.querySelectorAll('.mr-missing-list-row').forEach((nextRow) => {
const currentRow = currentRows.get(nextRow.dataset.missingKey || '');
const row = currentRow?.outerHTML === nextRow.outerHTML
? currentRow
: nextRow;
retainedRows.add(row);
- currentList.appendChild(row);
+ if (currentRowsHost === currentList) {
+ currentList.appendChild(row);
+ } else {
+ currentRowsHost.appendChild(row);
+ }
});
currentRows.forEach((row) => {
if (!retainedRows.has(row)) row.remove();
@@ -737,16 +898,90 @@ export const missingBrowserMethods = {
return true;
},
- wireMissingModelsBrowser(container, data, sortedMissingModels) {
- this.wireMissingBrowserSplitter(container);
-
- const browser = container.querySelector('.mr-missing-browser');
+ wireVisibleMissingModelRows(container, data, sortedMissingModels) {
const getCurrentData = () => this.cachedAnalysisData || data;
const getCurrentMissingModels = () => (
Array.isArray(this.missingModels)
? this.missingModels
: sortedMissingModels
);
+ const selectRow = (row) => {
+ const key = row.dataset.missingKey;
+ if (!key || key === this.selectedMissingModelKey) return;
+ this.selectedMissingModelKey = key;
+ this.displayMissingModels(
+ container,
+ getCurrentData(),
+ { selectionOnly: true }
+ );
+ };
+
+ container.querySelectorAll('.mr-missing-list-row').forEach((row) => {
+ if (this._wiredMissingModelRows.has(row)) return;
+ this._wiredMissingModelRows.add(row);
+ const checkbox = row.querySelector('.mr-missing-row-check');
+ if (checkbox) {
+ checkbox.addEventListener('click', (event) => {
+ event.stopPropagation();
+ checkbox._missingShiftClick = event.shiftKey;
+ });
+ checkbox.addEventListener('change', (event) => {
+ const key = row.dataset.missingKey;
+ if (!key) return;
+ const selected = checkbox.checked;
+ const isShiftRange = event.shiftKey || checkbox._missingShiftClick === true;
+ const currentMissingModels = getCurrentMissingModels();
+
+ if (isShiftRange && this.lastBatchSelectedMissingKey) {
+ this.applyBatchSelectionRange(
+ currentMissingModels,
+ this.lastBatchSelectedMissingKey,
+ key,
+ selected
+ );
+ } else {
+ this.setBatchSelectionForKey(key, selected);
+ }
+
+ this.lastBatchSelectedMissingKey = key;
+ this.refreshBatchSelectionUi();
+ this.updateBatchFooterButtons();
+ });
+ }
+
+ row.addEventListener('click', (event) => {
+ const clickedLocate = event.target instanceof Element && event.target.closest('.mr-missing-row-locate');
+ if (clickedLocate) return;
+ selectRow(row);
+ });
+
+ row.addEventListener('keydown', (event) => {
+ if (event.target !== row || (event.key !== 'Enter' && event.key !== ' ')) return;
+ event.preventDefault();
+ selectRow(row);
+ });
+ });
+
+ container.querySelectorAll('.mr-missing-row-locate').forEach((button) => {
+ if (this._wiredMissingLocateButtons.has(button)) return;
+ this._wiredMissingLocateButtons.add(button);
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ const rawNodeId = button.dataset.nodeId;
+ const numericNodeId = Number(rawNodeId);
+ this.locateNodeInGraph(Number.isNaN(numericNodeId) ? rawNodeId : numericNodeId, {
+ subgraphId: button.dataset.subgraphId || '',
+ isTopLevel: button.dataset.isTopLevel !== 'false'
+ });
+ });
+ });
+ },
+
+ wireMissingModelsBrowser(container, data, sortedMissingModels) {
+ this.wireMissingBrowserSplitter(container);
+
+ const browser = container.querySelector('.mr-missing-browser');
if (!(this._wiredMissingModelRows instanceof WeakSet)) {
this._wiredMissingModelRows = new WeakSet();
}
@@ -856,19 +1091,14 @@ export const missingBrowserMethods = {
});
}
- const selectRow = (row) => {
- const key = row.dataset.missingKey;
- if (!key || key === this.selectedMissingModelKey) return;
- this.selectedMissingModelKey = key;
- this.displayMissingModels(
- container,
- getCurrentData(),
- { selectionOnly: true }
- );
- };
-
+ const getCurrentMissingModels = () => (
+ Array.isArray(this.missingModels)
+ ? this.missingModels
+ : sortedMissingModels
+ );
const selectAllCheckbox = container.querySelector('.mr-missing-select-all-check');
- if (selectAllCheckbox) {
+ if (selectAllCheckbox && selectAllCheckbox.dataset.mlSelectAllBound !== 'true') {
+ selectAllCheckbox.dataset.mlSelectAllBound = 'true';
this.updateBatchSelectAllCheckbox();
selectAllCheckbox.addEventListener('click', (event) => {
event.stopPropagation();
@@ -884,66 +1114,8 @@ export const missingBrowserMethods = {
});
}
- container.querySelectorAll('.mr-missing-list-row').forEach(row => {
- if (this._wiredMissingModelRows.has(row)) return;
- this._wiredMissingModelRows.add(row);
- const checkbox = row.querySelector('.mr-missing-row-check');
- if (checkbox) {
- checkbox.addEventListener('click', (event) => {
- event.stopPropagation();
- checkbox._missingShiftClick = event.shiftKey;
- });
- checkbox.addEventListener('change', (event) => {
- const key = row.dataset.missingKey;
- if (!key) return;
- const selected = checkbox.checked;
- const isShiftRange = event.shiftKey || checkbox._missingShiftClick === true;
- const currentMissingModels = getCurrentMissingModels();
-
- if (isShiftRange && this.lastBatchSelectedMissingKey) {
- this.applyBatchSelectionRange(
- currentMissingModels,
- this.lastBatchSelectedMissingKey,
- key,
- selected
- );
- } else {
- this.setBatchSelectionForKey(key, selected);
- }
-
- this.lastBatchSelectedMissingKey = key;
- this.refreshBatchSelectionUi();
- this.updateBatchFooterButtons();
- });
- }
-
- row.addEventListener('click', (event) => {
- const clickedLocate = event.target instanceof Element && event.target.closest('.mr-missing-row-locate');
- if (clickedLocate) return;
- selectRow(row);
- });
-
- row.addEventListener('keydown', (event) => {
- if (event.target !== row || (event.key !== 'Enter' && event.key !== ' ')) return;
- event.preventDefault();
- selectRow(row);
- });
- });
-
- container.querySelectorAll('.mr-missing-row-locate').forEach(button => {
- if (this._wiredMissingLocateButtons.has(button)) return;
- this._wiredMissingLocateButtons.add(button);
- button.addEventListener('click', (event) => {
- event.preventDefault();
- event.stopPropagation();
- const rawNodeId = button.dataset.nodeId;
- const numericNodeId = Number(rawNodeId);
- this.locateNodeInGraph(Number.isNaN(numericNodeId) ? rawNodeId : numericNodeId, {
- subgraphId: button.dataset.subgraphId || '',
- isTopLevel: button.dataset.isTopLevel !== 'false'
- });
- });
- });
+ this.wireVisibleMissingModelRows(container, data, sortedMissingModels);
+ this.setupMissingModelsVirtualizer(container, data, sortedMissingModels);
const selectedMissing = sortedMissingModels.find(missing => this.getMissingModelKey(missing) === this.selectedMissingModelKey);
const detailPreserved = this._missingBrowserDetailPreserved === true;
@@ -1241,7 +1413,7 @@ export const missingBrowserMethods = {
startWidth: detailWidth,
bounds,
dragThreshold: 4,
- layoutFrameStride: 3,
+ layoutFrameStride: 1,
onPreview: (pendingWidth) => {
this._pendingMissingBrowserSplitWidth = pendingWidth;
this.activateMissingBrowserSplitUi();
@@ -2283,6 +2455,7 @@ export const missingBrowserMethods = {
}
this.wireMissingModelsBrowser(container, data, sortedMissingModels);
this.restoreMissingListScroll(container, listScrollSnapshot);
+ this.refreshMissingModelsVirtualizer(container, true);
this.scheduleInitialUrnLocalMatchRefresh(sortedMissingModels, container, data);
this.reconnectActiveSearchProgress(sortedMissingModels);
this.updateBatchFooterButtons();