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 += ` -
+
${this.escapeHtml(label)} ${this.escapeHtml(statusLabel)}
@@ -486,7 +539,7 @@ class LinkerManagerDialog extends ComfyDialog { const iconName = this.getSearchSourceIconName(sourceKey); const iconHtml = getSvgIcon(iconName, 'currentColor', 'ml-search-source-icon'); return ` - + ${iconHtml} ${this.escapeHtml(sourceLabel)} @@ -678,7 +731,7 @@ class LinkerManagerDialog extends ComfyDialog { if (downloadUrl) { actions += ` `; @@ -702,7 +755,7 @@ class LinkerManagerDialog extends ComfyDialog { ${sourcePill} -
+
${modelHtml} ${secondary || filename ? `${secondary || filename}` : ''}
@@ -814,9 +867,9 @@ class LinkerManagerDialog extends ComfyDialog { html += `
`; html += this.getConfidenceBadge(match.confidence); - html += `${formattedPath.display}`; + html += `${formattedPath.display}`; html += `${match.confidence === 100 ? 'Exact' : 'Partial'}`; - html += ``; html += `
`; @@ -838,9 +891,9 @@ class LinkerManagerDialog extends ComfyDialog { const modelData = encodeURIComponent(JSON.stringify(contextModel)); html += `
`; html += this.getConfidenceBadge(match.confidence); - html += `${match.filename || match.path?.split(/[/\\]/).pop()}`; + html += `${match.filename || match.path?.split(/[/\\]/).pop()}`; html += `Partial`; - html += ``; + html += ``; html += `
`; } html += `
`; @@ -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 { - + - + - + - + - + - + - +
File ?File ?
Hash (sha256) ?Hash (sha256) ?
CivitAI ?CivitAI ?
Name ?Name ?
Base Model ?Base Model ?
- Trained Words ? + Trained Words ? 0 selected @@ -1168,11 +1222,11 @@ class LinkerManagerDialog extends ComfyDialog {
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 += `
`; html += ``; html += ``; - html += ``; + html += ``; html += `
`; html += `
`; 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() {