Added first version of tab management
This commit is contained in:
@@ -152,7 +152,6 @@ function initLayoutResizer(layoutId) {
|
|||||||
|
|
||||||
// Re-initialize after HTMX swaps within this layout
|
// Re-initialize after HTMX swaps within this layout
|
||||||
layoutElement.addEventListener('htmx:afterSwap', function (event) {
|
layoutElement.addEventListener('htmx:afterSwap', function (event) {
|
||||||
console.log('Layout swapped:', event.detail.target);
|
|
||||||
initResizers();
|
initResizers();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -213,7 +212,7 @@ function initBoundaries(elementId, updateUrl) {
|
|||||||
* This function is called when switching between tabs to update both the content visibility
|
* This function is called when switching between tabs to update both the content visibility
|
||||||
* and the tab button states.
|
* 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) {
|
function updateTabs(controllerId) {
|
||||||
const controller = document.getElementById(controllerId);
|
const controller = document.getElementById(controllerId);
|
||||||
@@ -228,8 +227,8 @@ function updateTabs(controllerId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract manager ID from controller ID (remove '-content-controller' suffix)
|
// Extract manager ID from controller ID (remove '-controller' suffix)
|
||||||
const managerId = controllerId.replace('-content-controller', '');
|
const managerId = controllerId.replace('-controller', '');
|
||||||
|
|
||||||
// Hide all tab contents for this manager
|
// Hide all tab contents for this manager
|
||||||
const contentWrapper = document.getElementById(`${managerId}-content-wrapper`);
|
const contentWrapper = document.getElementById(`${managerId}-content-wrapper`);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class Commands(BaseCommands):
|
|||||||
def show_tab(self, tab_id):
|
def show_tab(self, tab_id):
|
||||||
return Command(f"{self._prefix}ShowTab",
|
return Command(f"{self._prefix}ShowTab",
|
||||||
"Activate or show a specific tab",
|
"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):
|
def close_tab(self, tab_id):
|
||||||
return Command(f"{self._prefix}CloseTab",
|
return Command(f"{self._prefix}CloseTab",
|
||||||
@@ -71,20 +71,10 @@ class Commands(BaseCommands):
|
|||||||
return (Command(f"{self._prefix}AddTab",
|
return (Command(f"{self._prefix}AddTab",
|
||||||
"Add a new tab",
|
"Add a new tab",
|
||||||
self._owner.on_new_tab, label, component, auto_increment).
|
self._owner.on_new_tab, label, component, auto_increment).
|
||||||
htmx(target=f"#{self._id}-content-controller"))
|
htmx(target=f"#{self._id}-controller"))
|
||||||
|
|
||||||
def update_boundaries(self):
|
|
||||||
return Command(f"{self._prefix}UpdateBoundaries",
|
|
||||||
"Update component boundaries",
|
|
||||||
self._owner.update_boundaries).htmx(target=None)
|
|
||||||
|
|
||||||
|
|
||||||
class TabsManager(MultipleInstance):
|
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
|
_tab_count = 0
|
||||||
|
|
||||||
def __init__(self, session, _id=None):
|
def __init__(self, session, _id=None):
|
||||||
@@ -264,8 +254,8 @@ class TabsManager(MultipleInstance):
|
|||||||
|
|
||||||
def _mk_tabs_controller(self):
|
def _mk_tabs_controller(self):
|
||||||
return Div(
|
return Div(
|
||||||
Div(id=f"{self._id}-content-controller", data_active_tab=f"{self._state.active_tab}"),
|
Div(id=f"{self._id}-controller", data_active_tab=f"{self._state.active_tab}"),
|
||||||
Script(f'updateTabs("{self._id}-content-controller");'),
|
Script(f'updateTabs("{self._id}-controller");'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_tabs_header(self, oob=False):
|
def _mk_tabs_header(self, oob=False):
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
from fasthtml.xtend import Script
|
||||||
|
|
||||||
from myfasthtml.controls.TabsManager import TabsManager
|
from myfasthtml.controls.TabsManager import TabsManager
|
||||||
from myfasthtml.core.instances import InstancesManager
|
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
|
from .conftest import session
|
||||||
|
|
||||||
|
|
||||||
@@ -20,11 +21,12 @@ class TestTabsManagerBehaviour:
|
|||||||
assert from_instance_manager == tabs_manager
|
assert from_instance_manager == tabs_manager
|
||||||
|
|
||||||
def test_i_can_add_tab(self, 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 is not None
|
||||||
assert tab_id in tabs_manager.get_state().tabs
|
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_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[tab_id]["component_id"] is None # Div is not BaseInstance
|
||||||
assert tabs_manager.get_state().tabs_order == [tab_id]
|
assert tabs_manager.get_state().tabs_order == [tab_id]
|
||||||
@@ -37,34 +39,57 @@ class TestTabsManagerBehaviour:
|
|||||||
assert len(tabs_manager.get_state().tabs) == 2
|
assert len(tabs_manager.get_state().tabs) == 2
|
||||||
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
|
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
|
||||||
assert tabs_manager.get_state().active_tab == 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:
|
class TestTabsManagerRender:
|
||||||
def test_i_can_render_when_no_tabs(self, tabs_manager):
|
def test_i_can_render_when_no_tabs(self, tabs_manager):
|
||||||
res = tabs_manager.render()
|
res = tabs_manager.render()
|
||||||
|
|
||||||
expected = 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(
|
expected = Div(
|
||||||
Div(
|
Div(
|
||||||
Button(),
|
Div(id=f"{tabs_manager.get_id()}-controller"),
|
||||||
id=f"{tabs_manager.get_id()}-header"
|
Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
|
||||||
),
|
),
|
||||||
Div(
|
Div(
|
||||||
Div("Content 1")
|
Div(NoChildren(), id=f"{tabs_manager.get_id()}-header"),
|
||||||
|
id=f"{tabs_manager.get_id()}-header-wrapper"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Div(id=f"{tabs_manager.get_id()}-None-content"),
|
||||||
|
id=f"{tabs_manager.get_id()}-content-wrapper"
|
||||||
),
|
),
|
||||||
id=tabs_manager.get_id(),
|
id=tabs_manager.get_id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert matches(res, expected)
|
assert matches(res, expected)
|
||||||
|
|
||||||
def test_i_can_render_when_multiple_tabs(self, tabs_manager):
|
def test_i_can_render_when_multiple_tabs(self, tabs_manager):
|
||||||
@@ -75,13 +100,22 @@ class TestTabsManagerRender:
|
|||||||
|
|
||||||
expected = Div(
|
expected = Div(
|
||||||
Div(
|
Div(
|
||||||
Button(),
|
Div(id=f"{tabs_manager.get_id()}-controller"),
|
||||||
Button(),
|
Script(f'updateTabs("{tabs_manager.get_id()}-controller");'),
|
||||||
Button(),
|
|
||||||
id=f"{tabs_manager.get_id()}-header"
|
|
||||||
),
|
),
|
||||||
Div(
|
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(),
|
id=tabs_manager.get_id(),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user