Migrate tooltips from title attributes to data-tooltip with custom JS implementation

This commit is contained in:
Dariusz L
2026-05-27 16:03:44 +02:00
parent 19f9d83e4a
commit 246fc376f3
2 changed files with 134 additions and 87 deletions
+10 -23
View File
@@ -2419,29 +2419,6 @@
border-color: rgba(78,161,255,0.28);
color: #d6eaff;
}
.ml-info-help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
background: rgba(255,255,255,0.14);
color: var(--ml-text-muted, #b4becb);
border-radius: 50%;
font-size: 10px;
font-weight: 700;
cursor: help;
margin-left: 4px;
vertical-align: middle;
border: 1px solid rgba(255,255,255,0.12);
user-select: none;
}
.ml-info-help:hover {
background: rgba(78,161,255,0.18);
color: #d9ecff;
border-color: rgba(78,161,255,0.3);
}
/* Images Section */
.ml-info-images {
margin-top: 16px;
@@ -2782,6 +2759,16 @@
background: rgba(255,255,255,0.03);
cursor: help;
flex: 0 0 auto;
margin-left: 4px;
vertical-align: middle;
user-select: none;
}
.ml-tooltip-badge:hover,
.ml-tooltip-badge:focus-visible {
background: rgba(78,161,255,0.18);
color: #d9ecff;
border-color: rgba(78,161,255,0.3);
outline: none;
}
.ml-global-tooltip {
position: fixed;
+124 -64
View File
@@ -83,6 +83,21 @@ class LinkerManagerDialog extends ComfyDialog {
this.createContent(),
this.createFooter()
]);
this.tooltipObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
this.bindTooltips(node);
}
}
}
});
this.tooltipObserver.observe(this.element, {
childList: true,
subtree: true
});
this.bindTooltips(this.element);
// Add click listener to hide context menu when clicking outside
this.boundHandleContextMenuClick = (e) => this.handleContextMenuOutsideClick(e);
@@ -145,6 +160,7 @@ class LinkerManagerDialog extends ComfyDialog {
showTooltip(target) {
if (!target || !this.tooltipElement) return;
this.normalizeTooltipTarget(target);
const text = target.getAttribute('data-tooltip');
if (!text) return;
@@ -175,14 +191,51 @@ class LinkerManagerDialog extends ComfyDialog {
this.tooltipElement.removeAttribute('data-visible');
}
normalizeTooltipTarget(target) {
if (!target || !target.getAttribute) return;
const title = target.getAttribute('title');
if (title && !target.getAttribute('data-tooltip')) {
target.setAttribute('data-tooltip', title);
}
if (target.hasAttribute('title')) {
target.removeAttribute('title');
}
if (target.classList?.contains('ml-tooltip-badge') && !target.hasAttribute('tabindex')) {
target.setAttribute('tabindex', '0');
}
}
setTooltip(target, text) {
if (!target || !text) return;
target.setAttribute('data-tooltip', text);
if (target.hasAttribute('title')) {
target.removeAttribute('title');
}
this.bindTooltips(target);
}
bindTooltips(container) {
if (!container) return;
container.querySelectorAll('.ml-tooltip-badge').forEach((badge) => {
badge.addEventListener('mouseenter', () => this.showTooltip(badge));
badge.addEventListener('focus', () => this.showTooltip(badge));
badge.addEventListener('mouseleave', () => this.hideTooltip());
badge.addEventListener('blur', () => this.hideTooltip());
const selector = '[data-tooltip], [title]';
const targets = [];
if (container.matches?.(selector)) {
targets.push(container);
}
if (container.querySelectorAll) {
targets.push(...container.querySelectorAll(selector));
}
targets.forEach((target) => {
this.normalizeTooltipTarget(target);
if (!target.dataset) return;
if (target.dataset.mlTooltipBound === '1') return;
target.dataset.mlTooltipBound = '1';
target.addEventListener('mouseenter', () => this.showTooltip(target));
target.addEventListener('focus', () => this.showTooltip(target));
target.addEventListener('mouseleave', () => this.hideTooltip());
target.addEventListener('blur', () => this.hideTooltip());
});
}
@@ -364,7 +417,7 @@ class LinkerManagerDialog extends ComfyDialog {
const statusLabel = progress?.message || statusLabels[status] || status;
const title = `${label}: ${statusLabel}`;
html += `
<div class="ml-search-progress-item ml-search-progress-${statusClass}" title="${this.escapeHtml(title)}">
<div class="ml-search-progress-item ml-search-progress-${statusClass}" data-tooltip="${this.escapeHtml(title)}">
<span class="ml-search-progress-source">${this.escapeHtml(label)}</span>
<span class="ml-search-progress-status">${this.escapeHtml(statusLabel)}</span>
</div>
@@ -486,7 +539,7 @@ class LinkerManagerDialog extends ComfyDialog {
const iconName = this.getSearchSourceIconName(sourceKey);
const iconHtml = getSvgIcon(iconName, 'currentColor', 'ml-search-source-icon');
return `
<span class="ml-search-source-pill ml-search-source-${sourceKey}" title="${this.escapeHtml(sourceLabel)}">
<span class="ml-search-source-pill ml-search-source-${sourceKey}" data-tooltip="${this.escapeHtml(sourceLabel)}">
${iconHtml}
<span>${this.escapeHtml(sourceLabel)}</span>
</span>
@@ -678,7 +731,7 @@ class LinkerManagerDialog extends ComfyDialog {
if (downloadUrl) {
actions += `
<button type="button" class="search-download-btn ml-search-result-action-btn"
title="Download"
data-tooltip="Download"
aria-label="Download ${this.escapeHtml(downloadFilename)}"
data-url="${this.escapeHtml(downloadUrl)}"
data-filename="${this.escapeHtml(downloadFilename)}"
@@ -689,7 +742,7 @@ class LinkerManagerDialog extends ComfyDialog {
actions += `
<button type="button"
class="search-open-page-btn ml-search-result-action-btn"
title="Open model page"
data-tooltip="Open model page"
aria-label="Open model page"
data-url="${this.escapeHtml(openUrl)}">${getSvgIcon('externalLink')}</button>
`;
@@ -702,7 +755,7 @@ class LinkerManagerDialog extends ComfyDialog {
<tr>
<td>${sourcePill}</td>
<td>
<div class="ml-search-result-model" title="${modelTitle}">
<div class="ml-search-result-model" data-tooltip="${modelTitle}">
<span>${modelHtml}</span>
${secondary || filename ? `<small>${secondary || filename}</small>` : ''}
</div>
@@ -814,9 +867,9 @@ class LinkerManagerDialog extends ComfyDialog {
html += `<div class="ml-match-row ${isBestMatch ? 'ml-best-match' : ''}" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">`;
html += this.getConfidenceBadge(match.confidence);
html += `<span class="ml-match-filename" title="${formattedPath.full}">${formattedPath.display}</span>`;
html += `<span class="ml-match-filename" data-tooltip="${this.escapeHtml(formattedPath.full)}">${formattedPath.display}</span>`;
html += `<span class="ml-match-status ${match.confidence === 100 ? 'ml-match-status-exact' : 'ml-match-status-partial'}">${match.confidence === 100 ? 'Exact' : 'Partial'}</span>`;
html += `<button id="${buttonId}" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only ml-local-link-btn" title="Link this local match" aria-label="Link this local match">`;
html += `<button id="${buttonId}" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only ml-local-link-btn" data-tooltip="Link this local match" aria-label="Link this local match">`;
html += getSvgIcon('link');
html += `</button>`;
html += `</div>`;
@@ -838,9 +891,9 @@ class LinkerManagerDialog extends ComfyDialog {
const modelData = encodeURIComponent(JSON.stringify(contextModel));
html += `<div class="ml-match-row" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">`;
html += this.getConfidenceBadge(match.confidence);
html += `<span class="ml-match-filename" title="${match.path || match.filename}">${match.filename || match.path?.split(/[/\\]/).pop()}</span>`;
html += `<span class="ml-match-filename" data-tooltip="${this.escapeHtml(match.path || match.filename)}">${match.filename || match.path?.split(/[/\\]/).pop()}</span>`;
html += `<span class="ml-match-status ml-match-status-partial">Partial</span>`;
html += `<button id="${altBtnId}" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only ml-local-link-btn" title="Link this local match" aria-label="Link this local match">${getSvgIcon('link')}</button>`;
html += `<button id="${altBtnId}" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only ml-local-link-btn" data-tooltip="Link this local match" aria-label="Link this local match">${getSvgIcon('link')}</button>`;
html += `</div>`;
}
html += `</div>`;
@@ -1082,6 +1135,7 @@ class LinkerManagerDialog extends ComfyDialog {
// Show the dialog
document.body.appendChild(dialog);
this.bindTooltips(dialog);
this.bindInfoDialogResizePersistence(dialog);
// Add close handlers
@@ -1133,29 +1187,29 @@ class LinkerManagerDialog extends ComfyDialog {
<table class="ml-info-table">
<tbody>
<tr class="ml-info-file-row">
<td><span>File <span class="ml-info-help" title="The model file name found locally or returned by CivitAI.">?</span></span></td>
<td><span>File <span class="ml-tooltip-badge" data-tooltip="The model file name found locally or returned by CivitAI.">?</span></span></td>
<td><span class="ml-info-file"></span></td>
</tr>
<tr class="ml-info-hash-row">
<td><span>Hash (sha256) <span class="ml-info-help" title="Unique fingerprint of the local file. Model Linker uses it to confirm the exact CivitAI version.">?</span></span></td>
<td><span>Hash (sha256) <span class="ml-tooltip-badge" data-tooltip="Unique fingerprint of the local file. Model Linker uses it to confirm the exact CivitAI version.">?</span></span></td>
<td><span class="ml-info-hash"></span></td>
</tr>
<tr class="ml-info-civitai-row">
<td><span>CivitAI <span class="ml-info-help" title="Opens the matching CivitAI model or version page when one was found.">?</span></span></td>
<td><span>CivitAI <span class="ml-tooltip-badge" data-tooltip="Opens the matching CivitAI model or version page when one was found.">?</span></span></td>
<td><span class="ml-info-civitai-link"></span></td>
</tr>
<tr class="ml-info-name-row">
<td><span>Name <span class="ml-info-help" title="Model name from CivitAI or local metadata.">?</span></span></td>
<td><span>Name <span class="ml-tooltip-badge" data-tooltip="Model name from CivitAI or local metadata.">?</span></span></td>
<td><span class="ml-info-name"></span></td>
</tr>
<tr class="ml-info-basemodel-row">
<td><span>Base Model <span class="ml-info-help" title="Base model this resource was made for, for example SD1.5, SDXL or Flux.">?</span></span></td>
<td><span>Base Model <span class="ml-tooltip-badge" data-tooltip="Base model this resource was made for, for example SD1.5, SDXL or Flux.">?</span></span></td>
<td><span class="ml-info-base-model"></span></td>
</tr>
<tr class="ml-info-trainedwords-row ml-hidden-initial">
<td>
<div class="ml-info-trained-words-label">
Trained Words <span class="ml-info-help" title="Trigger words recommended by the model author. Click the words you want, then copy them into your prompt.">?</span>
Trained Words <span class="ml-tooltip-badge" data-tooltip="Trigger words recommended by the model author. Click the words you want, then copy them into your prompt.">?</span>
<small class="ml-info-trained-words-meta">
<span class="ml-info-trained-words-count">0 selected</span>
<button type="button" class="ml-info-copy-trained-words" disabled>Copy</button>
@@ -1168,11 +1222,11 @@ class LinkerManagerDialog extends ComfyDialog {
</td>
</tr>
<tr class="ml-info-clipskip-row ml-hidden-initial">
<td><span>Clip Skip <span class="ml-info-help" title="Recommended Clip Skip value from the model author, if one is provided.">?</span></span></td>
<td><span>Clip Skip <span class="ml-tooltip-badge" data-tooltip="Recommended Clip Skip value from the model author, if one is provided.">?</span></span></td>
<td><span class="ml-info-clip-skip"></span></td>
</tr>
<tr class="ml-info-description-row ml-hidden-initial">
<td><span>Description <span class="ml-info-help" title="Model description from CivitAI or local metadata. Long descriptions are shortened until you click Show more.">?</span></span></td>
<td><span>Description <span class="ml-tooltip-badge" data-tooltip="Model description from CivitAI or local metadata. Long descriptions are shortened until you click Show more.">?</span></span></td>
<td>
<div class="ml-info-description-wrap">
<div class="ml-info-description"></div>
@@ -1596,7 +1650,7 @@ class LinkerManagerDialog extends ComfyDialog {
type="button"
class="ml-info-trained-word"
data-word="${this.escapeHtml(word)}"
title="${this.escapeHtml(word)}"
data-tooltip="${this.escapeHtml(word)}"
aria-pressed="false"
>
${this.escapeHtml(word)}
@@ -1604,6 +1658,7 @@ class LinkerManagerDialog extends ComfyDialog {
`).join('')}</div>`;
const row = trainedWordsEl.closest('tr');
if (row) row.style.display = '';
this.bindTooltips(trainedWordsEl);
this.updateSelectedTrainedWordsSummary(dialog);
} else {
const row = trainedWordsEl.closest('tr');
@@ -1951,27 +2006,31 @@ class LinkerManagerDialog extends ComfyDialog {
textContent: "Options",
onclick: () => this.switchTab('options')
});
const dragHandle = $el("div", {
id: "model-linker-drag-handle",
ondragstart: (e) => e.preventDefault()
}, [
$el("span", { textContent: "⠿" })
]);
this.setTooltip(dragHandle, "Drag window");
const fullscreenButton = $el("button", {
id: "model-linker-fullscreen-toggle",
className: "ml-window-btn ml-window-btn--fullscreen",
textContent: "⛶",
onclick: () => this.toggleFullScreen()
});
this.setTooltip(fullscreenButton, "Toggle full screen");
return $el("div.ml-dialog-shell", {}, [
$el("div.ml-dialog-topbar", {}, [
$el("div.ml-dialog-brand", {}, [
$el("div", {
id: "model-linker-drag-handle",
title: "Drag window",
ondragstart: (e) => e.preventDefault()
}, [
$el("span", { textContent: "⠿" })
]),
dragHandle,
$el("h2.ml-dialog-title", { textContent: "🔗 Model Linker" })
]),
$el("div.ml-dialog-controls", {}, [
$el("button", {
id: "model-linker-fullscreen-toggle",
className: "ml-window-btn ml-window-btn--fullscreen",
title: "Toggle full screen",
textContent: "⛶",
onclick: () => this.toggleFullScreen()
}),
fullscreenButton,
$el("button", {
className: "ml-window-btn ml-window-btn--close",
textContent: "×",
@@ -2610,7 +2669,7 @@ class LinkerManagerDialog extends ComfyDialog {
<div class="ml-options-input-row">
<label for="ml-options-civitai" class="ml-options-label">CivitAI API Key <a href="https://civitai.com/user/account" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used when downloading from CivitAI requires your account. Add this if CivitAI downloads fail or need access to models available to your account.">?</span></label>
<input id="ml-options-civitai" class="ml-options-input" type="password" placeholder="Paste CivitAI API key" value="${tokens.civitai_key}">
<button id="ml-options-civitai-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI API key" title="Show saved value">
<button id="ml-options-civitai-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI API key" data-tooltip="Show saved value">
${getSvgIcon('eye')}
</button>
</div>
@@ -2619,7 +2678,7 @@ class LinkerManagerDialog extends ComfyDialog {
<div class="ml-options-input-row">
<label for="ml-options-civitai-session" class="ml-options-label">CivitAI Session Token <span class="ml-tooltip-badge" data-tooltip="Optional. Makes CivitAI search use your logged-in session, so results can match what you see in the browser. Useful for NSFW or account-visible results.">?</span></label>
<input id="ml-options-civitai-session" class="ml-options-input" type="password" placeholder="Paste __Secure-civitai-token" value="${tokens.civitai_session_token}">
<button id="ml-options-civitai-session-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI session token" title="Show saved value">
<button id="ml-options-civitai-session-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI session token" data-tooltip="Show saved value">
${getSvgIcon('eye')}
</button>
</div>
@@ -2667,7 +2726,7 @@ class LinkerManagerDialog extends ComfyDialog {
<div class="ml-options-input-row">
<label for="ml-options-hf" class="ml-options-label">HuggingFace Token <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used to search and download files from Hugging Face repos your account can access, including gated repos. A read-only token is enough.">?</span></label>
<input id="ml-options-hf" class="ml-options-input" type="password" placeholder="Paste HuggingFace token" value="${tokens.hf_token}">
<button id="ml-options-hf-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved HuggingFace token" title="Show saved value">
<button id="ml-options-hf-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved HuggingFace token" data-tooltip="Show saved value">
${getSvgIcon('eye')}
</button>
</div>
@@ -2676,7 +2735,7 @@ class LinkerManagerDialog extends ComfyDialog {
<div class="ml-options-input-row">
<label for="ml-options-brave" class="ml-options-label">Brave Search API Key <a href="https://api-dashboard.search.brave.com/app/keys" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used only by the Brave fallback. It helps find Hugging Face files when Hugging Face search does not show the right repo.">?</span></label>
<input id="ml-options-brave" class="ml-options-input" type="password" placeholder="Paste Brave Search API key" value="${tokens.brave_search_api_key}">
<button id="ml-options-brave-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved Brave Search API key" title="Show saved value">
<button id="ml-options-brave-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved Brave Search API key" data-tooltip="Show saved value">
${getSvgIcon('eye')}
</button>
</div>
@@ -2782,7 +2841,7 @@ class LinkerManagerDialog extends ComfyDialog {
button.innerHTML = getVisibilityIcon(visible);
button.style.color = visible ? 'var(--ml-text)' : 'var(--ml-text-muted)';
button.setAttribute('aria-pressed', visible ? 'true' : 'false');
button.setAttribute('title', visible ? 'Hide saved value' : 'Show saved value');
this.setTooltip(button, visible ? 'Hide saved value' : 'Show saved value');
};
const bindVisibilityToggle = (input, button) => {
@@ -3125,9 +3184,9 @@ class LinkerManagerDialog extends ComfyDialog {
// Splitter between content and queue
this.splitterElement = $el("div", {
id: "model-linker-splitter",
title: "Drag to resize panels",
ondragstart: (e) => e.preventDefault()
});
this.setTooltip(this.splitterElement, "Drag to resize panels");
body.appendChild(this.contentElement);
body.appendChild(this.splitterElement);
@@ -3154,7 +3213,6 @@ class LinkerManagerDialog extends ComfyDialog {
try {
this.queueToggleIcon = $el("button", {
id: "queue-toggle-icon",
title: "Hide queued selections",
onclick: () => this.toggleQueueCollapsed()
}, [document.createTextNode('⮜')]);
body.appendChild(this.queueToggleIcon);
@@ -3369,10 +3427,10 @@ class LinkerManagerDialog extends ComfyDialog {
if (!this.queueToggleIcon) return;
if (this.queueCollapsed) {
this.queueToggleIcon.textContent = '⮞';
this.queueToggleIcon.title = 'Show queued selections';
this.setTooltip(this.queueToggleIcon, 'Show queued selections');
} else {
this.queueToggleIcon.textContent = '⮜';
this.queueToggleIcon.title = 'Hide queued selections';
this.setTooltip(this.queueToggleIcon, 'Hide queued selections');
}
}
@@ -4072,7 +4130,7 @@ class LinkerManagerDialog extends ComfyDialog {
const title = `${label}: ${statusLabels[status] || status}`;
const iconName = this.getSearchSourceIconName(item.source);
const iconHtml = getSvgIcon(iconName, 'currentColor', 'ml-missing-source-icon');
return `<span class="ml-missing-source-dot ml-missing-source-${statusClass}" title="${this.escapeHtml(title)}" aria-label="${this.escapeHtml(title)}">${iconHtml}</span>`;
return `<span class="ml-missing-source-dot ml-missing-source-${statusClass}" data-tooltip="${this.escapeHtml(title)}" aria-label="${this.escapeHtml(title)}">${iconHtml}</span>`;
}).join('');
}
@@ -4163,11 +4221,11 @@ class LinkerManagerDialog extends ComfyDialog {
data-missing-key="${this.escapeHtml(key)}">
<span class="ml-missing-row-index">${index + 1}</span>
<span class="ml-missing-row-model">
<span class="ml-missing-row-name" title="${this.escapeHtml(filename)}">${this.escapeHtml(formattedFilename.display)}</span>
<span class="ml-missing-row-name" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(formattedFilename.display)}</span>
<span class="ml-missing-row-node">${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(missing.node_id || ''))}</span>
</span>
<span class="ml-missing-row-type">${this.escapeHtml(typeLabel)}</span>
<span class="ml-missing-row-best" title="${this.escapeHtml(matchDisplay)}">
<span class="ml-missing-row-best" data-tooltip="${this.escapeHtml(matchDisplay)}">
${bestMatch ? this.escapeHtml(matchDisplay) : '<span class="ml-missing-row-none">-- No local match</span>'}
</span>
<span class="ml-missing-row-match ml-missing-row-match-${matchClass}">
@@ -4250,7 +4308,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `<code>${this.escapeHtml(label)}</code>`;
html += `</div>`;
if (folderDisplay) {
html += `<div class="ml-combo-folder" title="${this.escapeHtml(folderDisplay)}">Folder ${this.escapeHtml(folderDisplay)}</div>`;
html += `<div class="ml-combo-folder" data-tooltip="${this.escapeHtml(folderDisplay)}">Folder ${this.escapeHtml(folderDisplay)}</div>`;
}
html += `</div>`;
}
@@ -4437,7 +4495,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `<div class="ml-card-title-wrap">`;
const titleMetaParts = [];
let titlePrimaryHtml = `<span class="ml-card-title-primary" title="${missingFilename.full}">${missingFilename.display}</span>`;
let titlePrimaryHtml = `<span class="ml-card-title-primary" data-tooltip="${this.escapeHtml(missingFilename.full)}">${missingFilename.display}</span>`;
let titleSecondaryHtml = '';
const modelId = missing.urn_model_id || missing.urn?.model_id;
@@ -4446,7 +4504,7 @@ class LinkerManagerDialog extends ComfyDialog {
const urnLoadingId = `urn-loading-${missing.node_id}-${missing.widget_index}`;
if (missing.is_urn) {
titleMetaParts.push(`<span class="ml-card-title-eyebrow" title="${missingFilename.full}">${missingFilename.display}</span>`);
titleMetaParts.push(`<span class="ml-card-title-eyebrow" data-tooltip="${this.escapeHtml(missingFilename.full)}">${missingFilename.display}</span>`);
}
if (missing.is_urn && !missing.civitai_info) {
@@ -4479,9 +4537,9 @@ class LinkerManagerDialog extends ComfyDialog {
html += `<div class="ml-card-subtitle">`;
if (missing.category) {
html += `<span class="ml-category-chip" title="${missing.category}">${this.getCategoryDisplayName(missing.category)}</span>`;
html += `<span class="ml-category-chip" data-tooltip="${this.escapeHtml(missing.category)}">${this.getCategoryDisplayName(missing.category)}</span>`;
}
html += `<span id="${locateId}" class="${nodeChipClasses}"${nodeChipTitle ? ` title="${nodeChipTitle}"` : ''}>`;
html += `<span id="${locateId}" class="${nodeChipClasses}"${nodeChipTitle ? ` data-tooltip="${this.escapeHtml(nodeChipTitle)}"` : ''}>`;
if (missing.is_top_level !== false) {
html += this.getLocateIconHtml();
}
@@ -4510,7 +4568,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `<div class="ml-combo-row">`;
html += `<label class="ml-combo-label">Model</label>`;
html += `<input id="combo-input-${comboId}" class="ml-combo-input" type="text" placeholder="Type to filter local models...">`;
html += `<button id="combo-refresh-${comboId}" title="Reload local model list" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only">⟳</button>`;
html += `<button id="combo-refresh-${comboId}" data-tooltip="Reload local model list" class="ml-btn ml-btn-secondary ml-btn-sm ml-btn-icon-only">⟳</button>`;
html += `</div>`;
html += `<div id="combo-list-${comboId}" class="ml-combo-list"></div>`;
html += `</div>`;
@@ -5954,14 +6012,16 @@ class ModelLinker {
const { ComfyButton } = await import("../../../scripts/ui/components/button.js");
// Create button group with Model Linker button
const modelLinkerTooltip = "Open Model Linker to find or download missing workflow models. Shortcut: Ctrl+Shift+L.";
const modelLinkerButton = new ComfyButton({
icon: "link-variant",
action: () => this.openLinkerManager(),
content: "Model Linker",
classList: "comfyui-button comfyui-menu-mobile-collapse"
}).element;
this.dialog.setTooltip(modelLinkerButton, modelLinkerTooltip);
this.buttonGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "link-variant",
action: () => this.openLinkerManager(),
tooltip: "Open Model Linker to find or download missing workflow models. Shortcut: Ctrl+Shift+L.",
content: "Model Linker",
classList: "comfyui-button comfyui-menu-mobile-collapse"
}).element
modelLinkerButton
);
// Insert before settings group in the menu
@@ -6038,7 +6098,7 @@ class ModelLinker {
autoResolveBtn.id = 'model-linker-btn-container'; // Use this ID to prevent duplicate injection
autoResolveBtn.className = 'ml-popup-auto-resolve-btn';
autoResolveBtn.textContent = '🔗 Auto-resolve 100%';
autoResolveBtn.title = 'Link every missing model that already has an exact local match, then open Model Linker for the rest.';
this.dialog?.setTooltip(autoResolveBtn, 'Link every missing model that already has an exact local match, then open Model Linker for the rest.');
autoResolveBtn.addEventListener('click', async () => {
await this.handleAutoResolveInPopup(dialog, autoResolveBtn);
});
@@ -6368,7 +6428,6 @@ class ModelLinker {
this.linkerButton = $el("button", {
id: this.buttonId,
textContent: "🔗 Model Linker",
title: "Open Model Linker to find or download missing workflow models. Shortcut: Ctrl+Shift+L.",
onclick: () => {
this.openLinkerManager();
},
@@ -6376,6 +6435,7 @@ class ModelLinker {
});
document.body.appendChild(this.linkerButton);
this.dialog?.setTooltip(this.linkerButton, "Open Model Linker to find or download missing workflow models. Shortcut: Ctrl+Shift+L.");
}
openLinkerManager() {