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:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user