use super::{ActionSource, DataSource, dom_id, layout::into_node, render_child};
use crate::action::{Action, ActionMeta, Result};
use crate::i18n::Text;
use crate::render::{Render, escape};
use crate::screen::{Component, Node};
use serde::Serialize;
use serde_json::{Value as Json, json};
pub use zeyn_island::Remote;
into_node!(Island);
pub(crate) const MARKER: &str = "data-zeyn-island=\"";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IslandLoad {
Visible,
Interaction,
Idle,
}
pub struct Canvas;
impl Canvas {
#[allow(clippy::new_ret_no_self)]
pub fn new(name: &'static str, description: impl Into<Text>) -> Island {
Island::new(name).description(description)
}
}
pub struct Island {
name: &'static str,
props: String,
label: Text,
description: Option<Text>,
load: IslandLoad,
fallback: Vec<Node>,
remotes: Vec<(&'static str, Box<dyn DataSource>)>,
}
impl Island {
pub fn new(name: &'static str) -> Self {
Island {
name,
props: "null".into(),
label: Text::from(""),
description: None,
load: IslandLoad::Visible,
fallback: Vec::new(),
remotes: Vec::new(),
}
}
pub fn description(mut self, text: impl Into<Text>) -> Self {
self.description = Some(text.into());
self
}
pub fn props<P: Serialize>(mut self, props: &P) -> Self {
self.props = serde_json::to_string(props).unwrap_or_else(|_| "null".into());
self
}
pub fn label(mut self, label: impl Into<Text>) -> Self {
self.label = label.into();
self
}
pub fn fallback(mut self, node: impl Into<Node>) -> Self {
self.fallback.push(node.into());
self
}
pub fn on_interaction(mut self) -> Self {
self.load = IslandLoad::Interaction;
self
}
pub fn on_idle(mut self) -> Self {
self.load = IslandLoad::Idle;
self
}
pub fn action<R, A>(mut self, _action: A) -> Self
where
R: Remote,
A: Action<Input = R::Input, Output = Result<R::Output>>,
{
self.remotes.retain(|(k, _)| *k != R::NAME);
self.remotes.push((R::NAME, Box::new(ActionSource::<A>::new())));
self
}
pub fn actions(&self) -> Vec<(&'static str, ActionMeta)> {
self.remotes.iter().map(|(k, s)| (*k, s.action())).collect()
}
}
impl Component for Island {
fn render(&self, out: &mut String, r: &Render) {
let described = self.description.as_ref().map(|_| dom_id(r, "island-description"));
out.push_str(&format!(
r#"<div class="z-island" {MARKER}{}" data-zeyn-load="{}" data-zeyn-props="{}" role="region" aria-label="{}"{} data-testid="{}.island.{}">"#,
escape(self.name),
match self.load {
IslandLoad::Visible => "visible",
IslandLoad::Interaction => "interaction",
IslandLoad::Idle => "idle",
},
escape(&self.props),
r.text(&self.label),
described.as_ref().map(|id| format!(r#" aria-describedby="{id}""#)).unwrap_or_default(),
r.scope,
escape(self.name),
));
if let (Some(id), Some(text)) = (&described, &self.description) {
out.push_str(&format!(r#"<p class="z-island-description" id="{id}">{}</p>"#, r.text(text)));
}
out.push_str(r#"<div class="z-island-fallback" data-zeyn-island-fallback>"#);
for (i, node) in self.fallback.iter().enumerate() {
render_child(out, r, i, node);
}
out.push_str("</div></div>");
}
fn schema(&self) -> Json {
let props: Json = serde_json::from_str(&self.props).unwrap_or(Json::Null);
json!({
"type": "island",
"name": self.name,
"label": self.label,
"description": self.description,
"load": self.load,
"props": props,
"actions": self.remotes.iter().map(|(k, s)| json!({ "name": k, "action": s.action() })).collect::<Vec<_>>(),
"fallback": self.fallback.iter().map(Node::schema).collect::<Vec<_>>(),
})
}
fn children(&self) -> &[Node] {
&self.fallback
}
fn source(&self, key: &str) -> Option<&dyn DataSource> {
self.remotes.iter().find(|(k, _)| *k == key).map(|(_, s)| s.as_ref())
}
}