viernes, 1 de noviembre de 2024

Fast HTML (V). Creando un tree-view y mejora del menú desplegable anterior

 1. Introducción

Vamos a adaptar el código dado en iamkate a ver como sale

El código python con FasHtml (menu.py) es


#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
				),	
			**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 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
	)
	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]):
	return fh.Body(
    	fh.Ul(
			*[getSubTreeViewCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ], 
			cls='tree'
		),
		fh.Div('qqqqq',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.Script(src='../static/js/menu.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}"


El código css (static/css/treeview.css) para el tree view es:

.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;
  }
  
  .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;}	



El código css para los menús drop down o de persiana (static/css/menu.css) es:

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;}	


El código js (static/js/menu.js) para manejar la visibilidad de los submenús:

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("-----------------------------------------------------------");


La configuración de los menus (static/conf/menus.yml) es:

menus:
  - { id: 1    , description: 'Secretaria'                    , tooltip: 'Secretaria' , 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'}


Y la configuración de los tipos de imágenes asociadas a las acciones de los menús (static/conf/menu_icons.yml) es:

defa: 'fa-solid fa-folder'
view: 'fa-solid fa-table-cells'
rept: 'fa-solid fa-rectangle-list'
acti: 'fa-solid fa-gears'


Se puede ejecuta con el run de python o con "uvicorn menu:app" en el terminal. Ojo para ejecutar con el uvicorn hay que comentar la sentencia "fh.serve()"

Y tenemos 2 posibles pantallas, una para menús desplegables para la ruta "/" 



y la otra de tree view para la ruta "/tree"





No hay comentarios :

Publicar un comentario