From 857858ee6e0b9b5a64cc23597e1c6d6aa0cfa85f Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Wed, 20 Aug 2025 16:53:04 +0100 Subject: [PATCH 1/5] Update setHints.js to avoid sticking hints As described in #4137, this add a simple click handler to hide the hint when the mouse button is pressed. --- javascript/setHints.js | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/setHints.js b/javascript/setHints.js index b8fbafe5f..098c6876e 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -329,6 +329,7 @@ async function setHints(analyze = false) { } el.addEventListener('mouseover', tooltipShow); el.addEventListener('mouseout', tooltipHide); + el.addEventListener('click', tooltipHide); } else { // tooltips disabled } From 04b7b7652c54d83528687c721d6961b71c429180 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Mon, 25 Aug 2025 20:36:42 +0100 Subject: [PATCH 2/5] Fix Gradio removing event listeners and clearing attributes on tab change breaking hints --- javascript/setHints.js | 258 ++++++++++++++++++++++++----------------- 1 file changed, 153 insertions(+), 105 deletions(-) diff --git a/javascript/setHints.js b/javascript/setHints.js index 098c6876e..14ab4e6b4 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -9,8 +9,9 @@ const localeData = { type: 2, hint: null, btn: null, - expandTimeout: null, // New property for expansion timeout - currentElement: null, // Track current element for expansion + expandTimeout: null, + currentElement: null, + elementHintMap: new WeakMap(), // Store hints separately from DOM }; let localeTimeout = null; @@ -21,17 +22,17 @@ async function cycleLocale() { const index = allLocales.indexOf(localeData.prev); localeData.locale = allLocales[(index + 1) % allLocales.length]; localeData.btn.innerText = localeData.locale; - // localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : ''; localeData.finished = false; localeData.data = []; localeData.prev = localeData.locale; + localeData.elementHintMap = new WeakMap(); window.opts.ui_locale = localeData.locale; - setHints(); // eslint-disable-line no-use-before-define + setHints(); }, 250); } async function resetLocale() { - clearTimeout(localeTimeout); // Prevent the single click logic + clearTimeout(localeTimeout); localeData.locale = 'en'; log('resetLocale', localeData.locale); const index = allLocales.indexOf(localeData.locale); @@ -39,8 +40,9 @@ async function resetLocale() { localeData.btn.innerText = localeData.locale; localeData.finished = false; localeData.data = []; + localeData.elementHintMap = new WeakMap(); window.opts.ui_locale = localeData.locale; - setHints(); // eslint-disable-line no-use-before-define + setHints(); } async function tooltipCreate() { @@ -64,18 +66,15 @@ async function tooltipCreate() { if (window.opts.tooltips === 'UI tooltips') localeData.type = 2; } -async function expandTooltip(element, longHint) { +async function expandTooltip(element, hintData) { if (localeData.currentElement === element && localeData.hint.classList.contains('tooltip-show')) { - // Hide the progress ring const ring = localeData.hint.querySelector('.tooltip-progress-ring'); if (ring) { ring.style.opacity = '0'; } - // Expand the container localeData.hint.classList.add('tooltip-expanded'); - // After container starts expanding, reveal the long content setTimeout(() => { const longContent = localeData.hint.querySelector('.long-content'); if (longContent) { @@ -86,6 +85,15 @@ async function expandTooltip(element, longHint) { } async function tooltipShow(e) { + // Get hint data from WeakMap or dataset + const hintData = localeData.elementHintMap.get(e.target) || { + hint: e.target.dataset?.hint, + longHint: e.target.dataset?.longHint, + reload: e.target.dataset?.reload, + }; + + if (!hintData.hint) return; + // Clear any existing expansion timeout if (localeData.expandTimeout) { clearTimeout(localeData.expandTimeout); @@ -96,80 +104,76 @@ async function tooltipShow(e) { localeData.hint.classList.remove('tooltip-expanded'); localeData.currentElement = e.target; - if (e.target.dataset.hint) { - // Create progress ring SVG - const progressRing = ` -
- - - - -
- `; + // Create progress ring SVG + const progressRing = ` +
+ + + + +
+ `; - // Set up the complete content structure from the start - let content = ` -
- ${e.target.textContent} - ${e.target.dataset.longHint ? progressRing : ''} -
-
- ${e.target.dataset.hint} - `; + // Set up the complete content structure from the start + let content = ` +
+ ${e.target.textContent} + ${hintData.longHint ? progressRing : ''} +
+
+ ${hintData.hint} + `; - // Add long content if available, but keep it hidden - if (e.target.dataset.longHint) { - content += `
${e.target.dataset.longHint}
`; + // Add long content if available, but keep it hidden + if (hintData.longHint) { + content += `
${hintData.longHint}
`; + } + + // Add reload notice if needed + if (hintData.reload) { + const reloadType = hintData.reload; + let reloadText = ''; + + if (reloadType === 'model') { + reloadText = 'Requires model reload'; + } else if (reloadType === 'server') { + reloadText = 'Requires server restart'; } - // Add reload notice if needed - if (e.target.dataset.reload) { - const reloadType = e.target.dataset.reload; - let reloadText = ''; - - if (reloadType === 'model') { - reloadText = 'Requires model reload'; - } else if (reloadType === 'server') { - reloadText = 'Requires server restart'; - } - - if (reloadText) { - content += ` -
-
- ${reloadText} -
- `; - } + if (reloadText) { + content += ` +
+
+ ${reloadText} +
+ `; + } + } + + localeData.hint.innerHTML = content; + localeData.hint.classList.add('tooltip-show'); + + if (e.clientX > window.innerWidth / 2) { + localeData.hint.classList.add('tooltip-left'); + } else { + localeData.hint.classList.remove('tooltip-left'); + } + + // Set up expansion timer if long hint is available + if (hintData.longHint) { + const ring = localeData.hint.querySelector('.tooltip-progress-ring'); + const ringProgress = localeData.hint.querySelector('.ring-progress'); + + if (ring && ringProgress) { + setTimeout(() => { + ring.classList.add('active'); + ringProgress.classList.add('animate'); + }, 100); } - localeData.hint.innerHTML = content; - localeData.hint.classList.add('tooltip-show'); - - if (e.clientX > window.innerWidth / 2) { - localeData.hint.classList.add('tooltip-left'); - } else { - localeData.hint.classList.remove('tooltip-left'); - } - - // Set up expansion timer if long hint is available - if (e.target.dataset.longHint) { - // Start progress ring animation - const ring = localeData.hint.querySelector('.tooltip-progress-ring'); - const ringProgress = localeData.hint.querySelector('.ring-progress'); - - if (ring && ringProgress) { - // Show the ring and start animation - setTimeout(() => { - ring.classList.add('active'); - ringProgress.classList.add('animate'); - }, 100); - } - - localeData.expandTimeout = setTimeout(() => { - expandTooltip(e.target, e.target.dataset.longHint); - }, 3000); - } + localeData.expandTimeout = setTimeout(() => { + expandTooltip(e.target, hintData); + }, 3000); } } @@ -188,11 +192,11 @@ async function validateHints(json, elements) { json.missing = []; const data = Object.values(json).flat().filter((e) => e.hint.length > 0); for (const e of data) e.label = e.label.trim(); - let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); // should be case sensitive + let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); let duplicateUI = original.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort(); - original = [...new Set(original)]; // remove duplicates - duplicateUI = [...new Set(duplicateUI)]; // remove duplicates - const current = data.map((e) => e.label.toLowerCase().trim()).sort(); // should be case sensitive + original = [...new Set(original)]; + duplicateUI = [...new Set(duplicateUI)]; + const current = data.map((e) => e.label.toLowerCase().trim()).sort(); log('all elements:', original); log('all hints:', current); log('hints-differences', { elements: original.length, hints: current.length }); @@ -209,7 +213,7 @@ async function addMissingHints(json, missingHints) { json.missing = []; for (const h of missingHints.sort()) { if (h.length <= 1) continue; - json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); // Add longHint property + json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); } log('missing hints', missingHints); log('added missing hints:', { missing: json.missing }); @@ -223,8 +227,6 @@ async function removeOrphanedHints(json, orphanedHints) { } async function replaceButtonText(el) { - // https://www.nerdfonts.com/cheat-sheet - // use unicode of icon with format nf-md-_circle const textIcons = { Generate: '\uf144', Enqueue: '\udb81\udc17', @@ -257,7 +259,7 @@ async function getLocaleData(desiredLocale = null) { localeData.prev = localeData.locale; } log('getLocale', desiredLocale, localeData.locale); - // primary + let json = {}; try { let res = await fetch(`${window.subpath}/file=html/locale_${localeData.locale}.json`); @@ -279,68 +281,104 @@ async function getLocaleData(desiredLocale = null) { async function setHints(analyze = false) { let json = {}; let overrideData = []; - if (localeData.finished) return; + if (Object.keys(opts).length === 0) return; + const elements = [ ...Array.from(gradioApp().querySelectorAll('button')), ...Array.from(gradioApp().querySelectorAll('h2')), ...Array.from(gradioApp().querySelectorAll('label > span')), ...Array.from(gradioApp().querySelectorAll('.label-wrap > span')), + // Include tab buttons specifically + ...Array.from(gradioApp().querySelectorAll('.tab-nav > button')), ]; + if (elements.length === 0) return; + + // Load data only if not already loaded if (localeData.data.length === 0) { json = await getLocaleData(window.opts.ui_locale); overrideData = Object.values(json.override || {}).flat().filter((e) => e.hint.length > 0); const jsonData = Object.values(json).flat().filter((e) => e.hint.length > 0); localeData.data = [...overrideData, ...jsonData]; } + if (!localeData.hint) tooltipCreate(); + let localized = 0; let hints = 0; const t0 = performance.now(); + for (const possible of elements) { let el = possible; if (possible.querySelector('span')) el = possible.querySelector('span'); - if (el.children.length === 1 && el.firstElementChild.classList.contains('mask-icon')) continue; // skip icon buttons + if (el.children.length === 1 && el.firstElementChild.classList.contains('mask-icon')) continue; + + // Get text to match against + const elementText = el.dataset?.original || el.textContent; + let found; - if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim()); - else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim()); + if (elementText) { + found = localeData.data.find((l) => l.label.toLowerCase().trim() === elementText.toLowerCase().trim()); + } + if (found?.localized?.length > 0) { if (!el.dataset.original) el.dataset.original = el.textContent; localized++; el.textContent = found.localized; - } else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english + } else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { el.textContent = found.label; } - // replaceButtonText(el); + if (found?.hint?.length > 0) { hints++; + if (localeData.type === 1) { el.title = found.hint; } else if (localeData.type === 2) { + // Store hint data in both dataset and WeakMap + const hintData = { + hint: found.hint, + longHint: found.longHint || null, + reload: found.reload || null, + }; + + // Store in WeakMap for persistence + localeData.elementHintMap.set(el, hintData); + + // Also set dataset attributes for compatibility el.dataset.hint = found.hint; - // Set long hint if available - if (found.longHint && found.longHint.length > 0) { - el.dataset.longHint = found.longHint; - } - // Set reload type if available - if (found.reload && found.reload.length > 0) { - el.dataset.reload = found.reload; - } + if (found.longHint) el.dataset.longHint = found.longHint; + if (found.reload) el.dataset.reload = found.reload; + + // Remove old listeners if any + el.removeEventListener('mouseover', tooltipShow); + el.removeEventListener('mouseout', tooltipHide); + el.removeEventListener('click', tooltipHide); + + // Add new listeners el.addEventListener('mouseover', tooltipShow); el.addEventListener('mouseout', tooltipHide); el.addEventListener('click', tooltipHide); - } else { - // tooltips disabled } } } + localeData.finished = true; localeData.initial = false; const t1 = performance.now(); - // localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : ''; - log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) }); - // sortUIElements(); + + log('setHints', { + type: localeData.type, + locale: localeData.locale, + elements: elements.length, + localized, + hints, + data: localeData.data.length, + override: overrideData.length, + time: Math.round(t1 - t0), + }); + if (analyze) { const [missingHints, orphanedHints] = await validateHints(json, elements); await addMissingHints(json, missingHints); @@ -348,8 +386,18 @@ async function setHints(analyze = false) { } } +// Force refresh hints on tab changes +onUiTabChange(() => { + // Small delay to let DOM settle + setTimeout(() => { + localeData.finished = false; // Allow setHints to run again + setHints(); + }, 100); +}); + const analyzeHints = async () => { localeData.finished = false; localeData.data = []; + localeData.elementHintMap = new WeakMap(); await setHints(true); }; From 64e798f0eab9aefef0283bd67960092b41e39d46 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Tue, 26 Aug 2025 00:40:25 +0100 Subject: [PATCH 3/5] Update to resolve conflicts --- javascript/setHints.js | 231 ++++++++++++++++++++++++++--------------- 1 file changed, 149 insertions(+), 82 deletions(-) diff --git a/javascript/setHints.js b/javascript/setHints.js index 14ab4e6b4..68d5782cc 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -9,9 +9,10 @@ const localeData = { type: 2, hint: null, btn: null, - expandTimeout: null, - currentElement: null, + expandTimeout: null, // New property for expansion timeout + currentElement: null, // Track current element for expansion elementHintMap: new WeakMap(), // Store hints separately from DOM + delegationSetup: false, // Track if global delegation is setup }; let localeTimeout = null; @@ -22,17 +23,19 @@ async function cycleLocale() { const index = allLocales.indexOf(localeData.prev); localeData.locale = allLocales[(index + 1) % allLocales.length]; localeData.btn.innerText = localeData.locale; + // localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : ''; localeData.finished = false; localeData.data = []; localeData.prev = localeData.locale; localeData.elementHintMap = new WeakMap(); + // Don't reset delegationSetup as it should persist window.opts.ui_locale = localeData.locale; - setHints(); + setHints(); // eslint-disable-line no-use-before-define }, 250); } async function resetLocale() { - clearTimeout(localeTimeout); + clearTimeout(localeTimeout); // Prevent the single click logic localeData.locale = 'en'; log('resetLocale', localeData.locale); const index = allLocales.indexOf(localeData.locale); @@ -41,8 +44,9 @@ async function resetLocale() { localeData.finished = false; localeData.data = []; localeData.elementHintMap = new WeakMap(); + // Don't reset delegationSetup as it should persist window.opts.ui_locale = localeData.locale; - setHints(); + setHints(); // eslint-disable-line no-use-before-define } async function tooltipCreate() { @@ -68,13 +72,16 @@ async function tooltipCreate() { async function expandTooltip(element, hintData) { if (localeData.currentElement === element && localeData.hint.classList.contains('tooltip-show')) { + // Hide the progress ring const ring = localeData.hint.querySelector('.tooltip-progress-ring'); if (ring) { ring.style.opacity = '0'; } + // Expand the container localeData.hint.classList.add('tooltip-expanded'); + // After container starts expanding, reveal the long content setTimeout(() => { const longContent = localeData.hint.querySelector('.long-content'); if (longContent) { @@ -85,11 +92,14 @@ async function expandTooltip(element, hintData) { } async function tooltipShow(e) { - // Get hint data from WeakMap or dataset - const hintData = localeData.elementHintMap.get(e.target) || { - hint: e.target.dataset?.hint, - longHint: e.target.dataset?.longHint, - reload: e.target.dataset?.reload, + // For event delegation, ensure we have the right target + const target = e.target || e; + + // Get hint data from WeakMap first, then fall back to dataset + const hintData = localeData.elementHintMap.get(target) || { + hint: target.dataset?.hint, + longHint: target.dataset?.longHint, + reload: target.dataset?.reload, }; if (!hintData.hint) return; @@ -102,7 +112,7 @@ async function tooltipShow(e) { // Remove expanded class and reset current element localeData.hint.classList.remove('tooltip-expanded'); - localeData.currentElement = e.target; + localeData.currentElement = target; // Create progress ring SVG const progressRing = ` @@ -117,7 +127,7 @@ async function tooltipShow(e) { // Set up the complete content structure from the start let content = `
- ${e.target.textContent} + ${target.textContent} ${hintData.longHint ? progressRing : ''}
@@ -133,13 +143,11 @@ async function tooltipShow(e) { if (hintData.reload) { const reloadType = hintData.reload; let reloadText = ''; - if (reloadType === 'model') { reloadText = 'Requires model reload'; } else if (reloadType === 'server') { reloadText = 'Requires server restart'; } - if (reloadText) { content += `
@@ -161,10 +169,12 @@ async function tooltipShow(e) { // Set up expansion timer if long hint is available if (hintData.longHint) { + // Start progress ring animation const ring = localeData.hint.querySelector('.tooltip-progress-ring'); const ringProgress = localeData.hint.querySelector('.ring-progress'); if (ring && ringProgress) { + // Show the ring and start animation setTimeout(() => { ring.classList.add('active'); ringProgress.classList.add('animate'); @@ -172,7 +182,7 @@ async function tooltipShow(e) { } localeData.expandTimeout = setTimeout(() => { - expandTooltip(e.target, hintData); + expandTooltip(target, hintData); }, 3000); } } @@ -188,15 +198,51 @@ async function tooltipHide(e) { localeData.currentElement = null; } +// Setup global event delegation that persists through DOM changes +function setupGlobalDelegation() { + if (localeData.delegationSetup) return; + + // Use event delegation on document level for maximum persistence + document.addEventListener('mouseover', (e) => { + const target = e.target; + if (target && (target.dataset?.hasHint === 'true' || localeData.elementHintMap.has(target))) { + tooltipShow({ target, clientX: e.clientX, clientY: e.clientY }); + } + }, true); + + document.addEventListener('mouseout', (e) => { + const target = e.target; + if (target && (target.dataset?.hasHint === 'true' || localeData.elementHintMap.has(target))) { + if (!target.contains(e.relatedTarget)) { + tooltipHide({ target }); + } + } + }, true); + + document.addEventListener('click', (e) => { + if (localeData.hint && localeData.hint.classList.contains('tooltip-show')) { + tooltipHide({ target: localeData.currentElement }); + } + }, true); + + localeData.delegationSetup = true; + log('Global event delegation setup for tooltips'); +} + +// Setup global event delegation for tooltips (only once) +if (localeData.type === 2 && !localeData.delegationSetup) { + setupGlobalDelegation(); +} + async function validateHints(json, elements) { json.missing = []; const data = Object.values(json).flat().filter((e) => e.hint.length > 0); for (const e of data) e.label = e.label.trim(); - let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); + let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); // should be case sensitive let duplicateUI = original.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort(); - original = [...new Set(original)]; - duplicateUI = [...new Set(duplicateUI)]; - const current = data.map((e) => e.label.toLowerCase().trim()).sort(); + original = [...new Set(original)]; // remove duplicates + duplicateUI = [...new Set(duplicateUI)]; // remove duplicates + const current = data.map((e) => e.label.toLowerCase().trim()).sort(); // should be case sensitive log('all elements:', original); log('all hints:', current); log('hints-differences', { elements: original.length, hints: current.length }); @@ -213,7 +259,7 @@ async function addMissingHints(json, missingHints) { json.missing = []; for (const h of missingHints.sort()) { if (h.length <= 1) continue; - json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); + json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); // Add longHint property } log('missing hints', missingHints); log('added missing hints:', { missing: json.missing }); @@ -227,6 +273,8 @@ async function removeOrphanedHints(json, orphanedHints) { } async function replaceButtonText(el) { + // https://www.nerdfonts.com/cheat-sheet + // use unicode of icon with format nf-md-_circle const textIcons = { Generate: '\uf144', Enqueue: '\udb81\udc17', @@ -259,7 +307,7 @@ async function getLocaleData(desiredLocale = null) { localeData.prev = localeData.locale; } log('getLocale', desiredLocale, localeData.locale); - + // primary let json = {}; try { let res = await fetch(`${window.subpath}/file=html/locale_${localeData.locale}.json`); @@ -278,21 +326,61 @@ async function getLocaleData(desiredLocale = null) { return json; } +async function replaceTextContent(el, text) { + if (el.children.length === 1 && el.firstElementChild.classList.contains('mask-icon')) return; + if (el.querySelector('span')) el = el.querySelector('span'); + if (el.querySelector('div')) el = el.querySelector('div'); + if (el.classList.contains('mask-icon')) return; // skip icon buttons + if (el.dataset.selector) { // replace on rehosted child if exists + el = el.firstElementChild || el.querySelector(el.dataset.selector); + replaceTextContent(el, text); + return; + } + el.textContent = text; +} + +async function setHint(el, entry) { + // Store hint data in WeakMap for persistence + const hintData = { + hint: entry.hint, + longHint: entry.longHint || null, + reload: entry.reload || null, + }; + localeData.elementHintMap.set(el, hintData); + + if (localeData.type === 1) { + el.title = entry.hint; + } else if (localeData.type === 2) { + // Also set dataset attributes for compatibility + el.dataset.hint = entry.hint; + if (entry.longHint && entry.longHint.length > 0) el.dataset.longHint = entry.longHint; + if (entry.reload && entry.reload.length > 0) el.dataset.reload = entry.reload; + + // Mark element as having hints for event delegation + el.dataset.hasHint = 'true'; + + // Don't add individual listeners here - we'll use global delegation + } else { + // tooltips disabled + } +} + async function setHints(analyze = false) { let json = {}; let overrideData = []; - + // Remove early return to allow re-initialization after tab changes + // if (localeData.finished) return; if (Object.keys(opts).length === 0) return; - const elements = [ ...Array.from(gradioApp().querySelectorAll('button')), ...Array.from(gradioApp().querySelectorAll('h2')), ...Array.from(gradioApp().querySelectorAll('label > span')), ...Array.from(gradioApp().querySelectorAll('.label-wrap > span')), - // Include tab buttons specifically + // Include all tab buttons specifically ...Array.from(gradioApp().querySelectorAll('.tab-nav > button')), + ...Array.from(gradioApp().querySelectorAll('#settings .tab-nav > button')), + ...Array.from(gradioApp().querySelectorAll('#system .tab-nav > button')), ]; - if (elements.length === 0) return; // Load data only if not already loaded @@ -309,75 +397,33 @@ async function setHints(analyze = false) { let hints = 0; const t0 = performance.now(); - for (const possible of elements) { - let el = possible; - if (possible.querySelector('span')) el = possible.querySelector('span'); - if (el.children.length === 1 && el.firstElementChild.classList.contains('mask-icon')) continue; - - // Get text to match against - const elementText = el.dataset?.original || el.textContent; - + for (const el of elements) { + // localize elements text let found; - if (elementText) { - found = localeData.data.find((l) => l.label.toLowerCase().trim() === elementText.toLowerCase().trim()); - } + if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim()); + else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim()); if (found?.localized?.length > 0) { if (!el.dataset.original) el.dataset.original = el.textContent; localized++; - el.textContent = found.localized; - } else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { - el.textContent = found.label; + replaceTextContent(el, found.localized); + } else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english + replaceTextContent(el, found.label); } + // set hints - always re-apply to handle DOM changes if (found?.hint?.length > 0) { hints++; - - if (localeData.type === 1) { - el.title = found.hint; - } else if (localeData.type === 2) { - // Store hint data in both dataset and WeakMap - const hintData = { - hint: found.hint, - longHint: found.longHint || null, - reload: found.reload || null, - }; - - // Store in WeakMap for persistence - localeData.elementHintMap.set(el, hintData); - - // Also set dataset attributes for compatibility - el.dataset.hint = found.hint; - if (found.longHint) el.dataset.longHint = found.longHint; - if (found.reload) el.dataset.reload = found.reload; - - // Remove old listeners if any - el.removeEventListener('mouseover', tooltipShow); - el.removeEventListener('mouseout', tooltipHide); - el.removeEventListener('click', tooltipHide); - - // Add new listeners - el.addEventListener('mouseover', tooltipShow); - el.addEventListener('mouseout', tooltipHide); - el.addEventListener('click', tooltipHide); - } + setHint(el, found); } } localeData.finished = true; localeData.initial = false; const t1 = performance.now(); - - log('setHints', { - type: localeData.type, - locale: localeData.locale, - elements: elements.length, - localized, - hints, - data: localeData.data.length, - override: overrideData.length, - time: Math.round(t1 - t0), - }); + // localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : ''; + log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) }); + // sortUIElements(); if (analyze) { const [missingHints, orphanedHints] = await validateHints(json, elements); @@ -386,18 +432,39 @@ async function setHints(analyze = false) { } } -// Force refresh hints on tab changes +// Force refresh hints on tab changes (main tabs) onUiTabChange(() => { // Small delay to let DOM settle setTimeout(() => { localeData.finished = false; // Allow setHints to run again setHints(); - }, 100); + }, 25); +}); + +// Also handle any click on tab buttons directly +onUiUpdate(() => { + // Find all tab buttons + const tabButtons = gradioApp().querySelectorAll('.tab-nav > button, #settings .tab-nav > button, #system .tab-nav > button'); + + tabButtons.forEach((btn) => { + // Check if this button already has our click handler + if (!btn.dataset.hintClickHandlerAdded) { + btn.dataset.hintClickHandlerAdded = 'true'; + + // Add click handler to force hint refresh + btn.addEventListener('click', () => { + setTimeout(() => { + localeData.finished = false; + setHints(); + }, 25); // Slightly longer delay for tab content to load + }); + } + }); }); const analyzeHints = async () => { localeData.finished = false; localeData.data = []; - localeData.elementHintMap = new WeakMap(); + // Don't recreate WeakMap unless necessary to preserve existing mappings await setHints(true); }; From 3ab9614a4123758f117aadb2be47489080f14b3a Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Tue, 26 Aug 2025 17:20:38 +0100 Subject: [PATCH 4/5] Another setHints.js update - efficient event delegation Fixes the DOM recreation in an unnoticeable way --- javascript/setHints.js | 371 +++++++++++++++++++++-------------------- 1 file changed, 188 insertions(+), 183 deletions(-) diff --git a/javascript/setHints.js b/javascript/setHints.js index 68d5782cc..c957fabf4 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -11,8 +11,7 @@ const localeData = { btn: null, expandTimeout: null, // New property for expansion timeout currentElement: null, // Track current element for expansion - elementHintMap: new WeakMap(), // Store hints separately from DOM - delegationSetup: false, // Track if global delegation is setup + observer: null, // MutationObserver for DOM changes }; let localeTimeout = null; @@ -27,8 +26,6 @@ async function cycleLocale() { localeData.finished = false; localeData.data = []; localeData.prev = localeData.locale; - localeData.elementHintMap = new WeakMap(); - // Don't reset delegationSetup as it should persist window.opts.ui_locale = localeData.locale; setHints(); // eslint-disable-line no-use-before-define }, 250); @@ -43,8 +40,6 @@ async function resetLocale() { localeData.btn.innerText = localeData.locale; localeData.finished = false; localeData.data = []; - localeData.elementHintMap = new WeakMap(); - // Don't reset delegationSetup as it should persist window.opts.ui_locale = localeData.locale; setHints(); // eslint-disable-line no-use-before-define } @@ -68,9 +63,20 @@ async function tooltipCreate() { if (window.opts.tooltips === 'None') localeData.type = 0; if (window.opts.tooltips === 'Browser default') localeData.type = 1; if (window.opts.tooltips === 'UI tooltips') localeData.type = 2; + + // Setup event delegation for tooltips instead of individual listeners + if (localeData.type === 2) { + gradioApp().addEventListener('mouseover', tooltipShowDelegated); + gradioApp().addEventListener('mouseout', tooltipHideDelegated); + } + + // Initialize DOM observer for immediate hint application + if (!localeData.observer) { + initializeDOMObserver(); + } } -async function expandTooltip(element, hintData) { +async function expandTooltip(element, longHint) { if (localeData.currentElement === element && localeData.hint.classList.contains('tooltip-show')) { // Hide the progress ring const ring = localeData.hint.querySelector('.tooltip-progress-ring'); @@ -91,19 +97,20 @@ async function expandTooltip(element, hintData) { } } +async function tooltipShowDelegated(e) { + // Use event delegation to handle dynamically created elements + if (e.target.dataset && e.target.dataset.hint) { + tooltipShow(e); + } +} + +async function tooltipHideDelegated(e) { + if (e.target.dataset && e.target.dataset.hint) { + tooltipHide(e); + } +} + async function tooltipShow(e) { - // For event delegation, ensure we have the right target - const target = e.target || e; - - // Get hint data from WeakMap first, then fall back to dataset - const hintData = localeData.elementHintMap.get(target) || { - hint: target.dataset?.hint, - longHint: target.dataset?.longHint, - reload: target.dataset?.reload, - }; - - if (!hintData.hint) return; - // Clear any existing expansion timeout if (localeData.expandTimeout) { clearTimeout(localeData.expandTimeout); @@ -112,78 +119,80 @@ async function tooltipShow(e) { // Remove expanded class and reset current element localeData.hint.classList.remove('tooltip-expanded'); - localeData.currentElement = target; + localeData.currentElement = e.target; - // Create progress ring SVG - const progressRing = ` -
- - - - -
- `; + if (e.target.dataset.hint) { + // Create progress ring SVG + const progressRing = ` +
+ + + + +
+ `; - // Set up the complete content structure from the start - let content = ` -
- ${target.textContent} - ${hintData.longHint ? progressRing : ''} -
-
- ${hintData.hint} - `; + // Set up the complete content structure from the start + let content = ` +
+ ${e.target.textContent} + ${e.target.dataset.longHint ? progressRing : ''} +
+
+ ${e.target.dataset.hint} + `; - // Add long content if available, but keep it hidden - if (hintData.longHint) { - content += `
${hintData.longHint}
`; - } - - // Add reload notice if needed - if (hintData.reload) { - const reloadType = hintData.reload; - let reloadText = ''; - if (reloadType === 'model') { - reloadText = 'Requires model reload'; - } else if (reloadType === 'server') { - reloadText = 'Requires server restart'; - } - if (reloadText) { - content += ` -
-
- ${reloadText} -
- `; - } - } - - localeData.hint.innerHTML = content; - localeData.hint.classList.add('tooltip-show'); - - if (e.clientX > window.innerWidth / 2) { - localeData.hint.classList.add('tooltip-left'); - } else { - localeData.hint.classList.remove('tooltip-left'); - } - - // Set up expansion timer if long hint is available - if (hintData.longHint) { - // Start progress ring animation - const ring = localeData.hint.querySelector('.tooltip-progress-ring'); - const ringProgress = localeData.hint.querySelector('.ring-progress'); - - if (ring && ringProgress) { - // Show the ring and start animation - setTimeout(() => { - ring.classList.add('active'); - ringProgress.classList.add('animate'); - }, 100); + // Add long content if available, but keep it hidden + if (e.target.dataset.longHint) { + content += `
${e.target.dataset.longHint}
`; } - localeData.expandTimeout = setTimeout(() => { - expandTooltip(target, hintData); - }, 3000); + // Add reload notice if needed + if (e.target.dataset.reload) { + const reloadType = e.target.dataset.reload; + let reloadText = ''; + if (reloadType === 'model') { + reloadText = 'Requires model reload'; + } else if (reloadType === 'server') { + reloadText = 'Requires server restart'; + } + if (reloadText) { + content += ` +
+
+ ${reloadText} +
+ `; + } + } + + localeData.hint.innerHTML = content; + localeData.hint.classList.add('tooltip-show'); + + if (e.clientX > window.innerWidth / 2) { + localeData.hint.classList.add('tooltip-left'); + } else { + localeData.hint.classList.remove('tooltip-left'); + } + + // Set up expansion timer if long hint is available + if (e.target.dataset.longHint) { + // Start progress ring animation + const ring = localeData.hint.querySelector('.tooltip-progress-ring'); + const ringProgress = localeData.hint.querySelector('.ring-progress'); + + if (ring && ringProgress) { + // Show the ring and start animation + setTimeout(() => { + ring.classList.add('active'); + ringProgress.classList.add('animate'); + }, 100); + } + + localeData.expandTimeout = setTimeout(() => { + expandTooltip(e.target, e.target.dataset.longHint); + }, 3000); + } } } @@ -198,42 +207,6 @@ async function tooltipHide(e) { localeData.currentElement = null; } -// Setup global event delegation that persists through DOM changes -function setupGlobalDelegation() { - if (localeData.delegationSetup) return; - - // Use event delegation on document level for maximum persistence - document.addEventListener('mouseover', (e) => { - const target = e.target; - if (target && (target.dataset?.hasHint === 'true' || localeData.elementHintMap.has(target))) { - tooltipShow({ target, clientX: e.clientX, clientY: e.clientY }); - } - }, true); - - document.addEventListener('mouseout', (e) => { - const target = e.target; - if (target && (target.dataset?.hasHint === 'true' || localeData.elementHintMap.has(target))) { - if (!target.contains(e.relatedTarget)) { - tooltipHide({ target }); - } - } - }, true); - - document.addEventListener('click', (e) => { - if (localeData.hint && localeData.hint.classList.contains('tooltip-show')) { - tooltipHide({ target: localeData.currentElement }); - } - }, true); - - localeData.delegationSetup = true; - log('Global event delegation setup for tooltips'); -} - -// Setup global event delegation for tooltips (only once) -if (localeData.type === 2 && !localeData.delegationSetup) { - setupGlobalDelegation(); -} - async function validateHints(json, elements) { json.missing = []; const data = Object.values(json).flat().filter((e) => e.hint.length > 0); @@ -340,26 +313,16 @@ async function replaceTextContent(el, text) { } async function setHint(el, entry) { - // Store hint data in WeakMap for persistence - const hintData = { - hint: entry.hint, - longHint: entry.longHint || null, - reload: entry.reload || null, - }; - localeData.elementHintMap.set(el, hintData); - if (localeData.type === 1) { el.title = entry.hint; } else if (localeData.type === 2) { - // Also set dataset attributes for compatibility el.dataset.hint = entry.hint; if (entry.longHint && entry.longHint.length > 0) el.dataset.longHint = entry.longHint; if (entry.reload && entry.reload.length > 0) el.dataset.reload = entry.reload; - - // Mark element as having hints for event delegation - el.dataset.hasHint = 'true'; - - // Don't add individual listeners here - we'll use global delegation + // Don't add individual listeners - we use event delegation now + // This means we don't need to reattach listeners when elements are recreated + // el.addEventListener('mouseover', tooltipShow); + // el.addEventListener('mouseout', tooltipHide); } else { // tooltips disabled } @@ -368,41 +331,30 @@ async function setHint(el, entry) { async function setHints(analyze = false) { let json = {}; let overrideData = []; - // Remove early return to allow re-initialization after tab changes - // if (localeData.finished) return; + if (localeData.finished) return; if (Object.keys(opts).length === 0) return; const elements = [ ...Array.from(gradioApp().querySelectorAll('button')), ...Array.from(gradioApp().querySelectorAll('h2')), ...Array.from(gradioApp().querySelectorAll('label > span')), ...Array.from(gradioApp().querySelectorAll('.label-wrap > span')), - // Include all tab buttons specifically - ...Array.from(gradioApp().querySelectorAll('.tab-nav > button')), - ...Array.from(gradioApp().querySelectorAll('#settings .tab-nav > button')), - ...Array.from(gradioApp().querySelectorAll('#system .tab-nav > button')), ]; if (elements.length === 0) return; - - // Load data only if not already loaded if (localeData.data.length === 0) { json = await getLocaleData(window.opts.ui_locale); overrideData = Object.values(json.override || {}).flat().filter((e) => e.hint.length > 0); const jsonData = Object.values(json).flat().filter((e) => e.hint.length > 0); localeData.data = [...overrideData, ...jsonData]; } - if (!localeData.hint) tooltipCreate(); - let localized = 0; let hints = 0; const t0 = performance.now(); - for (const el of elements) { // localize elements text let found; if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim()); else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim()); - if (found?.localized?.length > 0) { if (!el.dataset.original) el.dataset.original = el.textContent; localized++; @@ -410,21 +362,18 @@ async function setHints(analyze = false) { } else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english replaceTextContent(el, found.label); } - - // set hints - always re-apply to handle DOM changes + // set hints if (found?.hint?.length > 0) { hints++; setHint(el, found); } } - localeData.finished = true; localeData.initial = false; const t1 = performance.now(); // localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : ''; log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) }); // sortUIElements(); - if (analyze) { const [missingHints, orphanedHints] = await validateHints(json, elements); await addMissingHints(json, missingHints); @@ -432,39 +381,95 @@ async function setHints(analyze = false) { } } -// Force refresh hints on tab changes (main tabs) -onUiTabChange(() => { - // Small delay to let DOM settle - setTimeout(() => { - localeData.finished = false; // Allow setHints to run again - setHints(); - }, 25); -}); - -// Also handle any click on tab buttons directly -onUiUpdate(() => { - // Find all tab buttons - const tabButtons = gradioApp().querySelectorAll('.tab-nav > button, #settings .tab-nav > button, #system .tab-nav > button'); - - tabButtons.forEach((btn) => { - // Check if this button already has our click handler - if (!btn.dataset.hintClickHandlerAdded) { - btn.dataset.hintClickHandlerAdded = 'true'; - - // Add click handler to force hint refresh - btn.addEventListener('click', () => { - setTimeout(() => { - localeData.finished = false; - setHints(); - }, 25); // Slightly longer delay for tab content to load - }); - } - }); -}); - const analyzeHints = async () => { localeData.finished = false; localeData.data = []; - // Don't recreate WeakMap unless necessary to preserve existing mappings await setHints(true); }; + +// Apply hints to a single element immediately +async function applyHintToElement(el) { + if (!localeData.data || localeData.data.length === 0) return; + if (!el.textContent) return; + + // Check if element matches our selector criteria + const isValidElement = + el.tagName === 'BUTTON' || + el.tagName === 'H2' || + (el.tagName === 'SPAN' && (el.parentElement?.tagName === 'LABEL' || el.parentElement?.classList.contains('label-wrap'))); + + if (!isValidElement) return; + + // Find matching hint data + let found; + if (el.dataset.original) { + found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim()); + } else { + found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim()); + } + + // Apply localization if found + if (found?.localized?.length > 0) { + if (!el.dataset.original) el.dataset.original = el.textContent; + replaceTextContent(el, found.localized); + } + + // Apply hint if found + if (found?.hint?.length > 0) { + setHint(el, found); + } +} + +// Initialize MutationObserver for immediate hint application +function initializeDOMObserver() { + if (localeData.observer) { + localeData.observer.disconnect(); + } + + localeData.observer = new MutationObserver((mutations) => { + // Process added nodes immediately + for (const mutation of mutations) { + if (mutation.type === 'childList') { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + // Apply hints to the node itself + applyHintToElement(node); + + // Apply hints to all relevant children + const elements = [ + ...Array.from(node.querySelectorAll('button')), + ...Array.from(node.querySelectorAll('h2')), + ...Array.from(node.querySelectorAll('label > span')), + ...Array.from(node.querySelectorAll('.label-wrap > span')), + ]; + + // Include the node itself if it matches + if (node.matches && ( + node.matches('button') || + node.matches('h2') || + node.matches('label > span') || + node.matches('.label-wrap > span') + )) { + elements.push(node); + } + + // Apply hints immediately to all found elements + elements.forEach(el => applyHintToElement(el)); + } + } + } + } + }); + + // Start observing the entire gradio app for changes + const targetNode = gradioApp(); + if (targetNode) { + localeData.observer.observe(targetNode, { + childList: true, + subtree: true + }); + } +} + +// Export for external use if needed +const forceReapplyHints = () => setHints(); From e54f5142e507aef27c9530540a97e80df009459f Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Tue, 26 Aug 2025 19:28:21 +0100 Subject: [PATCH 5/5] Cleanup and final lint --- javascript/setHints.js | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/javascript/setHints.js b/javascript/setHints.js index c957fabf4..0db1faff7 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -9,7 +9,7 @@ const localeData = { type: 2, hint: null, btn: null, - expandTimeout: null, // New property for expansion timeout + expandTimeout: null, // Property for expansion timeout currentElement: null, // Track current element for expansion observer: null, // MutationObserver for DOM changes }; @@ -66,13 +66,13 @@ async function tooltipCreate() { // Setup event delegation for tooltips instead of individual listeners if (localeData.type === 2) { - gradioApp().addEventListener('mouseover', tooltipShowDelegated); - gradioApp().addEventListener('mouseout', tooltipHideDelegated); + gradioApp().addEventListener('mouseover', tooltipShowDelegated); // eslint-disable-line no-use-before-define + gradioApp().addEventListener('mouseout', tooltipHideDelegated); // eslint-disable-line no-use-before-define } // Initialize DOM observer for immediate hint application if (!localeData.observer) { - initializeDOMObserver(); + initializeDOMObserver(); // eslint-disable-line no-use-before-define } } @@ -100,13 +100,13 @@ async function expandTooltip(element, longHint) { async function tooltipShowDelegated(e) { // Use event delegation to handle dynamically created elements if (e.target.dataset && e.target.dataset.hint) { - tooltipShow(e); + tooltipShow(e); // eslint-disable-line no-use-before-define } } async function tooltipHideDelegated(e) { if (e.target.dataset && e.target.dataset.hint) { - tooltipHide(e); + tooltipHide(e); // eslint-disable-line no-use-before-define } } @@ -319,10 +319,6 @@ async function setHint(el, entry) { el.dataset.hint = entry.hint; if (entry.longHint && entry.longHint.length > 0) el.dataset.longHint = entry.longHint; if (entry.reload && entry.reload.length > 0) el.dataset.reload = entry.reload; - // Don't add individual listeners - we use event delegation now - // This means we don't need to reattach listeners when elements are recreated - // el.addEventListener('mouseover', tooltipShow); - // el.addEventListener('mouseout', tooltipHide); } else { // tooltips disabled } @@ -393,10 +389,9 @@ async function applyHintToElement(el) { if (!el.textContent) return; // Check if element matches our selector criteria - const isValidElement = - el.tagName === 'BUTTON' || - el.tagName === 'H2' || - (el.tagName === 'SPAN' && (el.parentElement?.tagName === 'LABEL' || el.parentElement?.classList.contains('label-wrap'))); + const isValidElement = el.tagName === 'BUTTON' + || el.tagName === 'H2' + || (el.tagName === 'SPAN' && (el.parentElement?.tagName === 'LABEL' || el.parentElement?.classList.contains('label-wrap'))); if (!isValidElement) return; @@ -445,16 +440,16 @@ function initializeDOMObserver() { // Include the node itself if it matches if (node.matches && ( - node.matches('button') || - node.matches('h2') || - node.matches('label > span') || - node.matches('.label-wrap > span') + node.matches('button') + || node.matches('h2') + || node.matches('label > span') + || node.matches('.label-wrap > span') )) { elements.push(node); } // Apply hints immediately to all found elements - elements.forEach(el => applyHintToElement(el)); + elements.forEach((el) => applyHintToElement(el)); } } } @@ -466,7 +461,7 @@ function initializeDOMObserver() { if (targetNode) { localeData.observer.observe(targetNode, { childList: true, - subtree: true + subtree: true, }); } }