1. menu.py
#1. Imports
from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh
import yaml
#2. Defining Menu class
class Menu():
# Constructor
def __init__(self, id, description, action=None, tooltip=None, icon=None):
self.id = id
self.description = description
self.action = action
self.tooltip=tooltip #if tooltip is not None else description
self.icon=icon
self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
self.childrenIds=[]
# Alternative constructor from a dioctionary using @classmethod
@classmethod
def fromDict(cls, d:dict):
return cls(d['id'], d['description'], d.get('action'), d.get('tooltip'),d.get('icon'))
# Setter for parentId
def setParendId(self):
self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
# Setter for icon
def setIcon(self,dictIcons:dict):
if self.icon is None:
if len(self.childrenIds)>0:
self.icon=dictIcons.get('defa')
else:
act=self.action[:4]
self.icon=dictIcons.get(act)
#3. Load Resources from conf folder
#3.1 Load icons from conf/menu_icons.yml as a dict. The icons are font-awesome
def getMenuIcons(menuIconsFilePath: str='static/conf/menus.yml')->dict:
with open(menuIconsFilePath) as f:
iconDict=yaml.safe_load(f)
return iconDict
#3.2 Load menus from conf/menus.yml as a list of dict
def getMenus(menuFilePath: str='static/conf/menus.yml')->dict:
with open(menuFilePath) as f:
mnuDict=yaml.safe_load(f)
mnuLst=[Menu.fromDict(m) for m in mnuDict['menus']]
return mnuLst
#4. Convert the list of menus into a dict whose key is the id
# and get the children menus and icons
def getMenuDict(menus:list[Menu], dictIcons:dict={}):
menuDict={}
for i, m in enumerate(menus):
m.childrenIds=[elem.id for elem in filter(lambda x:x.parentId==m.id,menus)] # Set childrenIds
m.setIcon(dictIcons)
menuDict[m.id] = m
return menuDict
#5. Defining the drop down menu
#5. Get the code of a sub menu and its nested (children) menus
def getSubmenuCodeFH(id:int, menuDict:dict[int, Menu]):
menu=menuDict.get(id)
paramDict={}
SpanContent=''
if len(menu.childrenIds)==0:
paramDict['hx_post']=f"/action/{menu.action}"
paramDict['hx_target']="#message"
paramDict['hx_swap']="innerHTML"
else:
SpanContent=fh.I(cls='triang fa fa-caret-right')
#paramDict['href']="#"
#paramDict['hx-on:click']="alert('hola'); const e=document.getElementById('child"+str(id)+"');const sty=e.style.display; e.style.display= (sty=='block') ? 'none' :'block'; e.style.position='absolute' "
#paramDict['hx-on:click']="const e=document.getElementById('child"+str(id)+"');const sty=e.style.display; e.style.display= (sty=='block') ? 'none' :'block'; e.style.position='absolute' "
paramDict['hx-on:click']="showHideMenuChildren("+str(id)+")"
tttClas='tooltiptext' if menu.id<10 else 'tooltiptextchild'
return fh.Li(
fh.A(
menu.description,
fh.Span(
SpanContent,
cls='expand'
),
fh.Span(
menu.tooltip,
cls=tttClas
) if menu.tooltip is not None else '',
**paramDict,
cls='tooltip'
),
fh.Ul(*[getSubmenuCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child', id='child'+str(id)),
cls='parent'
)
# Get the code of the main menu
def getGeneralMenuCodeFH(menuDict:dict[int, Menu]):
return fh.Body(
fh.Div(id='navbar', cls='navbar'),
fh.Ul(
*[getSubmenuCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ],
#id='menu'
),
fh.Div('qqqqq',id='message', cls='message'),
cls='menu'
)
#5. Get subtree
# Get the code of a sub menu and its nested (children) menus
def getSubTreeViewCodeFH(id:int, menuDict:dict[int, Menu]):
menu=menuDict.get(id)
tttClas='tooltiptexttree'
paramDict={'cls':'tooltip'}
tooltip=fh.Span(
menu.tooltip,
cls=tttClas
) if menu.tooltip is not None else ''
icon=[fh.I(cls=menu.icon),fh.Span(' ',cls='right-spacer')]
if len(menu.childrenIds)==0:
paramDict['hx_post']=f"/action/{menu.action}"
paramDict['hx_target']="#message"
paramDict['hx_swap']="innerHTML"
return fh.Li(*icon,menu.description,tooltip,**paramDict)
else:
return fh.Li(
fh.Details(
fh.Summary(*icon,menu.description,tooltip,**paramDict),
fh.Ul(*[getSubTreeViewCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child'),
)
)
# Get the code of the main menu
def getGeneralTreeViewCodeFH(menuDict:dict[int, Menu]):
mainParamDict={'id':'main'}
sidebarParamDict={'id':'mySidebar','cls':'sidebar'}
btOpenParamDict={'cls':'openbtn inline-block','hx-on:click':"openCloseNav()"}
aCloseParamDict={'cls':'closebtn','hx-on:click':"openCloseNav()"}
ulParamDict={'id':'firsttreeul', 'cls':'tree'}
divAjuntParamDict={'cls':'ajuntament inline-block'}
return fh.Body(
fh.Div(
fh.Button(fh.I(cls='fa-regular fa-circle-xmark'), **aCloseParamDict),
fh.Ul(
*[getSubTreeViewCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ],
**ulParamDict,
),
**sidebarParamDict,
),
fh.Div(
fh.Div(fh.Button('☰ Obri', **btOpenParamDict),fh.H1('Ajuntament de Tavernes', **divAjuntParamDict)),
fh.Div('Here goes the content of the main window!!!'),
**mainParamDict,
),
fh.Div('Ací van els missatges ....',id='message', cls='message')
)
dictIcons=getMenuIcons('static/conf/menu_icons.yml')
menus=getMenus('static/conf/menus.yml')
menuDict=getMenuDict(menus,dictIcons=dictIcons)
#6. Defining Headers (Styles, Scripts, etc)
myHeaders = [
fh.Meta(name='viewport', content='width=device-width, initial-scale=1'),
#fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'),
fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'),
fh.Link(rel='stylesheet', href='../static/css/menu.css'),
fh.Link(rel='stylesheet', href='../static/css/treeview.css'),
fh.Link(rel='stylesheet', href='../static/css/sidebar.css'),
fh.Script(src='../static/js/menu.js'),
fh.Script(src='../static/js/sidebar.js')
]
#7. Creating FastAPI App
app = fh.FastHTML(hdrs=myHeaders)
#8. Mounting the static folder for accessing the menu.css
app.mount("/static", StaticFiles(directory="static"), name="static")
fh.serve()
#9. Defining the routes
@app.get("/")
def geto1():
return getGeneralMenuCodeFH(menuDict=menuDict)
@app.get("/tree")
def geto2():
return getGeneralTreeViewCodeFH(menuDict=menuDict)
#10. Menu Actions executed in a
@app.post("/action/{actEdu}")
def postAction(actEdu:str):
return f"Action received: {actEdu}"
2. css
2.1 static/css/menu.css
body {font-family: Arial, Helvetica, sans-serif;}
.navbar {overflow: hidden;background-color: black;position:fixed;top: 0;height:46px; width: 100%; z-index:-1}
#menu {padding-left:10px; }
.menu {padding-left:10px; }
.parent {display: block;position: relative;float: left;line-height: 30px;background-color: black;border-right:#CCC 1px solid;}
.parent a{margin: 10px;color: #FFFFFF;text-decoration: none;}
.parent:hover > ul {display:block;position:absolute;}
/*.parent li:hover {background-color: #F0F0F0;}*/
.menu .parent li:hover {background-color: Gainsboro;}
/*
.child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);} FALLA
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:300px;} SI
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:auto;} FALLA
*/
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:16em;z-index:2}
.menu .child li {background-color: #f9f9f9;line-height: 30px;border-right:#CCC 1px solid; width:100%;}
.menu .child li a{color: #000000;}
.menu .child .child{padding-top: 0px;}
/*ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}*/
.menu ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}
/*ul ul ul{left: 100%;top: 0;margin-left:1px;}*/
.menu ul ul ul{left: 100%;top: 0;margin-left:1px;}
/*li:hover {background-color: red;}*/
.menu li:hover {background-color: red;}
.menu .expand{float:right;margin-right:5px;}
.menu .triang{margin-top:6px;}
.message {position: fixed; bottom:50px;color: green;background-color: white;border-right:#CCC 1px solid;}
/*.tooltip {}*/
.menu .tooltip .tooltiptext {
visibility: hidden; width: 150px; font-size:85%;
color: black; background-color:gold;
text-align: center; border-radius: 6px; padding: 5px 0;
position: absolute; z-index: 1; top: -5px; left: 105%;
}
.menu .tooltip:hover .tooltiptext {visibility: visible;}
.menu .tooltip .tooltiptextchild {
visibility: hidden; width: 150px; font-size:85%;
color: black; background-color:gold;
text-align: center; border-radius: 6px; padding: 5px 0;
position: absolute; z-index: 1; top: -5px; left: 105%;
}
.menu .tooltip:hover .tooltiptextchild {visibility: visible;}
2.2 static/css/treeview.css
.tree {
--spacing: 1.5rem;
--radius: 10px;
line-height:var(--spacing); /*EDU*/
/*cursor:default;*/
cursor: pointer;
}
.tree li {
display: block;
position: relative;
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
}
.tree ul {
margin-left: calc(var(--radius) - var(--spacing));
padding-left: 0;
}
#firsttreeul {
margin-left: 0px;
padding-left: 10px;
}
.tree ul li {
border-left: 2px solid #ddd;
}
.tree ul li:last-child {
border-color: transparent;
}
.tree ul li::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / -2);
left: -2px;
width: calc(var(--spacing) + 2px);
height: calc(var(--spacing) + 1px);
border: solid #ddd;
border-width: 0 0 2px 2px;
}
.tree summary {
display: block;
/*cursor: pointer;*/
cursor: default;
}
.tree summary::marker,
.tree summary::-webkit-details-marker {
display: none;
}
.tree summary:focus {
outline: none;
}
.tree summary:focus-visible {
outline: 1px dotted #000;
}
.tree li::after,
.tree summary::before {
content: '';
display: block;
position: absolute;
top: calc(var(--spacing) / 2 - var(--radius));
left: calc(var(--spacing) - var(--radius) - 1px);
width: calc(2 * var(--radius));
height: calc(2 * var(--radius));
border-radius: 50%;
background: #ddd;
}
.tree summary::before {
z-index: 1;
background: #696 url('../svg/expand-collapse.svg') 0 0;
}
.tree details[open] > summary::before {
background-position: calc(-2 * var(--radius)) 0;
}
.right-space {
padding-right: 1em;
}
.tree .tooltip .tooltiptexttree {
/*visibility: hidden;*/
display:none;
width: 150px; font-size:85%;
color: black; background-color:gold;
text-align: center; border-radius: 6px; padding: 5px 0;
/*position: absolute;*/
/*top: -5px; left: 105%;*/
margin-left: 1em;
padding: 0px 0.5em 0px 0.5em;
width:fit-content;
z-index: 1;
}
.tree .tooltip:hover {color: green; font-weight: bold;}
/*.tree .tooltip:hover .tooltiptexttree {visibility: visible;} */
.tree .tooltip:hover .tooltiptexttree {display: inline-block;}
2.3 static/css/sidebar.css
.sidebar {
height: 100%;
width: 0;
position: fixed;
z-index: 1;
top: 0;
left: 0;
background-color: #f0f0f0;
overflow-x: hidden;
transition: 0.5s;
padding-top: 15px;
}
.sidebar .closebtn {
position: absolute;
top: 0;
right: 25px;
background-color: #04AA6D;
color:white;
font-size: 25px;
margin-left: 50px;
padding-left: 5px;
padding-right: 5px;
border: none;
border-radius: 8px
}
.sidebar .closebtn:hover {
background-color: #026e47;
font-weight: bold;
}
.openbtn {
font-size: 20px;
cursor: pointer;
background-color: #04AA6D;
color: white;
padding: 10px 15px;
border: none;
border-radius: 12px
}
.openbtn:hover {
background-color:#026e47;
}
#main {
transition: margin-left .5s;
padding-left: 16px;
}
.inline-block {
display: inline-block;
}
.ajuntament {
padding-left: 1em;
}
/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidebar {padding-top: 15px;}
.sidebar a {font-size: 18px;}
}
3. js
3.1 static/js/menu.js
showHideMenuChildren = function(id) {
let childId='child'+id.toString()
let e=document.getElementById(childId);
let sty=e.style.display;
// Hide the children of other menus
let other = document.querySelectorAll("[id^='child']");
for (let i = 0; i < other.length; i++) {
//if (!other[i].id.startsWith(childId) && !childId.startsWith(other[i].id)) {
//if (!other[i].id.startsWith(childId) ) {
if( childId.startsWith(other[i].id)) {
//alert(other[i].id + '-' + other[i].style.display + '-'+other[i].style.position)
other[i].style.display= (sty=='block') ? null : 'block';
other[i].style.position=(sty=='block') ? null : 'absolute'
} else {
other[i].style.display = null;
other[i].style.position = null;
}
console.log(other[i].id + '-' + other[i].style.display + '-'+other[i].style.position)
//}
}
console.log("-----------------------------------------------------------");
//Change the state of the elements
//e.style.display= (sty=='block') ? null : 'block';
//e.style.position=(sty=='block') ? null : 'absolute'
}
3.2 static/js/sidebar.js
{
let isOpen = false ;
const panelWidth = "400px";
function openCloseNav() {
let myWidth = isOpen ? "0" : panelWidth;
document.getElementById("mySidebar").style.width = myWidth;
document.getElementById("main").style.marginLeft = myWidth;
isOpen=!isOpen
}
}
4. datos de configuracion yml
4.1 static/conf/menus.yml
menus:
- { id: 1 , description: 'Secretaria' , tooltip: 'Secretaria guai!' , icon: 'fa-solid fa-skull-crossbones'}
- { id: 11 , description: "Llibre d'actes de plenari bonico" , action: null }
- { id: 111 , description: "1. Extreure els documents" , action: 'acti-1-extr-docs' }
- { id: 112 , description: "2. Signar documents Alcaldia" , action: 'acti-1-sign-alc' }
- { id: 113 , description: "3. Signar documents Secretaria", action: 'acti-1-sign-secr' }
- { id: 2 , description: 'Personal' }
- { id: 21 , description: 'Control Presència' , action: 'acti-2-contr-pres' }
- { id: 3 , description: 'Gestió Tributària' }
- { id: 31 , description: 'Menu 31' , action: 'rept-3-1' }
- { id: 32 , description: 'Menu 32' , action: 'view-3-2' }
- { id: 321 , description: 'Menu 321' , action: 'acti-3-21' , icon: 'fa-solid fa-skull-crossbones'}
- { id: 322 , description: 'Menu 322' }
- { id: 323 , description: 'Menu 323' }
- { id: 324 , description: 'Menu 324' , action: 'acti-3-24'}
- { id: 3221 , description: 'Menu 3221' , action: 'acti-3-221'}
- { id: 3222 , description: 'Menu 3222' , action: 'acti-3-222'}
- { id: 3223 , description: 'Menu 3223' , action: 'acti-3-223'}
- { id: 3224 , description: 'Menu 3224' , action: 'acti-3-224'}
- { id: 3225 , description: 'Menu 3225' , action: 'acti-3-225'}
- { id: 3226 , description: 'Menu 3221' , action: 'acti-3-221'}
- { id: 3227 , description: 'Menu 3222' , action: 'acti-3-222'}
- { id: 3228 , description: 'Menu 3223' , action: 'acti-3-223'}
- { id: 3229 , description: 'Menu 3224' , action: 'acti-3-224'}
- { id: 3231 , description: 'Menu 3221' , action: 'acti-3-221'}
- { id: 3232 , description: 'Menu 3222' , action: 'acti-3-222'}
- { id: 3233 , description: 'Menu 3223' , action: 'acti-3-223'}
- { id: 3234 , description: 'Menu 3224' , action: 'acti-3-224'}
- { id: 3235 , description: 'Menu 3225' , action: 'acti-3-225'}
- { id: 3236 , description: 'Menu 3221' , action: 'acti-3-221'}
- { id: 3237 , description: 'Menu 3222' , action: 'acti-3-222'}
- { id: 3238 , description: 'Menu 3223' , action: 'acti-3-223'}
- { id: 3239 , description: 'Menu 3224' , action: 'acti-3-224'}
- { id: 4 , description: 'Intervenció' , action: 'acti-4-intervencio'}
4.2 static/conf/menu_icons.yml
defa: 'fa-solid fa-folder'
view: 'fa-solid fa-table-cells'
rept: 'fa-solid fa-rectangle-list'
acti: 'fa-solid fa-gears'
Y sale: