diff --git a/src/myfasthtml/controls/CommandsDebugger.py b/src/myfasthtml/controls/CommandsDebugger.py index 312897a..8168ff9 100644 --- a/src/myfasthtml/controls/CommandsDebugger.py +++ b/src/myfasthtml/controls/CommandsDebugger.py @@ -5,6 +5,10 @@ from myfasthtml.core.network_utils import from_parent_child_list class CommandsDebugger(SingleInstance): + """ + Represents a debugger designed for visualizing and managing commands in a parent-child + hierarchical structure. + """ def __init__(self, parent, _id=None): super().__init__(parent, _id=_id) diff --git a/src/myfasthtml/controls/Dropdown.py b/src/myfasthtml/controls/Dropdown.py index 20e1695..145732f 100644 --- a/src/myfasthtml/controls/Dropdown.py +++ b/src/myfasthtml/controls/Dropdown.py @@ -22,6 +22,12 @@ class DropdownState: class Dropdown(MultipleInstance): + """ + Represents a dropdown component that can be toggled open or closed. This class is used + to create interactive dropdown elements, allowing for container and button customization. + The dropdown provides functionality to manage its state, including opening, closing, and + handling user interactions. + """ def __init__(self, parent, content=None, button=None, _id=None): super().__init__(parent, _id=_id) self.button = Div(button) if not isinstance(button, FT) else button diff --git a/src/myfasthtml/controls/FileUpload.py b/src/myfasthtml/controls/FileUpload.py index 2c828ab..7fbf3f0 100644 --- a/src/myfasthtml/controls/FileUpload.py +++ b/src/myfasthtml/controls/FileUpload.py @@ -35,6 +35,14 @@ class Commands(BaseCommands): class FileUpload(MultipleInstance): + """ + Represents a file upload component. + + This class provides functionality to handle the uploading process of a file, + extract sheet names from an Excel file, and enables users to select a specific + sheet for further processing. It integrates commands and state management + to ensure smooth operation within a parent application. + """ def __init__(self, parent, _id=None): super().__init__(parent, _id=_id) diff --git a/src/myfasthtml/controls/InstancesDebugger.py b/src/myfasthtml/controls/InstancesDebugger.py index cda50e1..5528b64 100644 --- a/src/myfasthtml/controls/InstancesDebugger.py +++ b/src/myfasthtml/controls/InstancesDebugger.py @@ -1,5 +1,6 @@ from myfasthtml.controls.Panel import Panel from myfasthtml.controls.VisNetwork import VisNetwork +from myfasthtml.core.commands import LambdaCommand from myfasthtml.core.instances import SingleInstance, InstancesManager from myfasthtml.core.network_utils import from_parent_child_list @@ -11,7 +12,8 @@ class InstancesDebugger(SingleInstance): def render(self): nodes, edges = self._get_nodes_and_edges() - vis_network = VisNetwork(self, nodes=nodes, edges=edges, _id="-vis") + c = LambdaCommand(lambda event_data: print("received", event_data)) + vis_network = VisNetwork(self, nodes=nodes, edges=edges, _id="-vis", events_handlers={"select_node": c}) return self._panel.set_main(vis_network) def _get_nodes_and_edges(self): diff --git a/src/myfasthtml/controls/Keyboard.py b/src/myfasthtml/controls/Keyboard.py index f3c2d77..ff7e4d8 100644 --- a/src/myfasthtml/controls/Keyboard.py +++ b/src/myfasthtml/controls/Keyboard.py @@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance class Keyboard(MultipleInstance): + """ + Represents a keyboard with customizable key combinations support. + + The Keyboard class allows managing key combinations and their corresponding + actions for a given parent object. + """ def __init__(self, parent, combinations=None, _id=None): super().__init__(parent, _id=_id) self.combinations = combinations or {} diff --git a/src/myfasthtml/controls/Mouse.py b/src/myfasthtml/controls/Mouse.py index 4149d1d..8f97124 100644 --- a/src/myfasthtml/controls/Mouse.py +++ b/src/myfasthtml/controls/Mouse.py @@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance class Mouse(MultipleInstance): + """ + Represents a mechanism to manage mouse event combinations and their associated commands. + + This class is used to add, manage, and render mouse event sequences with corresponding + commands, providing a flexible way to handle mouse interactions programmatically. + """ def __init__(self, parent, _id=None, combinations=None): super().__init__(parent, _id=_id) self.combinations = combinations or {} diff --git a/src/myfasthtml/controls/Panel.py b/src/myfasthtml/controls/Panel.py index 66a2bbf..74f2961 100644 --- a/src/myfasthtml/controls/Panel.py +++ b/src/myfasthtml/controls/Panel.py @@ -38,6 +38,14 @@ class Commands(BaseCommands): class Panel(MultipleInstance): + """ + Represents a user interface panel that supports customizable left, main, and right components. + + The `Panel` class is used to create and manage a panel layout with optional left, main, + and right sections. It provides functionality to set the components of the panel, toggle + sides, and adjust the width of the sides dynamically. The class also handles rendering + the panel with appropriate HTML elements and JavaScript for interactivity. + """ def __init__(self, parent, conf=None, _id=None): super().__init__(parent, _id=_id) self.conf = conf or PanelConf() diff --git a/src/myfasthtml/controls/Search.py b/src/myfasthtml/controls/Search.py index 67fbd58..a00e4b7 100644 --- a/src/myfasthtml/controls/Search.py +++ b/src/myfasthtml/controls/Search.py @@ -21,6 +21,21 @@ class Commands(BaseCommands): class Search(MultipleInstance): + """ + Represents a component for managing and filtering a list of items. + It uses fuzzy matching and subsequence matching to filter items. + + :ivar items_names: The name of the items used to filter. + :type items_names: str + :ivar items: The first set of items to filter. + :type items: list + :ivar filtered: A copy of the `items` list, representing the filtered items after a search operation. + :type filtered: list + :ivar get_attr: Callable function to extract string values from items for filtering. + :type get_attr: Callable[[Any], str] + :ivar template: Callable function to define how filtered items are rendered. + :type template: Callable[[Any], Any] + """ def __init__(self, parent: BaseInstance, _id=None, diff --git a/src/myfasthtml/controls/VisNetwork.py b/src/myfasthtml/controls/VisNetwork.py index a1da6fa..67b7882 100644 --- a/src/myfasthtml/controls/VisNetwork.py +++ b/src/myfasthtml/controls/VisNetwork.py @@ -25,19 +25,33 @@ class VisNetworkState(DbObject): }, "physics": {"enabled": True} } + self.events_handlers: dict = {} # {event_name: command_url} class VisNetwork(MultipleInstance): - def __init__(self, parent, _id=None, nodes=None, edges=None, options=None): + def __init__(self, parent, _id=None, nodes=None, edges=None, options=None, events_handlers=None): super().__init__(parent, _id=_id) logger.debug(f"VisNetwork created with id: {self._id}") + # possible events (expected in snake_case + # - select_node → selectNode + # - select → select + # - click → click + # - double_click → doubleClick + self._state = VisNetworkState(self) - self._update_state(nodes, edges, options) + + # Convert Commands to URLs + handlers_htmx_options = { + event_name: command.ajax_htmx_options() + for event_name, command in events_handlers.items() + } if events_handlers else {} + + self._update_state(nodes, edges, options, handlers_htmx_options) - def _update_state(self, nodes, edges, options): - logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}") - if not nodes and not edges and not options: + def _update_state(self, nodes, edges, options, events_handlers=None): + logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}, {events_handlers=}") + if not nodes and not edges and not options and not events_handlers: return state = self._state.copy() @@ -47,6 +61,8 @@ class VisNetwork(MultipleInstance): state.edges = edges if options is not None: state.options = options + if events_handlers is not None: + state.events_handlers = events_handlers self._state.update(state) @@ -70,6 +86,32 @@ class VisNetwork(MultipleInstance): # Convert Python options to JS js_options = json.dumps(self._state.options, indent=2) + # Map Python event names to vis-network event names + event_name_map = { + "select_node": "selectNode", + "select": "select", + "click": "click", + "double_click": "doubleClick" + } + + # Generate event handlers JavaScript + event_handlers_js = "" + for event_name, command_htmx_options in self._state.events_handlers.items(): + vis_event_name = event_name_map.get(event_name, event_name) + event_handlers_js += f""" + network.on('{vis_event_name}', function(params) {{ + const event_data = {{ + nodes: params.nodes, + edges: params.edges, + pointer: params.pointer + }}; + htmx.ajax('POST', '{command_htmx_options['url']}', {{ + values: {{event_data: JSON.stringify(event_data)}}, + swap: 'none' + }}); + }}); +""" + return ( Div( id=self._id, @@ -92,6 +134,7 @@ class VisNetwork(MultipleInstance): }}; const options = {js_options}; const network = new vis.Network(container, data, options); + {event_handlers_js} }})(); """) ) diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py index c58272b..0f56e3b 100644 --- a/src/myfasthtml/core/commands.py +++ b/src/myfasthtml/core/commands.py @@ -97,6 +97,16 @@ class BaseCommand: def url(self): return f"{ROUTE_ROOT}{Routes.Commands}?c_id={self.id}" + def ajax_htmx_options(self): + res = {"url": self.url} + if "hx-target" in self._htmx_extra: + res["target"] = self._htmx_extra["hx-target"] + if "hx-swap" in self._htmx_extra: + res["swap"] = self._htmx_extra["hx-swap"] + res["values"] = {} + + return res + def get_ft(self): return self._ft @@ -143,6 +153,13 @@ class Command(BaseCommand): return value.split(",") return value + def ajax_htmx_options(self): + res = super().ajax_htmx_options() + if self.kwargs: + res["values"] |= self.kwargs + res["values"]["c_id"] = f"{self.id}" # cannot be overridden + return res + def execute(self, client_response: dict = None): ret_from_bindings = [] diff --git a/tests/controls/conftest.py b/tests/controls/conftest.py index fd1170e..0acbb37 100644 --- a/tests/controls/conftest.py +++ b/tests/controls/conftest.py @@ -1,6 +1,6 @@ import pytest -from myfasthtml.core.instances import SingleInstance +from myfasthtml.core.instances import SingleInstance, InstancesManager class RootInstanceForTests(SingleInstance): @@ -25,4 +25,5 @@ def session(): @pytest.fixture(scope="session") def root_instance(session): + InstancesManager.reset() return RootInstanceForTests(session=session) diff --git a/tests/controls/test_layout.py b/tests/controls/test_layout.py index 06aa84c..9ac9385 100644 --- a/tests/controls/test_layout.py +++ b/tests/controls/test_layout.py @@ -35,15 +35,6 @@ class TestLayoutBehaviour: assert layout._main_content == content assert result == layout # Should return self for chaining - def test_i_can_set_footer_content(self, root_instance): - """Test setting footer content.""" - layout = Layout(root_instance, app_name="Test App") - content = Div("Footer content") - - layout.set_footer(content) - - assert layout._footer_content == content - def test_i_can_add_content_to_left_drawer(self, root_instance): """Test adding content to left drawer.""" layout = Layout(root_instance, app_name="Test App")