diff --git a/javascript/_textAreas.js b/javascript/_textAreas.js index bbbd15f..cc34989 100644 --- a/javascript/_textAreas.js +++ b/javascript/_textAreas.js @@ -54,7 +54,7 @@ function getTextAreas() { // Safety check if (!base) continue; - let allTextAreas = [...base.querySelectorAll("textarea")]; + let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")]; // Filter the text areas where the adjacent label matches one of the selectors let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText))); @@ -65,6 +65,54 @@ function getTextAreas() { return textAreas; } +function addOnDemandObservers(setupFunction) { + for (const [key, entry] of Object.entries(thirdParty)) { + if (!entry.onDemand) continue; + + let base = gradioApp().querySelector(entry.base); + let accordions = [...base.querySelectorAll(".gradio-accordion")]; + + if (!accordions) continue; + + accordions.forEach(acc => { + let accObserver = new MutationObserver((mutationList, observer) => { + for (const mutation of mutationList) { + if (mutation.type === "childList") { + let newChildren = mutation.addedNodes; + if (!newChildren) { + accObserver.disconnect(); + continue; + } + + newChildren.forEach(child => { + if (child.classList.contains("gradio-accordion") || child.querySelector(".gradio-accordion")) { + let newAccordions = [...child.querySelectorAll(".gradio-accordion")]; + newAccordions.forEach(nAcc => accObserver.observe(nAcc, { childList: true })); + } + }); + + if (entry.hasIds) { // If the entry has proper ids, we can just select them + [...gradioApp().querySelectorAll(entry.selectors.join(", "))].forEach(x => setupFunction(x)); + } else { // Otherwise, we have to find the text areas by their adjacent labels + let base = gradioApp().querySelector(entry.base); + + // Safety check + if (!base) continue; + + let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")]; + + // Filter the text areas where the adjacent label matches one of the selectors + let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText))); + matchingTextAreas.forEach(x => setupFunction(x)); + } + } + } + }); + accObserver.observe(acc, { childList: true }); + }); + }; +} + const thirdPartyIdSet = new Set(); // Get the identifier for the text area to differentiate between positive and negative function getTextAreaIdentifier(textArea) { diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index 5240eea..0f269ff 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -796,6 +796,42 @@ function navigateInList(textArea, event) { event.stopPropagation(); } +function addAutocompleteToArea(area) { + // Return if autocomplete is disabled for the current area type in config + let textAreaId = getTextAreaIdentifier(area); + if ((!CFG.activeIn.img2img && textAreaId.includes("img2img")) + || (!CFG.activeIn.txt2img && textAreaId.includes("txt2img")) + || (!CFG.activeIn.negativePrompts && textAreaId.includes("n")) + || (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) { + return; + } + + // Only add listeners once + if (!area.classList.contains('autocomplete')) { + // Add our new element + var resultsDiv = createResultsDiv(area); + area.parentNode.insertBefore(resultsDiv, area.nextSibling); + // Hide by default so it doesn't show up on page load + hideResults(area); + + // Add autocomplete event listener + area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime)); + // Add focusout event listener + area.addEventListener('focusout', debounce(() => hideResults(area), 400)); + // Add up and down arrow event listener + area.addEventListener('keydown', (e) => navigateInList(area, e)); + // CompositionEnd fires after the user has finished IME composing + // We need to block hide here to prevent the enter key from insta-closing the results + area.addEventListener('compositionend', () => { + hideBlocked = true; + setTimeout(() => { hideBlocked = false; }, 100); + }); + + // Add class so we know we've already added the listeners + area.classList.add('autocomplete'); + } +} + // One-time setup, triggered from onUiUpdate async function setup() { // Load external files needed by completion extensions @@ -804,6 +840,9 @@ async function setup() { // Find all textareas let textAreas = getTextAreas(); + // Add mutation observer to accordions inside a base that has onDemand set to true + addOnDemandObservers(addAutocompleteToArea); + // Add event listener to apply settings button so we can mirror the changes to our internal config let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary"); applySettingsButton?.addEventListener("click", () => { @@ -856,41 +895,7 @@ async function setup() { return; } - textAreas.forEach(area => { - // Return if autocomplete is disabled for the current area type in config - let textAreaId = getTextAreaIdentifier(area); - if ((!CFG.activeIn.img2img && textAreaId.includes("img2img")) - || (!CFG.activeIn.txt2img && textAreaId.includes("txt2img")) - || (!CFG.activeIn.negativePrompts && textAreaId.includes("n")) - || (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) { - return; - } - - // Only add listeners once - if (!area.classList.contains('autocomplete')) { - // Add our new element - var resultsDiv = createResultsDiv(area); - area.parentNode.insertBefore(resultsDiv, area.nextSibling); - // Hide by default so it doesn't show up on page load - hideResults(area); - - // Add autocomplete event listener - area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime)); - // Add focusout event listener - area.addEventListener('focusout', debounce(() => hideResults(area), 400)); - // Add up and down arrow event listener - area.addEventListener('keydown', (e) => navigateInList(area, e)); - // CompositionEnd fires after the user has finished IME composing - // We need to block hide here to prevent the enter key from insta-closing the results - area.addEventListener('compositionend', () => { - hideBlocked = true; - setTimeout(() => { hideBlocked = false; }, 100); - }); - - // Add class so we know we've already added the listeners - area.classList.add('autocomplete'); - } - }); + textAreas.forEach(area => addAutocompleteToArea(area)); // Add style to dom let acStyle = document.createElement('style');