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:
No hay comentarios :
Publicar un comentario