I can add and show tabs with lazy loading and content management
This commit is contained in:
18
src/app.py
18
src/app.py
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
import yaml
|
||||
from fasthtml import serve
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.Layout import Layout
|
||||
from myfasthtml.controls.TabsManager import TabsManager
|
||||
@@ -10,15 +10,16 @@ from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.instances import InstancesManager
|
||||
from myfasthtml.myfastapp import create_app
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, # Set logging level to DEBUG
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', # Log format
|
||||
datefmt='%Y-%m-%d %H:%M:%S', # Timestamp format
|
||||
)
|
||||
with open('logging.yaml', 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# At the top of your script or module
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
app, rt = create_app(protect_routes=True,
|
||||
mount_auth_app=True,
|
||||
pico=False,
|
||||
vis=True,
|
||||
title="MyFastHtml",
|
||||
live=True,
|
||||
base_url="http://localhost:5003")
|
||||
@@ -28,9 +29,6 @@ app, rt = create_app(protect_routes=True,
|
||||
def index(session):
|
||||
layout = InstancesManager.get(session, Ids.Layout, Layout, "Testing Layout")
|
||||
layout.set_footer("Goodbye World")
|
||||
for i in range(50):
|
||||
layout.left_drawer.add(Div(f"Left Drawer Item {i}"))
|
||||
layout.right_drawer.add(Div(f"Left Drawer Item {i}"))
|
||||
|
||||
tabs_manager = TabsManager(session, _id="main")
|
||||
|
||||
|
||||
54
src/logging.yaml
Normal file
54
src/logging.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
version: 1
|
||||
disable_existing_loggers: False
|
||||
|
||||
formatters:
|
||||
default:
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
handlers:
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: default
|
||||
|
||||
root:
|
||||
level: DEBUG
|
||||
handlers: [console]
|
||||
|
||||
|
||||
loggers:
|
||||
# Explicit logger configuration (example)
|
||||
multipart.multipart:
|
||||
level: INFO
|
||||
handlers: [console]
|
||||
propagate: False
|
||||
python_multipart.multipart:
|
||||
level: INFO
|
||||
handlers: [ console ]
|
||||
propagate: False
|
||||
httpcore:
|
||||
level: ERROR
|
||||
handlers: [ console ]
|
||||
propagate: False
|
||||
httpx:
|
||||
level: INFO
|
||||
handlers: [ console ]
|
||||
propagate: False
|
||||
watchfiles.main:
|
||||
level: ERROR
|
||||
handlers: [console]
|
||||
propagate: False
|
||||
|
||||
dbengine.dbengine:
|
||||
level: ERROR
|
||||
handlers: [console]
|
||||
propagate: False
|
||||
|
||||
passlib.registry:
|
||||
level: ERROR
|
||||
handlers: [console]
|
||||
propagate: False
|
||||
|
||||
socket.socket:
|
||||
level: ERROR
|
||||
handlers: [ console ]
|
||||
propagate: False
|
||||
@@ -380,6 +380,16 @@
|
||||
|
||||
/* Tab Content Area */
|
||||
.mf-tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
border: 2px solid black;
|
||||
/*background-color: var(--color-base-100);*/
|
||||
/*padding: 1rem;*/
|
||||
/*border-top: 1px solid var(--color-border-primary);*/
|
||||
}
|
||||
|
||||
.mf-tab-content-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background-color: var(--color-base-100);
|
||||
@@ -395,4 +405,9 @@
|
||||
height: 100%;
|
||||
@apply text-base-content/50;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mf-vis {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -206,4 +206,64 @@ function initBoundaries(elementId, updateUrl) {
|
||||
});
|
||||
observer.observe(container.parentNode, {childList: true});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tabs display by showing the active tab content and scrolling to make it visible.
|
||||
* This function is called when switching between tabs to update both the content visibility
|
||||
* and the tab button states.
|
||||
*
|
||||
* @param {string} controllerId - The ID of the tabs controller element (format: "{managerId}-content-controller")
|
||||
*/
|
||||
function updateTabs(controllerId) {
|
||||
const controller = document.getElementById(controllerId);
|
||||
if (!controller) {
|
||||
console.warn(`Controller ${controllerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTabId = controller.dataset.activeTab;
|
||||
if (!activeTabId) {
|
||||
console.warn('No active tab ID found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract manager ID from controller ID (remove '-content-controller' suffix)
|
||||
const managerId = controllerId.replace('-content-controller', '');
|
||||
|
||||
// Hide all tab contents for this manager
|
||||
const contentWrapper = document.getElementById(`${managerId}-content-wrapper`);
|
||||
if (contentWrapper) {
|
||||
contentWrapper.querySelectorAll('.mf-tab-content').forEach(content => {
|
||||
content.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show the active tab content
|
||||
const activeContent = document.getElementById(`${managerId}-${activeTabId}-content`);
|
||||
if (activeContent) {
|
||||
activeContent.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Update active tab button styling
|
||||
const header = document.getElementById(`${managerId}-header`);
|
||||
if (header) {
|
||||
// Remove active class from all tabs
|
||||
header.querySelectorAll('.mf-tab-button').forEach(btn => {
|
||||
btn.classList.remove('mf-tab-active');
|
||||
});
|
||||
|
||||
// Add active class to current tab
|
||||
const activeButton = header.querySelector(`[data-tab-id="${activeTabId}"]`);
|
||||
if (activeButton) {
|
||||
activeButton.classList.add('mf-tab-active');
|
||||
|
||||
// Scroll to make active tab visible if needed
|
||||
activeButton.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/myfasthtml/assets/vis-network.min.js
vendored
Normal file
34
src/myfasthtml/assets/vis-network.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,17 +1,39 @@
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fasthtml.common import Div, Span
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance, BaseInstance
|
||||
from myfasthtml.core.instances_helper import InstancesHelper
|
||||
from myfasthtml.icons.fluent_p1 import tabs24_regular
|
||||
from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular, tab_add24_regular
|
||||
|
||||
logger = logging.getLogger("TabsManager")
|
||||
|
||||
vis_nodes = [
|
||||
{"id": 1, "label": "Node 1"},
|
||||
{"id": 2, "label": "Node 2"},
|
||||
{"id": 3, "label": "Node 3"},
|
||||
{"id": 4, "label": "Node 4"},
|
||||
{"id": 5, "label": "Node 5"}
|
||||
]
|
||||
|
||||
vis_edges = [
|
||||
{"from": 1, "to": 3},
|
||||
{"from": 1, "to": 2},
|
||||
{"from": 2, "to": 4},
|
||||
{"from": 2, "to": 5},
|
||||
{"from": 3, "to": 3}
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Boundaries:
|
||||
@@ -37,7 +59,7 @@ class Commands(BaseCommands):
|
||||
def show_tab(self, tab_id):
|
||||
return Command(f"{self._prefix}ShowTab",
|
||||
"Activate or show a specific tab",
|
||||
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-content-controller", swap="outerHTML")
|
||||
|
||||
def close_tab(self, tab_id):
|
||||
return Command(f"{self._prefix}CloseTab",
|
||||
@@ -45,9 +67,10 @@ class Commands(BaseCommands):
|
||||
self._owner.close_tab, tab_id).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||
|
||||
def add_tab(self, label: str, component: Any, auto_increment=False):
|
||||
return Command(f"{self._prefix}AddTab",
|
||||
"Add a new tab",
|
||||
self._owner.on_new_tab, label, component, auto_increment).htmx(target=f"#{self._id}")
|
||||
return (Command(f"{self._prefix}AddTab",
|
||||
"Add a new tab",
|
||||
self._owner.on_new_tab, label, component, auto_increment).
|
||||
htmx(target=f"#{self._id}-content-controller"))
|
||||
|
||||
def update_boundaries(self):
|
||||
return Command(f"{self._prefix}UpdateBoundaries",
|
||||
@@ -73,107 +96,23 @@ class TabsManager(MultipleInstance):
|
||||
self._state = TabsManagerState(self)
|
||||
self.commands = Commands(self)
|
||||
self._boundaries = Boundaries()
|
||||
logger.debug(f"TabsManager created with id: {self._id}")
|
||||
logger.debug(f" tabs : {self._get_ordered_tabs()}")
|
||||
logger.debug(f" active tab : {self._state.active_tab}")
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
def _estimate_tab_width(self, label: str) -> int:
|
||||
"""
|
||||
Estimate the width of a tab based on its label.
|
||||
|
||||
Args:
|
||||
label: Tab label text
|
||||
|
||||
Returns:
|
||||
Estimated width in pixels
|
||||
"""
|
||||
char_width = len(label) * self.TAB_CHAR_WIDTH
|
||||
total_width = char_width + self.TAB_PADDING
|
||||
return max(total_width, self.TAB_MIN_WIDTH)
|
||||
def _get_ordered_tabs(self):
|
||||
return {tab_id: self._state.tabs.get(tab_id, None) for tab_id in self._state.tabs_order}
|
||||
|
||||
def _calculate_visible_tabs(self) -> tuple[list[str], list[str]]:
|
||||
"""
|
||||
Calculate which tabs should be visible and which should be in dropdown.
|
||||
Priority: active tab + adjacent tabs, then others.
|
||||
|
||||
Returns:
|
||||
Tuple of (visible_tab_ids, hidden_tab_ids)
|
||||
"""
|
||||
if self._boundaries.width == 0:
|
||||
# No width info yet, show all tabs
|
||||
return self._state.tabs_order.copy(), []
|
||||
|
||||
available_width = self._boundaries.width - self.DROPDOWN_BTN_WIDTH
|
||||
visible_tabs = []
|
||||
hidden_tabs = []
|
||||
|
||||
# Find active tab index
|
||||
active_index = -1
|
||||
if self._state.active_tab in self._state.tabs_order:
|
||||
active_index = self._state.tabs_order.index(self._state.active_tab)
|
||||
|
||||
# Calculate widths for all tabs
|
||||
tab_widths = {}
|
||||
for tab_id in self._state.tabs_order:
|
||||
label = self._state.tabs[tab_id].get('label', 'Untitled')
|
||||
tab_widths[tab_id] = self._estimate_tab_width(label)
|
||||
|
||||
# Strategy: Start with active tab, then add adjacent tabs alternating left/right
|
||||
used_width = 0
|
||||
added_indices = set()
|
||||
|
||||
# Always add active tab first if it exists
|
||||
if active_index >= 0:
|
||||
active_tab_id = self._state.tabs_order[active_index]
|
||||
used_width += tab_widths[active_tab_id]
|
||||
if used_width <= available_width:
|
||||
visible_tabs.append(active_tab_id)
|
||||
added_indices.add(active_index)
|
||||
else:
|
||||
# Even active tab doesn't fit, add to hidden
|
||||
hidden_tabs.append(active_tab_id)
|
||||
|
||||
# Add adjacent tabs alternating left and right
|
||||
left_offset = 1
|
||||
right_offset = 1
|
||||
|
||||
while used_width < available_width and len(added_indices) < len(self._state.tabs_order):
|
||||
added_this_round = False
|
||||
|
||||
# Try to add tab to the right
|
||||
right_index = active_index + right_offset
|
||||
if right_index < len(self._state.tabs_order) and right_index not in added_indices:
|
||||
tab_id = self._state.tabs_order[right_index]
|
||||
tab_width = tab_widths[tab_id]
|
||||
if used_width + tab_width <= available_width:
|
||||
visible_tabs.append(tab_id)
|
||||
added_indices.add(right_index)
|
||||
used_width += tab_width
|
||||
added_this_round = True
|
||||
right_offset += 1
|
||||
|
||||
# Try to add tab to the left
|
||||
left_index = active_index - left_offset
|
||||
if left_index >= 0 and left_index not in added_indices:
|
||||
tab_id = self._state.tabs_order[left_index]
|
||||
tab_width = tab_widths[tab_id]
|
||||
if used_width + tab_width <= available_width:
|
||||
visible_tabs.insert(0, tab_id) # Insert at beginning to maintain order
|
||||
added_indices.add(left_index)
|
||||
used_width += tab_width
|
||||
added_this_round = True
|
||||
left_offset += 1
|
||||
|
||||
# If we couldn't add any tab this round, we're done
|
||||
if not added_this_round:
|
||||
break
|
||||
|
||||
# All remaining tabs go to hidden
|
||||
for i, tab_id in enumerate(self._state.tabs_order):
|
||||
if i not in added_indices:
|
||||
hidden_tabs.append(tab_id)
|
||||
|
||||
return visible_tabs, hidden_tabs
|
||||
def _get_tab_content(self, tab_id):
|
||||
if tab_id not in self._state.tabs:
|
||||
return None
|
||||
tab_config = self._state.tabs[tab_id]
|
||||
if tab_config["component_type"] is None:
|
||||
return None
|
||||
return InstancesHelper.dynamic_get(self._session, tab_config["component_type"], tab_config["component_id"])
|
||||
|
||||
@staticmethod
|
||||
def _get_tab_count():
|
||||
@@ -182,10 +121,16 @@ class TabsManager(MultipleInstance):
|
||||
return res
|
||||
|
||||
def on_new_tab(self, label: str, component: Any, auto_increment=False):
|
||||
logger.debug(f"on_new_tab {label=}, {component=}, {auto_increment=}")
|
||||
if auto_increment:
|
||||
label = f"{label}_{self._get_tab_count()}"
|
||||
self.add_tab(label, component)
|
||||
return self
|
||||
tab_id = self.add_tab(label, component)
|
||||
component = component or VisNetwork(self._session, nodes=vis_nodes, edges=vis_edges)
|
||||
return (
|
||||
self._mk_tabs_controller(),
|
||||
self._wrap_tab_content(self._mk_tab_content(tab_id, component)),
|
||||
self._mk_tabs_header(True),
|
||||
)
|
||||
|
||||
def add_tab(self, label: str, component: Any, activate: bool = True) -> str:
|
||||
"""
|
||||
@@ -199,13 +144,14 @@ class TabsManager(MultipleInstance):
|
||||
Returns:
|
||||
tab_id: The UUID of the tab (new or existing)
|
||||
"""
|
||||
logger.debug(f"add_tab {label=}, component={component}, activate={activate}")
|
||||
# copy the state to avoid multiple database call
|
||||
state = self._state.copy()
|
||||
|
||||
# Extract component ID if the component has a get_id() method
|
||||
component_type, component_id = None, None
|
||||
if isinstance(component, BaseInstance):
|
||||
component_type = type(component).__name__
|
||||
component_type = component.get_prefix() if isinstance(component, BaseInstance) else type(component).__name__
|
||||
component_id = component.get_id()
|
||||
|
||||
# Check if a tab with the same component_type, component_id AND label already exists
|
||||
@@ -249,11 +195,23 @@ class TabsManager(MultipleInstance):
|
||||
return tab_id
|
||||
|
||||
def show_tab(self, tab_id):
|
||||
logger.debug(f"show_tab {tab_id=}")
|
||||
if tab_id not in self._state.tabs:
|
||||
logger.debug(f" Tab not found.")
|
||||
return None
|
||||
|
||||
logger.debug(f" Tab label is: {self._state.tabs[tab_id]['label']}")
|
||||
self._state.active_tab = tab_id
|
||||
return self
|
||||
|
||||
if tab_id not in self._state._tabs_content:
|
||||
logger.debug(f" Content does not exist. Creating it.")
|
||||
content = self._get_tab_content(tab_id)
|
||||
tab_content = self._mk_tab_content(tab_id, content)
|
||||
self._state._tabs_content[tab_id] = tab_content
|
||||
return self._mk_tabs_controller(), self._wrap_tab_content(tab_content)
|
||||
else:
|
||||
logger.debug(f" Content already exists. Just switch.")
|
||||
return self._mk_tabs_controller()
|
||||
|
||||
def close_tab(self, tab_id: str):
|
||||
"""
|
||||
@@ -265,6 +223,7 @@ class TabsManager(MultipleInstance):
|
||||
Returns:
|
||||
Self for chaining
|
||||
"""
|
||||
logger.debug(f"close_tab {tab_id=}")
|
||||
if tab_id not in self._state.tabs:
|
||||
return self
|
||||
|
||||
@@ -309,7 +268,9 @@ class TabsManager(MultipleInstance):
|
||||
return mk.icon(tab_add24_regular,
|
||||
id=f"{self._id}-add-tab-btn",
|
||||
cls="mf-tab-btn",
|
||||
command=self.commands.add_tab(f"Untitled", None, True))
|
||||
command=self.commands.add_tab(f"Untitled",
|
||||
None,
|
||||
True))
|
||||
|
||||
def _mk_tab_button(self, tab_id: str, tab_data: dict, in_dropdown: bool = False):
|
||||
"""
|
||||
@@ -345,15 +306,7 @@ class TabsManager(MultipleInstance):
|
||||
data_manager_id=self._id
|
||||
)
|
||||
|
||||
def _mk_tabs_header(self):
|
||||
"""
|
||||
Create the tabs header containing visible tab buttons and dropdown.
|
||||
|
||||
Returns:
|
||||
Div element containing visible tabs and dropdown button
|
||||
"""
|
||||
visible_tabs, hidden_tabs = self._calculate_visible_tabs()
|
||||
|
||||
def _mk_tabs_header(self, oob=False):
|
||||
# Create visible tab buttons
|
||||
visible_tab_buttons = [
|
||||
self._mk_tab_button(tab_id, self._state.tabs[tab_id])
|
||||
@@ -364,14 +317,28 @@ class TabsManager(MultipleInstance):
|
||||
header_content = [*visible_tab_buttons]
|
||||
|
||||
return Div(
|
||||
Div(
|
||||
Div(*header_content, cls="mf-tabs-header", id=f"{self._id}-header"),
|
||||
self._mk_show_tabs_menu(),
|
||||
cls="mf-tabs-header-wrapper"
|
||||
),
|
||||
Div(*header_content, cls="mf-tabs-header", id=f"{self._id}-header"),
|
||||
self._mk_show_tabs_menu(),
|
||||
id=f"{self._id}-header-wrapper",
|
||||
cls="mf-tabs-header-wrapper",
|
||||
hx_swap_oob="true" if oob else None
|
||||
),
|
||||
|
||||
def _mk_tabs_controller(self):
|
||||
return Div(
|
||||
Div(id=f"{self._id}-content-controller", data_active_tab=f"{self._state.active_tab}"),
|
||||
Script(f'updateTabs("{self._id}-content-controller");'),
|
||||
)
|
||||
|
||||
def _mk_tab_content(self):
|
||||
def _mk_tab_content(self, tab_id: str, content):
|
||||
is_active = tab_id == self._state.active_tab
|
||||
return Div(
|
||||
content if content else Div("No Content", cls="mf-empty-content"),
|
||||
cls=f"mf-tab-content {'hidden' if not is_active else ''}", # ← ici
|
||||
id=f"{self._id}-{tab_id}-content",
|
||||
)
|
||||
|
||||
def _mk_tab_content_wrapper(self):
|
||||
"""
|
||||
Create the active tab content area.
|
||||
|
||||
@@ -385,9 +352,9 @@ class TabsManager(MultipleInstance):
|
||||
content = component
|
||||
|
||||
return Div(
|
||||
content if content else Div("No Content", cls="mf-empty-content"),
|
||||
cls="mf-tab-content",
|
||||
id=f"{self._id}-content"
|
||||
self._mk_tab_content(self._state.active_tab, content),
|
||||
cls="mf-tab-content-wrapper",
|
||||
id=f"{self._id}-content-wrapper",
|
||||
)
|
||||
|
||||
def _mk_show_tabs_menu(self):
|
||||
@@ -405,6 +372,15 @@ class TabsManager(MultipleInstance):
|
||||
cls="dropdown dropdown-end"
|
||||
)
|
||||
|
||||
def _wrap_tab_content(self, tab_content):
|
||||
return Div(
|
||||
tab_content,
|
||||
hx_swap_oob=f"beforeend:#{self._id}-content-wrapper",
|
||||
)
|
||||
|
||||
def update_boundaries(self):
|
||||
return Script(f"updateBoundaries('{self._id}');")
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Render the complete TabsManager component.
|
||||
@@ -413,10 +389,11 @@ class TabsManager(MultipleInstance):
|
||||
Div element containing tabs header, content area, and resize script
|
||||
"""
|
||||
return Div(
|
||||
self._mk_tabs_controller(),
|
||||
self._mk_tabs_header(),
|
||||
self._mk_tab_content(),
|
||||
self._mk_tab_content_wrapper(),
|
||||
cls="mf-tabs-manager",
|
||||
id=self._id
|
||||
id=self._id,
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
|
||||
71
src/myfasthtml/controls/VisNetwork.py
Normal file
71
src/myfasthtml/controls/VisNetwork.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import logging
|
||||
|
||||
from fasthtml.components import Script, Div
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
|
||||
logger = logging.getLogger("VisNetwork")
|
||||
|
||||
class VisNetwork(MultipleInstance):
|
||||
def __init__(self, session, _id=None, nodes=None, edges=None, options=None):
|
||||
super().__init__(session, Ids.VisNetwork, _id=_id)
|
||||
logger.debug(f"VisNetwork created with id: {self._id}")
|
||||
|
||||
# Default values
|
||||
self.nodes = nodes or []
|
||||
self.edges = edges or []
|
||||
self.options = options or {
|
||||
"autoResize": True,
|
||||
"interaction": {
|
||||
"dragNodes": True,
|
||||
"zoomView": True,
|
||||
"dragView": True,
|
||||
},
|
||||
"physics": {"enabled": True}
|
||||
}
|
||||
|
||||
def render(self):
|
||||
# Prepare JS arrays (no JSON library needed)
|
||||
js_nodes = ",\n ".join(
|
||||
f'{{ id: {n["id"]}, label: "{n.get("label", "")}" }}'
|
||||
for n in self.nodes
|
||||
)
|
||||
js_edges = ",\n ".join(
|
||||
f'{{ from: {e["from"]}, to: {e["to"]} }}'
|
||||
for e in self.edges
|
||||
)
|
||||
|
||||
# Convert Python options to JS
|
||||
import json
|
||||
js_options = json.dumps(self.options, indent=2)
|
||||
|
||||
return (
|
||||
Div(
|
||||
id=self._id,
|
||||
cls="mf-vis",
|
||||
#style="width:100%; height:100%;", # Let parent control the layout
|
||||
),
|
||||
|
||||
# The script initializing Vis.js
|
||||
Script(f"""
|
||||
(function() {{
|
||||
const container = document.getElementById("{self._id}");
|
||||
const nodes = new vis.DataSet([
|
||||
{js_nodes}
|
||||
]);
|
||||
const edges = new vis.DataSet([
|
||||
{js_edges}
|
||||
]);
|
||||
const data = {{
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
}};
|
||||
const options = {js_options};
|
||||
const network = new vis.Network(container, data, options);
|
||||
}})();
|
||||
""")
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
return self.render()
|
||||
@@ -13,6 +13,7 @@ class Ids:
|
||||
Layout = "mf-layout"
|
||||
TabsManager = "mf-tabs-manager"
|
||||
UserProfile = "mf-user-profile"
|
||||
VisNetwork = "mf-vis-network"
|
||||
|
||||
|
||||
class mk:
|
||||
|
||||
@@ -159,7 +159,7 @@ class Command(BaseCommand):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret[1:]:
|
||||
if hasattr(r, 'attrs'):
|
||||
r.attrs["hx-swap-oob"] = "true"
|
||||
r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
|
||||
|
||||
if not ret_from_bindings:
|
||||
return ret
|
||||
|
||||
11
src/myfasthtml/core/instances_helper.py
Normal file
11
src/myfasthtml/core/instances_helper.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
|
||||
|
||||
class InstancesHelper:
|
||||
@staticmethod
|
||||
def dynamic_get(session, component_type: str, instance_id: str):
|
||||
if component_type == Ids.VisNetwork:
|
||||
return VisNetwork(session, _id=instance_id)
|
||||
|
||||
return None
|
||||
@@ -31,6 +31,7 @@ def get_asset_content(filename):
|
||||
|
||||
|
||||
def create_app(daisyui: Optional[bool] = True,
|
||||
vis: Optional[bool] = True,
|
||||
protect_routes: Optional[bool] = True,
|
||||
mount_auth_app: Optional[bool] = False,
|
||||
base_url: Optional[str] = None,
|
||||
@@ -63,6 +64,11 @@ def create_app(daisyui: Optional[bool] = True,
|
||||
Script(src="/myfasthtml/tailwindcss-browser@4.js"),
|
||||
]
|
||||
|
||||
if vis:
|
||||
hdrs += [
|
||||
Script(src="/myfasthtml/vis-network.min.js"),
|
||||
]
|
||||
|
||||
beforeware = create_auth_beforeware() if protect_routes else None
|
||||
app, rt = fasthtml.fastapp.fast_app(before=beforeware, hdrs=tuple(hdrs), **kwargs)
|
||||
|
||||
@@ -96,7 +102,7 @@ def create_app(daisyui: Optional[bool] = True,
|
||||
if mount_auth_app:
|
||||
# Setup authentication routes
|
||||
setup_auth_routes(app, rt, base_url=base_url)
|
||||
|
||||
|
||||
# create the AuthProxy instance
|
||||
AuthProxy(base_url) # using the auto register mechanism to expose it
|
||||
|
||||
|
||||
Reference in New Issue
Block a user