mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-07 13:49:51 -06:00
added tree view component
This commit is contained in:
@@ -13,6 +13,7 @@ import { ComponentId } from "./dataModels";
|
||||
import { ComponentPickerComponent } from "./components/componentPicker";
|
||||
import { ComponentTreeComponent } from "./components/componentTree";
|
||||
import { CustomListItemComponent } from "./components/customListItem";
|
||||
import { CustomTreeItemComponent } from "./components/customTreeItem";
|
||||
import { devToolsConnector } from "./app";
|
||||
import { DevToolsConnectorComponent } from "./components/devToolsConnector";
|
||||
import { DialogContainerComponent } from "./components/dialogContainer";
|
||||
@@ -80,6 +81,7 @@ const COMPONENT_CLASSES = {
|
||||
"ComponentPicker-builtin": ComponentPickerComponent,
|
||||
"ComponentTree-builtin": ComponentTreeComponent,
|
||||
"CustomListItem-builtin": CustomListItemComponent,
|
||||
"CustomTreeItem-builtin": CustomTreeItemComponent,
|
||||
"DevToolsConnector-builtin": DevToolsConnectorComponent,
|
||||
"DialogContainer-builtin": DialogContainerComponent,
|
||||
"Drawer-builtin": DrawerComponent,
|
||||
|
||||
@@ -16,6 +16,7 @@ export class CustomListItemComponent extends ComponentBase<CustomListItemState>
|
||||
createElement(): HTMLElement {
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-custom-list-item");
|
||||
element.classList.add("rio-selectable-item");
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
172
frontend/code/components/customTreeItem.ts
Normal file
172
frontend/code/components/customTreeItem.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
|
||||
import { ComponentId } from "../dataModels";
|
||||
import { componentsById } from "../componentManagement";
|
||||
|
||||
export type CustomTreeItemState = ComponentState & {
|
||||
_type_: "CustomTreeItem-builtin";
|
||||
expand_button: ComponentId | null;
|
||||
content: ComponentId;
|
||||
children_container: ComponentId | null;
|
||||
is_expanded: boolean;
|
||||
};
|
||||
|
||||
export class CustomTreeItemComponent extends ComponentBase<CustomTreeItemState> {
|
||||
createElement(): HTMLElement {
|
||||
const element = document.createElement("div");
|
||||
element.classList.add("rio-custom-tree-item");
|
||||
|
||||
// Header row for expand button and content
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.classList.add("rio-tree-header-row");
|
||||
element.appendChild(headerRow);
|
||||
|
||||
if (this.state.expand_button !== null) {
|
||||
const buttonElement =
|
||||
componentsById[this.state.expand_button].element;
|
||||
if (this.state.children_container !== null) {
|
||||
buttonElement.classList.add("rio-tree-expand-button");
|
||||
buttonElement.addEventListener(
|
||||
"click",
|
||||
this._toggleExpansion.bind(this)
|
||||
);
|
||||
} else {
|
||||
buttonElement.classList.add("rio-tree-expand-placeholder");
|
||||
}
|
||||
headerRow.appendChild(buttonElement);
|
||||
}
|
||||
|
||||
if (this.state.content !== null) {
|
||||
const contentContainerElement =
|
||||
componentsById[this.state.content].element;
|
||||
contentContainerElement.classList.add("rio-selectable-item");
|
||||
headerRow.appendChild(contentContainerElement);
|
||||
}
|
||||
|
||||
const childrenContainerElement = document.createElement("div");
|
||||
childrenContainerElement.classList.add("rio-tree-children");
|
||||
element.appendChild(childrenContainerElement);
|
||||
|
||||
if (this.state.children_container !== null) {
|
||||
childrenContainerElement.style.display = this.state.is_expanded
|
||||
? "block"
|
||||
: "none";
|
||||
childrenContainerElement.appendChild(
|
||||
componentsById[this.state.children_container].element
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
updateElement(
|
||||
deltaState: DeltaState<CustomTreeItemState>,
|
||||
latentComponents: Set<ComponentBase>
|
||||
): void {
|
||||
super.updateElement(deltaState, latentComponents);
|
||||
|
||||
const expandButton =
|
||||
deltaState.expand_button !== undefined
|
||||
? deltaState.expand_button
|
||||
: this.state.expand_button;
|
||||
const content =
|
||||
deltaState.content !== undefined
|
||||
? deltaState.content
|
||||
: this.state.content;
|
||||
const childrenContainer =
|
||||
deltaState.children_container !== undefined
|
||||
? deltaState.children_container
|
||||
: this.state.children_container;
|
||||
|
||||
// Update header row if changed
|
||||
if (
|
||||
this.state.expand_button !== expandButton ||
|
||||
this.state.content !== content
|
||||
) {
|
||||
const headerRowElement = this.element.querySelector(
|
||||
".rio-tree-header-row"
|
||||
) as HTMLElement;
|
||||
const headerChildren = [expandButton, content].filter(
|
||||
(id) => id !== null
|
||||
) as ComponentId[];
|
||||
this.replaceChildren(
|
||||
latentComponents,
|
||||
headerChildren,
|
||||
headerRowElement,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Update expand button listener if changed
|
||||
if (this.state.expand_button !== expandButton) {
|
||||
if (this.state.expand_button !== null) {
|
||||
const oldButtonElement =
|
||||
componentsById[this.state.expand_button].element;
|
||||
console.log("oldButton: ", oldButtonElement);
|
||||
|
||||
oldButtonElement.removeEventListener(
|
||||
"click",
|
||||
this._toggleExpansion.bind(this)
|
||||
);
|
||||
}
|
||||
if (expandButton !== null) {
|
||||
const newButtonElement = componentsById[expandButton].element;
|
||||
console.log("newButton: ", newButtonElement);
|
||||
|
||||
if (childrenContainer !== null) {
|
||||
newButtonElement.classList.add("rio-tree-expand-button");
|
||||
newButtonElement.addEventListener(
|
||||
"click",
|
||||
this._toggleExpansion.bind(this)
|
||||
);
|
||||
} else {
|
||||
newButtonElement.classList.add(
|
||||
"rio-tree-expand-placeholder"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const childrenContainerElement = this.element.querySelector(
|
||||
".rio-tree-children"
|
||||
) as HTMLElement;
|
||||
// Update children container if changed
|
||||
if (this.state.children_container !== childrenContainer) {
|
||||
const allChildren =
|
||||
childrenContainer !== null ? [childrenContainer] : [];
|
||||
this.replaceChildren(
|
||||
latentComponents,
|
||||
allChildren,
|
||||
childrenContainerElement,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Update expansion state if changed
|
||||
if (deltaState.is_expanded !== undefined) {
|
||||
childrenContainerElement.style.display = deltaState.is_expanded
|
||||
? "block"
|
||||
: "none";
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleExpansion(): void {
|
||||
this.state.is_expanded = !this.state.is_expanded;
|
||||
console.log("Toggling expansion to", this.state.is_expanded);
|
||||
|
||||
const childrenContainerElement = this.element.querySelector(
|
||||
".rio-tree-children"
|
||||
) as HTMLElement;
|
||||
childrenContainerElement.style.display = this.state.is_expanded
|
||||
? "block"
|
||||
: "none";
|
||||
|
||||
const expandButtonElement =
|
||||
componentsById[this.state.expand_button].element;
|
||||
expandButtonElement.textContent = this.state.is_expanded ? "▼" : "▶";
|
||||
|
||||
this.sendMessageToBackend({
|
||||
type: "toggleExpansion",
|
||||
is_expanded: this.state.is_expanded,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -170,12 +170,20 @@ export class ListViewComponent extends ComponentBase<ListViewState> {
|
||||
}
|
||||
|
||||
private _itemKey(item: HTMLElement): string | number | null {
|
||||
const component = componentsByElement.get(
|
||||
item.firstElementChild as HTMLElement
|
||||
);
|
||||
//const listItem = item.querySelector(".rio-custom-list-item");
|
||||
const listItem = item.firstElementChild as HTMLElement;
|
||||
const component = componentsByElement.get(listItem as HTMLElement);
|
||||
console.log("_itemKey: item:", item);
|
||||
console.log("_itemKey: listItem:", listItem);
|
||||
console.log("_itemKey: component:", component);
|
||||
const key = component?.state._key_ ?? null;
|
||||
if (key === null || key === "") {
|
||||
console.warn("No key found for item", item);
|
||||
console.warn(
|
||||
"_itemKey: No key found for item. found=",
|
||||
key,
|
||||
"item=",
|
||||
item
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
@@ -242,13 +250,15 @@ export class ListViewComponent extends ComponentBase<ListViewState> {
|
||||
this.element
|
||||
.querySelectorAll(".rio-listview-grouped")
|
||||
.forEach((item) => {
|
||||
const itemKey = this._itemKey(item);
|
||||
const listItem = item.querySelector(".rio-custom-list-item");
|
||||
if (listItem !== null && itemKey !== null) {
|
||||
if (this.state.selected_keys.includes(itemKey)) {
|
||||
listItem.classList.add("selected");
|
||||
} else {
|
||||
listItem.classList.remove("selected");
|
||||
const listItem = item.querySelector(".rio-selectable-item");
|
||||
if (listItem !== null) {
|
||||
const itemKey = this._itemKey(item);
|
||||
if (itemKey !== null) {
|
||||
if (this.state.selected_keys.includes(itemKey)) {
|
||||
listItem.classList.add("selected");
|
||||
} else {
|
||||
listItem.classList.remove("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,18 +71,18 @@
|
||||
background: var(--rio-local-bg-active);
|
||||
}
|
||||
|
||||
.rio-list-view.selectable .rio-custom-list-item.selected {
|
||||
.rio-list-view.selectable .rio-selectable-item.selected {
|
||||
background-color: var(--rio-global-secondary-bg);
|
||||
}
|
||||
|
||||
.rio-list-view.selectable .rio-custom-list-item.selected:hover {
|
||||
.rio-list-view.selectable .rio-selectable-item.selected:hover {
|
||||
background: var(--rio-global-secondary-bg-active);
|
||||
}
|
||||
|
||||
.rio-list-view.selectable .rio-custom-list-item:hover {
|
||||
.rio-list-view.selectable .rio-selectable-item:hover {
|
||||
background: var(--rio-local-bg-active);
|
||||
}
|
||||
|
||||
.rio-list-view.selectable .rio-custom-list-item {
|
||||
.rio-list-view.selectable .rio-selectable-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
32
frontend/css/components/tree_view_and_items.scss
Normal file
32
frontend/css/components/tree_view_and_items.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.rio-custom-tree-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rio-tree-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rio-tree-expand-button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.rio-tree-expand-placeholder {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.rio-selectable-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rio-tree-children {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
@use "components/linear_containers.scss";
|
||||
@use "components/link.scss";
|
||||
@use "components/list_view_and_items.scss";
|
||||
@use "components/tree_view_and_items.scss";
|
||||
@use "components/markdown.scss";
|
||||
@use "components/media_player.scss";
|
||||
@use "components/overlay.scss";
|
||||
|
||||
@@ -62,6 +62,8 @@ from .text import *
|
||||
from .text_input import *
|
||||
from .theme_context_switcher import *
|
||||
from .tooltip import *
|
||||
from .tree_items import *
|
||||
from .tree_view import *
|
||||
from .website import *
|
||||
from .webview import *
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import rio
|
||||
|
||||
from .fundamental_component import FundamentalComponent
|
||||
|
||||
__all__ = ["ListView"]
|
||||
__all__ = ["ListView", "ListViewSelectionChangeEvent"]
|
||||
|
||||
|
||||
class ListViewSelectionChangeEvent:
|
||||
|
||||
178
rio/components/tree_items.py
Normal file
178
rio/components/tree_items.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import typing as t
|
||||
|
||||
import typing_extensions as te
|
||||
|
||||
from ..utils import EventHandler
|
||||
from .component import Component
|
||||
from .fundamental_component import FundamentalComponent
|
||||
from .linear_containers import Column, Row
|
||||
from .text import Text
|
||||
|
||||
__all__ = ["CustomTreeItem", "AbstractTreeItem", "SimpleTreeItem"]
|
||||
|
||||
|
||||
class CustomTreeItem(FundamentalComponent):
|
||||
content: Component
|
||||
expand_button: Component | None = None
|
||||
children_container: Component | None = None
|
||||
is_expanded: bool = False
|
||||
on_expansion_changed: EventHandler[bool] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Component,
|
||||
*,
|
||||
expand_button: Component | None = None,
|
||||
children_container: Component | None = None,
|
||||
is_expanded: bool = False,
|
||||
on_expansion_changed: EventHandler[bool] = None,
|
||||
key: str | int | None = None,
|
||||
) -> None:
|
||||
super().__init__(key=key)
|
||||
self.content = content
|
||||
self.expand_button = expand_button
|
||||
self.children_container = children_container
|
||||
self.is_expanded = is_expanded
|
||||
self.on_expansion_changed = on_expansion_changed
|
||||
|
||||
async def _on_message_(self, msg: t.Any) -> None:
|
||||
assert isinstance(msg, dict), f"Invalid message: {msg}"
|
||||
assert msg.get("type") == "toggleExpansion", (
|
||||
f"Invalid message type: {msg.get('type')}"
|
||||
)
|
||||
is_expanded = msg.get("is_expanded")
|
||||
assert isinstance(is_expanded, bool), (
|
||||
f"Invalid is_expanded: {is_expanded}"
|
||||
)
|
||||
|
||||
# Update the state
|
||||
self._apply_delta_state_from_frontend({"is_expanded": is_expanded})
|
||||
|
||||
if self.on_expansion_changed:
|
||||
await self.call_event_handler(
|
||||
self.on_expansion_changed, is_expanded
|
||||
)
|
||||
|
||||
# Refresh the session
|
||||
await self.session._refresh()
|
||||
|
||||
|
||||
CustomTreeItem._unique_id_ = "CustomTreeItem-builtin"
|
||||
|
||||
|
||||
class AbstractTreeItem(Component):
|
||||
text: str
|
||||
children: list[te.Self] = []
|
||||
is_expanded: bool = False
|
||||
on_press: EventHandler[[]] = None
|
||||
on_expansion_changed: EventHandler[bool] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
*,
|
||||
children: list[te.Self] | None = None,
|
||||
is_expanded: bool = False,
|
||||
on_press: EventHandler[[]] = None,
|
||||
on_expansion_changed: EventHandler[bool] = None,
|
||||
key: str | int | None = None,
|
||||
) -> None:
|
||||
super().__init__(key=key)
|
||||
self.text = text
|
||||
self.children = children or []
|
||||
self.is_expanded = is_expanded
|
||||
self.on_press = on_press
|
||||
self.on_expansion_changed = on_expansion_changed
|
||||
|
||||
def build_content(self) -> Component:
|
||||
return Text(self.text, justify="left", selectable=False)
|
||||
|
||||
def build(self) -> Component:
|
||||
expand_button = Text(
|
||||
("▶" if not self.is_expanded else "▼") if self.children else "●",
|
||||
style="plain-text",
|
||||
key=f"expand_{self.key}",
|
||||
)
|
||||
if self.children:
|
||||
children_container = Column(
|
||||
*self.children,
|
||||
spacing=0.5,
|
||||
margin_left=2, # Indentation
|
||||
key=f"children_{self.key}",
|
||||
)
|
||||
else:
|
||||
children_container = None
|
||||
|
||||
return CustomTreeItem(
|
||||
expand_button=expand_button,
|
||||
content=self.build_content(),
|
||||
children_container=children_container,
|
||||
is_expanded=self.is_expanded,
|
||||
on_expansion_changed=self.on_expansion_changed,
|
||||
key=self.key,
|
||||
)
|
||||
|
||||
|
||||
class SimpleTreeItem(AbstractTreeItem):
|
||||
secondary_text: str = ""
|
||||
left_child: Component | None = None
|
||||
right_child: Component | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
*,
|
||||
secondary_text: str = "",
|
||||
left_child: Component | None = None,
|
||||
right_child: Component | None = None,
|
||||
children: list[te.Self] | None = None,
|
||||
is_expanded: bool = False,
|
||||
on_press: EventHandler[[]] = None,
|
||||
on_expansion_changed: EventHandler[bool] = None,
|
||||
key: str | int | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
text,
|
||||
key=key,
|
||||
children=children,
|
||||
is_expanded=is_expanded,
|
||||
on_press=on_press,
|
||||
on_expansion_changed=on_expansion_changed,
|
||||
)
|
||||
self.secondary_text = secondary_text
|
||||
self.left_child = left_child
|
||||
self.right_child = right_child
|
||||
|
||||
def build_content(self) -> Component:
|
||||
children = []
|
||||
if self.left_child:
|
||||
children.append(self.left_child)
|
||||
text_children = [super().build_content()]
|
||||
if self.secondary_text:
|
||||
text_children.append(
|
||||
Text(
|
||||
self.secondary_text,
|
||||
overflow="wrap",
|
||||
style="dim",
|
||||
justify="left",
|
||||
selectable=False,
|
||||
)
|
||||
)
|
||||
children.append(
|
||||
Column(
|
||||
*text_children,
|
||||
spacing=0.5,
|
||||
grow_x=True,
|
||||
align_y=0.5,
|
||||
grow_y=False,
|
||||
)
|
||||
)
|
||||
if self.right_child:
|
||||
children.append(self.right_child)
|
||||
|
||||
return Row(
|
||||
*children,
|
||||
spacing=1,
|
||||
grow_x=True,
|
||||
key=f"content_{self.key}",
|
||||
)
|
||||
32
rio/components/tree_view.py
Normal file
32
rio/components/tree_view.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import typing as t
|
||||
|
||||
from .component import Component
|
||||
from .list_view import ListView
|
||||
from .tree_items import AbstractTreeItem
|
||||
|
||||
__all__ = ["TreeView"]
|
||||
|
||||
|
||||
class TreeView(Component):
|
||||
root_items: list[AbstractTreeItem]
|
||||
selection_mode: t.Literal["none", "single", "multiple"] = "none"
|
||||
selected_keys: list[str | int] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*root_items: AbstractTreeItem,
|
||||
selection_mode: t.Literal["none", "single", "multiple"] = "none",
|
||||
selected_keys: list[str | int] = None,
|
||||
key: str | int | None = None,
|
||||
) -> None:
|
||||
super().__init__(key=key)
|
||||
self.root_items = list(root_items)
|
||||
self.selection_mode = selection_mode
|
||||
self.selected_keys = selected_keys or []
|
||||
|
||||
def build(self) -> Component:
|
||||
return ListView(
|
||||
*self.root_items,
|
||||
selection_mode=self.selection_mode,
|
||||
selected_keys=self.selected_keys,
|
||||
)
|
||||
Reference in New Issue
Block a user