diff --git a/web/css/linker-main.css b/web/css/linker-main.css index d8c1dc1..a424986 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -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; diff --git a/web/linker.js b/web/linker.js index bcbaafe..d1e8644 100644 --- a/web/linker.js +++ b/web/linker.js @@ -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 += ` -
| File ? | +File ? | |
| Hash (sha256) ? | +Hash (sha256) ? | |
| CivitAI ? | +CivitAI ? | |
| Name ? | +Name ? | |
| Base Model ? | +Base Model ? | |
|
- Trained Words ?
+ Trained Words ?
0 selected
|
||
| Clip Skip ? | +Clip Skip ? | |
| Description ? | +Description ? |
@@ -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('')} `;
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 {
@@ -2619,7 +2678,7 @@ class LinkerManagerDialog extends ComfyDialog {
@@ -2667,7 +2726,7 @@ class LinkerManagerDialog extends ComfyDialog {
@@ -2676,7 +2735,7 @@ class LinkerManagerDialog extends ComfyDialog {
@@ -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 `${iconHtml}`;
+ return `${iconHtml}`;
}).join('');
}
@@ -4163,11 +4221,11 @@ class LinkerManagerDialog extends ComfyDialog {
data-missing-key="${this.escapeHtml(key)}">
${index + 1}
- ${this.escapeHtml(formattedFilename.display)}
+ ${this.escapeHtml(formattedFilename.display)}
${this.escapeHtml(nodeLabel)} #${this.escapeHtml(String(missing.node_id || ''))}
${this.escapeHtml(typeLabel)}
-
+
${bestMatch ? this.escapeHtml(matchDisplay) : '-- No local match'}
@@ -4250,7 +4308,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `${this.escapeHtml(label)}`;
html += ``;
if (folderDisplay) {
- html += `Folder ${this.escapeHtml(folderDisplay)} `;
+ html += `Folder ${this.escapeHtml(folderDisplay)} `;
}
html += ``;
}
@@ -4437,7 +4495,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += ``;
const titleMetaParts = [];
- let titlePrimaryHtml = `${missingFilename.display}`;
+ let titlePrimaryHtml = `${missingFilename.display}`;
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(`${missingFilename.display}`);
+ titleMetaParts.push(`${missingFilename.display}`);
}
if (missing.is_urn && !missing.civitai_info) {
@@ -4479,9 +4537,9 @@ class LinkerManagerDialog extends ComfyDialog {
html += ` `;
if (missing.category) {
- html += `${this.getCategoryDisplayName(missing.category)}`;
+ html += `${this.getCategoryDisplayName(missing.category)}`;
}
- html += ``;
+ html += ``;
if (missing.is_top_level !== false) {
html += this.getLocateIconHtml();
}
@@ -4510,7 +4568,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += ` `;
@@ -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() {
`;
html += ``;
html += ``;
- html += ` `;
html += ``;
html += ` |