jueves, 31 de octubre de 2024

Fast HTML (IV). Creando un menú anticuado

1. Introducción

Para crear un menú desplegable usando CSSS y FastHTML, primeramente se crea un menú con html y luego se convierte a FastHtml.

Hay que tener en cuent que el código FastHTML requiere incluior los ficheros estáticos (CSS) tal como se vió en el post anterior.

Para ver como se hacía he tomado como referencia estas dos urls:

phppot de Vincy

w3schools 

La información de los menús a mostrar está en un fichero con formato YAML (static/conf/menu.yml) y será esta:

menus:
  - { id: 1    , description: 'Menu 1'    , action: null        }
  - { id: 11   , description: 'Menu 11'   , action: 'action11'  }
  - { id: 2    , description: 'Menu 2'    , action: 'action2'   }
  - { id: 3    , description: 'Menu 3'    , action: null        }
  - { id: 31   , description: 'Menu 31'   , action: 'action31'  }
  - { id: 32   , description: 'Menu 32'   , action: 'action32'  }
  - { id: 321  , description: 'Menu 321'  , action: 'action321' }
  - { id: 322  , description: 'Menu 322'  , action: null        }
  - { id: 323  , description: 'Menu 323'  , action: 'action323'}
  - { id: 324  , description: 'Menu 324'  , action: 'action324'}
  - { id: 3221 , description: 'Menu 3221' , action: 'action3221'}
  - { id: 3222 , description: 'Menu 3222' , action: 'action3222'}
  - { id: 3223 , description: 'Menu 3223' , action: 'action3223'}
  - { id: 3224 , description: 'Menu 3224' , action: 'action3224'}
  - { id: 3225 , description: 'Menu 3225' , action: 'action3225'}
  

En este fichero se han tomado estos criterios:

Los menús de la cabecera tienen un id con un solo dígito: Por ejemplo los ids: 1,2,3 

Los hijos del menú de cabecera tendrán 2 dígitos en el id, el dígito de la decena corresponde al id del menú padre. Por ejemplo el el id 11 es hijo del id 1, los ids 31 y 32 son hijos del menú con id 3

Análogamente si dividimos por 10 un id, el cociente nos dará el id del menú padre.


Al final nos queda este árbol de menus donde se muestra solamente los ids:

1
     11
2

     31
     32
           321
           322
                  3221
                  3222
                  3223
                  3224
                  3225
           323
           324  

El funcionamioento es el siguiente:

Definimos la clase Menu,  en el header defiimos las rutas de los css y librerías , defoinimos los menus después de leer el fichero yaml, creamos el menú y cuando seejecuta una cción en el post demomento modificamos el contenido del final de la página diciendo que acción se ha ejecutado

 2. Código python (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):
		self.id = id
		self.description = description
		self.action = action
		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['action'])
		
	# 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])
		

#3. 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
def getMenuDict(menus:list[Menu]):
	menuDict={}
	for i, m in enumerate(menus):
		m.childrenIds=[elem.id for elem in filter(lambda x:x.parentId==m.id,menus)] # Set childrenIds
		menuDict[m.id] = m
	return menuDict	
	
#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={}
	if len(menu.childrenIds)==0:
		paramDict['hx_post']=f"/action/{menu.action}"
		paramDict['hx_target']="#message"
		paramDict['hx_swap']="innerHTML"
		SpanContent=''
	else:
		SpanContent=fh.I(cls='triang fa fa-caret-right')
		href="#"
	tttClas='tooltiptext' if menu.id<10 else 'tooltiptextchild'	
	return fh.Li(
		fh.A(
				menu.description,
				fh.Span(
					SpanContent,
					cls='expand'
				),
				fh.Span(
					menu.description + '-tooltiptext',
					cls=tttClas
				),	
			**paramDict,
			cls='tooltip' 
		),
		fh.Ul(*[getSubmenuCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child'),
		cls='parent'
	)

	
# Get the codde 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')
    )



menus=getMenus('static/conf/menus.yml')
menuDict=getMenuDict(menus)

#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='../static/css/menu.css')
]

#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")

#9. Defining the routes
@app.get("/")
def geto1():
	return getGeneralMenuCodeFH(menuDict=menuDict) 

#10. Menu Actions executed in a
@app.post("/action/{actEdu}")
def postAction(actEdu:str):
	return f"Action received: {actEdu}"
	


3. Código CSS (static/css/menu.css)

Aquí hemos tenido en cuento los tool tip text ....


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

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

.child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);}
.child li {background-color: #f9f9f9;line-height: 30px;border-right:#CCC 1px solid; width:100%;}
.child li a{color: #000000;}
.child .child{padding-top: 0px;}

ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}
ul ul ul{left: 100%;top: 0;margin-left:1px;}

li:hover {background-color: red;}

.expand{float:right;margin-right:5px;}
.triang{margin-top:6px;}
.message {position: fixed; bottom:50px;color: green;background-color: white;border-right:#CCC 1px solid;}	

.tooltip {}
.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%;
}
.tooltip:hover .tooltiptext {visibility: visible;}


.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%;
}
.tooltip:hover .tooltiptextchild {visibility: visible;}	


4. Ejecución (uvicorn menu:app)

En el terminal ejecutmos uvicorn menu:app  



No hay comentarios :

Publicar un comentario