Added event handling support to VisNetwork and enhanced component documentation
This commit is contained in:
@@ -5,6 +5,10 @@ from myfasthtml.core.network_utils import from_parent_child_list
|
|||||||
|
|
||||||
|
|
||||||
class CommandsDebugger(SingleInstance):
|
class CommandsDebugger(SingleInstance):
|
||||||
|
"""
|
||||||
|
Represents a debugger designed for visualizing and managing commands in a parent-child
|
||||||
|
hierarchical structure.
|
||||||
|
"""
|
||||||
def __init__(self, parent, _id=None):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ class DropdownState:
|
|||||||
|
|
||||||
|
|
||||||
class Dropdown(MultipleInstance):
|
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):
|
def __init__(self, parent, content=None, button=None, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.button = Div(button) if not isinstance(button, FT) else button
|
self.button = Div(button) if not isinstance(button, FT) else button
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ class Commands(BaseCommands):
|
|||||||
|
|
||||||
|
|
||||||
class FileUpload(MultipleInstance):
|
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):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from myfasthtml.controls.Panel import Panel
|
from myfasthtml.controls.Panel import Panel
|
||||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||||
|
from myfasthtml.core.commands import LambdaCommand
|
||||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||||
from myfasthtml.core.network_utils import from_parent_child_list
|
from myfasthtml.core.network_utils import from_parent_child_list
|
||||||
|
|
||||||
@@ -11,7 +12,8 @@ class InstancesDebugger(SingleInstance):
|
|||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
nodes, edges = self._get_nodes_and_edges()
|
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)
|
return self._panel.set_main(vis_network)
|
||||||
|
|
||||||
def _get_nodes_and_edges(self):
|
def _get_nodes_and_edges(self):
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance
|
|||||||
|
|
||||||
|
|
||||||
class Keyboard(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):
|
def __init__(self, parent, combinations=None, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.combinations = combinations or {}
|
self.combinations = combinations or {}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ from myfasthtml.core.instances import MultipleInstance
|
|||||||
|
|
||||||
|
|
||||||
class Mouse(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):
|
def __init__(self, parent, _id=None, combinations=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.combinations = combinations or {}
|
self.combinations = combinations or {}
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ class Commands(BaseCommands):
|
|||||||
|
|
||||||
|
|
||||||
class Panel(MultipleInstance):
|
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):
|
def __init__(self, parent, conf=None, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.conf = conf or PanelConf()
|
self.conf = conf or PanelConf()
|
||||||
|
|||||||
@@ -21,6 +21,21 @@ class Commands(BaseCommands):
|
|||||||
|
|
||||||
|
|
||||||
class Search(MultipleInstance):
|
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,
|
def __init__(self,
|
||||||
parent: BaseInstance,
|
parent: BaseInstance,
|
||||||
_id=None,
|
_id=None,
|
||||||
|
|||||||
@@ -25,19 +25,33 @@ class VisNetworkState(DbObject):
|
|||||||
},
|
},
|
||||||
"physics": {"enabled": True}
|
"physics": {"enabled": True}
|
||||||
}
|
}
|
||||||
|
self.events_handlers: dict = {} # {event_name: command_url}
|
||||||
|
|
||||||
|
|
||||||
class VisNetwork(MultipleInstance):
|
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)
|
super().__init__(parent, _id=_id)
|
||||||
logger.debug(f"VisNetwork created with id: {self._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._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):
|
def _update_state(self, nodes, edges, options, events_handlers=None):
|
||||||
logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}")
|
logger.debug(f"Updating VisNetwork state with {nodes=}, {edges=}, {options=}, {events_handlers=}")
|
||||||
if not nodes and not edges and not options:
|
if not nodes and not edges and not options and not events_handlers:
|
||||||
return
|
return
|
||||||
|
|
||||||
state = self._state.copy()
|
state = self._state.copy()
|
||||||
@@ -47,6 +61,8 @@ class VisNetwork(MultipleInstance):
|
|||||||
state.edges = edges
|
state.edges = edges
|
||||||
if options is not None:
|
if options is not None:
|
||||||
state.options = options
|
state.options = options
|
||||||
|
if events_handlers is not None:
|
||||||
|
state.events_handlers = events_handlers
|
||||||
|
|
||||||
self._state.update(state)
|
self._state.update(state)
|
||||||
|
|
||||||
@@ -70,6 +86,32 @@ class VisNetwork(MultipleInstance):
|
|||||||
# Convert Python options to JS
|
# Convert Python options to JS
|
||||||
js_options = json.dumps(self._state.options, indent=2)
|
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 (
|
return (
|
||||||
Div(
|
Div(
|
||||||
id=self._id,
|
id=self._id,
|
||||||
@@ -92,6 +134,7 @@ class VisNetwork(MultipleInstance):
|
|||||||
}};
|
}};
|
||||||
const options = {js_options};
|
const options = {js_options};
|
||||||
const network = new vis.Network(container, data, options);
|
const network = new vis.Network(container, data, options);
|
||||||
|
{event_handlers_js}
|
||||||
}})();
|
}})();
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ class BaseCommand:
|
|||||||
def url(self):
|
def url(self):
|
||||||
return f"{ROUTE_ROOT}{Routes.Commands}?c_id={self.id}"
|
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):
|
def get_ft(self):
|
||||||
return self._ft
|
return self._ft
|
||||||
|
|
||||||
@@ -143,6 +153,13 @@ class Command(BaseCommand):
|
|||||||
return value.split(",")
|
return value.split(",")
|
||||||
return value
|
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):
|
def execute(self, client_response: dict = None):
|
||||||
ret_from_bindings = []
|
ret_from_bindings = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from myfasthtml.core.instances import SingleInstance
|
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||||
|
|
||||||
|
|
||||||
class RootInstanceForTests(SingleInstance):
|
class RootInstanceForTests(SingleInstance):
|
||||||
@@ -25,4 +25,5 @@ def session():
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def root_instance(session):
|
def root_instance(session):
|
||||||
|
InstancesManager.reset()
|
||||||
return RootInstanceForTests(session=session)
|
return RootInstanceForTests(session=session)
|
||||||
|
|||||||
@@ -35,15 +35,6 @@ class TestLayoutBehaviour:
|
|||||||
assert layout._main_content == content
|
assert layout._main_content == content
|
||||||
assert result == layout # Should return self for chaining
|
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):
|
def test_i_can_add_content_to_left_drawer(self, root_instance):
|
||||||
"""Test adding content to left drawer."""
|
"""Test adding content to left drawer."""
|
||||||
layout = Layout(root_instance, app_name="Test App")
|
layout = Layout(root_instance, app_name="Test App")
|
||||||
|
|||||||
Reference in New Issue
Block a user