use super::layout::into_node;
use super::widgets::screen_href;
use crate::i18n::Text;
use crate::render::{Render, escape};
use crate::screen::{Component, Node, ScreenDef};
use serde_json::{Value as Json, json};
into_node!(Chips);
pub const MAX_CHIPS: usize = 50;
type FilterFn = Box<dyn Fn(&[&str]) -> Json + Send + Sync>;
enum Kind {
Links(Vec<(Text, &'static str, Json)>),
Filter {
screen: &'static str,
params: FilterFn,
options: Vec<(String, Text)>,
current: Vec<String>,
many: bool,
},
}
pub struct Chips {
label: Text,
kind: Kind,
}
impl Chips {
pub fn links(label: impl Into<Text>) -> Self {
Chips {
label: label.into(),
kind: Kind::Links(Vec::new()),
}
}
pub fn link<S: ScreenDef>(
mut self,
text: impl Into<Text>,
_screen: S,
params: S::Params,
) -> Self {
if let Kind::Links(links) = &mut self.kind
&& links.len() < MAX_CHIPS
{
links.push((
text.into(),
S::META.name,
serde_json::to_value(params).unwrap_or(Json::Null),
));
}
self
}
pub fn filter<S: ScreenDef>(
label: impl Into<Text>,
_screen: S,
params: impl Fn(&[&str]) -> S::Params + Send + Sync + 'static,
) -> Self {
Chips {
label: label.into(),
kind: Kind::Filter {
screen: S::META.name,
params: Box::new(move |values| {
serde_json::to_value(params(values)).unwrap_or(Json::Null)
}),
options: Vec::new(),
current: Vec::new(),
many: false,
},
}
}
pub fn option(mut self, value: impl Into<String>, text: impl Into<Text>) -> Self {
if let Kind::Filter { options, .. } = &mut self.kind
&& options.len() < MAX_CHIPS
{
options.push((value.into(), text.into()));
}
self
}
pub fn current<V: Into<String>>(mut self, values: impl IntoIterator<Item = V>) -> Self {
if let Kind::Filter { current, .. } = &mut self.kind {
*current = values
.into_iter()
.map(Into::into)
.filter(|v: &String| !v.is_empty())
.take(MAX_CHIPS)
.collect();
}
self
}
pub fn multiple(mut self) -> Self {
if let Kind::Filter { many, .. } = &mut self.kind {
*many = true;
}
self
}
}
impl Component for Chips {
fn render(&self, out: &mut String, r: &Render) {
let label = r.text(&self.label);
match &self.kind {
Kind::Links(links) => {
out.push_str(&format!(
r#"<nav class="z-chips" aria-label="{label}"><ul class="z-chips-list">"#
));
for (text, screen, params) in links {
out.push_str(&format!(
r#"<li><a class="z-chip" href="{}" data-zeyn-base-href>{}</a></li>"#,
screen_href(r, screen, params),
r.text(text)
));
}
out.push_str("</ul></nav>");
}
Kind::Filter {
screen,
params,
options,
current,
many,
} => {
let pressed: Vec<&str> = options
.iter()
.map(|(v, _)| v.as_str())
.filter(|v| current.iter().any(|c| c == v))
.collect();
out.push_str(&format!(
r#"<div class="z-chips z-chips-filter" role="group" aria-label="{label}"><ul class="z-chips-list">"#
));
for (value, text) in options {
let on = pressed.contains(&value.as_str());
let next: Vec<&str> = if *many {
options
.iter()
.map(|(v, _)| v.as_str())
.filter(|v| if v == value { !on } else { pressed.contains(v) })
.collect()
} else if on {
Vec::new()
} else {
vec![value.as_str()]
};
out.push_str(&format!(
r#"<li><a class="z-chip" role="button" aria-pressed="{on}" href="{}" data-zeyn-chip data-value="{}" data-zeyn-base-href><span class="z-chip-check" aria-hidden="true"></span>{}</a></li>"#,
screen_href(r, screen, ¶ms(&next)),
escape(value),
r.text(text)
));
}
out.push_str("</ul></div>");
}
}
}
fn schema(&self) -> Json {
match &self.kind {
Kind::Links(links) => json!({
"type": "chips",
"label": self.label,
"links": links
.iter()
.map(|(t, s, p)| json!({ "text": t, "screen": s, "params": p }))
.collect::<Vec<_>>(),
}),
Kind::Filter {
screen,
options,
current,
many,
..
} => json!({
"type": "filter_chips",
"label": self.label,
"screen": screen,
"options": options
.iter()
.map(|(v, t)| json!({ "value": v, "text": t }))
.collect::<Vec<_>>(),
"current": current,
"multiple": many,
}),
}
}
}