use std::collections::HashMap;
use topcoat::{
context::Cx,
runtime::{Event, Signal, signal},
view::*,
};
use super::{
Schema,
fields::{ChoiceField, CustomField, Field, TextField},
tree::Node,
};
use crate::{DeclarationErrorKind, topcoat_compat::async_page, values::FormValues};
pub trait Watched<F> {
#[doc(hidden)]
fn key(&self) -> &str;
#[doc(hidden)]
fn checkbox(&self) -> bool {
false
}
}
impl<F> Watched<F> for TextField<F> {
fn key(&self) -> &str {
self.name()
}
}
impl<F> Watched<F> for ChoiceField<F> {
fn key(&self) -> &str {
self.name()
}
}
impl<F> Watched<F> for CustomField<F> {
fn key(&self) -> &str {
self.name()
}
fn checkbox(&self) -> bool {
self.is_checkbox()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Condition {
watched: String,
checkbox: bool,
values: Vec<String>,
parses_when_hidden: bool,
}
impl Condition {
pub(crate) fn new<F>(
watched: &impl Watched<F>,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
watched: watched.key().to_string(),
checkbox: watched.checkbox(),
values: values.into_iter().map(Into::into).collect(),
parses_when_hidden: false,
}
}
pub(crate) fn watching(
key: impl Into<String>,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
watched: key.into(),
checkbox: false,
values: values.into_iter().map(Into::into).collect(),
parses_when_hidden: true,
}
}
pub(crate) fn holds(&self, values: &FormValues) -> bool {
let posted = match values.text(&self.watched) {
Some(value) => value.trim(),
None if self.checkbox => "false",
None => "",
};
self.values.iter().any(|value| value == posted)
}
pub(crate) fn guard<'a>(
&self,
cx: &'a Cx,
scope: &str,
values: &FormValues,
inner: BoxView<'a>,
) -> BoxView<'a> {
let condition = self.clone();
let initial = self.holds(values);
let scope = scope.to_string();
async_page(async move {
let shown = shown(cx, &scope, &condition, initial);
let enabled = shown.clone();
Ok(view! {
<fieldset
class="contents"
:disabled=$(!enabled.get())
:hidden=$(!shown.get())
>
(inner)
</fieldset>
})
})
}
}
pub(crate) fn disable<'a>(
cx: &'a Cx,
condition: &Condition,
scope: &str,
values: &FormValues,
inner: BoxView<'a>,
) -> BoxView<'a> {
let condition = condition.clone();
let initial = condition.holds(values);
let scope = scope.to_string();
async_page(async move {
let holds = shown(cx, &scope, &condition, initial);
Ok(view! {
<fieldset class="contents" :disabled=$(holds.get())>(inner)</fieldset>
})
})
}
fn shown(cx: &Cx, scope: &str, condition: &Condition, initial: bool) -> Signal<bool> {
let page = topcoat::context::try_context::<http::request::Parts>(cx)
.map(|parts| parts.uri.path().to_string())
.unwrap_or_default();
signal(&cx.keyed(signal_key(&page, scope, condition)), move || {
initial
})
}
fn signal_key(
page: &str,
scope: &str,
condition: &Condition,
) -> (&'static str, String, String, String, String) {
(
"tablo-condition",
page.to_string(),
scope.to_string(),
condition.watched.clone(),
condition.values.join("\u{1f}"),
)
}
pub(crate) fn watch<'a>(
cx: &'a Cx,
field: &Field,
posted: &str,
conditions: &[Condition],
scope: &str,
values: &FormValues,
control: BoxView<'a>,
) -> BoxView<'a> {
let scope = scope.to_string();
let conditions: Vec<(Condition, bool)> = conditions
.iter()
.map(|condition| (condition.clone(), condition.holds(values)))
.collect();
let checkbox = field.is_checkbox();
let key = posted.to_string();
async_page(async move {
let mut view = control;
for (condition, initial) in conditions {
let shown = shown(cx, &scope, &condition, initial);
if checkbox {
let on = condition.values.iter().any(|value| value == "true");
let off = condition.values.iter().any(|value| value == "false");
let name = key.clone();
view = view! {
<div
class="contents"
@change=$(|e: Event| if e.target.name == name {
shown.set(if e.target.checked { on } else { off })
})
>
(view)
</div>
}
.boxed();
continue;
}
let mut values = condition.values.into_iter();
let Some(first) = values.next() else {
continue;
};
let (set, name) = (shown.clone(), key.clone());
view = view! {
<div
class="contents"
@change=$(|e: Event| if e.target.name == name {
set.set(e.target.value.trim() == first)
})
>
(view)
</div>
}
.boxed();
for value in values {
let (set, name) = (shown.clone(), key.clone());
view = view! {
<div
class="contents"
@change=$(|e: Event| if e.target.name == name {
if e.target.value.trim() == value {
set.set(true)
}
})
>
(view)
</div>
}
.boxed();
}
}
Ok(view)
})
}
impl Node {
fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
match self {
Node::Field(index) => fields[*index].condition(),
Node::Section(s) => s.condition.as_ref(),
Node::Group(g) => g.condition.as_ref(),
Node::Grid(g) => g.condition.as_ref(),
Node::Tabs(t) => t.condition.as_ref(),
Node::Tab(t) => t.condition.as_ref(),
Node::Wizard(w) => w.condition.as_ref(),
Node::Step(s) => s.condition.as_ref(),
Node::Embedded(_) => None,
}
}
}
#[derive(Default)]
struct Scan<'s> {
fields: Vec<(usize, Vec<&'s Condition>)>,
conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
disabling: Vec<(usize, &'s Condition)>,
}
impl<'s> Scan<'s> {
fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
let mut scan = Self::default();
scan.walk(nodes, fields, &mut Vec::new());
for (index, chain) in &scan.fields {
if let Some(condition) = fields[*index].disabling() {
scan.disabling.push((*index, condition));
scan.conditions.push((condition, chain.clone()));
}
}
scan
}
fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
for node in nodes {
let own = node.condition(fields);
if let Some(condition) = own {
self.conditions.push((condition, chain.clone()));
chain.push(condition);
}
match node {
Node::Field(index) => self.fields.push((*index, chain.clone())),
Node::Embedded(embedded) => {
embedded.visit_fields(&mut |index| {
let own = fields[index].condition();
if let Some(condition) = own {
self.conditions.push((condition, chain.clone()));
chain.push(condition);
}
self.fields.push((index, chain.clone()));
if own.is_some() {
chain.pop();
}
});
}
node => self.walk(node.children().unwrap_or_default(), fields, chain),
}
if own.is_some() {
chain.pop();
}
}
}
fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
self.fields
.iter()
.find(|(index, _)| fields[*index].name() == key)
.map(|(_, chain)| chain.as_slice())
}
}
impl<F> Schema<F> {
pub(crate) fn condition_hidden(&self, values: &FormValues) -> Vec<String> {
Scan::of(&self.nodes, &self.fields)
.fields
.into_iter()
.filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
.map(|(index, _)| self.fields[index].name().to_string())
.collect()
}
pub(crate) fn condition_disabled(&self, values: &FormValues) -> Vec<String> {
let scan = Scan::of(&self.nodes, &self.fields);
let hidden: Vec<usize> = scan
.fields
.iter()
.filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
.map(|(index, _)| *index)
.collect();
scan.disabling
.into_iter()
.filter(|(index, condition)| !hidden.contains(index) && condition.holds(values))
.map(|(index, _)| self.fields[index].name().to_string())
.collect()
}
pub(crate) fn condition_watchers(&self) -> Vec<(String, Option<String>)> {
let scan = Scan::of(&self.nodes, &self.fields);
let mut watchers = Vec::new();
for (condition, _) in &scan.conditions {
let disabled = scan
.disabling
.iter()
.filter(|(_, c)| std::ptr::eq(*c, *condition))
.map(|(index, _)| *index);
let mut guards = scan
.fields
.iter()
.filter(|(_, chain)| chain.iter().any(|c| std::ptr::eq(*c, *condition)))
.map(|(index, _)| *index)
.chain(disabled)
.map(|index| self.fields[index].name().to_string())
.peekable();
if guards.peek().is_none() {
watchers.push((condition.watched.clone(), None));
}
watchers.extend(guards.map(|key| (condition.watched.clone(), Some(key))));
}
watchers
}
pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
let driven = out.entry(condition.watched.clone()).or_default();
if !driven.contains(condition) {
driven.push(condition.clone());
}
}
out
}
pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
let scan = Scan::of(&self.nodes, &self.fields);
let mut errors = Vec::new();
for (condition, guarding) in &scan.conditions {
let watched = condition.watched.clone();
match scan.guarding(&self.fields, &condition.watched) {
None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
field: watched.clone(),
}),
Some(chain) if !chain.iter().all(|hiding| guarding.contains(hiding)) => {
errors.push(DeclarationErrorKind::HiddenWatchedField {
field: watched.clone(),
});
}
Some(_) => {}
}
if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched)
&& field.disabling().is_some()
{
errors.push(DeclarationErrorKind::DisabledWatchedField {
field: watched.clone(),
});
}
if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
for value in &condition.values {
if field.can_post(value) == Some(false) {
errors.push(DeclarationErrorKind::UnpostedConditionValue {
field: watched.clone(),
value: value.clone(),
});
}
}
}
}
for (index, chain) in &scan.fields {
let field = &self.fields[*index];
let hides_unparsed = chain.iter().any(|condition| !condition.parses_when_hidden);
if hides_unparsed && field.is_required() {
errors.push(DeclarationErrorKind::RequiredConditionalField {
field: field.name().to_string(),
});
}
}
errors
}
}
#[cfg(test)]
mod tests;