diff --git a/html/extra-networks-no-cards.html b/html/extra-networks-no-cards.html deleted file mode 100644 index 389358d6c..000000000 --- a/html/extra-networks-no-cards.html +++ /dev/null @@ -1,8 +0,0 @@ -
-

Nothing here. Add some content to the following directories:

- - -
- diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index f541afe57..06f76192f 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -13,6 +13,49 @@ const extraPageUserMetadataEditors = {}; // A flag used by the `waitForBool` promise to determine when we first load Ui Options. const initialUiOptionsLoaded = { state: false }; +/** Helper functions for checking types and simplifying logging. */ + +const isString = x => typeof x === "string" || x instanceof String; +const isStringLogError = x => { + if (isString(x)) { + return true; + } + console.error("expected string, got:", typeof x); + return false; +}; +const isNull = x => typeof x === "null" || x === null; +const isUndefined = x => typeof x === "undefined" || x === undefined; +// checks both null and undefined for simplicity sake. +const isNullOrUndefined = x => isNull(x) || isUndefined(x); +const isNullOrUndefinedLogError = x => { + if (isNullOrUndefined(x)) { + console.error("Variable is null/undefined."); + return true; + } + return false; +}; + +const isElement = x => x instanceof Element; +const isElementLogError = x => { + if (isElement(x)) { + return true; + } + console.error("expected element type, got:", typeof x); + return false; +} + +const getElementByIdLogError = selector => { + let elem = gradioApp().getElementById(selector); + isElementLogError(elem); + return elem; +}; + +const querySelectorLogError = selector => { + let elem = gradioApp().querySelector(selector); + isElementLogError(elem); + return elem; +} + const debounce = (handler, timeout_ms) => { /** Debounces a function call. * @@ -40,7 +83,7 @@ const debounce = (handler, timeout_ms) => { }; } -function waitForElement(selector) { +const waitForElement = selector => { /** Promise that waits for an element to exist in DOM. */ return new Promise(resolve => { if (document.querySelector(selector)) { @@ -61,7 +104,7 @@ function waitForElement(selector) { }); } -function waitForBool(o) { +const waitForBool = o => { /** Promise that waits for a boolean to be true. * * `o` must be an Object of the form: @@ -79,7 +122,7 @@ function waitForBool(o) { }); } -function waitForKeyInObject(o) { +const waitForKeyInObject = o => { /** Promise that waits for a key to exist in an object. * * `o` must be an Object of the form: @@ -100,7 +143,7 @@ function waitForKeyInObject(o) { }); } -function waitForValueInObject(o) { +const waitForValueInObject = o => { /** Promise that waits for a key value pair in an Object. * * `o` must be an Object of the form: @@ -171,7 +214,7 @@ function extraNetworksRegisterPromptForTab(tabname, id) { } function extraNetworksSetupTabContent(tabname, pane, controls_div) { - let tabname_full = pane.id; + const tabname_full = pane.id; let txt_search; Promise.all([ @@ -197,7 +240,7 @@ function extraNetworksSetupTabContent(tabname, pane, controls_div) { }; // Debounce search text input. This way we only search after user is done typing. - let search_input_debounce = debounce(() => { + const search_input_debounce = debounce(() => { extraNetworksApplyFilter(tabname_full); }, SEARCH_INPUT_DEBOUNCE_TIME_MS); txt_search.addEventListener("keyup", search_input_debounce); @@ -208,7 +251,7 @@ function extraNetworksSetupTabContent(tabname, pane, controls_div) { }); // Insert the controls into the page. - var controls = gradioApp().querySelector(`#${tabname_full}_controls`); + const controls = gradioApp().querySelector(`#${tabname_full}_controls`); controls_div.insertBefore(controls, null); if (pane.style.display != "none") { extraNetworksShowControlsForPage(tabname, tabname_full); @@ -231,7 +274,6 @@ function extraNetworksSetupTab(tabname) { this_tab.querySelectorAll(`:scope > .tabitem[id^="${tabname}_"]`).forEach((elem) => { extraNetworksSetupTabContent(tabname, elem, controls_div); }); - }).then(() => { extraNetworksRegisterPromptForTab(tabname, `${tabname}_prompt`); extraNetworksRegisterPromptForTab(tabname, `${tabname}_neg_prompt`); }); @@ -295,29 +337,28 @@ function extraNetworksApplyFilter(tabname_full) { // 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. const txt_search = gradioApp().querySelector(`#${tabname_full}_extra_search`); - if (!txt_search) { + if (!isElementLogError(txt_search)) { return; } // tree view buttons let btn = gradioApp().querySelector(`#${tabname_full}_tree_list_content_area .tree-list-item[data-selected=""]`); - if (btn && btn.dataset.path !== txt_search.value) { - delete btn.dataset.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 = gradioApp().querySelector(`#${tabname_full}_dirs .extra-network-dirs-view-button[data-selected=""]`); - if (btn && btn.textContent.trim() !== txt_search.value) { + if (isElement(btn) && btn.textContent.trim() !== txt_search.value) { delete btn.dataset.selected; } } function extraNetworksClusterizersEnable(tabname_full) { - /** Enables the selected tab's clusterizer and disables all others. */ + /** Enables the selected tab's clusterize lists and disables all others. */ // iterate over tabnames for (const [_tabname_full, tab_clusterizers] of Object.entries(clusterizers)) { // iterate over clusterizers in tab for (const v of Object.values(tab_clusterizers)) { - // Enable the active tab, disable all others. v.enable(_tabname_full === tabname_full); } } @@ -346,6 +387,7 @@ function extraNetworksSetupEventHandlers() { // Handle doubleclick events from the resize handle. // This will automatically fit the left pane to the size of its largest loaded row. window.addEventListener("resizeHandleDblClick", (e) => { + // See resizeHandle.js::onDoubleClick() for event detail. e.stopPropagation(); const tabname_full = e.target.closest(".extra-network-pane").dataset.tabnameFull; // This event is only applied to the currently selected tab if has clusterize list. @@ -373,7 +415,6 @@ function setupExtraNetworks() { waitForBool(initialUiOptionsLoaded).then(() => { extraNetworksSetupTab('txt2img'); extraNetworksSetupTab('img2img'); - }).then(() => { extraNetworksSetupEventHandlers(); }); } @@ -448,15 +489,6 @@ function saveCardPreview(event, tabname, filename) { event.preventDefault(); } -function extraNetworksSearchButton(event, tabname_full) { - var searchTextarea = gradioApp().querySelector("#" + tabname_full + "_extra_search"); - var button = event.target; - var text = button.classList.contains("search-all") ? "" : button.textContent.trim(); - - searchTextarea.value = text; - updateInput(searchTextarea); -} - function extraNetworksTreeProcessFileClick(event, btn, tabname_full) { /** * Processes `onclick` events when user clicks on files in tree. @@ -503,7 +535,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname_full) { let prev_selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']"); clusterizers[tabname_full].tree_list.onRowExpandClick(div_id, btn); let selected_elem = gradioApp().querySelector(".tree-list-item[data-selected='']"); - if (prev_selected_elem instanceof Element && !(selected_elem instanceof Element)) { + if (isElement(prev_selected_elem) && !isElement(selected_elem)) { // if a selected element was removed, clear filter. _updateSearch(""); } @@ -535,7 +567,8 @@ function extraNetworksTreeOnClick(event, tabname_full) { } function extraNetworkDirsOnClick(event, tabname_full) { - var txt_search = gradioApp().querySelector(`#${tabname_full}_extra_search`); + /** Handles `onclick` events for buttons in the directory view. */ + let txt_search = gradioApp().querySelector(`#${tabname_full}_extra_search`); function _deselect_all() { // deselect all buttons @@ -568,18 +601,14 @@ function extraNetworkDirsOnClick(event, tabname_full) { } function extraNetworksControlSearchClearOnClick(event, tabname_full) { - /** Clears the search text. */ + /** 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", - { - detail: { - tabname_full: tabname_full - } - } + { detail: { tabname_full: tabname_full } }, ) ); } @@ -601,15 +630,10 @@ function extraNetworksControlSortModeOnClick(event, tabname_full) { } function extraNetworksControlSortDirOnClick(event, tabname_full) { - /** - * Handles `onclick` events for the Sort Direction button. + /** 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. - * - * @param event The generated event. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. */ if (event.currentTarget.dataset.sortDir.toLowerCase() == "ascending") { event.currentTarget.dataset.sortDir = "descending"; @@ -626,14 +650,9 @@ function extraNetworksControlSortDirOnClick(event, tabname_full) { } function extraNetworksControlTreeViewOnClick(event, tabname_full) { - /** - * Handles `onclick` events for the Tree View button. + /** Handles `onclick` events for the Tree View button. * * Toggles the tree view in the extra networks pane. - * - * @param event The generated event. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. */ var button = event.currentTarget; button.classList.toggle("extra-network-control--enabled"); @@ -644,14 +663,9 @@ function extraNetworksControlTreeViewOnClick(event, tabname_full) { } function extraNetworksControlDirsViewOnClick(event, tabname_full) { - /** - * Handles `onclick` events for the Dirs View button. + /** Handles `onclick` events for the Dirs View button. * * Toggles the directory view in the extra networks pane. - * - * @param event The generated event. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. */ var button = event.currentTarget; button.classList.toggle("extra-network-control--enabled"); @@ -661,17 +675,12 @@ function extraNetworksControlDirsViewOnClick(event, tabname_full) { } function extraNetworksControlRefreshOnClick(event, tabname_full) { - /** - * Handles `onclick` events for the Refresh Page button. + /** 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. - * - * @param event The generated event. - * @param tabname_full The full active tabname. - * i.e. txt2img_lora, img2img_checkpoints, etc. */ // reset states initialUiOptionsLoaded.state = false; diff --git a/javascript/extraNetworksClusterizeList.js b/javascript/extraNetworksClusterizeList.js index 3fddb60dd..5431c298b 100644 --- a/javascript/extraNetworksClusterizeList.js +++ b/javascript/extraNetworksClusterizeList.js @@ -4,49 +4,6 @@ const RESIZE_DEBOUNCE_TIME_MS = 250; const INT_COLLATOR = new Intl.Collator([], { numeric: true }); const STR_COLLATOR = new Intl.Collator("en", { numeric: true, sensitivity: "base" }); -/** Helper functions for checking types and simplifying logging. */ - -const isString = x => typeof x === "string" || x instanceof String; -const isStringLogError = x => { - if (isString(x)) { - return true; - } - console.error("expected string, got:", typeof x); - return false; -}; -const isNull = x => typeof x === "null" || x === null; -const isUndefined = x => typeof x === "undefined" || x === undefined; -// checks both null and undefined for simplicity sake. -const isNullOrUndefined = x => isNull(x) || isUndefined(x); -const isNullOrUndefinedLogError = x => { - if (isNullOrUndefined(x)) { - console.error("Variable is null/undefined."); - return true; - } - return false; -}; - -const isElement = x => x instanceof Element; -const isElementLogError = x => { - if (isElement(x)) { - return true; - } - console.error("expected element type, got:", typeof x); - return false; -} - -const getElementByIdLogError = selector => { - let elem = gradioApp().getElementById(selector); - isElementLogError(elem); - return elem; -}; - -const querySelectorLogError = selector => { - let elem = gradioApp().querySelector(selector); - isElementLogError(elem); - return elem; -} - const getComputedPropertyDims = (elem, prop) => { /** Returns the top/left/bottom/right float dimensions of an element for the specified property. */ const style = window.getComputedStyle(elem, null); @@ -99,7 +56,6 @@ const getComputedDims = elem => { width: width + margin.width + padding.width + border.width, height: height + margin.height + padding.height + border.height, } - } function compress(string) { @@ -130,7 +86,7 @@ function decompress(base64string) { }); } -const parseHtml = function (str) { +const htmlStringToElement = function (str) { /** Converts an HTML string into an Element type. */ let parser = new DOMParser(); let tmp = parser.parseFromString(str, "text/html"); @@ -210,7 +166,8 @@ class ExtraNetworksClusterize { // Stores the current encoded string so we can compare against future versions. this.encoded_str = ""; - this.no_data_text = "Directory is empty."; + this.no_data_text = "No results."; + this.no_data_class = "clusterize-no-data"; this.n_rows = 1; this.n_cols = 1; @@ -253,14 +210,21 @@ class ExtraNetworksClusterize { } parseJson(encoded_str) { /** promise */ - /** Parses a base64 encoded and gzipped JSON string and sets up a clusterize instance. */ + /** Parses a base64 encoded and gzipped JSON string and sets up a clusterize instance. */ return new Promise(resolve => { // Skip parsing if the string hasnt actually updated. if (this.encoded_str === encoded_str) { return resolve(); } Promise.resolve(encoded_str) - .then(v => { if (!isNullOrUndefined(this.clusterize)) {this.clear();} return v; }) + .then(v => { + if (!isNullOrUndefined(this.clusterize)) { + this.data_obj = {}; + this.data_obj_keys_sorted = []; + this.clear(); + } + return v; + }) .then(v => decompress(v)) .then(v => JSON.parse(v)) .then(v => this.updateJson(v)) @@ -273,7 +237,7 @@ class ExtraNetworksClusterize { updateJson(json) { /** promise */ console.error("Base class method called. Must be overridden by subclass."); - return new Promise(resolve => {return resolve();}); + return new Promise(resolve => { return resolve(); }); } sortByDivId() { @@ -376,12 +340,12 @@ class ExtraNetworksClusterize { // If no rows exist, we need to add one so we can calculate rows/cols. // We remove this row before returning. - if (this.rowCount() === 0){// || this.content_elem.innerHTML === "") { + if (this.rowCount() === 0) {// || this.content_elem.innerHTML === "") { this.clear(); this.update([this.data_obj[this.data_obj_keys_sorted[0]].html]); clear_before_return = true; } - + const child = this.content_elem.querySelector(":not(.clusterize-extra-row)"); if (isNullOrUndefined(child)) { if (clear_before_return) { @@ -389,7 +353,7 @@ class ExtraNetworksClusterize { return rebuild_required; } } - + // Calculate the visible rows and colums for the clusterize-content area. let n_cols = calcColsPerRow(this.content_elem, child); let n_rows = calcRowsPerCol(this.scroll_elem, child); @@ -493,6 +457,7 @@ class ExtraNetworksClusterize { blocks_in_cluster: this.blocks_in_cluster, show_no_data_row: this.show_no_data_row, no_data_text: this.no_data_text, + no_data_class: this.no_data_class, callbacks: this.callbacks, } ); @@ -505,7 +470,7 @@ class ExtraNetworksClusterize { onElementDetached(elem_id) { /** Callback whenever one of our elements has become detached from the DOM. */ - switch(elem_id) { + switch (elem_id) { case this.data_id: waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem); break; @@ -565,7 +530,7 @@ class ExtraNetworksClusterize { this.onElementDetached(content_elem.id); } }); - this.element_observer.observe(gradioApp(), {subtree: true, childList: true, attributes: true}); + this.element_observer.observe(gradioApp(), { subtree: true, childList: true, attributes: true }); } setupResizeHandlers() { @@ -636,6 +601,7 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { constructor(...args) { super(...args); + this.no_data_text = "No directories/files"; this.selected_div_id = null; } @@ -663,7 +629,7 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { let text_size = style.getPropertyValue("--button-large-text-size"); for (const [k, v] of Object.entries(json)) { let div_id = k; - let parsed_html = parseHtml(v); + let parsed_html = htmlStringToElement(v); // parent_id = -1 if item is at root level let parent_id = "parentId" in parsed_html.dataset ? parsed_html.dataset.parentId : -1; let expanded = "expanded" in parsed_html.dataset; @@ -722,7 +688,7 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { this.data_obj[child_id].active = false; if (this.data_obj[child_id].selected) { // deselect the child only if it is selected. - let elem = parseHtml(this.data_obj[child_id].html); + let elem = htmlStringToElement(this.data_obj[child_id].html); delete elem.dataset.selected; this.data_obj[child_id].selected = false; this.updateDivContent(child_id, elem); @@ -759,7 +725,17 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { this.updateRows(); } - onRowSelected(div_id, elem) { + _setRowSelectedState(div_id, elem, new_state) { + if (new_state) { + elem.dataset.selected = ""; + } else { + delete elem.dataset.selected; + } + this.data_obj[div_id].selected = new_state; + this.updateDivContent(div_id, elem); + } + + onRowSelected(div_id, elem, override) { /** Selects a row and deselects all others. */ if (!isElementLogError(elem)) { return; @@ -769,27 +745,27 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { return; } - if (!isNullOrUndefined(this.selected_div_id) && div_id !== this.selected_div_id) { - let prev_elem = parseHtml(this.data_obj[this.selected_div_id].html); - delete prev_elem.dataset.selected; - this.updateDivContent(this.selected_div_id, prev_elem); - this.data_obj[this.selected_div_id].selected = false; + if (!isNullOrUndefined(override)) { + override = (override === true); + } + if (!isNullOrUndefined(this.selected_div_id) && div_id !== this.selected_div_id) { + // deselect the current selected row + let prev_elem = htmlStringToElement(this.data_obj[this.selected_div_id].html); + this._setRowSelectedState(this.selected_div_id, prev_elem, false); + + // select the new row + this._setRowSelectedState(div_id, elem, true); this.selected_div_id = div_id; - elem.dataset.selected = ""; - this.data_obj[div_id].selected = true; - this.updateDivContent(this.selected_div_id, elem); } else { - if ("selected" in elem.dataset) { - delete elem.dataset.selected; - this.data_obj[div_id].selected = false; + // toggle the passed row's selected state. + if (this.data_obj[div_id].selected) { + this._setRowSelectedState(div_id, elem, false); this.selected_div_id = null; } else { - elem.dataset.selected = ""; - this.data_obj[div_id].selected = true; + this._setRowSelectedState(div_id, elem, true); this.selected_div_id = div_id; } - this.updateDivContent(div_id, elem); } this.updateRows(); } @@ -837,6 +813,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { constructor(...args) { super(...args); + this.no_data_text = "No files matching filter."; this.sort_mode_str = "path"; this.sort_dir_str = "ascending"; this.filter_str = ""; @@ -847,7 +824,7 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { return new Promise(resolve => { for (const [i, [k, v]] of Object.entries(Object.entries(json))) { let div_id = k; - let parsed_html = parseHtml(v); + let parsed_html = htmlStringToElement(v); let search_only = isElement(parsed_html.querySelector(".search_only")); let search_terms_elem = parsed_html.querySelector(".search_terms"); let search_terms = "";