1use std::collections::HashMap;
12
13use topcoat::{
14 context::Cx,
15 runtime::{Event, Signal, signal},
16 view::*,
17};
18
19use super::{
20 Schema,
21 fields::{ChoiceField, CustomField, Field, TextField},
22 tree::Node,
23};
24use crate::{DeclarationErrorKind, topcoat_compat::async_page, values::FormValues};
25
26pub trait Watched<F> {
33 #[doc(hidden)]
34 fn key(&self) -> &str;
35
36 #[doc(hidden)]
37 fn checkbox(&self) -> bool {
38 false
39 }
40}
41
42impl<F> Watched<F> for TextField<F> {
43 fn key(&self) -> &str {
44 self.name()
45 }
46}
47
48impl<F> Watched<F> for ChoiceField<F> {
49 fn key(&self) -> &str {
50 self.name()
51 }
52}
53
54impl<F> Watched<F> for CustomField<F> {
55 fn key(&self) -> &str {
56 self.name()
57 }
58
59 fn checkbox(&self) -> bool {
60 self.is_checkbox()
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
67pub(crate) struct Condition {
68 watched: String,
69 checkbox: bool,
71 values: Vec<String>,
72 parses_when_hidden: bool,
75}
76
77impl Condition {
78 pub(crate) fn new<F>(
79 watched: &impl Watched<F>,
80 values: impl IntoIterator<Item = impl Into<String>>,
81 ) -> Self {
82 Self {
83 watched: watched.key().to_string(),
84 checkbox: watched.checkbox(),
85 values: values.into_iter().map(Into::into).collect(),
86 parses_when_hidden: false,
87 }
88 }
89
90 pub(crate) fn watching(
93 key: impl Into<String>,
94 values: impl IntoIterator<Item = impl Into<String>>,
95 ) -> Self {
96 Self {
97 watched: key.into(),
98 checkbox: false,
99 values: values.into_iter().map(Into::into).collect(),
100 parses_when_hidden: true,
101 }
102 }
103
104 pub(crate) fn holds(&self, values: &FormValues) -> bool {
107 let posted = match values.text(&self.watched) {
108 Some(value) => value.trim(),
109 None if self.checkbox => "false",
110 None => "",
111 };
112 self.values.iter().any(|value| value == posted)
113 }
114
115 pub(crate) fn guard<'a>(
118 &self,
119 cx: &'a Cx,
120 scope: &str,
121 values: &FormValues,
122 inner: BoxView<'a>,
123 ) -> BoxView<'a> {
124 let condition = self.clone();
125 let initial = self.holds(values);
126 let scope = scope.to_string();
127 async_page(async move {
128 let shown = shown(cx, &scope, &condition, initial);
129 let enabled = shown.clone();
130 Ok(view! {
131 <fieldset
132 class="contents"
133 :disabled=$(!enabled.get())
134 :hidden=$(!shown.get())
135 >
136 (inner)
137 </fieldset>
138 })
139 })
140 }
141}
142
143pub(crate) fn disable<'a>(
146 cx: &'a Cx,
147 condition: &Condition,
148 scope: &str,
149 values: &FormValues,
150 inner: BoxView<'a>,
151) -> BoxView<'a> {
152 let condition = condition.clone();
153 let initial = condition.holds(values);
154 let scope = scope.to_string();
155 async_page(async move {
156 let holds = shown(cx, &scope, &condition, initial);
157 Ok(view! {
158 <fieldset class="contents" :disabled=$(holds.get())>(inner)</fieldset>
159 })
160 })
161}
162
163fn shown(cx: &Cx, scope: &str, condition: &Condition, initial: bool) -> Signal<bool> {
170 let page = topcoat::context::try_context::<http::request::Parts>(cx)
171 .map(|parts| parts.uri.path().to_string())
172 .unwrap_or_default();
173 signal(&cx.keyed(signal_key(&page, scope, condition)), move || {
174 initial
175 })
176}
177
178fn signal_key(
180 page: &str,
181 scope: &str,
182 condition: &Condition,
183) -> (&'static str, String, String, String, String) {
184 (
185 "tablo-condition",
186 page.to_string(),
187 scope.to_string(),
188 condition.watched.clone(),
189 condition.values.join("\u{1f}"),
190 )
191}
192
193pub(crate) fn watch<'a>(
202 cx: &'a Cx,
203 field: &Field,
204 posted: &str,
205 conditions: &[Condition],
206 scope: &str,
207 values: &FormValues,
208 control: BoxView<'a>,
209) -> BoxView<'a> {
210 let scope = scope.to_string();
211 let conditions: Vec<(Condition, bool)> = conditions
212 .iter()
213 .map(|condition| (condition.clone(), condition.holds(values)))
214 .collect();
215 let checkbox = field.is_checkbox();
216 let key = posted.to_string();
217 async_page(async move {
218 let mut view = control;
219 for (condition, initial) in conditions {
220 let shown = shown(cx, &scope, &condition, initial);
221 if checkbox {
222 let on = condition.values.iter().any(|value| value == "true");
224 let off = condition.values.iter().any(|value| value == "false");
225 let name = key.clone();
226 view = view! {
227 <div
228 class="contents"
229 @change=$(|e: Event| if e.target.name == name {
230 shown.set(if e.target.checked { on } else { off })
231 })
232 >
233 (view)
234 </div>
235 }
236 .boxed();
237 continue;
238 }
239 let mut values = condition.values.into_iter();
240 let Some(first) = values.next() else {
241 continue;
242 };
243 let (set, name) = (shown.clone(), key.clone());
244 view = view! {
245 <div
246 class="contents"
247 @change=$(|e: Event| if e.target.name == name {
248 set.set(e.target.value.trim() == first)
249 })
250 >
251 (view)
252 </div>
253 }
254 .boxed();
255 for value in values {
256 let (set, name) = (shown.clone(), key.clone());
257 view = view! {
258 <div
259 class="contents"
260 @change=$(|e: Event| if e.target.name == name {
261 if e.target.value.trim() == value {
262 set.set(true)
263 }
264 })
265 >
266 (view)
267 </div>
268 }
269 .boxed();
270 }
271 }
272 Ok(view)
273 })
274}
275
276impl Node {
277 fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
279 match self {
280 Node::Field(index) => fields[*index].condition(),
281 Node::Section(s) => s.condition.as_ref(),
282 Node::Group(g) => g.condition.as_ref(),
283 Node::Grid(g) => g.condition.as_ref(),
284 Node::Tabs(t) => t.condition.as_ref(),
285 Node::Tab(t) => t.condition.as_ref(),
286 Node::Wizard(w) => w.condition.as_ref(),
287 Node::Step(s) => s.condition.as_ref(),
288 Node::Embedded(_) => None,
289 }
290 }
291}
292
293#[derive(Default)]
295struct Scan<'s> {
296 fields: Vec<(usize, Vec<&'s Condition>)>,
298 conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
300 disabling: Vec<(usize, &'s Condition)>,
302}
303
304impl<'s> Scan<'s> {
305 fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
306 let mut scan = Self::default();
307 scan.walk(nodes, fields, &mut Vec::new());
308 for (index, chain) in &scan.fields {
311 if let Some(condition) = fields[*index].disabling() {
312 scan.disabling.push((*index, condition));
313 scan.conditions.push((condition, chain.clone()));
314 }
315 }
316 scan
317 }
318
319 fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
320 for node in nodes {
321 let own = node.condition(fields);
322 if let Some(condition) = own {
323 self.conditions.push((condition, chain.clone()));
324 chain.push(condition);
325 }
326 match node {
327 Node::Field(index) => self.fields.push((*index, chain.clone())),
328 Node::Embedded(embedded) => {
329 embedded.visit_fields(&mut |index| {
332 let own = fields[index].condition();
333 if let Some(condition) = own {
334 self.conditions.push((condition, chain.clone()));
335 chain.push(condition);
336 }
337 self.fields.push((index, chain.clone()));
338 if own.is_some() {
339 chain.pop();
340 }
341 });
342 }
343 node => self.walk(node.children().unwrap_or_default(), fields, chain),
344 }
345 if own.is_some() {
346 chain.pop();
347 }
348 }
349 }
350
351 fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
353 self.fields
354 .iter()
355 .find(|(index, _)| fields[*index].name() == key)
356 .map(|(_, chain)| chain.as_slice())
357 }
358}
359
360impl<F> Schema<F> {
361 pub(crate) fn condition_hidden(&self, values: &FormValues) -> Vec<String> {
363 Scan::of(&self.nodes, &self.fields)
364 .fields
365 .into_iter()
366 .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
367 .map(|(index, _)| self.fields[index].name().to_string())
368 .collect()
369 }
370
371 pub(crate) fn condition_disabled(&self, values: &FormValues) -> Vec<String> {
374 let scan = Scan::of(&self.nodes, &self.fields);
375 let hidden: Vec<usize> = scan
376 .fields
377 .iter()
378 .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
379 .map(|(index, _)| *index)
380 .collect();
381 scan.disabling
382 .into_iter()
383 .filter(|(index, condition)| !hidden.contains(index) && condition.holds(values))
384 .map(|(index, _)| self.fields[index].name().to_string())
385 .collect()
386 }
387
388 pub(crate) fn condition_watchers(&self) -> Vec<(String, Option<String>)> {
391 let scan = Scan::of(&self.nodes, &self.fields);
392 let mut watchers = Vec::new();
393 for (condition, _) in &scan.conditions {
394 let disabled = scan
395 .disabling
396 .iter()
397 .filter(|(_, c)| std::ptr::eq(*c, *condition))
398 .map(|(index, _)| *index);
399 let mut guards = scan
400 .fields
401 .iter()
402 .filter(|(_, chain)| chain.iter().any(|c| std::ptr::eq(*c, *condition)))
403 .map(|(index, _)| *index)
404 .chain(disabled)
405 .map(|index| self.fields[index].name().to_string())
406 .peekable();
407 if guards.peek().is_none() {
408 watchers.push((condition.watched.clone(), None));
409 }
410 watchers.extend(guards.map(|key| (condition.watched.clone(), Some(key))));
411 }
412 watchers
413 }
414
415 pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
417 let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
418 for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
419 let driven = out.entry(condition.watched.clone()).or_default();
420 if !driven.contains(condition) {
421 driven.push(condition.clone());
422 }
423 }
424 out
425 }
426
427 pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
432 let scan = Scan::of(&self.nodes, &self.fields);
433 let mut errors = Vec::new();
434 for (condition, guarding) in &scan.conditions {
435 let watched = condition.watched.clone();
436 match scan.guarding(&self.fields, &condition.watched) {
437 None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
438 field: watched.clone(),
439 }),
440 Some(chain) if !chain.iter().all(|hiding| guarding.contains(hiding)) => {
444 errors.push(DeclarationErrorKind::HiddenWatchedField {
445 field: watched.clone(),
446 });
447 }
448 Some(_) => {}
449 }
450 if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched)
451 && field.disabling().is_some()
452 {
453 errors.push(DeclarationErrorKind::DisabledWatchedField {
454 field: watched.clone(),
455 });
456 }
457 if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
458 for value in &condition.values {
459 if field.can_post(value) == Some(false) {
460 errors.push(DeclarationErrorKind::UnpostedConditionValue {
461 field: watched.clone(),
462 value: value.clone(),
463 });
464 }
465 }
466 }
467 }
468 for (index, chain) in &scan.fields {
469 let field = &self.fields[*index];
470 let hides_unparsed = chain.iter().any(|condition| !condition.parses_when_hidden);
472 if hides_unparsed && field.is_required() {
473 errors.push(DeclarationErrorKind::RequiredConditionalField {
474 field: field.name().to_string(),
475 });
476 }
477 }
478 errors
479 }
480}
481
482#[cfg(test)]
483mod tests;