Added IconsHelper and updated Keyboard to support require_inside flag

This commit is contained in:
2026-02-20 20:35:09 +01:00
parent b09763b1eb
commit 13f292fc9d
12 changed files with 219 additions and 76 deletions

View File

@@ -16,12 +16,13 @@ from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.DslEditor import DslEditorConf
from myfasthtml.controls.IconsHelper import IconsHelper
from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.controls.helpers import mk, icons, column_type_defaults
from myfasthtml.controls.helpers import mk, column_type_defaults
from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
from myfasthtml.core.dbmanager import DbObject
@@ -261,7 +262,7 @@ class DataGrid(MultipleInstance):
}
self._key_support = {
"esc": self.commands.on_key_pressed(),
"esc": {"command": self.commands.on_key_pressed(), "require_inside": True},
}
logger.debug(f"DataGrid '{self.get_table_name()}' with id='{self._id}' created.")
@@ -647,7 +648,7 @@ class DataGrid(MultipleInstance):
def _mk_header_name(col_def: DataGridColumnState):
return Div(
mk.label(col_def.title, icon=icons.get(col_def.type, None)),
mk.label(col_def.title, icon=IconsHelper.get(col_def.type)),
# make room for sort and filter indicators
cls="flex truncate cursor-default",
data_tooltip=col_def.title,

View File

@@ -5,9 +5,10 @@ from fasthtml.components import *
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.DataGridFormulaEditor import DataGridFormulaEditor
from myfasthtml.controls.DslEditor import DslEditorConf
from myfasthtml.controls.IconsHelper import IconsHelper
from myfasthtml.controls.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.controls.helpers import icons, mk
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.dsls import DslsManager
@@ -179,7 +180,7 @@ class DataGridColumnsManager(MultipleInstance):
),
mk.mk(
Div(
Div(mk.label(col_def.col_id, icon=icons.get(col_def.type, None), cls="ml-2")),
Div(mk.label(col_def.col_id, icon=IconsHelper.get(col_def.type), cls="ml-2")),
Div(mk.icon(chevron_right20_regular), cls="mr-2"),
cls="dt2-column-manager-label"
),

View File

@@ -97,7 +97,7 @@ class Dropdown(MultipleInstance):
self._mk_content(),
cls="mf-dropdown-wrapper"
),
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
Keyboard(self, _id="-keyboard").add("esc", self.commands.close(), require_inside=True),
Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
id=self._id
)

View File

@@ -0,0 +1,71 @@
from myfasthtml.core.constants import ColumnType
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
number_row20_regular
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
checkbox_checked20_filled, math_formula16_regular
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
default_icons = {
None: question20_regular,
True: checkbox_checked20_regular,
False: checkbox_unchecked20_regular,
"Brain": brain_circuit20_regular,
ColumnType.RowIndex: number_symbol20_regular,
ColumnType.Text: text_field20_regular,
ColumnType.Number: number_row20_regular,
ColumnType.Datetime: calendar_ltr20_regular,
ColumnType.Bool: checkbox_checked20_filled,
ColumnType.Enum: text_bullet_list_square20_regular,
ColumnType.Formula: math_formula16_regular,
}
class IconsHelper:
_icons = default_icons.copy()
@staticmethod
def get(name, package=None):
"""
Fetches and returns an icon resource based on the provided name and optional package. If the icon is not already
cached, it will attempt to dynamically load the icon from the available modules under the `myfasthtml.icons` package.
This method uses an internal caching mechanism to store previously fetched icons for future quick lookups.
:param name: The name of the requested icon.
:param package: The optional sub-package to limit the search for the requested icon. If not provided, the method will
iterate through all available modules within the `myfasthtml.icons` package.
:return: The requested icon resource if found; otherwise, returns None.
:rtype: object or None
"""
if name in IconsHelper._icons:
return IconsHelper._icons[name]
import importlib
import pkgutil
import myfasthtml.icons as icons_pkg
_UTILITY_MODULES = {'manage_icons', 'update_icons'}
if package:
module = importlib.import_module(f"myfasthtml.icons.{package}")
icon = getattr(module, name, None)
if icon is not None:
IconsHelper._icons[name] = icon
return icon
for _, modname, _ in pkgutil.iter_modules(icons_pkg.__path__):
if modname in _UTILITY_MODULES:
continue
module = importlib.import_module(f"myfasthtml.icons.{modname}")
icon = getattr(module, name, None)
if icon is not None:
IconsHelper._icons[name] = icon
return icon
return None
@staticmethod
def reset():
IconsHelper._icons = default_icons.copy()

View File

@@ -17,12 +17,16 @@ class Keyboard(MultipleInstance):
super().__init__(parent, _id=_id)
self.combinations = combinations or {}
def add(self, sequence: str, command: Command):
self.combinations[sequence] = command
def add(self, sequence: str, command: Command, require_inside: bool = True):
self.combinations[sequence] = {"command": command, "require_inside": require_inside}
return self
def render(self):
str_combinations = {sequence: command.get_htmx_params() for sequence, command in self.combinations.items()}
str_combinations = {}
for sequence, value in self.combinations.items():
params = value["command"].get_htmx_params()
params["require_inside"] = value.get("require_inside", True)
str_combinations[sequence] = params
return Script(f"add_keyboard_support('{self._parent.get_id()}', '{json.dumps(str_combinations)}')")
def __ft__(self):

View File

@@ -4,6 +4,7 @@ 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 logging
import uuid
from dataclasses import dataclass, field
from typing import Optional
@@ -19,6 +20,19 @@ from myfasthtml.core.instances import MultipleInstance
from myfasthtml.icons.fluent_p1 import chevron_right20_regular, edit20_regular
from myfasthtml.icons.fluent_p2 import chevron_down20_regular, add_circle20_regular, delete20_regular
logger = logging.getLogger("TreeView")
@dataclass
class TreeViewConf:
edit_node: bool = True
add_node: bool = True
delete_node: bool = True
edit_leaf: bool = True
add_leaf: bool = True
delete_leaf: bool = True
icons: dict = None
@dataclass
class TreeNode:
@@ -157,7 +171,7 @@ class TreeView(MultipleInstance):
- Node selection
"""
def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None):
def __init__(self, parent, items: Optional[dict] = None, conf: TreeViewConf = None, _id: Optional[str] = None):
"""
Initialize TreeView component.
@@ -168,10 +182,14 @@ class TreeView(MultipleInstance):
"""
super().__init__(parent, _id=_id)
self._state = TreeViewState(self)
self.conf = conf or TreeViewConf()
self.commands = Commands(self)
if items:
self._state.items = items
if self.conf.icons:
self._state.icon_config = self.conf.icons
def set_icon_config(self, config: dict[str, str]):
"""
@@ -207,7 +225,7 @@ class TreeView(MultipleInstance):
else:
parent.children.append(node.id)
def ensure_path(self, path: str):
def ensure_path(self, path: str, node_type="folder"):
"""Add a node to the tree based on a path string.
Args:
@@ -237,7 +255,7 @@ class TreeView(MultipleInstance):
node = [node for node in current_nodes if node.label == part]
if len(node) == 0:
# create the node
node = TreeNode(label=part, type="folder")
node = TreeNode(label=part, type=node_type)
self.add_node(node, parent_id=parent_id)
else:
node = node[0]
@@ -341,6 +359,7 @@ class TreeView(MultipleInstance):
def _cancel_rename(self):
"""Cancel renaming operation."""
logger.debug("_cancel_rename")
self._state.editing = None
return self
@@ -380,12 +399,22 @@ class TreeView(MultipleInstance):
def _render_action_buttons(self, node_id: str):
"""Render action buttons for a node (visible on hover)."""
return Div(
mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)),
mk.icon(edit20_regular, command=self.commands.start_rename(node_id)),
mk.icon(delete20_regular, command=self.commands.delete_node(node_id)),
cls="mf-treenode-actions"
)
is_leaf = len(self._state.items[node_id].children) == 0
conf = self.conf
add_visible = conf.add_leaf if is_leaf else conf.add_node
edit_visible = conf.edit_leaf if is_leaf else conf.edit_node
delete_visible = conf.delete_leaf if is_leaf else conf.delete_node
buttons = []
if add_visible:
buttons.append(mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)))
if edit_visible:
buttons.append(mk.icon(edit20_regular, command=self.commands.start_rename(node_id)))
if delete_visible:
buttons.append(mk.icon(delete20_regular, command=self.commands.delete_node(node_id)))
return Div(*buttons, cls="mf-treenode-actions")
def _render_node(self, node_id: str, level: int = 0):
"""
@@ -404,10 +433,13 @@ class TreeView(MultipleInstance):
is_editing = node_id == self._state.editing
has_children = len(node.children) > 0
# Toggle icon
toggle = mk.icon(
chevron_down20_regular if is_expanded else chevron_right20_regular if has_children else None,
command=self.commands.toggle_node(node_id))
# Toggle icon (only for nodes with children)
if has_children:
toggle = mk.icon(
chevron_down20_regular if is_expanded else chevron_right20_regular,
command=self.commands.toggle_node(node_id))
else:
toggle = None
# Label or input for editing
if is_editing:
@@ -426,7 +458,7 @@ class TreeView(MultipleInstance):
node_element = Div(
toggle,
label_element,
self._render_action_buttons(node_id),
*([self._render_action_buttons(node_id)] if not is_editing else []),
cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}",
style=f"padding-left: {level * 20}px"
)
@@ -461,7 +493,7 @@ class TreeView(MultipleInstance):
return Div(
*[self._render_node(node_id) for node_id in root_nodes],
Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard"),
Keyboard(self, {"esc": {"command": self.commands.cancel_rename(), "require_inside": False}}, _id="-keyboard"),
id=self._id,
cls="mf-treeview"
)

View File

@@ -5,12 +5,6 @@ from myfasthtml.core.bindings import Binding
from myfasthtml.core.commands import Command, CommandTemplate
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.utils import merge_classes
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \
number_symbol20_regular
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
checkbox_checked20_filled, math_formula16_regular
from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
class Ids:
@@ -148,26 +142,9 @@ class mk:
return ft
icons = {
None: question20_regular,
True: checkbox_checked20_regular,
False: checkbox_unchecked20_regular,
"Brain": brain_circuit20_regular,
ColumnType.RowIndex: number_symbol20_regular,
ColumnType.Text: text_field20_regular,
ColumnType.Number: number_row20_regular,
ColumnType.Datetime: calendar_ltr20_regular,
ColumnType.Bool: checkbox_checked20_filled,
ColumnType.Enum: text_bullet_list_square20_regular,
ColumnType.Formula: math_formula16_regular,
}
column_type_defaults = {
ColumnType.Number: 0,
ColumnType.Text: "",
ColumnType.Bool: False,
ColumnType.Datetime: pd.NaT,
}