diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index cb9fd96af..d8f9ac046 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -133,7 +133,7 @@
+ class="extra-network-content--container">
{dirs_html} diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 4f6673f81..9bcbdfbe2 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -11,13 +11,6 @@ const storedPopupIds = {}; const extraPageUserMetadataEditors = {}; // A flag used by the `waitForBool` promise to determine when we first load Ui Options. const initialUiOptionsLoaded = { state: false }; -// A proxy listener to detect and handle changes on JSON Data -const extra_networks_json_proxy = {}; -const extra_networks_proxy_listener = setupProxyListener( - extra_networks_json_proxy, - () => { }, - proxyJsonUpdated, -); function waitForElement(selector) { /** Promise that waits for an element to exist in DOM. */ @@ -104,30 +97,6 @@ function waitForValueInObject(o) { }); } -function setupProxyListener(target, pre_handler, post_handler) { - /** Sets up a listener for variable changes. */ - var proxy = new Proxy(target, { - set: function (t, k, v) { - pre_handler.call(t, k, v); - t[k] = v; - post_handler.call(t, k, v); - return true; - } - }); - return proxy -} - -function proxyJsonUpdated(k, v) { - /** Callback triggered when JSON data is updated by the proxy listener. */ - // use `this` for current object - if (!(v.dataset.tabnameFull in clusterizers)) { - // Clusterizers not yet initialized. - return; - } - clusterizers[v.dataset.tabnameFull][v.dataset.proxyName].parseJson(v.dataset.json); - clusterizers_initial_load[v.dataset.tabnameFull].state = true; -} - function toggleCss(key, css, enable) { var style = document.getElementById(key); if (enable && !style) { @@ -159,9 +128,7 @@ function extraNetworksRefreshTab(tabname_full) { div_tree.classList.toggle("hidden", !btn_tree_view.classList.contains("extra-network-control--enabled")); waitForKeyInObject({ k: tabname_full, v: clusterizers }).then(() => { - extraNetworksClusterizersEnable(tabname_full); - extraNetworksForceUpdateData(tabname_full); - extraNetworksClusterizersRebuild(tabname_full, false); + extraNetworkClusterizersOnTabLoad(tabname_full); }); } @@ -189,16 +156,18 @@ function extraNetworksSetupTabContent(tabname, pane, controls_div) { ]).then(() => { // Now that we have our elements in DOM, we create the clusterize lists. clusterizers[tabname_full] = { - cards_list: new ExtraNetworksClusterizeCardsList({ - scroll_id: `${tabname_full}_cards_list_scroll_area`, - content_id: `${tabname_full}_cards_list_content_area`, - txt_search_elem: txt_search, - }), tree_list: new ExtraNetworksClusterizeTreeList({ + data_id: `${tabname_full}_tree_list_data`, scroll_id: `${tabname_full}_tree_list_scroll_area`, content_id: `${tabname_full}_tree_list_content_area`, txt_search_elem: txt_search, }), + cards_list: new ExtraNetworksClusterizeCardsList({ + data_id: `${tabname_full}_cards_list_data`, + scroll_id: `${tabname_full}_cards_list_scroll_area`, + content_id: `${tabname_full}_cards_list_content_area`, + txt_search_elem: txt_search, + }), }; clusterizers_initial_load[tabname_full] = { state: false }; @@ -225,6 +194,23 @@ function extraNetworksSetupTabContent(tabname, pane, controls_div) { if (pane.style.display != "none") { extraNetworksShowControlsForPage(tabname, tabname_full); } + + // Handle extra-network-pane resize events. + let resize_observer_timer; + let done_resize_observer_interval_ms = 250; + let resize_observer = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === pane) { + clearTimeout(resize_observer_timer); + resize_observer_timer = setTimeout( + () => extraNetworksClusterizersOnResize(tabname_full), + done_resize_observer_interval_ms, + ); + break; + } + } + }); + resize_observer.observe(pane); }) } @@ -291,15 +277,20 @@ function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, t extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt); extraNetworksShowControlsForPage(tabname, tabname_full); + console.log("HERE!!!:", clusterizers[tabname_full].cards_list.scroll_elem.isConnected); + Promise.all([ waitForKeyInObject({ obj: clusterizers, k: tabname_full }), waitForKeyInObject({ obj: clusterizers_initial_load, k: tabname_full }).then(() => { - waitForBool(clusterizers_initial_load[tabname_full]).then(() => { return; }); + waitForBool(clusterizers_initial_load[tabname_full]).then(() => { + Promise.all([ + clusterizers[tabname_full].tree_list.waitForElements(), + clusterizers[tabname_full].cards_list.waitForElements(), + ]).then(() => {return;}); + }); }), ]).then(() => { - extraNetworksClusterizersEnable(tabname_full); - extraNetworksForceUpdateData(tabname_full); - extraNetworksClusterizersRebuild(tabname_full, false); + extraNetworkClusterizersOnTabLoad(tabname_full); }); } @@ -313,35 +304,6 @@ function extraNetworksApplyFilter(tabname_full) { clusterizers[tabname_full].cards_list.applyFilter(); } -function extraNetworksForceUpdateData(tabname_full) { - for (const elem of gradioApp().querySelectorAll(`.extra-network-script-data[data-tabname-full="${tabname_full}"]`)) { - extra_networks_proxy_listener[`${elem.dataset.tabnameFull}_${elem.dataset.proxyName}`] = elem; - } -} - -function extraNetworksSetupData() { - // Manually force read the json data. - for (const elem of gradioApp().querySelectorAll(".extra-network-script-data")) { - extra_networks_proxy_listener[`${elem.dataset.tabnameFull}_${elem.dataset.proxyName}`] = elem; - } -} - -function extraNetworksClusterizersUpdateRows(tabname_full) { - if (tabname_full !== undefined && tabname_full in clusterizers) { - for (const v of Object.values(clusterizers[tabname_full])) { - v.updateRows(); - } - return; - } - // 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)) { - v.updateRows(); - } - } -} - function extraNetworksClusterizersEnable(tabname_full) { // iterate over tabnames for (const [_tabname_full, tab_clusterizers] of Object.entries(clusterizers)) { @@ -353,6 +315,22 @@ function extraNetworksClusterizersEnable(tabname_full) { } } +function extraNetworksClusterizersUpdateRows(tabname_full) { + if (tabname_full !== undefined && tabname_full in clusterizers) { + for (const v of Object.values(clusterizers[tabname_full])) { + //v.updateRows(); + } + return; + } + // 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)) { + //v.updateRows(); + } + } +} + function extraNetworksClusterizersRebuild(tabname_full, force) { // rebuild only the specified tab if (tabname_full in clusterizers) { @@ -370,26 +348,42 @@ function extraNetworksClusterizersRebuild(tabname_full, force) { } } +function extraNetworkClusterizersOnTabLoad(tabname_full) { + /** Enables a tab's clusterizer, updates its data, and rebuilds it. */ + if (!(tabname_full in clusterizers)) { + return; + } + + extraNetworksClusterizersEnable(tabname_full); + for (const v of Object.values(clusterizers[tabname_full])) { + v.load(); + } +} + +function extraNetworksClusterizersOnResize(tabname_full) { + extraNetworksClusterizersUpdateRows(tabname_full); +} + function extraNetworksSetupEventHandlers() { // Handle window resizes. Delay of 500ms after resize before firing an event // as a way of "debouncing" resizes. var resize_timer; + var resize_timeout_ms = 500; window.addEventListener("resize", () => { clearTimeout(resize_timer); - resize_timer = setTimeout(function () { - // Update rows for each list. - extraNetworksClusterizersUpdateRows(); - }, 500); // ms + resize_timer = setTimeout(() => {extraNetworksClusterizersOnResize();}, resize_timeout_ms); }); // Handle resizeHandle resizes. Only fires on mouseup after resizing. window.addEventListener("resizeHandleResized", (e) => { + e.stopPropagation(); const tabname_full = e.target.closest(".extra-network-pane").id.replace("_pane", ""); // Force update rows after resizing. - extraNetworksClusterizersUpdateRows(tabname_full); + extraNetworksClusterizersOnResize(tabname_full); }); window.addEventListener("resizeHandleDblClick", (e) => { + e.stopPropagation(); const tabname_full = e.target.closest(".extra-network-pane").id.replace("_pane", ""); // This event is only applied to the currently selected tab if has clusterize list. if (!(tabname_full in clusterizers)) { @@ -405,12 +399,14 @@ function extraNetworksSetupEventHandlers() { content.querySelectorAll(".tree-list-item").forEach((row) => { // Temporarily set the grid columns to maximize column width // so we can calculate the full width of the row. + const row_style = window.getComputedStyle(row, null); const prev_grid_template_columns = row.style.gridTemplateColumns; - row.style.gridTemplateColumns = "repeat(5, max-content)"; + const n_cols = row_style.getPropertyValue("grid-template-columns").split(" ").length; + row.style.gridTemplateColumns = `repeat(${n_cols}, max-content)`; if (row.scrollWidth > max_width) { max_width = row.scrollWidth; } - row.style.gridTemplateColumns = prev_grid_template_columns; + //row.style.gridTemplateColumns = prev_grid_template_columns; }); if (max_width <= 0) { return; @@ -425,7 +421,7 @@ function extraNetworksSetupEventHandlers() { e.detail.setLeftColGridTemplate(e.target, max_width); - extraNetworksClusterizersUpdateRows(tabname_full); + extraNetworksClusterizersOnResize(tabname_full); }); } @@ -733,7 +729,7 @@ function extraNetworksControlTreeViewOnClick(event, tabname_full) { var show = !button.classList.contains("extra-network-control--enabled"); gradioApp().getElementById(`${tabname_full}_tree_list_scroll_area`).parentElement.classList.toggle("hidden", show); - extraNetworksClusterizersUpdateRows(tabname_full); + extraNetworksClusterizersonResize(tabname_full); } function extraNetworksControlDirsViewOnClick(event, tabname_full) { @@ -751,7 +747,7 @@ function extraNetworksControlDirsViewOnClick(event, tabname_full) { var show = !button.classList.contains("extra-network-control--enabled"); gradioApp().getElementById(`${tabname_full}_dirs`).classList.toggle("hidden", show); - extraNetworksClusterizersUpdateRows(tabname_full); + extraNetworksClusterizersOnResize(tabname_full); } function extraNetworksControlRefreshOnClick(event, tabname_full) { diff --git a/javascript/extraNetworksClusterizeList.js b/javascript/extraNetworksClusterizeList.js index a3f3b9351..21d842d08 100644 --- a/javascript/extraNetworksClusterizeList.js +++ b/javascript/extraNetworksClusterizeList.js @@ -2,6 +2,46 @@ const INT_COLLATOR = new Intl.Collator([], { numeric: true }); const STR_COLLATOR = new Intl.Collator("en", { numeric: true, sensitivity: "base" }); +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)) { + return true; + } + console.error("Variable is null/undefined."); + 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 = x => { + let elem = gradioApp().getElementById(x); + isElementLogError(elem); + return elem; +}; + +const querySelectorLogError = x => { + let elem = gradioApp().querySelector(x); + isElementLogError(elem); + return elem; +} + function compress(string) { /** Compresses a string into a base64 encoded GZipped string. */ const cs = new CompressionStream('gzip'); @@ -86,6 +126,7 @@ class ExtraNetworksClusterize { /** Base class for a clusterize list. Cannot be used directly. */ constructor( { + data_id, scroll_id, content_id, txt_search_elem, @@ -100,13 +141,13 @@ class ExtraNetworksClusterize { callbacks: {}, } ) { - if (scroll_id === undefined) { - console.error("scroll_id is undefined!"); - } - if (content_id === undefined) { - console.error("content_id is undefined!"); - } + // Do not continue if any of the required parameters are invalid. + if (!isStringLogError(data_id)) { return; } + if (!isStringLogError(scroll_id)) { return; } + if (!isStringLogError(content_id)) { return; } + if (!isElementLogError(txt_search_elem)) { return; } + this.data_id = data_id; this.scroll_id = scroll_id; this.content_id = content_id; this.txt_search_elem = txt_search_elem; @@ -115,6 +156,17 @@ class ExtraNetworksClusterize { this.show_no_data_row = show_no_data_row; this.callbacks = callbacks; + this.clusterize = null; + + this.data_elem = null; + this.scroll_elem = null; + this.content_elem = null; + + this.resize_observer = null; + this.resize_observer_timer = null; + this.resize_observer_timeout_ms = 250; + this.element_observer = null; + this.enabled = false; this.encoded_str = ""; @@ -122,29 +174,23 @@ class ExtraNetworksClusterize { this.no_data_text = "Directory is empty."; this.no_data_class = "nocards"; - this.scroll_elem = document.getElementById(this.scroll_id); - this.content_elem = document.getElementById(this.content_id); - this.n_rows = 1; this.n_cols = 1; - this.sort_fn = this.sortByDivId; - this.sort_reverse = false; - this.data_obj = {}; this.data_obj_keys_sorted = []; - this.clusterize = new Clusterize( - { - rows: [], - scrollId: this.scroll_id, - contentId: this.content_id, - rows_in_block: this.rows_in_block, - blocks_in_cluster: this.blocks_in_cluster, - show_no_data_row: this.show_no_data_row, - callbacks: this.callbacks, - } - ); + this.sort_fn = this.sortByDivId; + this.sort_reverse = false; + + Promise.all([ + waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem), + waitForElement(`#${this.scroll_id}`).then((elem) => this.scroll_elem = elem), + waitForElement(`#${this.content_id}`).then((elem) => this.content_elem = elem), + ]).then(() => { + this.setupElementObservers(); + this.setupResizeHandlers(); + }); } enable(enabled) { @@ -157,22 +203,35 @@ class ExtraNetworksClusterize { } } - parseJson(encoded_str) { - // Skip parsing if the string hasnt actually updated. - if (this.encoded_str === encoded_str) { - return; - } - - Promise.resolve(encoded_str) - .then(v => decompress(v)) - .then(v => JSON.parse(v)) - .then(v => this.updateJson(v)) - .then(() => this.encoded_str = encoded_str); + load() { + return waitForElement(`#${this.data_id}`) + .then((elem) => this.data_elem = elem) + .then(() => this.parseJson(this.data_elem.dataset.json)) + .then(() => this.init()) + .then(() => this.repair()) + .then(() => this.applyFilter()); } - updateJson(json) { + parseJson(encoded_str) { /** promise */ + return new Promise(resolve => { + // Skip parsing if the string hasnt actually updated. + if (this.encoded_str === encoded_str) { + console.log("no change"); + return resolve(); + } + return resolve( + Promise.resolve(encoded_str) + .then(v => decompress(v)) + .then(v => JSON.parse(v)) + .then(v => this.updateJson(v)) + .then(() => {console.log("parse json done"); this.encoded_str = encoded_str; }) + ); + }); + } + + updateJson(json) { /** promise */ /** Must be overridden by inherited class. */ - return; + return new Promise(resolve => {return resolve();}); } sortByDivId() { @@ -185,12 +244,12 @@ class ExtraNetworksClusterize { if (this.sort_reverse) { this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse(); } - this.updateRows(); } applyFilter() { - // the base class filter just sorts the values and updates the rows. + /** Must be implemented by subclasses. */ this.applySort(); + this.updateRows(); } filterRows(obj) { @@ -220,106 +279,275 @@ class ExtraNetworksClusterize { return false; } - fixElementDOM() { - /** Fixes element association in DOM. Returns true if element was replaced in DOM. */ - // If association for elements is broken, replace them with instance version. - if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) { - document.getElementById(this.scroll_id).replaceWith(this.scroll_elem); - return true; - } - return false; - } - updateRows() { - this.clusterize.refresh(); - - // If we don't have any entries to the list, then just clear the clusterize - // area and return. - if (this.data_obj_keys_sorted.length === 0 || !(this.data_obj_keys_sorted[0] in this.data_obj)) { - this.clusterize.clear(); + // If we don't have any entries in the dataset, then just clear the list and return. + if (this.data_obj_keys_sorted.length === 0 || Object.keys(this.data_obj).length === 0) { return; } - if (this.fixElementDOM()) { - // Add single item so we can calculate the dimensions without wasting time. - this.clusterize.clear(); - this.clusterize.update([this.data_obj[this.data_obj_keys_sorted[0]].element.outerHTML]); - } + this.refresh(); - this.updateItemDims(); - // Clear the list before adding new items to prevent weird scrolling issues. - this.clusterize.clear(); - this.clusterize.update(this.filterRows(this.data_obj)); + // Rebuild with `force=false` so we only rebuild if dimensions change. + this.rebuild(false); } - nrows() { - return this.clusterize.getRowsAmount(); - } + recalculateDims() { + let rebuild_required = false; + let clear_before_return = false; - updateItemDims() { if (!this.enabled) { // Inactive list is not displayed on screen. Would error if trying to resize. - return; + return false; } - if (this.nrows() <= 0) { + if (Object.keys(this.data_obj).length === 0 || this.data_obj_keys_sorted.length === 0) { // If there is no data then just skip. - return; + return false; } - if (!this.content_elem.isConnected || this.content_elem.firstElementChild === undefined || this.content_elem.firstElementChild === null) { - // Elements do not exist on page yet or content is empty. Skip. - return; + // 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 === "") { + this.clear(); + this.update([this.data_obj[this.data_obj_keys_sorted[0]].element.outerHTML]); + clear_before_return = true; } - + // Calculate the visible rows and colums for the clusterize-content area. let n_cols = calcColsPerRow(this.content_elem); - let n_rows = calcRowsPerCol(this.content_elem.parentElement, this.content_elem); - - n_cols = isNaN(n_cols) || n_cols <= 0 ? 1 : n_cols; - n_rows = isNaN(n_rows) || n_rows <= 0 ? 1 : n_rows; + let n_rows = calcRowsPerCol(this.scroll_elem, this.content_elem); + n_cols = (isNaN(n_cols) || n_cols <= 0) ? 1 : n_cols; + n_rows = (isNaN(n_rows) || n_rows <= 0) ? 1 : n_rows; if (n_cols != this.n_cols || n_rows != this.n_rows) { // Sizes have changed. Update the instance values. this.n_cols = n_cols; this.n_rows = n_rows; this.rows_in_block = this.n_rows; + rebuild_required = true; } + + // If we added a temporary row earlier, remove before returning. + if (clear_before_return) { + this.clear(); + } + + return rebuild_required; + } + + waitForElements() { + return new Promise(resolve => { + Promise.all([ + waitForElement(`#${this.data_id}`), + waitForElement(`#${this.scroll_id}`), + waitForElement(`#${this.content_id}`), + ]).then(() => { + return resolve(); + }); + }); + } + + repair() { + /** Fixes element association in DOM. Returns true if element was replaced in DOM. */ + // If association for elements is broken, replace them with instance version. + if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) { + gradioApp().getElementById(this.scroll_id).replaceWith(this.scroll_elem); + // Fix resize observers since they are bound to each element individually. + if (!isNullOrUndefined(this.resize_observer)) { + this.resize_observer.disconnect(); + this.resize_observer.observe(this.scroll_elem); + this.resize_observer.observe(this.content_elem); + } + // Make sure to refresh forcefully after updating the dom. + this.refresh(true); + return true; + } + return false; } rebuild(force) { - /** Rebuilds the clusterize object. - * When force=true, the existing Clusterize instance is destroyed then - * re-instantiated. When force=false or isnt passed, then we just - * update the DOM's scroll element with this instance's scroll_elem. - */ - if (force) { - this.clusterize.destroy(); - - // Get new references to elements since they may have changed. - this.scroll_elem = document.getElementById(this.scroll_id); - this.content_elem = document.getElementById(this.content_id); - this.clusterize = new Clusterize( - { - rows: this.filterRows(this.data_obj), - scrollId: this.scroll_id, - contentId: this.content_id, - rows_in_block: this.rows_in_block, - 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, - } - ); - } else { - // If association for elements is broken, replace them with this version. - if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) { - document.getElementById(this.scroll_id).replaceWith(this.scroll_elem); - } + // Only accept boolean values for `force` parameter. Default to false. + if (force !== true) { + force = false; } - // Apply existing sort/filter. - this.applyFilter(); + if (isNullOrUndefined(this.clusterize)) { + // If we have already initialized, don't do it again. + console.log("rebuild:: init"); + this.init(); + } else if (this.recalculateDims() || force) { + console.log("rebuild:: full", this.scroll_id); + this.destroy(); + this.clusterize = null; + this.init(); + } else { + console.log("rebuild:: update", this.scroll_id); + this.update(); + } + } + + init(rows) { + if (!isNullOrUndefined(this.clusterize)) { + // If we have already initialized, don't do it again. + return; + } + + if (isNullOrUndefined(rows) && isNullOrUndefined(this.data_obj)) { + // data hasnt been loaded yet and we arent provided any. skip. + return; + } + + if (isNullOrUndefined(rows)) { + rows = this.data_obj; + } else if (Array.isArray(rows) && !(rows.every(row => isString(row)))) { + console.error("Invalid data type for rows. Expected array[string]."); + return; + } + + this.clusterize = new Clusterize( + { + rows: this.filterRows(rows), + scrollId: this.scroll_id, + contentId: this.content_id, + rows_in_block: this.rows_in_block, + 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, + } + ); + } + + onResize(elem_id) { + console.log("element resized:", elem_id); + this.updateRows(); + } + + onElementAdded(elem_id) { + switch(elem_id) { + case this.data_id: + waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem); + break; + case this.scroll_id: + this.repair(); + break; + case this.content_id: + this.repair(); + break; + default: + break; + } + console.log("onElementAdded::", elem_id, document.body.contains(this.scroll_elem)); + } + + onElementRemoved(elem_id) { + switch(elem_id) { + case this.data_id: + waitForElement(`#${this.data_id}`).then((elem) => this.data_elem = elem); + break; + case this.scroll_id: + this.repair(); + break; + case this.content_id: + this.repair(); + break; + default: + break; + } + console.log("onElementRemoved::", elem_id, document.body.contains(this.scroll_elem)); + } + + onDataChanged(data) { + console.log("onDataChanged::", this.data_id); + this.parseJson(data); + } + + setupElementObservers() { + this.element_observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === "childList") { + // added + if (mutation.addedNodes.length > 0) { + for (const node of mutation.addedNodes) { + if (node.id === this.data_id || node.id === this.scroll_id || node.id === this.content_id) { + this.onElementAdded(node.id); + } + } + } + // removed + if (mutation.removedNodes.length > 0) { + for (const node of mutation.removedNodes) { + if (node.id === this.data_id || node.id === this.scroll_id || node.id === this.content_id) { + this.onElementRemoved(node.id); + } + } + } + } else if (mutation.type === "attributes") { + if (mutation.target.id === this.data_id && mutation.attributeName === "data-json") { + this.onDataChanged(mutation.target.dataset.json); + } + } + } + }); + this.element_observer.observe(gradioApp(), {subtree: true, childList: true, attributes: true}); + } + + setupResizeHandlers() { + this.resize_observer = new ResizeObserver((entries) => { + for (const entry of entries) { + console.log("resizeObserver:", entry.target.id); + if (entry.target.id === this.scroll_id || entry.target.id === this.content_id) { + clearTimeout(this.resize_observer_timer); + this.resize_observer_timer = setTimeout(() => this.onResize(entry.id), this.resize_observer_timeout_ms); + } + } + }); + + this.resize_observer.observe(this.scroll_elem); + this.resize_observer.observe(this.content_elem); + } + + /* ==== Clusterize.Js FUNCTION WRAPPERS ==== */ + refresh(force) { + /** Refreshes the clusterize instance so that it can recalculate its dims. */ + if (isNullOrUndefined(this.clusterize)) { + return; + } + + // Only allow boolean variables. default to false. + if (force !== true) { + force = false; + } + this.clusterize.refresh(force); + } + + rowCount() { + /** Gets the total (not only visible) row count in the clusterize instance. */ + return this.clusterize.getRowsAmount(); + } + + clear() { + /** Removes all rows. */ + this.clusterize.clear(); + } + + update(rows) { + /** Adds rows from a list of element strings. */ + if (rows === undefined || rows === null) { + // If not passed, use the default method of getting rows. + rows = this.filterRows(this.data_obj); + } else if (!Array.isArray(rows) || !(rows.every(row => typeof row === "string"))) { + console.error("Invalid data type for rows. Expected array[string]."); + return; + } + this.clusterize.update(rows); + } + + destroy() { + /** Destroys a clusterize instance and removes its rows from the page. */ + // If `true` isnt passed, then clusterize dumps every row to the DOM. + // This kills performance so we never want to do this. + this.clusterize.destroy(true); } } @@ -344,54 +572,57 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { } updateJson(json) { - var style = getComputedStyle(document.body); - //let spacing_sm = style.getPropertyValue("--spacing-sm"); - 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)[0]; - // 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; - let depth = Number(parsed_html.dataset.depth); - parsed_html.style.paddingLeft = `calc(${depth} * ${text_size})`; - parsed_html.style.boxShadow = this.getBoxShadow(depth); + return new Promise(resolve => { + var style = getComputedStyle(document.body); + //let spacing_sm = style.getPropertyValue("--spacing-sm"); + 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)[0]; + // 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; + let depth = Number(parsed_html.dataset.depth); + parsed_html.style.paddingLeft = `calc(${depth} * ${text_size})`; + parsed_html.style.boxShadow = this.getBoxShadow(depth); - // Add the updated html to the data object. - this.data_obj[div_id] = { - element: parsed_html, - active: parent_id === -1, // always show root - expanded: expanded || (parent_id === -1), // always expand root - parent: parent_id, - children: [], // populated later - }; - } - - // Build list of children for each element in dataset. - for (const [k, v] of Object.entries(this.data_obj)) { - if (v.parent === -1) { - continue; - } else if (!(v.parent in this.data_obj)) { - console.error("parent not in data:", v.parent); - } else { - this.data_obj[v.parent].children.push(k); + // Add the updated html to the data object. + this.data_obj[div_id] = { + element: parsed_html, + active: parent_id === -1, // always show root + expanded: expanded || (parent_id === -1), // always expand root + parent: parent_id, + children: [], // populated later + }; } - } - // Handle expanding of rows on initial load - for (const [k, v] of Object.entries(this.data_obj)) { - if (v.parent === -1) { - // Always show root level. - this.data_obj[k].active = true; - } else if (this.data_obj[v.parent].expanded && this.data_obj[v.parent].active) { - // Parent is both active and expanded. show child - this.data_obj[k].active = true; - } else { - this.data_obj[k].active = false; + // Build list of children for each element in dataset. + for (const [k, v] of Object.entries(this.data_obj)) { + if (v.parent === -1) { + continue; + } else if (!(v.parent in this.data_obj)) { + console.error("parent not in data:", v.parent); + } else { + this.data_obj[v.parent].children.push(k); + } } - } - this.applyFilter(); + // Handle expanding of rows on initial load + for (const [k, v] of Object.entries(this.data_obj)) { + if (v.parent === -1) { + // Always show root level. + this.data_obj[k].active = true; + } else if (this.data_obj[v.parent].expanded && this.data_obj[v.parent].active) { + // Parent is both active and expanded. show child + this.data_obj[k].active = true; + } else { + this.data_obj[k].active = false; + } + } + //this.applyFilter(); + console.log("updateJson:: done", this.scroll_id); + return resolve(); + }); } removeChildRows(div_id) { @@ -423,17 +654,21 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { } updateJson(json) { - for (const [k, v] of Object.entries(json)) { - let div_id = k; - let parsed_html = parseHtml(v)[0]; - // Add the updated html to the data object. - this.data_obj[div_id] = { - element: parsed_html, - active: true, - }; - } - - this.applyFilter(); + return new Promise(resolve => { + for (const [k, v] of Object.entries(json)) { + let div_id = k; + let parsed_html = parseHtml(v)[0]; + // Add the updated html to the data object. + this.data_obj[div_id] = { + element: parsed_html, + active: true, + }; + } + //this.applyFilter(); + console.log("updateJson:: done", this.scroll_id); + if (this.scroll_id.includes("textual")) { console.log(this.data_obj); } + return resolve(); + }); } filterRows(obj) { diff --git a/javascript/resizeHandle.js b/javascript/resizeHandle.js index bb1d61a9f..1ad6b8c78 100644 --- a/javascript/resizeHandle.js +++ b/javascript/resizeHandle.js @@ -1,8 +1,8 @@ (function() { const GRADIO_MIN_WIDTH = 320; const PAD = 16; - const DEBOUNCE_TIME = 100; - const DOUBLE_TAP_DELAY = 200; //ms + const DEBOUNCE_TIME_MS = 250; + const DOUBLE_TAP_DELAY_MS = 250; const R = { tracking: false, @@ -113,7 +113,7 @@ if (evt.changedTouches.length !== 1) return; const currentTime = new Date().getTime(); - if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY) { + if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY_MS) { onDoubleClick(evt); return; } @@ -204,7 +204,7 @@ for (const parent of parents) { afterResize(parent); } - }, DEBOUNCE_TIME); + }, DEBOUNCE_TIME_MS); }); setupResizeHandle = setup; diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index e2eb82f6a..2482ca696 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -208,6 +208,7 @@ class ExtraNetworksPage: label: str, btn_type: str, dir_is_empty: bool = False, + metadata: Optional[str] = None, parent_id: Optional[int] = None, data_depth: Optional[int] = None, data_path: Optional[str] = None, @@ -242,9 +243,10 @@ class ExtraNetworksPage: "name": label, } ) - action_list_item_action_trailing += self.btn_metadata_tpl.format( - **{"extra_networks_tabname": self.extra_networks_tabname, "name": label} - ) + if metadata: + action_list_item_action_trailing += self.btn_metadata_tpl.format( + **{"extra_networks_tabname": self.extra_networks_tabname, "name": label} + ) action_list_item_action_trailing += "
" data_attributes = "" @@ -341,7 +343,7 @@ class ExtraNetworksPage: if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. onclick_js_tpl = ( - "cardClicked('{tabname}_{extra_networks_tabname}', {prompt}, {neg_prompt}, {allow_neg});" + "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});" ) onclick = onclick_js_tpl.format( **{ @@ -359,6 +361,7 @@ class ExtraNetworksPage: parent_id=parent_id, tabname=tabname, label=v.item["name"], + metadata=v.item.get("metadata", None), data_depth=depth, data_path=v.item["filename"], data_hash=v.item["shorthash"], @@ -400,7 +403,7 @@ class ExtraNetworksPage: onclick = item.get("onclick", None) if onclick is None: # Don't quote prompt/neg_prompt since they are stored as js strings already. - onclick_js_tpl = "cardClicked('{tabname}_{extra_networks_tabname}', {prompt}, {neg_prompt}, {allow_neg});" + onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});" onclick = onclick_js_tpl.format( **{ "tabname": tabname, @@ -525,7 +528,7 @@ class ExtraNetworksPage: tabname=tabname, ) res = base64.b64encode(gzip.compress(json.dumps(res).encode("utf-8"))).decode("utf-8") - return f'' + return f'' # FIXME def create_dirs_view_html(self, tabname: str) -> str: @@ -588,7 +591,7 @@ class ExtraNetworksPage: res[i] = self.create_item_html(tabname, item, self.card_tpl, div_id=i) res = base64.b64encode(gzip.compress(json.dumps(res).encode("utf-8"))).decode("utf-8") - return f'' + return f'' def create_html(self, tabname, *, empty=False): """Generates an HTML string for the current pane. @@ -814,9 +817,6 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): ).then( fn=lambda: None, _js="setupAllResizeHandles", - ).then( - fn=lambda: None, - _js="extraNetworksSetupData", ) return ui diff --git a/style.css b/style.css index 89627b13a..d3ab364d1 100644 --- a/style.css +++ b/style.css @@ -1181,7 +1181,7 @@ body.resizing .resize-handle { .clusterize-scroll { width: 100%; height: 100%; - overflow: auto; + overflow: clip auto; } .clusterize-content { @@ -1241,6 +1241,10 @@ body.resizing .resize-handle { } .extra-network-content--container { + display: flex; + flex-wrap: nowrap; + width: 100%; + height: 100%; flex-direction: column; } @@ -1254,6 +1258,7 @@ body.resizing .resize-handle { .extra-network-content--cards { flex: 1; flex-direction: row; + min-height: 0; /* prevent children from oversizing this container */ } .extra-network-content.resize-handle-col {