107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
from fastcore.xml import FT
|
|
from fasthtml.components import Div
|
|
|
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
|
from myfasthtml.controls.Keyboard import Keyboard
|
|
from myfasthtml.controls.Mouse import Mouse
|
|
from myfasthtml.core.commands import Command
|
|
from myfasthtml.core.instances import MultipleInstance
|
|
|
|
|
|
class Commands(BaseCommands):
|
|
def close(self):
|
|
return Command("Close",
|
|
"Close Dropdown",
|
|
self._owner,
|
|
self._owner.close).htmx(target=f"#{self._owner.get_id()}-content")
|
|
|
|
def click(self):
|
|
return Command("Click",
|
|
"Click on Dropdown",
|
|
self._owner,
|
|
self._owner.on_click).htmx(target=f"#{self._owner.get_id()}-content")
|
|
|
|
|
|
class DropdownState:
|
|
def __init__(self):
|
|
self.opened = False
|
|
|
|
|
|
class Dropdown(MultipleInstance):
|
|
"""
|
|
Interactive dropdown component that toggles open/closed on button click.
|
|
|
|
Provides automatic close behavior when clicking outside or pressing ESC.
|
|
Supports configurable positioning relative to the trigger button.
|
|
|
|
Args:
|
|
parent: Parent instance (required).
|
|
content: Content to display in the dropdown panel.
|
|
button: Trigger element that toggles the dropdown.
|
|
_id: Custom ID for the instance.
|
|
position: Vertical position relative to button.
|
|
- "below" (default): Dropdown appears below the button.
|
|
- "above": Dropdown appears above the button.
|
|
align: Horizontal alignment relative to button.
|
|
- "left" (default): Aligns to the left edge of the button.
|
|
- "right": Aligns to the right edge of the button.
|
|
- "center": Centers relative to the button.
|
|
|
|
Example:
|
|
dropdown = Dropdown(
|
|
parent=root,
|
|
button=Button("Menu"),
|
|
content=Ul(Li("Option 1"), Li("Option 2")),
|
|
position="below",
|
|
align="right"
|
|
)
|
|
"""
|
|
|
|
def __init__(self, parent, content=None, button=None, _id=None,
|
|
position="below", align="left"):
|
|
super().__init__(parent, _id=_id)
|
|
self.button = Div(button) if not isinstance(button, FT) else button
|
|
self.content = content
|
|
self.commands = Commands(self)
|
|
self._state = DropdownState()
|
|
self._position = position
|
|
self._align = align
|
|
|
|
def toggle(self):
|
|
self._state.opened = not self._state.opened
|
|
return self._mk_content()
|
|
|
|
def close(self):
|
|
self._state.opened = False
|
|
return self._mk_content()
|
|
|
|
def on_click(self, combination, is_inside: bool, is_button: bool = False):
|
|
if combination == "click":
|
|
if is_button:
|
|
self._state.opened = not self._state.opened
|
|
else:
|
|
self._state.opened = is_inside
|
|
return self._mk_content()
|
|
|
|
def _mk_content(self):
|
|
position_cls = f"mf-dropdown-{self._position}"
|
|
align_cls = f"mf-dropdown-{self._align}"
|
|
return Div(self.content,
|
|
cls=f"mf-dropdown {position_cls} {align_cls} {'is-visible' if self._state.opened else ''}",
|
|
id=f"{self._id}-content"),
|
|
|
|
def render(self):
|
|
return Div(
|
|
Div(
|
|
Div(self.button if self.button else "None", cls="mf-dropdown-btn"),
|
|
self._mk_content(),
|
|
cls="mf-dropdown-wrapper"
|
|
),
|
|
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
|
|
Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
|
|
id=self._id
|
|
)
|
|
|
|
def __ft__(self):
|
|
return self.render()
|