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;
}
.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%;
left: 0%;
}
.model-manager.sidebar-top {
.model-manager[data-sidebar-state="top"] {
height: 50%;
top: 0%;
}
.model-manager.sidebar-bottom {
.model-manager[data-sidebar-state="bottom"] {
height: 50%;
top: 50%;
}
.model-manager.sidebar-right {
.model-manager[data-sidebar-state="right"] {
width: 50%;
left: 50%;
}
.model-manager .sidebar-buttons .sidebar-button-active {
border-color: var(--fg-color);
color: var(--fg-color);
overflow: hidden;
}
/* common */
.model-manager h1 {
min-width: 0;
@@ -165,15 +174,6 @@
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 */
.model-manager .model-manager-panel {
color: var(--fg-color);
@@ -528,6 +528,11 @@
float: right;
}
.model-manager .model-manager-head .topbar-right select {
padding: 0;
font-size: 24px;
}
/* search dropdown */
.model-manager .search-models {
display: flex;

View File

@@ -3482,83 +3482,164 @@ class SettingsView {
}
}
class SidebarButtons {
/** @type {HTMLDivElement} */
element = null;
/** @type {ModelManager} */
#modelManager = null;
/**
* @param {Event} e
*/
#setSidebar(e) {
// TODO: settings["sidebar-default-width"]
// TODO: settings["sidebar-default-height"]
// TODO: draggable resize?
const button = e.target;
const modelManager = this.#modelManager.element;
const sidebarButtons = this.element.children;
const buttonActiveState = "sidebar-button-active";
for (let i = 0; i < sidebarButtons.length; i++) {
sidebarButtons[i].classList.remove(buttonActiveState);
}
let buttonIndex;
for (buttonIndex = 0; buttonIndex < sidebarButtons.length; buttonIndex++) {
const sidebarButton = sidebarButtons[buttonIndex];
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);
}
/**
* @param {String[]} labels
* @param {[(event: Event) => Promise<void>]} callbacks
* @returns {HTMLDivElement}
*/
function GenerateRadioButtonGroup(labels, callbacks = []) {
const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active";
const radioButtonGroup = $el("div.radio-button-group", []);
const buttons = [];
for (let i = 0; i < labels.length; i++) {
const text = labels[i];
const callback = callbacks[i] ?? (() => {});
buttons.push(
$el("button.radio-button", {
textContent: text,
onclick: (event) => {
const targetIsActive = event.target.classList.contains(RADIO_BUTTON_GROUP_ACTIVE);
if (targetIsActive) {
return;
}
const children = radioButtonGroup.children;
for (let i = 0; i < children.length; i++) {
children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
}
event.target.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
callback(event);
},
})
);
}
/**
* @param {ModelManager} modelManager
*/
constructor(modelManager) {
this.#modelManager = modelManager;
$el("div.sidebar-buttons",
{
$: (el) => (this.element = el),
},
[
$el("button.icon-button", {
textContent: "◨",
onclick: (event) => this.#setSidebar(event),
}),
$el("button.icon-button", {
textContent: "⬒",
onclick: (event) => this.#setSidebar(event),
}),
$el("button.icon-button", {
textContent: "⬓",
onclick: (event) => this.#setSidebar(event),
}),
$el("button.icon-button", {
textContent: "◧",
onclick: (event) => this.#setSidebar(event),
}),
]);
radioButtonGroup.append.apply(radioButtonGroup, buttons);
buttons[0]?.classList.add(RADIO_BUTTON_GROUP_ACTIVE);
return radioButtonGroup;
}
/**
* @param {String[]} labels
* @param {[(event: Event) => Promise<void>]} activationCallbacks
* @param {(event: Event) => Promise<void>} deactivationCallback
* @returns {HTMLDivElement}
*/
function GenerateToggleRadioButtonGroup(labels, activationCallbacks = [], deactivationCallback = () => {}) {
const RADIO_BUTTON_GROUP_ACTIVE = "radio-button-group-active";
const radioButtonGroup = $el("div.radio-button-group", []);
const buttons = [];
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 targetIsActive = event.target.classList.contains(RADIO_BUTTON_GROUP_ACTIVE);
const children = radioButtonGroup.children;
for (let i = 0; i < children.length; i++) {
children[i].classList.remove(RADIO_BUTTON_GROUP_ACTIVE);
}
if (targetIsActive) {
deactivationCallback(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 {
@@ -3580,6 +3661,9 @@ class ModelManager extends ComfyDialog {
/** @type {SettingsView} */
#settingsView = null;
/** @type {HTMLDivElement} */
#topbarRight = null;
/** @type {HTMLDivElement} */
#tabManagerButtons = null;
@@ -3633,17 +3717,36 @@ class ModelManager extends ComfyDialog {
const tabInfoButtons = this.#modelInfo.elements.tabButtons;
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(
"div.comfy-modal.model-manager",
{
$: (el) => (this.element = el),
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.model-manager-panel", [
$el("div.model-manager-head", [
$el("div.topbar-right", [
$el("div.topbar-right", {
$: (el) => (this.#topbarRight = el),
}, [
$el("button.icon-button", {
textContent: "✖",
onclick: async() => {
@@ -3659,7 +3762,8 @@ class ModelManager extends ComfyDialog {
textContent: "⬅",
onclick: async() => { await this.#tryHideModelInfo(true); },
}),
(new SidebarButtons(this)).element,
sidebarSelect,
sidebarButtonGroup,
]),
$el("div.topbar-left", [
$el("div", [
@@ -3689,6 +3793,18 @@ class ModelManager extends ComfyDialog {
new ResizeObserver(GenerateDynamicTabTextCallback(modelManager, tabManagerButtons, 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();
}