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, topcoat_compat::async_page, values::FormValues};
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 pub(crate) 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 signal(&cx.keyed(signal_key(&page, scope, condition)), move || {
153 initial
154 })
155}
156
157fn signal_key(
159 page: &str,
160 scope: &str,
161 condition: &Condition,
162) -> (&'static str, String, String, String, String) {
163 (
164 "tablo-condition",
165 page.to_string(),
166 scope.to_string(),
167 condition.watched.clone(),
168 condition.values.join("\u{1f}"),
169 )
170}
171
172pub(crate) fn watch<'a>(
180 cx: &'a Cx,
181 field: &Field,
182 conditions: &[Condition],
183 scope: &str,
184 values: &FormValues,
185 control: BoxView<'a>,
186) -> BoxView<'a> {
187 let scope = scope.to_string();
188 let conditions: Vec<(Condition, bool)> = conditions
189 .iter()
190 .map(|condition| (condition.clone(), condition.holds(values)))
191 .collect();
192 let checkbox = field.is_checkbox();
193 let key = field.name().to_string();
194 async_page(async move {
195 let mut view = control;
196 for (condition, initial) in conditions {
197 let shown = shown(cx, &scope, &condition, initial);
198 if checkbox {
199 let on = condition.values.iter().any(|value| value == "true");
201 let off = condition.values.iter().any(|value| value == "false");
202 let name = key.clone();
203 view = view! {
204 cx =>
205 <div
206 class="contents"
207 @change=$(|e: Event| if e.target.name == name {
208 shown.set(if e.target.checked { on } else { off })
209 })
210 >
211 (view)
212 </div>
213 }
214 .boxed();
215 continue;
216 }
217 let mut values = condition.values.into_iter();
218 let Some(first) = values.next() else {
219 continue;
220 };
221 let (set, name) = (shown.clone(), key.clone());
222 view = view! {
223 cx =>
224 <div
225 class="contents"
226 @change=$(|e: Event| if e.target.name == name {
227 set.set(e.target.value.trim() == first)
228 })
229 >
230 (view)
231 </div>
232 }
233 .boxed();
234 for value in values {
235 let (set, name) = (shown.clone(), key.clone());
236 view = view! {
237 cx =>
238 <div
239 class="contents"
240 @change=$(|e: Event| if e.target.name == name {
241 if e.target.value.trim() == value {
242 set.set(true)
243 }
244 })
245 >
246 (view)
247 </div>
248 }
249 .boxed();
250 }
251 }
252 Ok(view)
253 })
254}
255
256impl Node {
257 fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
259 match self {
260 Node::Field(index) => fields[*index].condition(),
261 Node::Section(s) => s.condition.as_ref(),
262 Node::Group(g) => g.condition.as_ref(),
263 Node::Grid(g) => g.condition.as_ref(),
264 Node::Tabs(t) => t.condition.as_ref(),
265 Node::Tab(t) => t.condition.as_ref(),
266 Node::Embedded(_) => None,
267 }
268 }
269}
270
271#[derive(Default)]
273struct Scan<'s> {
274 fields: Vec<(usize, Vec<&'s Condition>)>,
276 conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
278}
279
280impl<'s> Scan<'s> {
281 fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
282 let mut scan = Self::default();
283 scan.walk(nodes, fields, &mut Vec::new());
284 scan
285 }
286
287 fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
288 for node in nodes {
289 let own = node.condition(fields);
290 if let Some(condition) = own {
291 self.conditions.push((condition, chain.clone()));
292 chain.push(condition);
293 }
294 match node {
295 Node::Field(index) => self.fields.push((*index, chain.clone())),
296 Node::Embedded(embedded) => {
297 embedded.visit_fields(&mut |index| {
300 let own = fields[index].condition();
301 if let Some(condition) = own {
302 self.conditions.push((condition, chain.clone()));
303 chain.push(condition);
304 }
305 self.fields.push((index, chain.clone()));
306 if own.is_some() {
307 chain.pop();
308 }
309 });
310 }
311 node => self.walk(node.children().unwrap_or_default(), fields, chain),
312 }
313 if own.is_some() {
314 chain.pop();
315 }
316 }
317 }
318
319 fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
321 self.fields
322 .iter()
323 .find(|(index, _)| fields[*index].name() == key)
324 .map(|(_, chain)| chain.as_slice())
325 }
326}
327
328impl<F> Schema<F> {
329 pub(crate) fn condition_hidden(&self, values: &FormValues) -> Vec<String> {
331 Scan::of(&self.nodes, &self.fields)
332 .fields
333 .into_iter()
334 .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
335 .map(|(index, _)| self.fields[index].name().to_string())
336 .collect()
337 }
338
339 pub(crate) fn condition_watchers(&self) -> Vec<(String, Option<String>)> {
342 let scan = Scan::of(&self.nodes, &self.fields);
343 let mut watchers = Vec::new();
344 for (condition, _) in &scan.conditions {
345 let mut guards = scan
346 .fields
347 .iter()
348 .filter(|(_, chain)| chain.iter().any(|c| std::ptr::eq(*c, *condition)))
349 .map(|(index, _)| self.fields[*index].name().to_string())
350 .peekable();
351 if guards.peek().is_none() {
352 watchers.push((condition.watched.clone(), None));
353 }
354 watchers.extend(guards.map(|key| (condition.watched.clone(), Some(key))));
355 }
356 watchers
357 }
358
359 pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
361 let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
362 for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
363 let driven = out.entry(condition.watched.clone()).or_default();
364 if !driven.contains(condition) {
365 driven.push(condition.clone());
366 }
367 }
368 out
369 }
370
371 pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
375 let scan = Scan::of(&self.nodes, &self.fields);
376 let mut errors = Vec::new();
377 for (condition, guarding) in &scan.conditions {
378 let watched = condition.watched.clone();
379 match scan.guarding(&self.fields, &condition.watched) {
380 None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
381 field: watched.clone(),
382 }),
383 Some(chain)
387 if !chain
388 .iter()
389 .all(|hiding| guarding.iter().any(|c| std::ptr::eq(*c, *hiding))) =>
390 {
391 errors.push(DeclarationErrorKind::HiddenWatchedField {
392 field: watched.clone(),
393 });
394 }
395 Some(_) => {}
396 }
397 if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
398 for value in &condition.values {
399 if field.can_post(value) == Some(false) {
400 errors.push(DeclarationErrorKind::UnpostedConditionValue {
401 field: watched.clone(),
402 value: value.clone(),
403 });
404 }
405 }
406 }
407 }
408 for (index, chain) in &scan.fields {
409 let field = &self.fields[*index];
410 let hides_unparsed = chain.iter().any(|condition| !condition.parses_when_hidden);
412 if hides_unparsed && field.is_required() {
413 errors.push(DeclarationErrorKind::RequiredConditionalField {
414 field: field.name().to_string(),
415 });
416 }
417 }
418 errors
419 }
420}
421
422#[cfg(test)]
423mod tests;