Added comprehensive documentation for TreeView and Layout components, including usage examples, advanced features, and developer references.
This commit is contained in:
574
docs/Layout.md
574
docs/Layout.md
@@ -1,11 +1,505 @@
|
|||||||
# Layout control
|
# Layout Component
|
||||||
|
|
||||||
## Overview
|
## Introduction
|
||||||
|
|
||||||
This component renders the global layout of the application.
|
The Layout component provides a complete application structure with fixed header and footer, a scrollable main content
|
||||||
This is only one instance per session.
|
area, and optional collapsible side drawers. It's designed to be the foundation of your FastHTML application's UI.
|
||||||
|
|
||||||
## State
|
**Key features:**
|
||||||
|
|
||||||
|
- Fixed header and footer that stay visible while scrolling
|
||||||
|
- Collapsible left and right drawers for navigation, tools, or auxiliary content
|
||||||
|
- Resizable drawers with drag handles
|
||||||
|
- Automatic state persistence per session
|
||||||
|
- Single instance per session (singleton pattern)
|
||||||
|
|
||||||
|
**Common use cases:**
|
||||||
|
|
||||||
|
- Application with navigation sidebar
|
||||||
|
- Dashboard with tools panel
|
||||||
|
- Admin interface with settings drawer
|
||||||
|
- Documentation site with table of contents
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Here's a minimal example showing an application with a navigation sidebar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.Layout import Layout
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
|
||||||
|
# Create the layout instance
|
||||||
|
layout = Layout(parent=root_instance, app_name="My App")
|
||||||
|
|
||||||
|
# Add navigation items to the left drawer
|
||||||
|
layout.left_drawer.add(
|
||||||
|
mk.mk(Div("Home"), command=Command(...))
|
||||||
|
)
|
||||||
|
layout.left_drawer.add(
|
||||||
|
mk.mk(Div("About"), command=Command(...))
|
||||||
|
)
|
||||||
|
layout.left_drawer.add(
|
||||||
|
mk.mk(Div("Contact"), command=Command(...))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the main content
|
||||||
|
layout.set_main(
|
||||||
|
Div(
|
||||||
|
H1("Welcome"),
|
||||||
|
P("This is the main content area")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Render the layout
|
||||||
|
return layout
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a complete application layout with:
|
||||||
|
|
||||||
|
- A header displaying the app name and drawer toggle button
|
||||||
|
- A collapsible left drawer with interactive navigation items
|
||||||
|
- A main content area that updates when navigation items are clicked
|
||||||
|
- An empty footer
|
||||||
|
|
||||||
|
**Note:** Navigation items use commands to update the main content area without page reload. See the Commands section
|
||||||
|
below for details.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Creating a Layout
|
||||||
|
|
||||||
|
The Layout component is a `SingleInstance`, meaning there's only one instance per session. Create it by providing a
|
||||||
|
parent instance and an application name:
|
||||||
|
|
||||||
|
```python
|
||||||
|
layout = Layout(parent=root_instance, app_name="My Application")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Zones
|
||||||
|
|
||||||
|
The Layout provides six content zones where you can add components:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ Header │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ header_left │ │ header_right │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
├─────────┬────────────────────────────────────┬───────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ left │ │ right │
|
||||||
|
│ drawer │ Main Content │ drawer │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
├─────────┴────────────────────────────────────┴───────────┤
|
||||||
|
│ Footer │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ footer_left │ │ footer_right │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zone details:**
|
||||||
|
|
||||||
|
| Zone | Typical Use |
|
||||||
|
|----------------|-----------------------------------------------|
|
||||||
|
| `header_left` | App logo, menu button, breadcrumbs |
|
||||||
|
| `header_right` | User profile, notifications, settings |
|
||||||
|
| `left_drawer` | Navigation menu, tree view, filters |
|
||||||
|
| `right_drawer` | Tools panel, properties inspector, debug info |
|
||||||
|
| `footer_left` | Copyright, legal links, version |
|
||||||
|
| `footer_right` | Status indicators, connection state |
|
||||||
|
|
||||||
|
### Adding Content to Zones
|
||||||
|
|
||||||
|
Use the `.add()` method to add components to any zone:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Header
|
||||||
|
layout.header_left.add(Div("Logo"))
|
||||||
|
layout.header_right.add(Div("User: Admin"))
|
||||||
|
|
||||||
|
# Drawers
|
||||||
|
layout.left_drawer.add(Div("Navigation"))
|
||||||
|
layout.right_drawer.add(Div("Tools"))
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
layout.footer_left.add(Div("© 2024 My App"))
|
||||||
|
layout.footer_right.add(Div("v1.0.0"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Main Content
|
||||||
|
|
||||||
|
The main content area displays your page content and can be updated dynamically:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set initial content
|
||||||
|
layout.set_main(
|
||||||
|
Div(
|
||||||
|
H1("Dashboard"),
|
||||||
|
P("Welcome to your dashboard")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update later (typically via commands)
|
||||||
|
layout.set_main(
|
||||||
|
Div(
|
||||||
|
H1("Settings"),
|
||||||
|
P("Configure your preferences")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controlling Drawers
|
||||||
|
|
||||||
|
By default, both drawers are visible. The drawer state is managed automatically:
|
||||||
|
|
||||||
|
- Users can toggle drawers using the icon buttons in the header
|
||||||
|
- Users can resize drawers by dragging the handle
|
||||||
|
- Drawer state persists within the session
|
||||||
|
|
||||||
|
The initial drawer widths are:
|
||||||
|
|
||||||
|
- Left drawer: 250px
|
||||||
|
- Right drawer: 250px
|
||||||
|
|
||||||
|
These can be adjusted by users and the state is preserved automatically.
|
||||||
|
|
||||||
|
## Content System
|
||||||
|
|
||||||
|
### Understanding Groups
|
||||||
|
|
||||||
|
Each content zone (header_left, header_right, drawers, footer) supports **groups** to organize related items. Groups are
|
||||||
|
separated visually by dividers and can have optional labels.
|
||||||
|
|
||||||
|
### Adding Content to Groups
|
||||||
|
|
||||||
|
When adding content, you can optionally specify a group name:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add items to different groups in the left drawer
|
||||||
|
layout.left_drawer.add(Div("Dashboard"), group="main")
|
||||||
|
layout.left_drawer.add(Div("Analytics"), group="main")
|
||||||
|
layout.left_drawer.add(Div("Settings"), group="preferences")
|
||||||
|
layout.left_drawer.add(Div("Profile"), group="preferences")
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates two groups:
|
||||||
|
|
||||||
|
- **main**: Dashboard, Analytics
|
||||||
|
- **preferences**: Settings, Profile
|
||||||
|
|
||||||
|
A visual divider automatically appears between groups.
|
||||||
|
|
||||||
|
### Custom Group Labels
|
||||||
|
|
||||||
|
You can provide a custom FastHTML element to display as the group header:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add a styled group header
|
||||||
|
layout.left_drawer.add_group(
|
||||||
|
"Navigation",
|
||||||
|
group_ft=Div("MAIN MENU", cls="font-bold text-sm opacity-60 px-2 py-1")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then add items to this group
|
||||||
|
layout.left_drawer.add(Div("Home"), group="Navigation")
|
||||||
|
layout.left_drawer.add(Div("About"), group="Navigation")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ungrouped Content
|
||||||
|
|
||||||
|
If you don't specify a group, content is added to the default (`None`) group:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# These items are in the default group
|
||||||
|
layout.left_drawer.add(Div("Quick Action 1"))
|
||||||
|
layout.left_drawer.add(Div("Quick Action 2"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preventing Duplicates
|
||||||
|
|
||||||
|
The Content system automatically prevents adding duplicate items based on their `id` attribute:
|
||||||
|
|
||||||
|
```python
|
||||||
|
item = Div("Unique Item", id="my-item")
|
||||||
|
layout.left_drawer.add(item)
|
||||||
|
layout.left_drawer.add(item) # Ignored - already added
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group Rendering Options
|
||||||
|
|
||||||
|
Groups render differently depending on the zone:
|
||||||
|
|
||||||
|
**In drawers** (vertical layout):
|
||||||
|
|
||||||
|
- Groups stack vertically
|
||||||
|
- Dividers are horizontal lines
|
||||||
|
- Group labels appear above their content
|
||||||
|
|
||||||
|
**In header/footer** (horizontal layout):
|
||||||
|
|
||||||
|
- Groups arrange side-by-side
|
||||||
|
- Dividers are vertical lines
|
||||||
|
- Group labels are typically hidden
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Resizable Drawers
|
||||||
|
|
||||||
|
Both drawers can be resized by users via drag handles:
|
||||||
|
|
||||||
|
- **Drag handle location**:
|
||||||
|
- Left drawer: Right edge
|
||||||
|
- Right drawer: Left edge
|
||||||
|
- **Width constraints**: 150px (minimum) to 600px (maximum)
|
||||||
|
- **Persistence**: Resized width is automatically saved in the session state
|
||||||
|
|
||||||
|
Users can drag the handle to adjust drawer width. The new width is preserved throughout their session.
|
||||||
|
|
||||||
|
### Programmatic Drawer Control
|
||||||
|
|
||||||
|
You can control drawers programmatically using commands:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Toggle drawer visibility
|
||||||
|
toggle_left = layout.commands.toggle_drawer("left")
|
||||||
|
toggle_right = layout.commands.toggle_drawer("right")
|
||||||
|
|
||||||
|
# Update drawer width
|
||||||
|
update_left_width = layout.commands.update_drawer_width("left", width=300)
|
||||||
|
update_right_width = layout.commands.update_drawer_width("right", width=350)
|
||||||
|
```
|
||||||
|
|
||||||
|
These commands are typically used with buttons or other interactive elements:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add a button to toggle the right drawer
|
||||||
|
button = mk.button("Toggle Tools", command=layout.commands.toggle_drawer("right"))
|
||||||
|
layout.header_right.add(button)
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Persistence
|
||||||
|
|
||||||
|
The Layout automatically persists its state within the user's session:
|
||||||
|
|
||||||
|
| State Property | Description | Default |
|
||||||
|
|----------------------|---------------------------------|---------|
|
||||||
|
| `left_drawer_open` | Whether left drawer is visible | `True` |
|
||||||
|
| `right_drawer_open` | Whether right drawer is visible | `True` |
|
||||||
|
| `left_drawer_width` | Left drawer width in pixels | `250` |
|
||||||
|
| `right_drawer_width` | Right drawer width in pixels | `250` |
|
||||||
|
|
||||||
|
State changes (toggle, resize) are automatically saved and restored within the session.
|
||||||
|
|
||||||
|
### Dynamic Content Updates
|
||||||
|
|
||||||
|
Content zones can be updated dynamically during the session:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Initial setup
|
||||||
|
layout.left_drawer.add(Div("Item 1"))
|
||||||
|
|
||||||
|
|
||||||
|
# Later, add more items (e.g., in a command handler)
|
||||||
|
def add_dynamic_content():
|
||||||
|
layout.left_drawer.add(Div("New Item"), group="dynamic")
|
||||||
|
return layout.left_drawer # Return updated drawer for HTMX swap
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: When updating content dynamically, you typically return the updated zone to trigger an HTMX swap.
|
||||||
|
|
||||||
|
### CSS Customization
|
||||||
|
|
||||||
|
The Layout uses CSS classes that you can customize:
|
||||||
|
|
||||||
|
| Class | Element |
|
||||||
|
|----------------------------|----------------------------------|
|
||||||
|
| `mf-layout` | Root layout container |
|
||||||
|
| `mf-layout-header` | Header section |
|
||||||
|
| `mf-layout-footer` | Footer section |
|
||||||
|
| `mf-layout-main` | Main content area |
|
||||||
|
| `mf-layout-drawer` | Drawer container |
|
||||||
|
| `mf-layout-left-drawer` | Left drawer specifically |
|
||||||
|
| `mf-layout-right-drawer` | Right drawer specifically |
|
||||||
|
| `mf-layout-drawer-content` | Scrollable content within drawer |
|
||||||
|
| `mf-resizer` | Resize handle |
|
||||||
|
| `mf-layout-group` | Content group wrapper |
|
||||||
|
|
||||||
|
You can override these classes in your custom CSS to change colors, spacing, or behavior.
|
||||||
|
|
||||||
|
### User Profile Integration
|
||||||
|
|
||||||
|
The Layout automatically includes a UserProfile component in the header right area. This component handles user
|
||||||
|
authentication display and logout functionality when auth is enabled.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Dashboard with Navigation Sidebar
|
||||||
|
|
||||||
|
A typical dashboard application with a navigation menu in the left drawer:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.Layout import Layout
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
|
||||||
|
# Create layout
|
||||||
|
layout = Layout(parent=root_instance, app_name="Analytics Dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
# Navigation menu in left drawer
|
||||||
|
def show_dashboard():
|
||||||
|
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
|
||||||
|
return layout._mk_main()
|
||||||
|
|
||||||
|
|
||||||
|
def show_reports():
|
||||||
|
layout.set_main(Div(H1("Reports"), P("Detailed analytics reports")))
|
||||||
|
return layout._mk_main()
|
||||||
|
|
||||||
|
|
||||||
|
def show_settings():
|
||||||
|
layout.set_main(Div(H1("Settings"), P("Configure your preferences")))
|
||||||
|
return layout._mk_main()
|
||||||
|
|
||||||
|
|
||||||
|
# Add navigation items with groups
|
||||||
|
layout.left_drawer.add_group("main", group_ft=Div("MENU", cls="font-bold text-xs px-2 opacity-60"))
|
||||||
|
layout.left_drawer.add(mk.mk(Div("Dashboard"), command=Command("nav_dash", "Show dashboard", show_dashboard)),
|
||||||
|
group="main")
|
||||||
|
layout.left_drawer.add(mk.mk(Div("Reports"), command=Command("nav_reports", "Show reports", show_reports)),
|
||||||
|
group="main")
|
||||||
|
|
||||||
|
layout.left_drawer.add_group("config", group_ft=Div("CONFIGURATION", cls="font-bold text-xs px-2 opacity-60"))
|
||||||
|
layout.left_drawer.add(mk.mk(Div("Settings"), command=Command("nav_settings", "Show settings", show_settings)),
|
||||||
|
group="config")
|
||||||
|
|
||||||
|
# Header content
|
||||||
|
layout.header_left.add(Div("📊 Analytics", cls="font-bold"))
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
layout.footer_left.add(Div("© 2024 Analytics Co."))
|
||||||
|
layout.footer_right.add(Div("v1.0.0"))
|
||||||
|
|
||||||
|
# Set initial main content
|
||||||
|
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Development Tool with Debug Panel
|
||||||
|
|
||||||
|
An application with development tools in the right drawer:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.Layout import Layout
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
|
||||||
|
# Create layout
|
||||||
|
layout = Layout(parent=root_instance, app_name="Dev Tools")
|
||||||
|
|
||||||
|
# Main content: code editor
|
||||||
|
layout.set_main(
|
||||||
|
Div(
|
||||||
|
H2("Code Editor"),
|
||||||
|
Textarea("# Write your code here", rows=20, cls="w-full font-mono")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Right drawer: debug and tools
|
||||||
|
layout.right_drawer.add_group("debug", group_ft=Div("DEBUG INFO", cls="font-bold text-xs px-2 opacity-60"))
|
||||||
|
layout.right_drawer.add(Div("Console output here..."), group="debug")
|
||||||
|
layout.right_drawer.add(Div("Variables: x=10, y=20"), group="debug")
|
||||||
|
|
||||||
|
layout.right_drawer.add_group("tools", group_ft=Div("TOOLS", cls="font-bold text-xs px-2 opacity-60"))
|
||||||
|
layout.right_drawer.add(Button("Run Code"), group="tools")
|
||||||
|
layout.right_drawer.add(Button("Clear Console"), group="tools")
|
||||||
|
|
||||||
|
# Header
|
||||||
|
layout.header_left.add(Div("DevTools IDE"))
|
||||||
|
layout.header_right.add(Button("Save"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Minimal Layout (Main Content Only)
|
||||||
|
|
||||||
|
A simple layout without drawers, focusing only on main content:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.Layout import Layout
|
||||||
|
|
||||||
|
# Create layout
|
||||||
|
layout = Layout(parent=root_instance, app_name="Simple Blog")
|
||||||
|
|
||||||
|
# Header
|
||||||
|
layout.header_left.add(Div("My Blog", cls="text-xl font-bold"))
|
||||||
|
layout.header_right.add(A("About", href="/about"))
|
||||||
|
|
||||||
|
# Main content
|
||||||
|
layout.set_main(
|
||||||
|
Article(
|
||||||
|
H1("Welcome to My Blog"),
|
||||||
|
P("This is a simple blog layout without side drawers."),
|
||||||
|
P("The focus is on the content in the center.")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
layout.footer_left.add(Div("© 2024 Blog Author"))
|
||||||
|
layout.footer_right.add(A("RSS", href="/rss"))
|
||||||
|
|
||||||
|
# Note: Drawers are present but can be collapsed by users if not needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Dynamic Content Loading
|
||||||
|
|
||||||
|
Loading content dynamically based on user interaction:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.Layout import Layout
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
|
||||||
|
layout = Layout(parent=root_instance, app_name="Dynamic App")
|
||||||
|
|
||||||
|
|
||||||
|
# Function that loads content dynamically
|
||||||
|
def load_page(page_name):
|
||||||
|
# Simulate loading different content
|
||||||
|
content = {
|
||||||
|
"home": Div(H1("Home"), P("Welcome to the home page")),
|
||||||
|
"profile": Div(H1("Profile"), P("User profile information")),
|
||||||
|
"settings": Div(H1("Settings"), P("Application settings")),
|
||||||
|
}
|
||||||
|
layout.set_main(content.get(page_name, Div("Page not found")))
|
||||||
|
return layout._mk_main()
|
||||||
|
|
||||||
|
|
||||||
|
# Create navigation commands
|
||||||
|
pages = ["home", "profile", "settings"]
|
||||||
|
for page in pages:
|
||||||
|
cmd = Command(f"load_{page}", f"Load {page} page", load_page, page)
|
||||||
|
layout.left_drawer.add(
|
||||||
|
mk.mk(Div(page.capitalize()), command=cmd)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set initial content
|
||||||
|
layout.set_main(Div(H1("Home"), P("Welcome to the home page")))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Developer Reference
|
||||||
|
|
||||||
|
This section contains technical details for developers working on the Layout component itself.
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
The Layout component maintains the following state properties:
|
||||||
|
|
||||||
| Name | Type | Description | Default |
|
| Name | Type | Description | Default |
|
||||||
|----------------------|---------|----------------------------------|---------|
|
|----------------------|---------|----------------------------------|---------|
|
||||||
@@ -14,32 +508,28 @@ This is only one instance per session.
|
|||||||
| `left_drawer_width` | integer | Width of the left drawer | 250 |
|
| `left_drawer_width` | integer | Width of the left drawer | 250 |
|
||||||
| `right_drawer_width` | integer | Width of the right drawer | 250 |
|
| `right_drawer_width` | integer | Width of the right drawer | 250 |
|
||||||
|
|
||||||
## Commands
|
### Commands
|
||||||
|
|
||||||
|
Available commands for programmatic control:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|-----------------------------------------|----------------------------------------------------------------------------------------|
|
|-----------------------------------------|----------------------------------------------------------------------------------------|
|
||||||
| `toggle_drawer(side)` | Toggles the drawer on the specified side |
|
| `toggle_drawer(side)` | Toggles the drawer on the specified side |
|
||||||
| `update_drawer_width(side, width=None)` | Updates the drawer width on the specified side. The width is given by the HTMX request |
|
| `update_drawer_width(side, width=None)` | Updates the drawer width on the specified side. The width is given by the HTMX request |
|
||||||
|
|
||||||
## Ids
|
### Public Methods
|
||||||
|
|
||||||
| Name | Description |
|
| Method | Description |
|
||||||
|-------------|-------------------|
|
|---------------------|-----------------------------|
|
||||||
| `layout` | Singleton |
|
| `set_main(content)` | Sets the main content area |
|
||||||
| `layout_h` | header |
|
| `render()` | Renders the complete layout |
|
||||||
| `layout_hl` | header left side |
|
|
||||||
| `layout_hr` | header right side |
|
### High Level Hierarchical Structure
|
||||||
| `layout_f` | footer |
|
|
||||||
| `layout_fl` | footer left side |
|
|
||||||
| `layout_fr` | footer right side |
|
|
||||||
| `layout_ld` | left drawer |
|
|
||||||
| `layout_rd` | right drawer |
|
|
||||||
|
|
||||||
## High Level Hierarchical Structure
|
|
||||||
```
|
```
|
||||||
Div(id="layout")
|
Div(id="layout")
|
||||||
├── Header
|
├── Header
|
||||||
│ ├── Div(id="layout_hl")
|
│ ├── Div(id="layout_hl")
|
||||||
│ │ ├── Icon # Left drawer icon button
|
│ │ ├── Icon # Left drawer icon button
|
||||||
│ │ └── Div # Left content for the header
|
│ │ └── Div # Left content for the header
|
||||||
│ └── Div(id="layout_hr")
|
│ └── Div(id="layout_hr")
|
||||||
@@ -47,7 +537,47 @@ Div(id="layout")
|
|||||||
│ └── UserProfile # user profile icon button
|
│ └── UserProfile # user profile icon button
|
||||||
├── Div # Left Drawer
|
├── Div # Left Drawer
|
||||||
├── Main # Main content
|
├── Main # Main content
|
||||||
├── Div # Right Drawer
|
├── Div # Right Drawer
|
||||||
├── Footer # Footer
|
├── Footer # Footer
|
||||||
└── Script # To initialize the resizing
|
└── Script # To initialize the resizing
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Element IDs
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|-------------|-------------------------------------|
|
||||||
|
| `layout` | Root layout container (singleton) |
|
||||||
|
| `layout_h` | Header section (not currently used) |
|
||||||
|
| `layout_hl` | Header left side |
|
||||||
|
| `layout_hr` | Header right side |
|
||||||
|
| `layout_f` | Footer section (not currently used) |
|
||||||
|
| `layout_fl` | Footer left side |
|
||||||
|
| `layout_fr` | Footer right side |
|
||||||
|
| `layout_ld` | Left drawer |
|
||||||
|
| `layout_rd` | Right drawer |
|
||||||
|
|
||||||
|
### Internal Methods
|
||||||
|
|
||||||
|
These methods are used internally for rendering:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---------------------------|--------------------------------------------------------|
|
||||||
|
| `_mk_header()` | Renders the header component |
|
||||||
|
| `_mk_footer()` | Renders the footer component |
|
||||||
|
| `_mk_main()` | Renders the main content area |
|
||||||
|
| `_mk_left_drawer()` | Renders the left drawer |
|
||||||
|
| `_mk_right_drawer()` | Renders the right drawer |
|
||||||
|
| `_mk_left_drawer_icon()` | Renders the left drawer toggle icon |
|
||||||
|
| `_mk_right_drawer_icon()` | Renders the right drawer toggle icon |
|
||||||
|
| `_mk_content_wrapper()` | Static method to wrap content with groups and dividers |
|
||||||
|
|
||||||
|
### Content Class
|
||||||
|
|
||||||
|
The `Layout.Content` nested class manages content zones:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|-----------------------------------|----------------------------------------------------------|
|
||||||
|
| `add(content, group=None)` | Adds content to a group, prevents duplicates based on ID |
|
||||||
|
| `add_group(group, group_ft=None)` | Creates a new group with optional custom header element |
|
||||||
|
| `get_content()` | Returns dictionary of groups and their content |
|
||||||
|
| `get_groups()` | Returns list of (group_name, group_ft) tuples |
|
||||||
596
docs/TreeView.md
Normal file
596
docs/TreeView.md
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
# TreeView Component
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The TreeView component provides an interactive hierarchical data visualization with full CRUD operations. It's designed for displaying tree-structured data like file systems, organizational charts, or navigation menus with inline editing capabilities.
|
||||||
|
|
||||||
|
**Key features:**
|
||||||
|
|
||||||
|
- Expand/collapse nodes with visual indicators
|
||||||
|
- Add child and sibling nodes dynamically
|
||||||
|
- Inline rename with keyboard support (ESC to cancel)
|
||||||
|
- Delete nodes (only leaf nodes without children)
|
||||||
|
- Node selection tracking
|
||||||
|
- Persistent state per session
|
||||||
|
- Configurable icons per node type
|
||||||
|
|
||||||
|
**Common use cases:**
|
||||||
|
|
||||||
|
- File/folder browser
|
||||||
|
- Category/subcategory management
|
||||||
|
- Organizational hierarchy viewer
|
||||||
|
- Navigation menu builder
|
||||||
|
- Document outline editor
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Here's a minimal example showing a file system tree:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
|
|
||||||
|
# Create TreeView instance
|
||||||
|
tree = TreeView(parent=root_instance, _id="file-tree")
|
||||||
|
|
||||||
|
# Add root folder
|
||||||
|
root = TreeNode(id="root", label="Documents", type="folder")
|
||||||
|
tree.add_node(root)
|
||||||
|
|
||||||
|
# Add some files
|
||||||
|
file1 = TreeNode(id="file1", label="report.pdf", type="file")
|
||||||
|
file2 = TreeNode(id="file2", label="budget.xlsx", type="file")
|
||||||
|
tree.add_node(file1, parent_id="root")
|
||||||
|
tree.add_node(file2, parent_id="root")
|
||||||
|
|
||||||
|
# Expand root to show children
|
||||||
|
tree.expand_all()
|
||||||
|
|
||||||
|
# Render the tree
|
||||||
|
return tree
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an interactive tree where users can:
|
||||||
|
- Click chevrons to expand/collapse folders
|
||||||
|
- Click labels to select items
|
||||||
|
- Use action buttons (visible on hover) to add, rename, or delete nodes
|
||||||
|
|
||||||
|
**Note:** All interactions use commands and update via HTMX without page reload.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Creating a TreeView
|
||||||
|
|
||||||
|
TreeView is a `MultipleInstance`, allowing multiple trees per session. Create it with a parent instance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tree = TreeView(parent=root_instance, _id="my-tree")
|
||||||
|
```
|
||||||
|
|
||||||
|
### TreeNode Structure
|
||||||
|
|
||||||
|
Nodes are represented by the `TreeNode` dataclass:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from myfasthtml.controls.TreeView import TreeNode
|
||||||
|
|
||||||
|
node = TreeNode(
|
||||||
|
id="unique-id", # Auto-generated UUID if not provided
|
||||||
|
label="Node Label", # Display text
|
||||||
|
type="default", # Type for icon mapping
|
||||||
|
parent=None, # Parent node ID (None for root)
|
||||||
|
children=[] # List of child node IDs
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Nodes
|
||||||
|
|
||||||
|
Add nodes using the `add_node()` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add root node
|
||||||
|
root = TreeNode(id="root", label="Root", type="folder")
|
||||||
|
tree.add_node(root)
|
||||||
|
|
||||||
|
# Add child node
|
||||||
|
child = TreeNode(label="Child 1", type="item")
|
||||||
|
tree.add_node(child, parent_id="root")
|
||||||
|
|
||||||
|
# Add with specific position
|
||||||
|
sibling = TreeNode(label="Child 2", type="item")
|
||||||
|
tree.add_node(sibling, parent_id="root", insert_index=0) # Insert at start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
TreeView
|
||||||
|
├── Root Node 1
|
||||||
|
│ ├── [>] Child 1-1 # Collapsed node with children
|
||||||
|
│ ├── [ ] Child 1-2 # Leaf node (no children)
|
||||||
|
│ └── [v] Child 1-3 # Expanded node
|
||||||
|
│ ├── [ ] Grandchild
|
||||||
|
│ └── [ ] Grandchild
|
||||||
|
└── Root Node 2
|
||||||
|
└── [>] Child 2-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Legend:**
|
||||||
|
- `[>]` - Collapsed node (has children)
|
||||||
|
- `[v]` - Expanded node (has children)
|
||||||
|
- `[ ]` - Leaf node (no children)
|
||||||
|
|
||||||
|
### Expanding Nodes
|
||||||
|
|
||||||
|
Control node expansion programmatically:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Expand all nodes with children
|
||||||
|
tree.expand_all()
|
||||||
|
|
||||||
|
# Expand specific nodes by adding to opened list
|
||||||
|
tree._state.opened.append("node-id")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Users can also toggle nodes by clicking the chevron icon.
|
||||||
|
|
||||||
|
## Interactive Features
|
||||||
|
|
||||||
|
### Node Selection
|
||||||
|
|
||||||
|
Users can select nodes by clicking on labels. The selected node is visually highlighted:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Programmatically select a node
|
||||||
|
tree._state.selected = "node-id"
|
||||||
|
|
||||||
|
# Check current selection
|
||||||
|
current = tree._state.selected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Nodes
|
||||||
|
|
||||||
|
Users can add nodes via action buttons (visible on hover):
|
||||||
|
|
||||||
|
**Add Child:**
|
||||||
|
- Adds a new node as a child of the target node
|
||||||
|
- Automatically expands the parent
|
||||||
|
- Creates node with same type as parent
|
||||||
|
|
||||||
|
**Add Sibling:**
|
||||||
|
- Adds a new node next to the target node (same parent)
|
||||||
|
- Inserts after the target node
|
||||||
|
- Cannot add sibling to root nodes
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Programmatically add child
|
||||||
|
tree._add_child(parent_id="root", new_label="New Child")
|
||||||
|
|
||||||
|
# Programmatically add sibling
|
||||||
|
tree._add_sibling(node_id="child1", new_label="New Sibling")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renaming Nodes
|
||||||
|
|
||||||
|
Users can rename nodes via the edit button:
|
||||||
|
|
||||||
|
1. Click the edit icon (visible on hover)
|
||||||
|
2. Input field appears with current label
|
||||||
|
3. Press Enter to save (triggers command)
|
||||||
|
4. Press ESC to cancel (keyboard shortcut)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Programmatically start rename
|
||||||
|
tree._start_rename("node-id")
|
||||||
|
|
||||||
|
# Save rename
|
||||||
|
tree._save_rename("node-id", "New Label")
|
||||||
|
|
||||||
|
# Cancel rename
|
||||||
|
tree._cancel_rename()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting Nodes
|
||||||
|
|
||||||
|
Users can delete nodes via the delete button:
|
||||||
|
|
||||||
|
**Restrictions:**
|
||||||
|
- Can only delete leaf nodes (no children)
|
||||||
|
- Attempting to delete a node with children raises an error
|
||||||
|
- Deleted node is removed from parent's children list
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Programmatically delete node
|
||||||
|
tree._delete_node("node-id") # Raises ValueError if node has children
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content System
|
||||||
|
|
||||||
|
### Node Types and Icons
|
||||||
|
|
||||||
|
Assign types to nodes for semantic grouping and custom icon display:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Define node types
|
||||||
|
root = TreeNode(label="Project", type="project")
|
||||||
|
folder = TreeNode(label="src", type="folder")
|
||||||
|
file = TreeNode(label="main.py", type="python-file")
|
||||||
|
|
||||||
|
# Configure icons for types
|
||||||
|
tree.set_icon_config({
|
||||||
|
"project": "fluent.folder_open",
|
||||||
|
"folder": "fluent.folder",
|
||||||
|
"python-file": "fluent.document_python"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Icon configuration is stored in state and persists within the session.
|
||||||
|
|
||||||
|
### Hierarchical Organization
|
||||||
|
|
||||||
|
Nodes automatically maintain parent-child relationships:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get node's children
|
||||||
|
node = tree._state.items["node-id"]
|
||||||
|
child_ids = node.children
|
||||||
|
|
||||||
|
# Get node's parent
|
||||||
|
parent_id = node.parent
|
||||||
|
|
||||||
|
# Navigate tree programmatically
|
||||||
|
for child_id in node.children:
|
||||||
|
child_node = tree._state.items[child_id]
|
||||||
|
print(child_node.label)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding Root Nodes
|
||||||
|
|
||||||
|
Root nodes are nodes without a parent:
|
||||||
|
|
||||||
|
```python
|
||||||
|
root_nodes = [
|
||||||
|
node_id for node_id, node in tree._state.items.items()
|
||||||
|
if node.parent is None
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
TreeView includes keyboard support for common operations:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `ESC` | Cancel rename operation |
|
||||||
|
|
||||||
|
Additional shortcuts can be added via the Keyboard component:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from myfasthtml.controls.Keyboard import Keyboard
|
||||||
|
|
||||||
|
tree = TreeView(parent=root_instance)
|
||||||
|
# ESC handler is automatically included for cancel rename
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
TreeView maintains persistent state within the session:
|
||||||
|
|
||||||
|
| State Property | Type | Description |
|
||||||
|
|----------------|------|-------------|
|
||||||
|
| `items` | `dict[str, TreeNode]` | All nodes indexed by ID |
|
||||||
|
| `opened` | `list[str]` | IDs of expanded nodes |
|
||||||
|
| `selected` | `str \| None` | Currently selected node ID |
|
||||||
|
| `editing` | `str \| None` | Node being renamed (if any) |
|
||||||
|
| `icon_config` | `dict[str, str]` | Type-to-icon mapping |
|
||||||
|
|
||||||
|
### Dynamic Updates
|
||||||
|
|
||||||
|
TreeView updates are handled via commands that return the updated tree:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Commands automatically target the tree for HTMX swap
|
||||||
|
cmd = tree.commands.toggle_node("node-id")
|
||||||
|
# When executed, returns updated TreeView with new state
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Customization
|
||||||
|
|
||||||
|
TreeView uses CSS classes for styling:
|
||||||
|
|
||||||
|
| Class | Element |
|
||||||
|
|-------|---------|
|
||||||
|
| `mf-treeview` | Root container |
|
||||||
|
| `mf-treenode-container` | Container for node and its children |
|
||||||
|
| `mf-treenode` | Individual node row |
|
||||||
|
| `mf-treenode.selected` | Selected node highlight |
|
||||||
|
| `mf-treenode-label` | Node label text |
|
||||||
|
| `mf-treenode-input` | Input field during rename |
|
||||||
|
| `mf-treenode-actions` | Action buttons container (hover) |
|
||||||
|
|
||||||
|
You can override these classes to customize appearance:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mf-treenode.selected {
|
||||||
|
background-color: #e0f2fe;
|
||||||
|
border-left: 3px solid #0284c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-treenode-actions {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mf-treenode:hover .mf-treenode-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: File System Browser
|
||||||
|
|
||||||
|
A file/folder browser with different node types:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
|
|
||||||
|
# Create tree
|
||||||
|
tree = TreeView(parent=root_instance, _id="file-browser")
|
||||||
|
|
||||||
|
# Configure icons
|
||||||
|
tree.set_icon_config({
|
||||||
|
"folder": "fluent.folder",
|
||||||
|
"python": "fluent.document_python",
|
||||||
|
"text": "fluent.document_text"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Build file structure
|
||||||
|
root = TreeNode(id="root", label="my-project", type="folder")
|
||||||
|
tree.add_node(root)
|
||||||
|
|
||||||
|
src = TreeNode(id="src", label="src", type="folder")
|
||||||
|
tree.add_node(src, parent_id="root")
|
||||||
|
|
||||||
|
main = TreeNode(label="main.py", type="python")
|
||||||
|
utils = TreeNode(label="utils.py", type="python")
|
||||||
|
tree.add_node(main, parent_id="src")
|
||||||
|
tree.add_node(utils, parent_id="src")
|
||||||
|
|
||||||
|
readme = TreeNode(label="README.md", type="text")
|
||||||
|
tree.add_node(readme, parent_id="root")
|
||||||
|
|
||||||
|
# Expand to show structure
|
||||||
|
tree.expand_all()
|
||||||
|
|
||||||
|
return tree
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Category Management
|
||||||
|
|
||||||
|
Managing product categories with inline editing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
|
|
||||||
|
tree = TreeView(parent=root_instance, _id="categories")
|
||||||
|
|
||||||
|
# Root categories
|
||||||
|
electronics = TreeNode(id="elec", label="Electronics", type="category")
|
||||||
|
tree.add_node(electronics)
|
||||||
|
|
||||||
|
# Subcategories
|
||||||
|
computers = TreeNode(label="Computers", type="subcategory")
|
||||||
|
phones = TreeNode(label="Phones", type="subcategory")
|
||||||
|
tree.add_node(computers, parent_id="elec")
|
||||||
|
tree.add_node(phones, parent_id="elec")
|
||||||
|
|
||||||
|
# Products (leaf nodes)
|
||||||
|
laptop = TreeNode(label="Laptops", type="product")
|
||||||
|
desktop = TreeNode(label="Desktops", type="product")
|
||||||
|
tree.add_node(laptop, parent_id=computers.id)
|
||||||
|
tree.add_node(desktop, parent_id=computers.id)
|
||||||
|
|
||||||
|
tree.expand_all()
|
||||||
|
return tree
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Document Outline Editor
|
||||||
|
|
||||||
|
Building a document outline with headings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
|
|
||||||
|
tree = TreeView(parent=root_instance, _id="outline")
|
||||||
|
|
||||||
|
# Document structure
|
||||||
|
doc = TreeNode(id="doc", label="My Document", type="document")
|
||||||
|
tree.add_node(doc)
|
||||||
|
|
||||||
|
# Chapters
|
||||||
|
ch1 = TreeNode(id="ch1", label="Chapter 1: Introduction", type="heading1")
|
||||||
|
ch2 = TreeNode(id="ch2", label="Chapter 2: Methods", type="heading1")
|
||||||
|
tree.add_node(ch1, parent_id="doc")
|
||||||
|
tree.add_node(ch2, parent_id="doc")
|
||||||
|
|
||||||
|
# Sections
|
||||||
|
sec1_1 = TreeNode(label="1.1 Background", type="heading2")
|
||||||
|
sec1_2 = TreeNode(label="1.2 Objectives", type="heading2")
|
||||||
|
tree.add_node(sec1_1, parent_id="ch1")
|
||||||
|
tree.add_node(sec1_2, parent_id="ch1")
|
||||||
|
|
||||||
|
# Subsections
|
||||||
|
subsec = TreeNode(label="1.1.1 Historical Context", type="heading3")
|
||||||
|
tree.add_node(subsec, parent_id=sec1_1.id)
|
||||||
|
|
||||||
|
tree.expand_all()
|
||||||
|
return tree
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Dynamic Tree with Event Handling
|
||||||
|
|
||||||
|
Responding to tree events with custom logic:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fasthtml.common import *
|
||||||
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
|
|
||||||
|
tree = TreeView(parent=root_instance, _id="dynamic-tree")
|
||||||
|
|
||||||
|
# Initial structure
|
||||||
|
root = TreeNode(id="root", label="Tasks", type="folder")
|
||||||
|
tree.add_node(root)
|
||||||
|
|
||||||
|
# Function to handle selection
|
||||||
|
def on_node_selected(node_id):
|
||||||
|
# Custom logic when node is selected
|
||||||
|
node = tree._state.items[node_id]
|
||||||
|
tree._select_node(node_id)
|
||||||
|
|
||||||
|
# Update a detail panel elsewhere in the UI
|
||||||
|
return Div(
|
||||||
|
H3(f"Selected: {node.label}"),
|
||||||
|
P(f"Type: {node.type}"),
|
||||||
|
P(f"Children: {len(node.children)}")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Override select command with custom handler
|
||||||
|
# (In practice, you'd extend the Commands class or use event callbacks)
|
||||||
|
|
||||||
|
tree.expand_all()
|
||||||
|
return tree
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Developer Reference
|
||||||
|
|
||||||
|
This section contains technical details for developers working on the TreeView component itself.
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
The TreeView component maintains the following state properties:
|
||||||
|
|
||||||
|
| Name | Type | Description | Default |
|
||||||
|
|------|------|-------------|---------|
|
||||||
|
| `items` | `dict[str, TreeNode]` | All nodes indexed by ID | `{}` |
|
||||||
|
| `opened` | `list[str]` | Expanded node IDs | `[]` |
|
||||||
|
| `selected` | `str \| None` | Selected node ID | `None` |
|
||||||
|
| `editing` | `str \| None` | Node being renamed | `None` |
|
||||||
|
| `icon_config` | `dict[str, str]` | Type-to-icon mapping | `{}` |
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Available commands for programmatic control:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `toggle_node(node_id)` | Toggle expand/collapse state |
|
||||||
|
| `add_child(parent_id)` | Add child node to parent |
|
||||||
|
| `add_sibling(node_id)` | Add sibling node after target |
|
||||||
|
| `start_rename(node_id)` | Enter rename mode for node |
|
||||||
|
| `save_rename(node_id)` | Save renamed node label |
|
||||||
|
| `cancel_rename()` | Cancel rename operation |
|
||||||
|
| `delete_node(node_id)` | Delete node (if no children) |
|
||||||
|
| `select_node(node_id)` | Select a node |
|
||||||
|
|
||||||
|
All commands automatically target the TreeView component for HTMX updates.
|
||||||
|
|
||||||
|
### Public Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `add_node(node, parent_id, insert_index)` | Add a node to the tree |
|
||||||
|
| `expand_all()` | Expand all nodes with children |
|
||||||
|
| `set_icon_config(config)` | Configure icons for node types |
|
||||||
|
| `render()` | Render the complete TreeView |
|
||||||
|
|
||||||
|
### TreeNode Dataclass
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class TreeNode:
|
||||||
|
id: str # Unique identifier (auto-generated UUID)
|
||||||
|
label: str = "" # Display text
|
||||||
|
type: str = "default" # Node type for icon mapping
|
||||||
|
parent: Optional[str] = None # Parent node ID
|
||||||
|
children: list[str] = [] # Child node IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
### High Level Hierarchical Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Div(id="treeview", cls="mf-treeview")
|
||||||
|
├── Div(cls="mf-treenode-container", data-node-id="root1")
|
||||||
|
│ ├── Div(cls="mf-treenode")
|
||||||
|
│ │ ├── Icon # Toggle chevron
|
||||||
|
│ │ ├── Span(cls="mf-treenode-label") | Input(cls="mf-treenode-input")
|
||||||
|
│ │ └── Div(cls="mf-treenode-actions")
|
||||||
|
│ │ ├── Icon # Add child
|
||||||
|
│ │ ├── Icon # Rename
|
||||||
|
│ │ └── Icon # Delete
|
||||||
|
│ └── Div(cls="mf-treenode-container") # Child nodes (if expanded)
|
||||||
|
│ └── ...
|
||||||
|
├── Div(cls="mf-treenode-container", data-node-id="root2")
|
||||||
|
│ └── ...
|
||||||
|
└── Keyboard # ESC handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element IDs and Attributes
|
||||||
|
|
||||||
|
| Attribute | Element | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `id` | Root Div | TreeView component ID |
|
||||||
|
| `data-node-id` | Node container | Node's unique ID |
|
||||||
|
|
||||||
|
### Internal Methods
|
||||||
|
|
||||||
|
These methods are used internally for rendering and state management:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `_toggle_node(node_id)` | Toggle expand/collapse state |
|
||||||
|
| `_add_child(parent_id, new_label)` | Add child node implementation |
|
||||||
|
| `_add_sibling(node_id, new_label)` | Add sibling node implementation |
|
||||||
|
| `_start_rename(node_id)` | Enter rename mode |
|
||||||
|
| `_save_rename(node_id, node_label)` | Save renamed node |
|
||||||
|
| `_cancel_rename()` | Cancel rename operation |
|
||||||
|
| `_delete_node(node_id)` | Delete node if no children |
|
||||||
|
| `_select_node(node_id)` | Select a node |
|
||||||
|
| `_render_action_buttons(node_id)` | Render hover action buttons |
|
||||||
|
| `_render_node(node_id, level)` | Recursively render node and children |
|
||||||
|
|
||||||
|
### Commands Class
|
||||||
|
|
||||||
|
The `Commands` nested class provides command factory methods:
|
||||||
|
|
||||||
|
| Method | Returns |
|
||||||
|
|--------|---------|
|
||||||
|
| `toggle_node(node_id)` | Command to toggle node |
|
||||||
|
| `add_child(parent_id)` | Command to add child |
|
||||||
|
| `add_sibling(node_id)` | Command to add sibling |
|
||||||
|
| `start_rename(node_id)` | Command to start rename |
|
||||||
|
| `save_rename(node_id)` | Command to save rename |
|
||||||
|
| `cancel_rename()` | Command to cancel rename |
|
||||||
|
| `delete_node(node_id)` | Command to delete node |
|
||||||
|
| `select_node(node_id)` | Command to select node |
|
||||||
|
|
||||||
|
All commands are automatically configured with HTMX targeting.
|
||||||
|
|
||||||
|
### Integration with Keyboard Component
|
||||||
|
|
||||||
|
TreeView includes a Keyboard component for ESC key handling:
|
||||||
|
|
||||||
|
```python
|
||||||
|
Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard")
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables users to press ESC to cancel rename operations without clicking.
|
||||||
@@ -138,6 +138,18 @@ class Layout(SingleInstance):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def toggle_drawer(self, side: Literal["left", "right"]):
|
def toggle_drawer(self, side: Literal["left", "right"]):
|
||||||
|
"""
|
||||||
|
Toggle the state of a drawer (open or close) based on the specified side. This
|
||||||
|
method also generates the corresponding icon and drawer elements for the
|
||||||
|
selected side.
|
||||||
|
|
||||||
|
:param side: The side of the drawer to toggle. Must be either "left" or "right".
|
||||||
|
:type side: Literal["left", "right"]
|
||||||
|
:return: A tuple containing the updated drawer icon and drawer elements for
|
||||||
|
the specified side.
|
||||||
|
:rtype: Tuple[Any, Any]
|
||||||
|
:raises ValueError: If the provided `side` is not "left" or "right".
|
||||||
|
"""
|
||||||
logger.debug(f"Toggle drawer: {side=}, {self._state.left_drawer_open=}")
|
logger.debug(f"Toggle drawer: {side=}, {self._state.left_drawer_open=}")
|
||||||
if side == "left":
|
if side == "left":
|
||||||
self._state.left_drawer_open = not self._state.left_drawer_open
|
self._state.left_drawer_open = not self._state.left_drawer_open
|
||||||
|
|||||||
Reference in New Issue
Block a user