// Prevent eslint errors on functions defined in other files. /*global ExtraNetworksClusterizeTreeList, ExtraNetworksClusterizeCardsList, */ /*eslint no-undef: "error"*/ const SEARCH_INPUT_DEBOUNCE_TIME_MS = 250; const re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/; const re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g; const re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/; const re_extranet_g_neg = /\(([^:^>]+:[\d.]+)\)/g; const activePromptTextarea = {}; const clusterizers = {}; var globalPopup = null; var globalPopupInner = null; const storedPopupIds = {}; const extraPageUserMetadataEditors = {}; // A flag used by the `waitForBool` promise to determine when we first load Ui Options. const initialUiOptionsLoaded = {state: false}; // const popup = contents => { if (!globalPopup) { globalPopup = document.createElement('div'); globalPopup.classList.add('global-popup'); var close = document.createElement('div'); close.classList.add('global-popup-close'); close.addEventListener("click", closePopup); close.title = "Close"; globalPopup.appendChild(close); globalPopupInner = document.createElement('div'); globalPopupInner.classList.add('global-popup-inner'); globalPopup.appendChild(globalPopupInner); gradioApp().querySelector('.main').appendChild(globalPopup); } globalPopupInner.innerHTML = ''; globalPopupInner.appendChild(contents); globalPopup.style.display = "flex"; }; const popupId = id => { if (!storedPopupIds[id]) { storedPopupIds[id] = gradioApp().getElementById(id); } popup(storedPopupIds[id]); }; const closePopup = () => { if (!globalPopup) return; globalPopup.style.display = "none"; }; // ==== GENERAL EXTRA NETWORKS FUNCTIONS ==== const extraNetworksClusterizersLoadTab = async ( { tabname_full = "", selected = false, fetch_data = false, } ) => { if (!keyExistsLogError(clusterizers, tabname_full)) { return; } for (const v of Object.values(clusterizers[tabname_full])) { if (fetch_data) { await v.initDataDefault(); } else { await v.refresh(true); } } }; const extraNetworksRegisterPromptForTab = (tabname, id) => { var textarea = gradioApp().querySelector(`#${id} > label > textarea`); if (!activePromptTextarea[tabname]) { activePromptTextarea[tabname] = textarea; } textarea.addEventListener("focus", function() { activePromptTextarea[tabname] = textarea; }); }; const extraNetworksMovePromptToTab = (tabname, id, showPrompt, showNegativePrompt) => { if (!gradioApp().querySelector('.toprow-compact-tools')) return; // only applicable for compact prompt layout var promptContainer = gradioApp().getElementById(`${tabname}_prompt_container`); var prompt = gradioApp().getElementById(`${tabname}_prompt_row`); var negPrompt = gradioApp().getElementById(`${tabname}_neg_prompt_row`); var elem = id ? gradioApp().getElementById(id) : null; if (showNegativePrompt && elem) { elem.insertBefore(negPrompt, elem.firstChild); } else { promptContainer.insertBefore(negPrompt, promptContainer.firstChild); } if (showPrompt && elem) { elem.insertBefore(prompt, elem.firstChild); } else { promptContainer.insertBefore(prompt, promptContainer.firstChild); } if (elem) { elem.classList.toggle('extra-page-prompts-active', showNegativePrompt || showPrompt); } }; const extraNetworksShowControlsForPage = (tabname, tabname_full) => { gradioApp().querySelectorAll(`#${tabname}_extra_tabs .extra-network-controls-div > div`).forEach((elem) => { let show = `${tabname_full}_controls` === elem.id; elem.classList.toggle("hidden", !show); }); }; const extraNetworksRemoveFromPrompt = (textarea, text, is_neg) => { let match = text.match(is_neg ? re_extranet_neg : re_extranet); let replaced = false; let res; let prefix = opts.extra_networks_add_text_separator; if (match) { const content = match[1]; const postfix = match[2]; let idx = -1; res = textarea.value.replaceAll( is_neg ? re_extranet_g_neg : re_extranet_g, (found, net, pos) => { match = found.match(is_neg ? re_extranet_neg : re_extranet); if (match[1] === content) { replaced = true; idx = pos; return ""; } return found; }, ); if (idx >= 0) { if (postfix && res.slice(idx, postfix.length) === postfix) { res = res.slice(0, idx) + res.slice(idx + postfix.length); } if (res.slice(idx - prefix.length, prefix.length) === prefix) { res = res.slice(0, idx - prefix.length) + res.slice(idx); } } } else { res = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), ""); replaced = (res !== textarea.value); } if (replaced) { textarea.value = res; return true; } return false; }; const extraNetworksUpdatePrompt = (textarea, text, is_neg) => { if (!extraNetworksRemoveFromPrompt(textarea, text, is_neg)) { textarea.value = textarea.value + opts.extra_networks_add_text_separator + text; } updateInput(textarea); }; const extraNetworksSaveCardPreview = (event, tabname, filename) => { const textarea = gradioApp().querySelector(`#${tabname}_preview_filename > label > textarea`); const button = gradioApp().getElementById(`${tabname}_save_preview`); textarea.value = filename; updateInput(textarea); button.click(); event.stopPropagation(); event.preventDefault(); }; const extraNetworksFlattenMetadata = obj => { const result = {}; // Convert any stringified JSON objects to actual objects for (const key of Object.keys(obj)) { if (typeof obj[key] === 'string') { try { const parsed = JSON.parse(obj[key]); if (parsed && typeof parsed === 'object') { obj[key] = parsed; } } catch (error) { continue; } } } // Flatten the object for (const key of Object.keys(obj)) { if (typeof obj[key] === 'object' && obj[key] !== null) { const nested = extraNetworksFlattenMetadata(obj[key]); for (const nestedKey of Object.keys(nested)) { result[`${key}/${nestedKey}`] = nested[nestedKey]; } } else { result[key] = obj[key]; } } // Special case for handling modelspec keys for (const key of Object.keys(result)) { if (key.startsWith("modelspec.")) { result[key.replaceAll(".", "/")] = result[key]; delete result[key]; } } // Add empty keys to designate hierarchy for (const key of Object.keys(result)) { const parts = key.split("/"); for (let i = 1; i < parts.length; i++) { const parent = parts.slice(0, i).join("/"); if (!result[parent]) { result[parent] = ""; } } } return result; }; const extraNetworksShowMetadata = text => { try { let parsed = JSON.parse(text); if (parsed && typeof parsed === 'object') { parsed = extraNetworksFlattenMetadata(parsed); const table = createVisualizationTable(parsed, 0); popup(table); return; } } catch (error) { console.error(error); } var elem = document.createElement('pre'); elem.classList.add('popup-metadata'); elem.textContent = text; popup(elem); return; }; const extraNetworksRefreshSingleCard = (tabname, extra_networks_tabname, name) => { requestGet( "./sd_extra_networks/get-single-card", {tabname: tabname, extra_networks_tabname: extra_networks_tabname, name: name}, (data) => { if (data && data.html) { const card = gradioApp().querySelector(`${tabname}_${extra_networks_tabname}_cards > .card[data-name="${name}"]`); const new_div = document.createElement("div"); new_div.innerHTML = data.html; const new_card = new_div.firstElementChild; new_card.style.display = ""; card.parentElement.insertBefore(new_card, card); card.parentElement.removeChild(card); } }, ); }; const extraNetworksRefreshTab = async tabname_full => { /** called from python when user clicks the extra networks refresh tab button */ // Reapply controls since they don't change on refresh. const controls = gradioApp().getElementById(`${tabname_full}_controls`); let btn_dirs_view = controls.querySelector(".extra-network-control--dirs-view"); let btn_tree_view = controls.querySelector(".extra-network-control--tree-view"); const pane = gradioApp().getElementById(`${tabname_full}_pane`); let div_dirs = pane.querySelector(".extra-network-content--dirs-view"); let div_tree = pane.querySelector(`.extra-network-content.resize-handle-col:has(> #${tabname_full}_tree_list_scroll_area)`); // Remove "hidden" class if button is enabled, otherwise add it. div_dirs.classList.toggle("hidden", !("selected" in btn_dirs_view.dataset)); div_tree.classList.toggle("hidden", !("selected" in btn_tree_view.dataset)); await waitForKeyInObject({k: tabname_full, obj: clusterizers}); for (const _tabname_full of Object.keys(clusterizers)) { let selected = _tabname_full == tabname_full; await extraNetworksClusterizersLoadTab({ tabname_full: _tabname_full, selected: selected, fetch_data: true, }); } }; const extraNetworksAutoSetTreeWidth = pane => { if (!isElementLogError(pane)) { return; } const tabname_full = pane.dataset.tabnameFull; // This event is only applied to the currently selected tab if has clusterize lists. if (!keyExists(clusterizers, tabname_full)) { return; } const row = pane.querySelector(".resize-handle-row"); if (!isElementLogError(row)) { return; } const left_col = row.firstElementChild; if (!isElementLogError(left_col)) { return; } // If the left column is hidden then we don't want to do anything. if (left_col.classList.contains("hidden")) { return; } const pad = parseFloat(row.style.gridTemplateColumns.split(" ")[1]); const min_left_col_width = parseFloat(left_col.style.flexBasis.slice(0, -2)); // We know that the tree list is the left column. That is the only one we want to resize. let max_width = clusterizers[tabname_full].tree_list.getMaxRowWidth(); // Add the resize handle's padding to the result and default to minLeftColWidth if necessary. max_width = Math.max(max_width + pad, min_left_col_width); // Mimicks resizeHandle.js::setLeftColGridTemplate(). row.style.gridTemplateColumns = `${max_width}px ${pad}px 1fr`; }; const extraNetworksApplyFilter = tabname_full => { if (!keyExistsLogError(clusterizers, tabname_full)) { return; } const pane = gradioApp.getElementById(`${tabname_full}_pane`); if (!isElementLogError(pane)) { return; } const txt_search = gradioApp().querySelector(`#${tabname_full}_controls .extra-network-control--search-text`); if (!isElementLogError(txt_search)) { return; } // We only want to filter/sort the cards list. clusterizers[tabname_full].cards_list.filterData(txt_search.value.toLowerCase()); // If the search input has changed since selecting a button to populate it // then we want to disable the button that previously populated the search input. // tree view buttons let btn = pane.querySelector(".tree-list-item[data-selected='']"); if (isElement(btn) && btn.dataset.path !== txt_search.value && "selected" in btn.dataset) { clusterizers[tabname_full].tree_list.onRowSelected(btn.dataset.divId, btn, false); } // dirs view buttons btn = pane.querySelector(".extra-network-dirs-view-button[data-selected='']"); if (isElement(btn) && btn.textContent.trim() !== txt_search.value) { delete btn.dataset.selected; } }; // ==== EVENT HANDLING ==== const extraNetworksInitCardsData = async () => { return await requestGetPromise( "./sd_extra_networks/init-cards-data", { tabname: tabname, extra_networks_tabname: extra_networks_tabname, }, ); }; const extraNetworksInitTreeData = async () => { return await requestGetPromise( "./sd_extra_networks/init-tree-data", { tabname: tabname, extra_networks_tabname: extra_networks_tabname, }, ); }; const extraNetworksOnInitData = async class_name => { if (class_name === "ExtraNetworksClusterizeTreeList") { return await extraNetworksInitCardsData(); } else if (class_name === "ExtraNetworksClusterizeCardsList") { return await extraNetworksInitTreeData(); } }; const extraNetworksFetchCardsData = async (extra_networks_tabname, div_ids) => { return await requestGetPromise( "./sd_extra_networks/fetch-cards-data", { extra_networks_tabname: extra_networks_tabname, div_ids: div_ids, }, ); }; const extraNetworksFetchTreeData = async (extra_networks_tabname, div_ids) => { return await requestGetPromise( "./sd_extra_networks/fetch-tree-data", { extra_networks_tabname: extra_networks_tabname, div_ids: div_ids, }, ); }; const extraNetworksOnFetchData = async (class_name, extra_networks_tabname, div_ids) => { if (class_name === "ExtraNetworksClusterizeTreeList") { return await extraNetworksFetchCardsData(extra_networks_tabname, div_ids); } else if (class_name === "ExtraNetworksClusterizeCardsList") { return await extraNetworksFetchTreeData(extra_networks_tabname, div_ids); } }; const extraNetworksFetchMetadata = (extra_networks_tabname, card_name) => { const _showError = () => { extraNetworksShowMetadata("there was an error getting metadata"); }; requestGet( "./sd_extra_networks/metadata", {extra_networks_tabname: extra_networks_tabname, item: card_name}, function(data) { if (data && data.metadata) { extraNetworksShowMetadata(data.metadata); } else { _showError(); } }, _showError, ); }; const extraNetworksUnrelatedTabSelected = tabname => { /** called from python when user selects an unrelated tab (generate) */ extraNetworksMovePromptToTab(tabname, '', false, false); extraNetworksShowControlsForPage(tabname, null); }; const extraNetworksTabSelected = async (tabname, id, showPrompt, showNegativePrompt, tabname_full) => { /** called from python when user selects an extra networks tab */ extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); extraNetworksShowControlsForPage(tabname, tabname_full); await waitForKeyInObject({k: tabname_full, obj: clusterizers}); await extraNetworksClusterizersLoadTab(tabname_full); }; const extraNetworksBtnDirsViewItemOnClick = (event, tabname_full) => { /** Handles `onclick` events for buttons in the directory view. */ const txt_search = gradioApp().querySelector(`#${tabname_full}_controls .extra-network-control--search-text`); const _deselect_all_buttons = () => { gradioApp().querySelectorAll(".extra-network-dirs-view-button").forEach((elem) => { delete elem.dataset.selected; }); }; const _select_button = elem => { _deselect_all_buttons(); // Update search input with select button's path. elem.dataset.selected = ""; txt_search.value = elem.textContent.trim(); }; const _deselect_button = elem => { delete elem.dataset.selected; txt_search.value = ""; }; if ("selected" in event.target.dataset) { _deselect_button(event.target); } else { _select_button(event.target); } updateInput(txt_search); extraNetworksApplyFilter(tabname_full); }; const extraNetworksControlSearchClearOnClick = (event, tabname_full) => { /** Dispatches custom event when the `clear` button in a search input is clicked. */ let clear_btn = event.target.closest(".extra-network-control--search-clear"); let txt_search = clear_btn.previousElementSibling; txt_search.value = ""; txt_search.dispatchEvent( new CustomEvent( "extra-network-control--search-clear", {bubbles: true, detail: {tabname_full: tabname_full}}, ) ); }; const extraNetworksControlSortModeOnClick = (event, tabname_full) => { /** Handles `onclick` events for Sort Mode buttons. */ event.currentTarget.parentElement.querySelectorAll('.extra-network-control--sort-mode').forEach(elem => { delete elem.dataset.selected; }); event.currentTarget.dataset.selected = ""; if (!keyExists(clusterizers, tabname_full)) { return; } const sort_mode_str = event.currentTarget.dataset.sortMode.toLowerCase(); clusterizers[tabname_full].cards_list.sort_mode_str = sort_mode_str; extraNetworksApplyFilter(tabname_full); }; const extraNetworksControlSortDirOnClick = (event, tabname_full) => { /** Handles `onclick` events for the Sort Direction button. * * Modifies the data attributes of the Sort Direction button to cycle between * ascending and descending sort directions. */ const curr_sort_dir_str = event.currentTarget.dataset.sortDir.toLowerCase(); if (!["ascending", "descending"].includes(curr_sort_dir_str)) { console.error(`Invalid sort_dir_str: ${curr_sort_dir_str}`); return; } let sort_dir_str = curr_sort_dir_str === "ascending" ? "descending" : "ascending"; event.currentTarget.dataset.sortDir = sort_dir_str; event.currentTarget.setAttribute("title", `Sort ${sort_dir_str}`); if (!keyExists(clusterizers, tabname_full)) { return; } clusterizers[tabname_full].cards_list.sort_dir_str = sort_dir_str; extraNetworksApplyFilter(tabname_full); }; const extraNetworksControlTreeViewOnClick = (event, tabname_full) => { /** Handles `onclick` events for the Tree View button. * * Toggles the tree view in the extra networks pane. */ let show; if ("selected" in event.currentTarget.dataset) { delete event.currentTarget.dataset.selected; show = false; } else { event.currentTarget.dataset.selected = ""; show = true; } if (!keyExists(clusterizers, tabname_full)) { return; } clusterizers[tabname_full].tree_list.scroll_elem.parentElement.classList.toggle("hidden", !show); //clusterizers[tabname_full].tree_list.enable(show); }; const extraNetworksControlDirsViewOnClick = (event, tabname_full) => { /** Handles `onclick` events for the Dirs View button. * * Toggles the directory view in the extra networks pane. */ const show = !("selected" in event.currentTarget.dataset); if (show) { event.currentTarget.dataset.selected = ""; } else { delete event.currentTarget.dataset.selected; } const pane = gradioApp().getElementById(`${tabname_full}_pane`); pane.querySelector(".extra-network-content--dirs-view").classList.toggle("hidden", !show); }; const extraNetworksControlRefreshOnClick = (event, tabname_full) => { /** Handles `onclick` events for the Refresh Page button. * * In order to actually call the python functions in `ui_extra_networks.py` * to refresh the page, we created an empty gradio button in that file with an * event handler that refreshes the page. So what this function here does * is it manually raises a `click` event on that button. */ // reset states initialUiOptionsLoaded.state = false; // We want to reset all clusterizers on refresh click so that the viewing area // shows that it is loading new data. for (const _tabname_full of Object.keys(clusterizers)) { for (const v of Object.values(clusterizers[_tabname_full])) { v.clear(); } } // Fire an event for this button click. gradioApp().getElementById(`${tabname_full}_extra_refresh_internal`).dispatchEvent(new Event("click")); }; const extraNetworksCardOnClick = (event, tabname) => { const elem = event.currentTarget; const prompt_elem = gradioApp().querySelector(`#${tabname}_prompt > label > textarea`); const neg_prompt_elem = gradioApp().querySelector(`#${tabname}_neg_prompt > label > textarea`); if ("negPrompt" in elem.dataset) { extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt); extraNetworksUpdatePrompt(neg_prompt_elem, elem.dataset.negPrompt); } else if ("allowNeg" in elem.dataset) { extraNetworksUpdatePrompt(activePromptTextarea[tabname], elem.dataset.prompt); } else { extraNetworksUpdatePrompt(prompt_elem, elem.dataset.prompt); } }; const extraNetworksTreeFileOnClick = (event, btn, tabname_full) => { return; }; const extraNetworksTreeDirectoryOnClick = (event, btn, tabname_full) => { return; }; const extraNetworksTreeOnClick = (event, tabname_full) => { const btn = event.target.closest(".tree-list-item"); if (!isElementLogError(btn)) { return; } if (btn.dataset.treeEntryType === "file") { extraNetworksTreeFileOnClick(event, btn, tabname_full); } else { extraNetworksTreeDirectoryOnClick(event, btn, tabname_full); } event.stopPropagation(); }; const extraNetworksBtnShowMetadataOnClick = (event, extra_networks_tabname, card_name) => { extraNetworksFetchMetadata(extra_networks_tabname, card_name); event.stopPropagation(); }; const extraNetworksBtnEditMetadataOnClick = (event, tabname_full, card_name) => { const id = `${tabname_full}_edit_user_metadata`; let editor = extraPageUserMetadataEditors[id]; if (isNullOrUndefined(editor)) { editor = {}; editor.page = gradioApp().getElementById(id); editor.nameTextarea = gradioApp().querySelector(`#${id}_name textarea`); editor.button = gradioApp().querySelector(`#${id}_button`); extraPageUserMetadataEditors[id] = editor; } editor.nameTextarea.value = card_name; updateInput(editor.nameTextarea); editor.button.click(); popup(editor.page); }; const extraNetworksBtnCopyPathOnClick = (event, path) => { copyToClipboard(path); event.stopPropagation(); }; // ==== MAIN SETUP ==== const extraNetworksSetupEventDelegators = () => { /** Sets up event delegators for all extraNetworks tabs. * * These event handlers are not tied to any specific elements on the page. * We do this because elements within each tab may be removed and replaced * which would break references to elements in DOM and thus prevent any event * listeners from firing. */ window.addEventListener("resizeHandleDblClick", event => { // See resizeHandle.js::onDoubleClick() for event detail. event.stopPropagation(); extraNetworksAutoSetTreeWidth(event.target.closest(".extra-network-pane")); }); // Update search filter whenever the search input's clear button is pressed. window.addEventListener("extra-network-control--search-clear", event => { event.stopPropagation(); extraNetworksApplyFilter(event.detail.tabname_full); }); // Debounce search text input. This way we only search after user is done typing. const search_input_debounce = debounce((tabname_full) => { extraNetworksApplyFilter(tabname_full); }, SEARCH_INPUT_DEBOUNCE_TIME_MS); window.addEventListener("keyup", event => { const controls = event.target.closest(".extra-network-controls"); if (isElement(controls)) { const tabname_full = controls.dataset.tabnameFull; const target = event.target.closest(".extra-network-control--search-text"); if (isElement(target)) { search_input_debounce.call(target, tabname_full); } } }); window.addEventListener("keydown", event => { if (event.key === "Escape") { closePopup(); } }); }; const extraNetworksSetupTabContent = async (tabname, pane, controls_div) => { const tabname_full = pane.id; const extra_networks_tabname = tabname_full.replace(`${tabname}_`, ""); const controls = await waitForElement(`#${tabname_full}_pane .extra-network-controls`); const tree_scroll_elem = await waitForElement(`#${tabname_full}_tree_list_scroll_area`); const tree_content_elem = await waitForElement(`#${tabname_full}_tree_list_content_area`); const cards_scroll_elem = await waitForElement(`#${tabname_full}_cards_list_scroll_area`); const cards_content_elem = await waitForElement(`#${tabname_full}_cards_list_content_area`); await waitForElement(`#${tabname_full}_pane .extra-network-content--dirs-view`); console.log("BEFORE:", tree_scroll_elem, cards_scroll_elem); controls.id = `${tabname_full}_controls`; controls_div.insertBefore(controls, null); clusterizers[tabname_full] = { tree_list: new ExtraNetworksClusterizeTreeList({ tabname: tabname, extra_networks_tabname: extra_networks_tabname, scrollElem: tree_scroll_elem, contentElem: tree_content_elem, tag: "div", callbacks: { initData: extraNetworksOnInitData, fetchData: extraNetworksOnFetchData, }, }), cards_list: new ExtraNetworksClusterizeCardsList({ tabname: tabname, extra_networks_tabname: extra_networks_tabname, scrollElem: cards_scroll_elem, contentElem: cards_content_elem, tag: "div", callbacks: { initData: extraNetworksOnInitData, fetchData: extraNetworksOnFetchData, }, }), }; if (pane.style.display !== "none") { extraNetworksShowControlsForPage(tabname, tabname_full); } await extraNetworksClusterizersLoadTab({ tabname_full: tabname_full, selected: false, fetch_data: true, }); }; const extraNetworksSetupTab = async (tabname) => { let controls_div; const this_tab = await waitForElement(`#${tabname}_extra_tabs`); const tab_nav = await waitForElement(`#${tabname}_extra_tabs > div.tab-nav`); controls_div = document.createElement("div"); controls_div.classList.add("extra-network-controls-div"); tab_nav.appendChild(controls_div); tab_nav.insertBefore(controls_div, null); const panes = this_tab.querySelectorAll(`:scope > .tabitem[id^="${tabname}_"]`); for (const pane of panes) { await extraNetworksSetupTabContent(tabname, pane, controls_div); } extraNetworksRegisterPromptForTab(tabname, `${tabname}_prompt`); extraNetworksRegisterPromptForTab(tabname, `${tabname}_neg_prompt`); }; const extraNetworksSetup = async () => { await waitForBool(initialUiOptionsLoaded); extraNetworksSetupTab('txt2img'); extraNetworksSetupTab('img2img'); extraNetworksSetupEventDelegators(); }; onUiLoaded(extraNetworksSetup); onOptionsChanged(() => initialUiOptionsLoaded.state = true);