Added first version of tab management

This commit is contained in:
2025-11-16 15:29:56 +01:00
parent c38a012c74
commit ca238303b8
3 changed files with 64 additions and 41 deletions

View File

@@ -152,7 +152,6 @@ function initLayoutResizer(layoutId) {
// Re-initialize after HTMX swaps within this layout
layoutElement.addEventListener('htmx:afterSwap', function (event) {
console.log('Layout swapped:', event.detail.target);
initResizers();
});
}
@@ -213,7 +212,7 @@ function initBoundaries(elementId, updateUrl) {
* This function is called when switching between tabs to update both the content visibility
* and the tab button states.
*
* @param {string} controllerId - The ID of the tabs controller element (format: "{managerId}-content-controller")
* @param {string} controllerId - The ID of the tabs controller element (format: "{managerId}-controller")
*/
function updateTabs(controllerId) {
const controller = document.getElementById(controllerId);
@@ -228,8 +227,8 @@ function updateTabs(controllerId) {
return;
}
// Extract manager ID from controller ID (remove '-content-controller' suffix)
const managerId = controllerId.replace('-content-controller', '');
// Extract manager ID from controller ID (remove '-controller' suffix)
const managerId = controllerId.replace('-controller', '');
// Hide all tab contents for this manager
const contentWrapper = document.getElementById(`${managerId}-content-wrapper`);

View File

@@ -60,7 +60,7 @@ class Commands(BaseCommands):
def show_tab(self, tab_id):
return Command(f"{self._prefix}ShowTab",
"Activate or show a specific tab",
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-content-controller", swap="outerHTML")
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-controller", swap="outerHTML")
def close_tab(self, tab_id):
return Command(f"{self._prefix}CloseTab",
@@ -71,20 +71,10 @@ class Commands(BaseCommands):
return (Command(f"{self._prefix}AddTab",
"Add a new tab",
self._owner.on_new_tab, label, component, auto_increment).
htmx(target=f"#{self._id}-content-controller"))
def update_boundaries(self):
return Command(f"{self._prefix}UpdateBoundaries",
"Update component boundaries",
self._owner.update_boundaries).htmx(target=None)
htmx(target=f"#{self._id}-controller"))
class TabsManager(MultipleInstance):
# Constants for width calculation
TAB_CHAR_WIDTH = 8 # pixels per character
TAB_PADDING = 40 # padding + close button space
TAB_MIN_WIDTH = 80 # minimum tab width
DROPDOWN_BTN_WIDTH = 40 # width of the dropdown button
_tab_count = 0
def __init__(self, session, _id=None):
@@ -264,8 +254,8 @@ class TabsManager(MultipleInstance):
def _mk_tabs_controller(self):
return Div(
Div(id=f"{self._id}-content-controller", data_active_tab=f"{self._state.active_tab}"),
Script(f'updateTabs("{self._id}-content-controller");'),
Div(id=f"{self._id}-controller", data_active_tab=f"{self._state.active_tab}"),
Script(f'updateTabs("{self._id}-controller");'),
)
def _mk_tabs_header(self, oob=False):

View File

@@ -1,9 +1,10 @@
import pytest
from fasthtml.components import *
from fasthtml.xtend import Script
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.core.instances import InstancesManager
from myfasthtml.test.matcher import matches, NoChildren
from myfasthtml.test.matcher import matches, NoChildren, StartsWith
from .conftest import session
@@ -20,11 +21,12 @@ class TestTabsManagerBehaviour:
assert from_instance_manager == tabs_manager
def test_i_can_add_tab(self, tabs_manager):
tab_id = tabs_manager.add_tab("Users", Div("Content 1"))
tab_id = tabs_manager.add_tab("Tab1", Div("Content 1"))
assert tab_id is not None
assert tab_id in tabs_manager.get_state().tabs
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Users"
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Tab1"
assert tabs_manager.get_state().tabs[tab_id]["id"] == tab_id
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None # Div is not BaseInstance
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None # Div is not BaseInstance
assert tabs_manager.get_state().tabs_order == [tab_id]
@@ -38,33 +40,56 @@ class TestTabsManagerBehaviour:
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
assert tabs_manager.get_state().active_tab == tab_id2
def test_i_can_show_tab(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
assert tabs_manager.get_state().active_tab == tab_id2 # last crated tab is active
tabs_manager.show_tab(tab_id1)
assert tabs_manager.get_state().active_tab == tab_id1
def test_i_can_close_tab(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.add_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id2)
assert len(tabs_manager.get_state().tabs) == 2
assert [tab_id for tab_id in tabs_manager.get_state().tabs] == [tab_id1, tab_id3]
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id3]
assert tabs_manager.get_state().active_tab == tab_id3 # last tab stays active
def test_i_still_have_an_active_tab_after_close(self, tabs_manager):
tab_id1 = tabs_manager.add_tab("Tab1", Div("Content 1"))
tab_id2 = tabs_manager.add_tab("Tab2", Div("Content 2"))
tab_id3 = tabs_manager.add_tab("Tab3", Div("Content 3"))
tabs_manager.close_tab(tab_id3) # close the currently active tab
assert tabs_manager.get_state().active_tab == tab_id1 # default to the first tab
class TestTabsManagerRender:
def test_i_can_render_when_no_tabs(self, tabs_manager):
res = tabs_manager.render()
expected = Div(
Div(
Div(id=f"{tabs_manager.get_id()}-controller"),
Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
),
Div(
Div(NoChildren(), id=f"{tabs_manager.get_id()}-header"),
Div(id=f"{tabs_manager.get_id()}-content"),
id=tabs_manager.get_id(),
)
assert matches(res, expected)
def test_i_can_render_when_one_tab(self, tabs_manager):
tabs_manager.add_tab("Users", Div("Content 1"))
res = tabs_manager.render()
expected = Div(
Div(
Button(),
id=f"{tabs_manager.get_id()}-header"
id=f"{tabs_manager.get_id()}-header-wrapper"
),
Div(
Div("Content 1")
Div(id=f"{tabs_manager.get_id()}-None-content"),
id=f"{tabs_manager.get_id()}-content-wrapper"
),
id=tabs_manager.get_id(),
)
assert matches(res, expected)
def test_i_can_render_when_multiple_tabs(self, tabs_manager):
@@ -75,13 +100,22 @@ class TestTabsManagerRender:
expected = Div(
Div(
Button(),
Button(),
Button(),
id=f"{tabs_manager.get_id()}-header"
Div(id=f"{tabs_manager.get_id()}-controller"),
Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
),
Div(
Div("Content 3")
Div(
Div(), # tab_button
Div(), # tab_button
Div(), # tab_button
id=f"{tabs_manager.get_id()}-header"
),
id=f"{tabs_manager.get_id()}-header-wrapper"
),
Div(
Div(id=StartsWith(tabs_manager.get_id())),
# Lasy loading for the other contents
id=f"{tabs_manager.get_id()}-content-wrapper"
),
id=tabs_manager.get_id(),
)