1use std::collections::HashMap;
11
12use topcoat::{
13 context::Cx,
14 runtime::{Event, Signal, signal},
15 view::*,
16};
17
18use super::{
19 Schema,
20 fields::{ChoiceField, CustomField, Field, TextField},
21 tree::Node,
22};
23use crate::{DeclarationErrorKind, form::FormValues, topcoat_compat::async_page};
24
25pub trait Watched<F> {
32 #[doc(hidden)]
33 fn key(&self) -> &str;
34
35 #[doc(hidden)]
36 fn checkbox(&self) -> bool {
37 false
38 }
39}
40
41impl<F> Watched<F> for TextField<F> {
42 fn key(&self) -> &str {
43 self.name()
44 }
45}
46
47impl<F> Watched<F> for ChoiceField<F> {
48 fn key(&self) -> &str {
49 self.name()
50 }
51}
52
53impl<F> Watched<F> for CustomField<F> {
54 fn key(&self) -> &str {
55 self.name()
56 }
57
58 fn checkbox(&self) -> bool {
59 self.is_checkbox()
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct Condition {
66 watched: String,
67 checkbox: bool,
69 values: Vec<String>,
70 parses_when_hidden: bool,
73}
74
75impl Condition {
76 pub(crate) fn new<F>(
77 watched: &impl Watched<F>,
78 values: impl IntoIterator<Item = impl Into<String>>,
79 ) -> Self {
80 Self {
81 watched: watched.key().to_string(),
82 checkbox: watched.checkbox(),
83 values: values.into_iter().map(Into::into).collect(),
84 parses_when_hidden: false,
85 }
86 }
87
88 pub(crate) fn watching(
91 key: impl Into<String>,
92 values: impl IntoIterator<Item = impl Into<String>>,
93 ) -> Self {
94 Self {
95 watched: key.into(),
96 checkbox: false,
97 values: values.into_iter().map(Into::into).collect(),
98 parses_when_hidden: true,
99 }
100 }
101
102 fn holds(&self, values: &FormValues) -> bool {
105 let posted = match values.text(&self.watched) {
106 Some(value) => value.trim(),
107 None if self.checkbox => "false",
108 None => "",
109 };
110 self.values.iter().any(|value| value == posted)
111 }
112
113 pub(crate) fn guard<'a>(
116 &self,
117 cx: &'a Cx,
118 scope: &str,
119 values: &FormValues,
120 inner: BoxView<'a>,
121 ) -> BoxView<'a> {
122 let condition = self.clone();
123 let initial = self.holds(values);
124 let scope = scope.to_string();
125 async_page(async move {
126 let shown = shown(cx, &scope, &condition, initial);
127 let enabled = shown.clone();
128 Ok(view! {
129 cx =>
130 <fieldset
131 class="contents"
132 :disabled=$(!enabled.get())
133 :hidden=$(!shown.get())
134 >
135 (inner)
136 </fieldset>
137 })
138 })
139 }
140}
141
142fn shown(cx: &Cx, scope: &str, condition: &Condition, initial: bool) -> Signal<bool> {
149 let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
150 .map(|parts| parts.uri.path().to_string())
151 .unwrap_or_default();
152 let values = condition.values.join("\u{1f}");
153 signal(
154 &cx.keyed((
155 "tablo-condition",
156 page,
157 scope,
158 condition.watched.as_str(),
159 values,
160 )),
161 move || initial,
162 )
163}
164
165pub(crate) fn watch<'a>(
173 cx: &'a Cx,
174 field: &Field,
175 conditions: &[Condition],
176 scope: &str,
177 values: &FormValues,
178 control: BoxView<'a>,
179) -> BoxView<'a> {
180 let scope = scope.to_string();
181 let conditions: Vec<(Condition, bool)> = conditions
182 .iter()
183 .map(|condition| (condition.clone(), condition.holds(values)))
184 .collect();
185 let checkbox = field.is_checkbox();
186 let key = field.name().to_string();
187 async_page(async move {
188 let mut view = control;
189 for (condition, initial) in conditions {
190 let shown = shown(cx, &scope, &condition, initial);
191 if checkbox {
192 let on = condition.values.iter().any(|value| value == "true");
194 let off = condition.values.iter().any(|value| value == "false");
195 let name = key.clone();
196 view = view! {
197 cx =>
198 <div
199 class="contents"
200 @change=$(|e: Event| if e.target.name == name {
201 shown.set(if e.target.checked { on } else { off })
202 })
203 >
204 (view)
205 </div>
206 }
207 .boxed();
208 continue;
209 }
210 let mut values = condition.values.into_iter();
211 let Some(first) = values.next() else {
212 continue;
213 };
214 let (set, name) = (shown.clone(), key.clone());
215 view = view! {
216 cx =>
217 <div
218 class="contents"
219 @change=$(|e: Event| if e.target.name == name {
220 set.set(e.target.value.trim() == first)
221 })
222 >
223 (view)
224 </div>
225 }
226 .boxed();
227 for value in values {
228 let (set, name) = (shown.clone(), key.clone());
229 view = view! {
230 cx =>
231 <div
232 class="contents"
233 @change=$(|e: Event| if e.target.name == name {
234 if e.target.value.trim() == value {
235 set.set(true)
236 }
237 })
238 >
239 (view)
240 </div>
241 }
242 .boxed();
243 }
244 }
245 Ok(view)
246 })
247}
248
249impl Node {
250 fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
252 match self {
253 Node::Field(index) => fields[*index].condition(),
254 Node::Section(s) => s.condition.as_ref(),
255 Node::Group(g) => g.condition.as_ref(),
256 Node::Grid(g) => g.condition.as_ref(),
257 Node::Embedded(_) | Node::Unbound(_) => None,
258 }
259 }
260}
261
262#[derive(Default)]
264struct Scan<'s> {
265 fields: Vec<(usize, Vec<&'s Condition>)>,
267 conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
269}
270
271impl<'s> Scan<'s> {
272 fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
273 let mut scan = Self::default();
274 scan.walk(nodes, fields, &mut Vec::new());
275 scan
276 }
277
278 fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
279 for node in nodes {
280 let own = node.condition(fields);
281 if let Some(condition) = own {
282 self.conditions.push((condition, chain.clone()));
283 chain.push(condition);
284 }
285 match node {
286 Node::Field(index) => self.fields.push((*index, chain.clone())),
287 Node::Embedded(embedded) => {
288 embedded.visit_fields(&mut |index| {
291 let own = fields[index].condition();
292 if let Some(condition) = own {
293 self.conditions.push((condition, chain.clone()));
294 chain.push(condition);
295 }
296 self.fields.push((index, chain.clone()));
297 if own.is_some() {
298 chain.pop();
299 }
300 });
301 }
302 node => self.walk(node.children().unwrap_or_default(), fields, chain),
303 }
304 if own.is_some() {
305 chain.pop();
306 }
307 }
308 }
309
310 fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
312 self.fields
313 .iter()
314 .find(|(index, _)| fields[*index].name() == key)
315 .map(|(_, chain)| chain.as_slice())
316 }
317}
318
319impl<F> Schema<F> {
320 pub(crate) fn condition_hidden(&self, values: &FormValues) -> Vec<String> {
322 Scan::of(&self.nodes, &self.fields)
323 .fields
324 .into_iter()
325 .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
326 .map(|(index, _)| self.fields[index].name().to_string())
327 .collect()
328 }
329
330 pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
332 let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
333 for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
334 let driven = out.entry(condition.watched.clone()).or_default();
335 if !driven.contains(condition) {
336 driven.push(condition.clone());
337 }
338 }
339 out
340 }
341
342 pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
346 let scan = Scan::of(&self.nodes, &self.fields);
347 let mut errors = Vec::new();
348 for (condition, guarding) in &scan.conditions {
349 let watched = condition.watched.clone();
350 match scan.guarding(&self.fields, &condition.watched) {
351 None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
352 field: watched.clone(),
353 }),
354 Some(chain)
358 if !chain
359 .iter()
360 .all(|hiding| guarding.iter().any(|c| std::ptr::eq(*c, *hiding))) =>
361 {
362 errors.push(DeclarationErrorKind::HiddenWatchedField {
363 field: watched.clone(),
364 });
365 }
366 Some(_) => {}
367 }
368 if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
369 for value in &condition.values {
370 if field.can_post(value) == Some(false) {
371 errors.push(DeclarationErrorKind::UnpostedConditionValue {
372 field: watched.clone(),
373 value: value.clone(),
374 });
375 }
376 }
377 }
378 }
379 for (index, chain) in &scan.fields {
380 let field = &self.fields[*index];
381 let hides_unparsed = chain.iter().any(|condition| !condition.parses_when_hidden);
383 if hides_unparsed && field.is_required() {
384 errors.push(DeclarationErrorKind::RequiredConditionalField {
385 field: field.name().to_string(),
386 });
387 }
388 }
389 errors
390 }
391}
392
393#[cfg(test)]
394mod tests;