first version. no icons, no user interaction

This commit is contained in:
2025-11-26 23:28:07 +01:00
parent ce5328fe34
commit a3783b5fb6
12 changed files with 1135 additions and 3 deletions

View File

@@ -28,7 +28,6 @@ class Dropdown(MultipleInstance):
self.content = content
self.commands = Commands(self)
self._state = DropdownState()
self._toggle_command = self.commands.toggle()
def toggle(self):
self._state.opened = not self._state.opened

View File

@@ -0,0 +1,395 @@
"""
TreeView component for hierarchical data visualization with inline editing.
This component provides an interactive tree structure with expand/collapse,
selection, and inline editing capabilities.
"""
import uuid
from dataclasses import dataclass, field
from typing import Optional
from fasthtml.components import Div, Input, Span, Button
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
@dataclass
class TreeNode:
"""
Represents a node in the tree structure.
Attributes:
id: Unique identifier (auto-generated UUID if not provided)
label: Display text for the node
type: Node type for icon mapping
parent: ID of parent node (None for root)
children: List of child node IDs
"""
id: str = field(default_factory=lambda: str(uuid.uuid4()))
label: str = ""
type: str = "default"
parent: Optional[str] = None
children: list[str] = field(default_factory=list)
class TreeViewState(DbObject):
"""
Persistent state for TreeView component.
Attributes:
items: Dictionary mapping node IDs to TreeNode instances
opened: List of expanded node IDs
selected: Currently selected node ID
editing: Node ID currently being edited (None if not editing)
icon_config: Mapping of node types to icon identifiers
"""
def __init__(self, owner):
super().__init__(owner)
with self.initializing():
self.items: dict[str, TreeNode] = {}
self.opened: list[str] = []
self.selected: Optional[str] = None
self.editing: Optional[str] = None
self.icon_config: dict[str, str] = {}
class Commands(BaseCommands):
"""Command handlers for TreeView actions."""
def toggle_node(self, node_id: str):
"""Create command to expand/collapse a node."""
return Command(
"ToggleNode",
f"Toggle node {node_id}",
self._owner._toggle_node,
node_id
)
def add_child(self, parent_id: str):
"""Create command to add a child node."""
return Command(
"AddChild",
f"Add child to {parent_id}",
self._owner._add_child,
parent_id
)
def add_sibling(self, node_id: str):
"""Create command to add a sibling node."""
return Command(
"AddSibling",
f"Add sibling to {node_id}",
self._owner._add_sibling,
node_id
)
def start_rename(self, node_id: str):
"""Create command to start renaming a node."""
return Command(
"StartRename",
f"Start renaming {node_id}",
self._owner._start_rename,
node_id
)
def save_rename(self, node_id: str):
"""Create command to save renamed node."""
return Command(
"SaveRename",
f"Save rename for {node_id}",
self._owner._save_rename,
node_id
)
def cancel_rename(self):
"""Create command to cancel renaming."""
return Command(
"CancelRename",
"Cancel rename",
self._owner._cancel_rename
)
def delete_node(self, node_id: str):
"""Create command to delete a node."""
return Command(
"DeleteNode",
f"Delete node {node_id}",
self._owner._delete_node,
node_id
)
def select_node(self, node_id: str):
"""Create command to select a node."""
return Command(
"SelectNode",
f"Select node {node_id}",
self._owner._select_node,
node_id
)
class TreeView(MultipleInstance):
"""
Interactive TreeView component with hierarchical data visualization.
Supports:
- Expand/collapse nodes
- Add child/sibling nodes
- Inline rename
- Delete nodes
- Node selection
"""
def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None):
"""
Initialize TreeView component.
Args:
parent: Parent instance
items: Optional initial items dictionary {node_id: TreeNode}
_id: Optional custom ID
"""
super().__init__(parent, _id=_id)
self._state = TreeViewState(self)
self.commands = Commands(self)
if items:
self._state.items = items
def set_icon_config(self, config: dict[str, str]):
"""
Set icon configuration for node types.
Args:
config: Dictionary mapping node types to icon identifiers
Format: {type: "provider.icon_name"}
"""
self._state.icon_config = config
def add_node(self, node: TreeNode, parent_id: Optional[str] = None):
"""
Add a node to the tree.
Args:
node: TreeNode instance to add
parent_id: Optional parent node ID (None for root)
"""
self._state.items[node.id] = node
node.parent = parent_id
if parent_id and parent_id in self._state.items:
parent = self._state.items[parent_id]
if node.id not in parent.children:
parent.children.append(node.id)
def expand_all(self):
"""Expand all nodes that have children."""
for node_id, node in self._state.items.items():
if node.children and node_id not in self._state.opened:
self._state.opened.append(node_id)
def _toggle_node(self, node_id: str):
"""Toggle expand/collapse state of a node."""
if node_id in self._state.opened:
self._state.opened.remove(node_id)
else:
self._state.opened.append(node_id)
return self.render()
def _add_child(self, parent_id: str, new_label: Optional[str] = None):
"""Add a child node to a parent."""
if parent_id not in self._state.items:
raise ValueError(f"Parent node {parent_id} does not exist")
parent = self._state.items[parent_id]
new_node = TreeNode(
label=new_label or "New Node",
type=parent.type,
parent=parent_id
)
self._state.items[new_node.id] = new_node
parent.children.append(new_node.id)
# Auto-expand parent
if parent_id not in self._state.opened:
self._state.opened.append(parent_id)
return self.render()
def _add_sibling(self, node_id: str, new_label: Optional[str] = None):
"""Add a sibling node next to a node."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
node = self._state.items[node_id]
if node.parent is None:
raise ValueError("Cannot add sibling to root node")
parent = self._state.items[node.parent]
new_node = TreeNode(
label=new_label or "New Node",
type=node.type,
parent=node.parent
)
self._state.items[new_node.id] = new_node
# Insert after current node
idx = parent.children.index(node_id)
parent.children.insert(idx + 1, new_node.id)
return self.render()
def _start_rename(self, node_id: str):
"""Start renaming a node (sets editing state)."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.editing = node_id
return self.render()
def _save_rename(self, node_id: str, new_label: str):
"""Save renamed node with new label."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.items[node_id].label = new_label
self._state.editing = None
return self.render()
def _cancel_rename(self):
"""Cancel renaming operation."""
self._state.editing = None
return self.render()
def _delete_node(self, node_id: str):
"""Delete a node (only if it has no children)."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
node = self._state.items[node_id]
if node.children:
raise ValueError(f"Cannot delete node {node_id} with children")
# Remove from parent's children list
if node.parent and node.parent in self._state.items:
parent = self._state.items[node.parent]
parent.children.remove(node_id)
# Remove from state
del self._state.items[node_id]
if node_id in self._state.opened:
self._state.opened.remove(node_id)
if self._state.selected == node_id:
self._state.selected = None
return self.render()
def _select_node(self, node_id: str):
"""Select a node."""
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
self._state.selected = node_id
return self.render()
def _render_action_buttons(self, node_id: str):
"""Render action buttons for a node (visible on hover)."""
return Div(
Button("+child", data_action="add_child"),
Button("+sibling", data_action="add_sibling"),
Button("rename", data_action="rename"),
Button("delete", data_action="delete"),
cls="mf-treenode-actions"
)
def _render_node(self, node_id: str, level: int = 0):
"""
Render a single node and its children recursively.
Args:
node_id: ID of node to render
level: Indentation level
Returns:
Div containing the node and its children
"""
node = self._state.items[node_id]
is_expanded = node_id in self._state.opened
is_selected = node_id == self._state.selected
is_editing = node_id == self._state.editing
has_children = len(node.children) > 0
# Toggle icon
toggle = Span(
"" if is_expanded else "" if has_children else " ",
cls="mf-treenode-toggle"
)
# Label or input for editing
if is_editing:
label_element = Input(
value=node.label,
cls="mf-treenode-input"
)
else:
label_element = Span(
node.label,
cls="mf-treenode-label"
)
# Node element
node_element = Div(
toggle,
label_element,
self._render_action_buttons(node_id),
cls=f"mf-treenode {'selected' if is_selected else ''}",
data_node_id=node_id,
data_level=str(level)
)
# Children (if expanded)
children_elements = []
if is_expanded and has_children:
for child_id in node.children:
children_elements.append(
self._render_node(child_id, level + 1)
)
return Div(
node_element,
*children_elements,
cls="mf-treenode-container"
)
def render(self):
"""
Render the complete TreeView.
Returns:
Div: Complete TreeView HTML structure
"""
# Find root nodes (nodes without parent)
root_nodes = [
node_id for node_id, node in self._state.items.items()
if node.parent is None
]
return Div(
*[self._render_node(node_id) for node_id in root_nodes],
id=self._id,
cls="mf-treeview"
)
def __ft__(self):
"""FastHTML magic method for rendering."""
return self.render()