Dynamic sidebar controls. When the screen is too narrow, it automatically swaps to a select dropdown instead of toggleable, single selection buttons.

This commit is contained in:
Christian Bastian
2024-07-21 22:07:29 -04:00
parent a348dbafbb
commit 35250a99a3
2 changed files with 217 additions and 96 deletions

View File

@@ -28,32 +28,41 @@
gap: 16px; gap: 16px;
} }
.model-manager.sidebar-left { /* sidebar buttons */
.model-manager .sidebar-buttons {
overflow: hidden;
color: var(--input-text);
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
.model-manager .sidebar-buttons .radio-button-group-active {
border-color: var(--fg-color);
color: var(--fg-color);
overflow: hidden;
}
.model-manager[data-sidebar-state="left"] {
width: 50%; width: 50%;
left: 0%; left: 0%;
} }
.model-manager.sidebar-top { .model-manager[data-sidebar-state="top"] {
height: 50%; height: 50%;
top: 0%; top: 0%;
} }
.model-manager.sidebar-bottom { .model-manager[data-sidebar-state="bottom"] {
height: 50%; height: 50%;
top: 50%; top: 50%;
} }
.model-manager.sidebar-right { .model-manager[data-sidebar-state="right"] {
width: 50%; width: 50%;
left: 50%; left: 50%;
} }
.model-manager .sidebar-buttons .sidebar-button-active {
border-color: var(--fg-color);
color: var(--fg-color);
overflow: hidden;
}
/* common */ /* common */
.model-manager h1 { .model-manager h1 {
min-width: 0; min-width: 0;
@@ -165,15 +174,6 @@
user-select: none; user-select: none;
} }
/* sidebar buttons */
.model-manager .sidebar-buttons {
overflow: hidden;
color: var(--input-text);
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
/* main content */ /* main content */
.model-manager .model-manager-panel { .model-manager .model-manager-panel {
color: var(--fg-color); color: var(--fg-color);
@@ -528,6 +528,11 @@
float: right; float: right;
} }
.model-manager .model-manager-head .topbar-right select {
padding: 0;
font-size: 24px;
}
/* search dropdown */ /* search dropdown */
.model-manager .search-models { .model-manager .search-models {
display: flex; display: flex;

View File

@@ -3482,83 +3482,164 @@ class SettingsView {
} }
} }
class SidebarButtons { /**
/** @type {HTMLDivElement} */ * @param {String[]} labels
element = null; * @param {[(event: Event) => Promise<void>]} callbacks
* @returns {HTMLDivElement}
/** @type {ModelManager} */ */
#modelManager = null; function GenerateRadioButtonGroup(labels, callbacks = []) {
const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active";
/** const radioButtonGroup = $el("div.radio-button-group", []);
* @param {Event} e const buttons = [];
*/ for (let i = 0; i < labels.length; i++) {
#setSidebar(e) { const text = labels[i];
// TODO: settings["sidebar-default-width"] const callback = callbacks[i] ?? (() => {});
// TODO: settings["sidebar-default-height"] buttons.push(
// TODO: draggable resize? $el("button.radio-button", {
const button = e.target; textContent: text,
const modelManager = this.#modelManager.element; onclick: (event) => {
const sidebarButtons = this.element.children; const targetIsActive = event.target.classList.contains(RADIO_BUTTON_GROUP_ACTIVE);
if (targetIsActive) {
const buttonActiveState = "sidebar-button-active"; return;
for (let i = 0; i < sidebarButtons.length; i++) { }
sidebarButtons[i].classList.remove(buttonActiveState); const children = radioButtonGroup.children;
} for (let i = 0; i < children.length; i++) {
children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
let buttonIndex; }
for (buttonIndex = 0; buttonIndex < sidebarButtons.length; buttonIndex++) { event.target.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
const sidebarButton = sidebarButtons[buttonIndex]; callback(event);
if (sidebarButton === button) { },
break; })
} );
}
const sidebarStates = ["sidebar-right", "sidebar-top", "sidebar-bottom", "sidebar-left"]; // TODO: magic numbers
let stateIndex;
for (stateIndex = 0; stateIndex < sidebarStates.length; stateIndex++) {
const state = sidebarStates[stateIndex];
if (modelManager.classList.contains(state)) {
modelManager.classList.remove(state);
break;
}
}
if (stateIndex != buttonIndex) {
const newSidebarState = sidebarStates[buttonIndex];
modelManager.classList.add(newSidebarState);
const sidebarButton = sidebarButtons[buttonIndex];
sidebarButton.classList.add(buttonActiveState);
}
} }
radioButtonGroup.append.apply(radioButtonGroup, buttons);
buttons[0]?.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
return radioButtonGroup;
}
/** /**
* @param {ModelManager} modelManager * @param {String[]} labels
*/ * @param {[(event: Event) => Promise<void>]} activationCallbacks
constructor(modelManager) { * @param {(event: Event) => Promise<void>} deactivationCallback
this.#modelManager = modelManager; * @returns {HTMLDivElement}
$el("div.sidebar-buttons", */
{ function GenerateToggleRadioButtonGroup(labels, activationCallbacks = [], deactivationCallback = () => {}) {
$: (el) => (this.element = el), const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active";
}, const radioButtonGroup = $el("div.radio-button-group", []);
[ const buttons = [];
$el("button.icon-button", { for (let i = 0; i < labels.length; i++) {
textContent: "◨", const text = labels[i];
onclick: (event) => this.#setSidebar(event), const activationCallback = activationCallbacks[i] ?? (() => {});
}), buttons.push(
$el("button.icon-button", { $el("button.radio-button", {
textContent: "⬒", textContent: text,
onclick: (event) => this.#setSidebar(event), onclick: (event) => {
}), const targetIsActive = event.target.classList.contains(RADIO_BUTTON_GROUP_ACTIVE);
$el("button.icon-button", { const children = radioButtonGroup.children;
textContent: "⬓", for (let i = 0; i < children.length; i++) {
onclick: (event) => this.#setSidebar(event), children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
}), }
$el("button.icon-button", { if (targetIsActive) {
textContent: "◧", deactivationCallback(event);
onclick: (event) => this.#setSidebar(event), }
}), else {
]); event.target.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
activationCallback(event);
}
},
})
);
} }
radioButtonGroup.append.apply(radioButtonGroup, buttons);
return radioButtonGroup;
}
/**
* Coupled-state select and radio buttons (hidden first radio button)
* @param {String[]} labels
* @param {[(button: HTMLButtonElement) => Promise<void>]} activationCallbacks
* @returns {[HTMLDivElement, HTMLSelectElement]}
*/
function GenerateSidebarToggleRadioAndSelect(labels, activationCallbacks = []) {
const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active";
const radioButtonGroup = $el("div.radio-button-group", []);
const buttons = [];
const select = $el("select", {
name: "sidebar-select",
onchange: (event) => {
const select = event.target;
const children = select.children;
let value = undefined;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.selected) {
value = child.value;
}
}
for (let i = 0; i < buttons.length; i++) {
const button = buttons[i];
if (button.textContent === value) {
for (let i = 0; i < buttons.length; i++) {
buttons[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
}
button.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
activationCallbacks[i](button);
break;
}
}
},
}, labels.map((option) => {
return $el("option", {
value: option,
}, option);
})
);
for (let i = 0; i < labels.length; i++) {
const text = labels[i];
const activationCallback = activationCallbacks[i] ?? (() => {});
buttons.push(
$el("button.radio-button", {
textContent: text,
onclick: (event) => {
const button = event.target;
let textContent = button.textContent;
const targetIsActive = button.classList.contains(RADIO_BUTTON_GROUP_ACTIVE);
if (button === buttons[0] && buttons[0].classList.contains(RADIO_BUTTON_GROUP_ACTIVE)) {
// do not deactivate 0
return;
}
// update button
const children = radioButtonGroup.children;
for (let i = 0; i < children.length; i++) {
children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
}
if (targetIsActive) {
// return to 0
textContent = labels[0];
buttons[0].classList.add(RADIO_BUTTON_GROUP_ACTIVE);
activationCallbacks[0](buttons[0]);
}
else {
// move to >0
button.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
activationCallback(button);
}
// update selection
for (let i = 0; i < select.children.length; i++) {
const option = select.children[i];
option.selected = option.value === textContent;
}
},
})
);
}
radioButtonGroup.append.apply(radioButtonGroup, buttons);
buttons[0].click();
buttons[0].style.display = "none";
return [radioButtonGroup, select];
} }
class ModelManager extends ComfyDialog { class ModelManager extends ComfyDialog {
@@ -3580,6 +3661,9 @@ class ModelManager extends ComfyDialog {
/** @type {SettingsView} */ /** @type {SettingsView} */
#settingsView = null; #settingsView = null;
/** @type {HTMLDivElement} */
#topbarRight = null;
/** @type {HTMLDivElement} */ /** @type {HTMLDivElement} */
#tabManagerButtons = null; #tabManagerButtons = null;
@@ -3633,17 +3717,36 @@ class ModelManager extends ComfyDialog {
const tabInfoButtons = this.#modelInfo.elements.tabButtons; const tabInfoButtons = this.#modelInfo.elements.tabButtons;
const tabInfoContents = this.#modelInfo.elements.tabContents; const tabInfoContents = this.#modelInfo.elements.tabContents;
const [sidebarButtonGroup, sidebarSelect] = GenerateSidebarToggleRadioAndSelect(
["◼", "◨", "⬒", "⬓", "◧"],
[
() => { this.element.dataset["sidebarState"] = "none"; },
() => { this.element.dataset["sidebarState"] = "right"; },
() => { this.element.dataset["sidebarState"] = "top"; },
() => { this.element.dataset["sidebarState"] = "bottom"; },
() => { this.element.dataset["sidebarState"] = "left"; },
],
);
sidebarButtonGroup.classList.add("sidebar-buttons");
const sidebarButtonGroupChildren = sidebarButtonGroup.children;
for (let i = 0; i < sidebarButtonGroupChildren.length; i++) {
sidebarButtonGroupChildren[i].classList.add("icon-button");
}
const modelManager = $el( const modelManager = $el(
"div.comfy-modal.model-manager", "div.comfy-modal.model-manager",
{ {
$: (el) => (this.element = el), $: (el) => (this.element = el),
parent: document.body, parent: document.body,
dataset: { "sidebarState": "none" },
}, },
[ [
$el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left $el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left
$el("div.model-manager-panel", [ $el("div.model-manager-panel", [
$el("div.model-manager-head", [ $el("div.model-manager-head", [
$el("div.topbar-right", [ $el("div.topbar-right", {
$: (el) => (this.#topbarRight = el),
}, [
$el("button.icon-button", { $el("button.icon-button", {
textContent: "✖", textContent: "✖",
onclick: async() => { onclick: async() => {
@@ -3659,7 +3762,8 @@ class ModelManager extends ComfyDialog {
textContent: "⬅", textContent: "⬅",
onclick: async() => { await this.#tryHideModelInfo(true); }, onclick: async() => { await this.#tryHideModelInfo(true); },
}), }),
(new SidebarButtons(this)).element, sidebarSelect,
sidebarButtonGroup,
]), ]),
$el("div.topbar-left", [ $el("div.topbar-left", [
$el("div", [ $el("div", [
@@ -3689,6 +3793,18 @@ class ModelManager extends ComfyDialog {
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 768)).observe(modelManager); new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 768)).observe(modelManager);
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 768)).observe(modelManager); new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabInfoButtons, 768)).observe(modelManager);
new ResizeObserver(() => {
const managerRect = document.body.getBoundingClientRect();
const isNarrow = managerRect.width < 768; // TODO: `minWidth` is a magic value
if (isNarrow) {
sidebarButtonGroup.style.display = "none";
sidebarSelect.style.display = "";
}
else {
sidebarButtonGroup.style.display = "";
sidebarSelect.style.display = "none";
}
}).observe(modelManager);
this.#init(); this.#init();
} }