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<Node>,
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, meta: impl Into<Node>) -> Self {
self.meta = Some(meta.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 suggestion(&self, r: &Render) -> Json {
let plain = |t: &Text| r.i18n.text(r.locale, t);
let href = self
.link
.as_ref()
.map(|(screen, params)| {
crate::screen::Routes::href(r, screen, params)
.unwrap_or_else(|| format!("/_zeyn/route/{screen}"))
});
let image = self
.image
.as_deref()
.filter(|src| safe_image(src) && super::embed::policy(r).allows_image(src));
json!({
"text": plain(&self.title),
"subtitle": self.subtitle.as_ref().map(plain),
"image": image,
"href": href,
})
}
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 mut meta = String::new();
if let Some(node) = &self.meta {
meta.push_str(r#"<span class="z-list-meta">"#);
node.render(&mut meta, r);
meta.push_str("</span>");
}
let body = format!(
r#"{image}<span class="z-list-body"><span class="z-list-title">{}</span>{}</span>{meta}"#,
r.text(&self.title),
line(&self.subtitle, "z-list-subtitle"),
);
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
.as_ref()
.map(Node::schema),
"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<_>>() })
}
}