From 250e7673e819c6816edf449b2b0f3ec9daf8dffb Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Tue, 30 Apr 2024 14:27:13 -0400 Subject: [PATCH] fix bugs with multi-function buttons and recursive filters. --- html/extra-networks-btn-chevron.html | 11 ++ javascript/extraNetworks.js | 270 +++++++++++++++----------- javascript/extraNetworksClusterize.js | 16 +- modules/ui_extra_networks.py | 12 +- style.css | 126 ++++++++---- 5 files changed, 267 insertions(+), 168 deletions(-) create mode 100644 html/extra-networks-btn-chevron.html diff --git a/html/extra-networks-btn-chevron.html b/html/extra-networks-btn-chevron.html new file mode 100644 index 000000000..81857f5ec --- /dev/null +++ b/html/extra-networks-btn-chevron.html @@ -0,0 +1,11 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index bc0896a5b..6e702fbfa 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -540,11 +540,12 @@ class ExtraNetworksTab { row.style.gridTemplateColumns = `${max_width}px ${pad}px 1fr`; } - async setDirectoryButtons({source_elem, source_selector, source_class}={}) { + async setDirectoryButtons({source_elem, source_selector, source_class, reset_all}={}) { // At least one argument must be specified. if (isNullOrUndefined(source_elem) && isNullOrUndefined(source_selector) - && isNullOrUndefined(source_class)) { + && isNullOrUndefined(source_class) + && isNullOrUndefined(reset_all)) { console.error("At least one argument must be specified.") return; } @@ -596,6 +597,13 @@ class ExtraNetworksTab { }; _set_recursion_depth.bind(this); + if (reset_all === true) { + _reset_all_buttons(); + await this.tree_list.onRowSelected(); // no args deselects all. + this.applyDirectoryFilter(); + return; + } + if (!_exists(source_elem) && isString(source_selector)) { source_elem = this.container_elem.querySelector(source_selector); } @@ -604,11 +612,17 @@ class ExtraNetworksTab { source_elem = this.container_elem.querySelector(`${source_class}[data-selected]`); } + // try to find any selected buttons to use as a source. + if (!_exists(source_elem)) { + source_elem = this.container_elem.querySelector("[data-selected]"); + } + // If we got here with no source elem, then we will take this to mean that // we are deselecting all. if (!_exists(source_elem)) { _reset_all_buttons(); await this.tree_list.onRowSelected(); // no args deselects all. + this.applyDirectoryFilter(); return; } @@ -626,6 +640,10 @@ class ExtraNetworksTab { } else { await this.tree_list.onRowSelected(); } + this.applyDirectoryFilter( + "selected" in source_elem.dataset ? data_path : null, + "recurse" in source_elem.dataset, + ); return; } @@ -643,6 +661,10 @@ class ExtraNetworksTab { await this.tree_list.onRowSelected(source_is_tree ? source_elem : other_elem); const div_id = source_is_tree ? source_elem.dataset.divId : other_elem.dataset.divId; _set_recursion_depth(div_id, data_recurse); + this.applyDirectoryFilter( + "selected" in source_elem.dataset ? data_path : null, + "recurse" in source_elem.dataset, + ); } } @@ -1129,11 +1151,11 @@ async function extraNetworksTreeDirectoryOnDblClick(event) { // stopPropagation so we don't also trigger event on parent since this btn is nested. event.stopPropagation(); const btn = event.target.closest(".tree-list-item"); - if ("expanded" in btn.dataset) { - await extraNetworksBtnTreeViewCollapseOnClick(event); - } else { - await extraNetworksBtnTreeViewExpandOnClick(event); - } + const pane = btn.closest(".extra-network-pane"); + const div_id = btn.dataset.divId; + const tab = extra_networks_tabs[pane.dataset.tabnameFull]; + await tab.tree_list.toggleRowExpanded(div_id); + tab.setDirectoryButtons({source_class: ".tree-list-item"}); } async function extraNetworksTreeDirectoryOnClick(event) { @@ -1141,48 +1163,36 @@ async function extraNetworksTreeDirectoryOnClick(event) { if (event.target.closest(".tree-list-item-action")) { return; } - const btn = event.target.closest(".tree-list-item"); const pane = btn.closest(".extra-network-pane"); const tab = extra_networks_tabs[pane.dataset.tabnameFull]; - tab.setDirectoryButtons({source_elem: btn}); } +async function extraNetworksTreeDirectoryChevronOnLongPress(event) { + // stopPropagation so we don't also trigger event on parent since this btn is nested. + event.stopPropagation(); + const chevron = event.target.closest(".tree-list-item-action--chevron"); + const btn = event.target.closest(".tree-list-item"); + const pane = btn.closest(".extra-network-pane"); + const div_id = btn.dataset.divId; + const tab = extra_networks_tabs[pane.dataset.tabnameFull]; + if ("expanded" in btn.dataset) { + await tab.tree_list.collapseAllRows(div_id); + } else { + await tab.tree_list.expandAllRows(div_id); + } + tab.setDirectoryButtons({source_class: ".tree-list-item"}); +} + async function extraNetworksBtnTreeViewChevronOnClick(event) { - // stopPropagation so we don't also trigger event on parent since this btn is nested. - event.stopPropagation(); - const btn = event.target.closest(".tree-list-item-action-chevron"); - const row = event.target.closest(".tree-list-item"); - const pane = btn.closest(".extra-network-pane"); - const div_id = row.dataset.divId; - const tab = extra_networks_tabs[pane.dataset.tabnameFull]; - - await tab.tree_list.onRowExpandClick(div_id, btn); - tab.setDirectoryButtons({source_class: ".tree-list-item"}); -} - -async function extraNetworksBtnTreeViewExpandOnClick(event) { // stopPropagation so we don't also trigger event on parent since this btn is nested. event.stopPropagation(); const btn = event.target.closest(".tree-list-item"); const pane = btn.closest(".extra-network-pane"); const div_id = btn.dataset.divId; const tab = extra_networks_tabs[pane.dataset.tabnameFull]; - - await tab.tree_list.onExpandAllClick(div_id); - tab.setDirectoryButtons({source_class: ".tree-list-item"}); -} - -async function extraNetworksBtnTreeViewCollapseOnClick(event) { - // stopPropagation so we don't also trigger event on parent since this btn is nested. - event.stopPropagation(); - const btn = event.target.closest(".tree-list-item"); - const pane = btn.closest(".extra-network-pane"); - const div_id = btn.dataset.divId; - const tab = extra_networks_tabs[pane.dataset.tabnameFull]; - - await tab.tree_list.onCollapseAllClick(div_id); + await tab.tree_list.toggleRowExpanded(div_id); tab.setDirectoryButtons({source_class: ".tree-list-item"}); } @@ -1278,9 +1288,6 @@ function extraNetworksSetupEventDelegators() { ".copy-path-button": extraNetworksBtnCopyPathOnClick, ".edit-button": extraNetworksBtnEditMetadataOnClick, ".metadata-button": extraNetworksBtnShowMetadataOnClick, - ".tree-list-item-action-chevron": extraNetworksBtnTreeViewChevronOnClick, - ".tree-list-item-action-expand": extraNetworksBtnTreeViewExpandOnClick, - ".tree-list-item-action-collapse": extraNetworksBtnTreeViewCollapseOnClick, ".extra-network-control--search-clear": extraNetworksControlSearchClearOnClick, ".extra-network-control--sort-mode": extraNetworksControlSortModeOnClick, ".extra-network-control--sort-dir": extraNetworksControlSortDirOnClick, @@ -1297,40 +1304,56 @@ function extraNetworksSetupEventDelegators() { } }); - // effectively just a click but we need to handle separate from "click" events - // since the same elements are also handling long press events. - const short_press_event_map = { - ".tree-list-item--dir": extraNetworksTreeDirectoryOnClick, - ".extra-network-dirs-view-button": extraNetworksBtnDirsViewItemOnClick, - } - const short_press_ignore_map = { - ".tree-list-item--dir": [".tree-list-item-action"], - } - const long_press_event_map = { - ".tree-list-item--dir": extraNetworksTreeDirectoryOnLongPress, - ".extra-network-dirs-view-button": extraNetworksBtnDirsViewItemOnLongPress, - }; - const long_press_ignore_map = { - ".tree-list-item--dir": [".tree-list-item-action"], - } + // Order in these maps matters since we may have separate events for both a div + // and for a child within that div however if the child is clicked then we wouldn't + // want to handle clicks for the parent as well. In this case, order the child's event + // before the parent and the parent will be ignored. + // Can add entries with handler=null to forcefully ignore specific event types. - const dbl_press_event_map = { - ".tree-list-item--dir": extraNetworksTreeDirectoryOnDblClick, - } + const short_press_event_map = [ + { + "selector": ".tree-list-item-action--chevron", + "handler": extraNetworksBtnTreeViewChevronOnClick, + }, + { + "selector": ".tree-list-item--dir", + "negative": ".tree-list-item-action", + "handler": extraNetworksTreeDirectoryOnClick, + }, + { + "selector": ".extra-network-dirs-view-button", + "handler": extraNetworksBtnDirsViewItemOnClick, + }, + ]; - const dbl_press_ignore_map = { - ".tree-list-item--dir": [".tree-list-item-action"], - } - - const on_short_press = (event, elem, selector) => { - let ignores = short_press_ignore_map[selector]; - ignores = ignores || []; - for (const ignore of Object.values(ignores)) { - if (event.target.closest(ignore)) { - return; - } + const long_press_event_map = [ + { + "selector": ".tree-list-item-action--chevron", + "handler": extraNetworksTreeDirectoryChevronOnLongPress, + }, + { + "selector": ".tree-list-item--dir", + "negative": ".tree-list-item-action", + "handler": extraNetworksTreeDirectoryOnLongPress, + }, + { + "selector": ".extra-network-dirs-view-button", + "handler": extraNetworksBtnDirsViewItemOnLongPress, + }, + ]; + + const dbl_press_event_map = [ + { + "selector": ".tree-list-item--dir", + "negative": ".tree-list-item-action", + "handler": extraNetworksTreeDirectoryOnDblClick, + }, + ]; + + const on_short_press = (event, elem, handler) => { + if (!handler) { + return; } - elem.classList.remove("pressed"); // Toggle if (elem.classList.contains("long-pressed")) { elem.classList.remove("long-pressed"); @@ -1342,18 +1365,13 @@ function extraNetworksSetupEventDelegators() { } elem.dispatchEvent(new Event("shortpress", event)); - short_press_event_map[selector](event); + handler(event); }; - const on_long_press = (event, elem, selector) => { - let ignores = long_press_ignore_map[selector]; - ignores = ignores || []; - for (const ignore of Object.values(ignores)) { - if (event.target.closest(ignore)) { - return; - } + const on_long_press = (event, elem, handler) => { + if (!handler) { + return; } - elem.classList.remove("pressed"); // If long pressed, we deselect. // Else we set as long pressed. if (elem.classList.contains("short-pressed")) { @@ -1368,61 +1386,91 @@ function extraNetworksSetupEventDelegators() { } elem.dispatchEvent(new Event("longpress", event)); - long_press_event_map[selector](event); + handler(event); }; - const on_dbl_press = (event, elem, selector) => { - let ignores = dbl_press_ignore_map[selector]; - ignores = ignores || []; - for (const ignore of Object.values(ignores)) { - if (event.target.closest(ignore)) { - return; - } + const on_dbl_press = (event, elem, handler) => { + if (!handler) { + return; } - elem.classList.remove("pressed"); - dbl_press_event_map[selector](event); + handler(event); } let press_timer; let press_time_ms = 800; - const event_maps = [ - short_press_event_map, - long_press_event_map, - dbl_press_event_map, - ]; - const selectors = Object.keys(Object.assign({}, ...event_maps)); window.addEventListener("mousedown", event => { - for (const selector of selectors) { - const elem = event.target.closest(selector); - if (elem) { + for (const obj of short_press_event_map) { + const elem = event.target.closest(obj.selector); + const neg = obj.negative ? event.target.closest(obj.negative) : null; + if (elem && !neg) { event.preventDefault(); + event.stopPropagation(); + elem.classList.add("pressed"); + } + } + + for (const obj of long_press_event_map) { + const elem = event.target.closest(obj.selector); + const neg = obj.negative ? event.target.closest(obj.negative) : null; + if (elem && !neg) { + event.preventDefault(); + event.stopPropagation(); + elem.classList.add("pressed"); + press_timer = setTimeout(() => { + elem.classList.remove("pressed"); + on_long_press(event, elem, obj.handler); + }, press_time_ms); + } + } + + for (const obj of dbl_press_event_map) { + const elem = event.target.closest(obj.selector); + const neg = obj.negative ? event.target.closest(obj.negative) : null; + if (elem && !neg) { + event.preventDefault(); + event.stopPropagation(); elem.classList.add("pressed"); - press_timer = setTimeout((event) => { - on_long_press(event, elem, selector); - }, press_time_ms, event); - return; } } }); window.addEventListener("mouseup", event => { - for (const selector of selectors) { - const elem = event.target.closest(selector); - if (elem) { + for (const obj of short_press_event_map) { + const elem = event.target.closest(obj.selector); + const neg = obj.negative ? event.target.closest(obj.negative) : null; + if (elem && !neg) { event.preventDefault(); + event.stopPropagation(); clearTimeout(press_timer); - if (elem.classList.contains("pressed")) { - if (event.detail === 1) { - on_short_press(event, elem, selector); - } else if (event.detail === 2) { - on_dbl_press(event, elem, selector); + if (event.detail === 1 + || !dbl_press_event_map.map(x => x.selector).includes(obj.selector) + ) { + elem.classList.remove("pressed"); + on_short_press(event, elem, obj.handler); } } - return; } } + + if (event.detail === 2) { + for (const obj of dbl_press_event_map) { + const elem = event.target.closest(obj.selector); + const neg = obj.negative ? event.target.closest(obj.negative) : null; + if (elem && !neg) { + event.preventDefault(); + event.stopPropagation(); + clearTimeout(press_timer); + if (elem.classList.contains("pressed")) { + elem.classList.remove("pressed"); + on_dbl_press(event, elem, obj.handler); + } + } + } + } + + // long_press_event_map is handled by the timer setup in "mousedown" handlers. }); } diff --git a/javascript/extraNetworksClusterize.js b/javascript/extraNetworksClusterize.js index 1d35a991d..6e0b70873 100644 --- a/javascript/extraNetworksClusterize.js +++ b/javascript/extraNetworksClusterize.js @@ -347,7 +347,8 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { return max_width; } - async onExpandAllClick(div_id) { + async expandAllRows(div_id) { + /** Recursively expands all directories below the passed div_id. */ if (!keyExistsLogError(this.data_obj, div_id)) { return; } @@ -372,7 +373,8 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { await this.sortData(); } - async onCollapseAllClick(div_id) { + async collapseAllRows(div_id) { + /** Recursively collapses all directories below the passed div_id. */ if (!keyExistsLogError(this.data_obj, div_id)) { return; } @@ -403,8 +405,8 @@ class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize { await this.sortData(); } - async onRowExpandClick(div_id, elem) { - /** Expands or collapses a row to show/hide children. */ + async toggleRowExpanded(div_id) { + /** Toggles a row between expanded and collapses states. */ if (!keyExistsLogError(this.data_obj, div_id)) { return; } @@ -584,11 +586,11 @@ class ExtraNetworksClusterizeCardsList extends ExtraNetworksClusterize { for (const [div_id, v] of Object.entries(this.data_obj)) { let visible = true; - if (this.directory_filter_recurse) { + if (this.directory_filter_str && this.directory_filter_recurse) { // Filter as directory with recurse shows all nested children. // Case sensitive comparison against the relative directory of each object. - if (this.directory_filter_str && !this.directory_filter_str.startsWith(v.rel_parent_dir)) { - this.data_obj[div_id].visible = false; + this.data_obj[div_id].visible = v.rel_parent_dir.startsWith(this.directory_filter_str); + if (!this.data_obj[div_id].visible) { continue; } } else { diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index c7dc8fd1a..20250f4a6 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -238,6 +238,7 @@ class ExtraNetworksPage: self.btn_show_metadata_tpl = shared.html("extra-networks-btn-show-metadata.html") self.btn_edit_metadata_tpl = shared.html("extra-networks-btn-edit-metadata.html") self.btn_dirs_view_item_tpl = shared.html("extra-networks-btn-dirs-view-item.html") + self.btn_chevron_tpl = shared.html("extra-networks-btn-chevron.html") def clear_data(self) -> None: self.is_ready = False @@ -316,26 +317,19 @@ class ExtraNetworksPage: # If not specified, title will just reflect the label btn_title = btn_title.strip() if btn_title else f'"{label}"' - action_list_item_action_leading = "" + action_list_item_action_leading = self.btn_chevron_tpl.format(extra_classes="") action_list_item_visual_leading = "🗀" action_list_item_visual_trailing = "" action_list_item_action_trailing = "" if dir_is_empty: - action_list_item_action_leading = "" + action_list_item_action_leading = self.btn_chevron_tpl.format(extra_classes="invisible") if btn_type == "file": action_list_item_visual_leading = "🗎" # Action buttons if item is not None: action_list_item_action_trailing += self.get_button_row(tabname, item) - else: - action_list_item_action_trailing += ( - "
" - "
" - "
" - "
" - ) data_attributes_str = "" for k, v in data_attributes.items(): diff --git a/style.css b/style.css index 68adbb47a..24c1d1b34 100644 --- a/style.css +++ b/style.css @@ -31,6 +31,10 @@ div.gradio-container{ display: none !important; } +.invisible { + visibility: hidden !important; +} + .compact{ background: transparent !important; padding: 0 !important; @@ -1269,7 +1273,7 @@ body.resizing .resize-handle { .resize-handle-row.resize-handle-hidden { display: flex !important; } -/* +/* Only recently supported in firefox. Switch to this eventually to simplify code. .resize-handle-row:has(> .resize-handle-col.hidden) { display: flex !important; } @@ -1513,46 +1517,68 @@ body.resizing .resize-handle { color: var(--button-secondary-text-color); } - -/* ==== TREE VIEW EXPAND/COLLAPSE BUTTONS ==== */ -.tree-list-item-action-expand::before { - content: "≫"; -} - -.tree-list-item-action-expand { - transform: rotate(90deg); -} - -.tree-list-item-action-collapse::before { - content: "≫"; -} - -.tree-list-item-action-collapse { - transform: rotate(-90deg); -} - /* ==== CHEVRON ICON ACTIONS ==== */ /* Define the animation for the arrow when it is clicked. */ -.tree-list-item-action-chevron { - display: inline-flex; +.tree-list-item-action--chevron { height: var(--button-large-text-size); width: var(--button-large-text-size); - mask-image: url('data:image/svg+xml,'); - mask-repeat: no-repeat; - mask-position: center center; - mask-size: 100%; - background: var(--input-placeholder-color); + background: transparent; + margin: 0 !important; + padding: 0 !important; + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); transform: rotate(45deg); + transition: transform 0.2s; + transition-delay: 0.3s; + z-index: 0; } -.tree-list-item[data-expanded] .tree-list-item-action-chevron { +.tree-list-item[data-expanded] .tree-list-item-action--chevron { -ms-transform: rotate(135deg); -webkit-transform: rotate(135deg); transform: rotate(135deg); - transition: transform 0.2s; - /* currently broken since we remove/add elements to clusterize.js list */ } +.tree-list-item[data-expanded] .tree-list-item-action--chevron.pressed { + -ms-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +.tree-list-item:not([data-expanded]) .tree-list-item-action--chevron.pressed { + -ms-transform: rotate(135deg); + -webkit-transform: rotate(135deg); + transform: rotate(135deg); +} + +.tree-list-item-action--chevron .chevron-icon-single, +.tree-list-item-action--chevron .chevron-icon-double { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transition: all 0.1s ease; + transition-delay: 0.3s; +} + +.tree-list-item-action--chevron:not(.pressed) .chevron-icon-single { + opacity: 1; + stroke: var(--input-placeholder-color); +} + +.tree-list-item-action--chevron.pressed .chevron-icon-double { + opacity: 1; + stroke: var(--button-primary-border-color); +} + +.tree-list-item-action--chevron.pressed .chevron-icon-single, +.tree-list-item-action--chevron:not(.pressed) .chevron-icon-double { + opacity: 0; +} + + + /* Text for button. */ .tree-list-item-label { position: relative; @@ -1657,6 +1683,7 @@ body.resizing .resize-handle { display: none; } +/* Long-press buttons. Wipe L->R effect when button is held, then toggles color. */ .extra-network-dirs-view-button { position: relative; overflow: hidden; @@ -1671,10 +1698,21 @@ body.resizing .resize-handle { } .extra-network-dirs-view-button[data-selected] { - background: var(--button-primary-background-fill); + background: var(--button-secondary-background-fill); } .extra-network-dirs-view-button[data-selected]:not(.pressed):hover { + -webkit-transition: all 0.05s ease-in-out; + transition: all 0.05s ease-in-out; + background: var(--button-secondary-background-fill-hover); +} + +.extra-network-dirs-view-button[data-recurse] { + background: var(--button-primary-background-fill); + border-color: var(--button-primary-border-color); +} + +.extra-network-dirs-view-button[data-recurse]:not(.pressed):hover { -webkit-transition: all 0.05s ease-in-out; transition: all 0.05s ease-in-out; background: var(--button-primary-background-fill-hover); @@ -1691,11 +1729,11 @@ body.resizing .resize-handle { z-index: -1; } -.extra-network-dirs-view-button:not([data-selected])::after { +.extra-network-dirs-view-button:not([data-recurse])::after { background: var(--button-secondary-background-fill-hover); } -.extra-network-dirs-view-button[data-selected]::after { +.extra-network-dirs-view-button[data-recurse]::after { background: var(--button-primary-background-fill-hover); } @@ -1704,16 +1742,16 @@ body.resizing .resize-handle { transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); } -.extra-network-dirs-view-button[data-recurse] { - box-shadow: - inset var(--spacing-sm) 0 0 0 var(--primary-700), - inset calc(-1 * var(--spacing-sm)) 0 0 0 var(--primary-700); +.extra-network-dirs-view-button[data-selected] { + outline: var(--spacing-xs) solid var(--button-primary-border-color); + outline-offset: calc(-1 * var(--spacing-xs)); } .tree-list-item { position: relative; overflow: hidden; z-index: 0; + user-select: none; } .tree-list-item:not(.pressed):hover { @@ -1726,25 +1764,31 @@ body.resizing .resize-handle { background: var(--button-secondary-background-fill); } -.tree-list-item .tree-list-item-action--leading::after { +.tree-list-item.pressed:hover { + -webkit-transition: all 0.05s ease-in-out; + transition: all 0.05s ease-in-out; + background: var(--button-secondary-background-fill); +} + +.tree-list-item::after { position: absolute; content: ""; top: 0; left: 0; width: 0; height: 100%; + background: var(--button-secondary-background-fill-hover); transition: all 0.35s cubic-bezier(0.215, 0.61, 0.355, 1); z-index: -1; } -.tree-list-item.pressed .tree-list-item-action--leading::after { +.tree-list-item.pressed::after { width: 100%; transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); } -.tree-list-item[data-recurse] .tree-list-item-action--leading .tree-list-item-action-chevron { - background: var(--button-primary-background-fill); - color: var(--primary-600); +.tree-list-item[data-recurse] .tree-list-item-action--chevron .chevron-icon-single { + stroke: var(--button-primary-border-color); } .tree-list-item-indent {