use super::catalog::safe_image;
use super::layout::into_node;
use super::widgets::screen_href;
use crate::i18n::Text;
use crate::model::Model;
use crate::render::{Render, escape};
use crate::screen::{Component, Node, ScreenDef};
use serde_json::{Value as Json, json};
into_node!(List);
pub struct ListItem {
title: Text,
subtitle: Option<Text>,
meta: Option<Text>,
image: Option<String>,
alt: Text,
link: Option<(&'static str, Json)>,
preview: super::preview::PreviewTarget,
}
impl ListItem {
pub fn new(title: impl Into<Text>) -> Self {
ListItem {
title: title.into(),
subtitle: None,
meta: None,
image: None,
alt: Text::from(""),
link: None,
preview: None,
}
}
pub fn subtitle(mut self, text: impl Into<Text>) -> Self {
self.subtitle = Some(text.into());
self
}
pub fn meta(mut self, text: impl Into<Text>) -> Self {
self.meta = Some(text.into());
self
}
pub fn image(mut self, src: impl Into<String>) -> Self {
self.image = Some(src.into());
self
}
pub fn alt(mut self, alt: impl Into<Text>) -> Self {
self.alt = alt.into();
self
}
pub fn link<S: ScreenDef>(mut self, _screen: S, params: S::Params) -> Self {
self.link = Some((
S::META.name,
serde_json::to_value(params).unwrap_or(Json::Null),
));
self
}
pub fn preview<S: ScreenDef>(mut self, _screen: S, params: S::Params) -> Self {
self.preview = Some((
S::META.name,
serde_json::to_value(params).unwrap_or(Json::Null),
));
self
}
pub(crate) fn html(&self, out: &mut String, r: &Render) {
let image = match &self.image {
Some(src) if safe_image(src) => format!(
r#"<img class="z-list-img" src="{}" alt="{}" width="48" height="48" loading="lazy" decoding="async">"#,
escape(src),
r.text(&self.alt)
),
Some(_) => r#"<span class="z-list-img"></span>"#.to_string(),
None => String::new(),
};
let line = |t: &Option<Text>, class: &str| {
t.as_ref()
.map(|t| format!(r#"<span class="{class}">{}</span>"#, r.text(t)))
.unwrap_or_default()
};
let body = format!(
r#"{image}<span class="z-list-body"><span class="z-list-title">{}</span>{}</span>{}"#,
r.text(&self.title),
line(&self.subtitle, "z-list-subtitle"),
line(&self.meta, "z-list-meta")
);
match &self.link {
Some((screen, params)) => out.push_str(&format!(
r#"<li class="z-list-item"><a class="z-list-row" href="{}" data-zeyn-base-href{}>{body}</a></li>"#,
screen_href(r, screen, params),
super::preview::preview_attr(&self.preview)
)),
None => out.push_str(&format!(r#"<li class="z-list-item"><div class="z-list-row">{body}</div></li>"#)),
}
}
fn schema(&self) -> Json {
json!({
"title": self.title,
"subtitle": self.subtitle,
"meta": self.meta,
"image": self.image,
"link": self.link.as_ref().map(|(s, p)| json!({ "screen": s, "params": p })),
})
}
}
#[derive(Default)]
pub struct List {
title: Option<Text>,
items: Vec<ListItem>,
}
impl List {
pub fn new() -> Self {
Self::default()
}
pub fn of<A, E>(source: A) -> super::CardGrid<E>
where
A: crate::action::Action<
Input = crate::action::DataQuery,
Output = crate::action::Result<crate::action::Page<E>>,
>,
E: Model,
{
super::CardGrid::of(source).into_list()
}
pub fn title(mut self, title: impl Into<Text>) -> Self {
self.title = Some(title.into());
self
}
pub fn item(mut self, item: ListItem) -> Self {
self.items.push(item);
self
}
}
impl Component for List {
fn render(&self, out: &mut String, r: &Render) {
let id = super::dom_id(r, "list");
out.push_str(r#"<section class="z-list-section">"#);
let labelled = match &self.title {
Some(t) => {
out.push_str(&format!(
r#"<h2 class="z-cards-title" id="{id}-title">{}</h2>"#,
r.text(t)
));
format!(r#" aria-labelledby="{id}-title""#)
}
None => String::new(),
};
out.push_str(&format!(r#"<ul class="z-list"{labelled}>"#));
for item in &self.items {
item.html(out, r);
}
out.push_str("</ul></section>");
}
fn schema(&self) -> Json {
json!({ "type": "list", "title": self.title, "items": self.items.iter().map(ListItem::schema).collect::<Vec<_>>() })
}
}