1use crate::components::*;
2use crate::*;
3use ::core::ops::*;
4use ::indexmap::IndexMap;
5use ::std::rc::Rc;
6
7#[derive(Clone, Default, Derivative, TypedBuilder)]
9#[builder(field_defaults(default, setter(into)))]
10#[derivative(Debug)]
11pub struct VecConfig<Config: Default> {
12 pub item: Config,
15 #[builder(setter(strip_option))]
17 pub item_container_class: Option<Oco<'static, str>>,
18 #[builder(setter(strip_option))]
20 pub item_class: Option<Oco<'static, str>>,
21 #[builder(setter(strip_option))]
23 pub item_style: Option<Oco<'static, str>>,
24 #[builder(setter(strip_option))]
26 pub item_label: Option<VecItemLabel>,
27 pub size: VecConfigSize,
29 pub add: Adornment,
31 pub remove: Adornment,
33}
34
35#[derive(Clone, Debug, Default, TypedBuilder)]
38#[builder(field_defaults(default, setter(into)))]
39pub struct VecItemLabel {
40 #[builder(setter(strip_option))]
42 pub class: Option<Oco<'static, str>>,
43 pub notation: Option<VecItemLabelNotation>,
45 pub punctuation: Option<VecItemLabelPunctuation>,
47 #[builder(setter(strip_option))]
49 pub style: Option<Oco<'static, str>>,
50}
51
52#[derive(Clone, Copy, Debug, Default)]
55pub enum VecItemLabelNotation {
56 CapitalLetter,
57 Letter,
58 #[default]
59 Number,
60}
61
62#[derive(Clone, Copy, Debug, Default)]
65pub enum VecItemLabelPunctuation {
66 Parenthesis,
67 #[default]
68 Period,
69}
70
71#[derive(Clone, Copy, Debug, Default)]
72pub enum VecConfigSize {
73 Bounded {
74 min: Option<usize>,
75 max: Option<usize>,
76 },
77 Const(usize),
78 #[default]
79 Unbounded,
80}
81
82#[derive(Clone, Default, Derivative)]
83#[derivative(Debug)]
84pub enum Adornment {
85 None,
86 #[default]
87 Default,
88 Component(#[derivative(Debug = "ignore")] AdornmentComponent),
89 Spec(AdornmentSpec),
90}
91
92pub type AdornmentComponent = Rc<dyn Fn(Rc<dyn Fn(web_sys::MouseEvent)>, StyleSignal) -> View + 'static>;
97
98#[derive(Clone, Debug, TypedBuilder)]
99#[builder(field_defaults(default, setter(into)))]
100pub struct AdornmentSpec {
101 #[builder(setter(strip_option))]
102 pub class: Option<Oco<'static, str>>,
103 #[builder(default = 24)]
104 pub height: usize,
105 #[builder(setter(strip_option))]
106 pub style: Option<Oco<'static, str>>,
107 pub text: Option<Oco<'static, str>>,
108 #[builder(default = 24)]
109 pub width: usize,
110}
111
112impl Default for AdornmentSpec {
113 fn default() -> Self {
114 Self {
115 class: None,
116 height: 24,
117 style: None,
118 text: None,
119 width: 24,
120 }
121 }
122}
123
124impl<T: DefaultHtmlElement> DefaultHtmlElement for Vec<T> {
125 type El = Vec<T::El>;
126}
127
128#[derive(Clone, Copy, Debug)]
129pub struct VecSignalItem<Signal> {
130 id: usize,
131 signal: Signal,
132}
133
134impl<T, El> FormField<Vec<El>> for Vec<T>
135where
136 T: Clone + FormField<El>,
137 <T as FormField<El>>::Signal: Clone + std::fmt::Debug,
138{
139 type Config = VecConfig<<T as FormField<El>>::Config>;
140 type Signal = FormFieldSignal<IndexMap<usize, VecSignalItem<<T as FormField<El>>::Signal>>>;
141
142 fn default_signal(config: &Self::Config, initial: Option<Self>) -> Self::Signal {
143 FormFieldSignal::new_with_default_value(initial.map(|x| {
144 x.into_iter()
145 .enumerate()
146 .map(|(i, initial)| {
147 (
148 i,
149 VecSignalItem {
150 id: i,
151 signal: T::default_signal(&config.item, Some(initial)),
152 },
153 )
154 })
155 .collect::<IndexMap<_, _>>()
156 }))
157 }
158 fn is_initial_value(signal: &Self::Signal) -> bool {
159 signal.initial.with(|initial| {
160 signal.value.with(|value| match initial.as_ref() {
161 Some(initial) => {
162 let no_keys_changed = value.len() == initial.len()
163 && value.last().map(|item| item.0) == initial.last().map(|item| item.0);
164 if !no_keys_changed {
165 return false;
166 }
167 value.iter().all(|(_, item)| T::is_initial_value(&item.signal))
168 }
169 None => value.is_empty(),
170 })
171 })
172 }
173 fn into_signal(self, config: &Self::Config, initial: Option<Self>) -> Self::Signal {
174 let has_initial = initial.is_some();
175 let mut initial = initial
176 .map(|x| x.into_iter().map(Some).collect::<Vec<_>>())
177 .unwrap_or_default();
178 if initial.len() < self.len() {
179 initial.append(&mut vec![None; self.len() - initial.len()]);
180 }
181 let value = self
182 .into_iter()
183 .zip(initial)
184 .enumerate()
185 .map(|(i, (item, initial))| {
186 (
187 i,
188 VecSignalItem {
189 id: i,
190 signal: item.into_signal(&config.item, initial),
191 },
192 )
193 })
194 .collect::<IndexMap<_, _>>();
195 let initial = has_initial.then(|| value.clone());
196 FormFieldSignal::new(value, initial)
197 }
198 fn try_from_signal(signal: Self::Signal, config: &Self::Config) -> Result<Self, FormError> {
199 signal.with(|value| {
200 value
201 .iter()
202 .map(|(_, item)| T::try_from_signal(item.signal.clone(), &config.item))
203 .collect()
204 })
205 }
206 fn recurse(signal: &Self::Signal) {
207 signal.with(|sig| sig.iter().for_each(|(_, sig)| T::recurse(&sig.signal)))
208 }
209 fn reset_initial_value(signal: &Self::Signal) {
210 signal.value.update(|value| {
211 value.iter().for_each(|(_, item)| T::reset_initial_value(&item.signal));
212 });
213 }
214 fn with_error<O>(_: &Self::Signal, f: impl FnOnce(Option<&FormError>) -> O) -> O {
215 f(None)
216 }
217}
218
219impl<T, El, S> FormComponent<Vec<El>> for Vec<T>
220where
221 T: Clone + FormComponent<El, Signal = FormFieldSignal<S>>,
222 S: Clone + Eq + 'static + std::fmt::Debug,
223 <T as FormField<El>>::Config: std::fmt::Debug,
224{
225 fn render(props: RenderProps<Self::Signal, Self::Config>) -> impl IntoView {
226 let (min_items, max_items) = props.config.size.split();
227
228 let next_id =
229 create_rw_signal(
230 props
231 .signal
232 .with(|items| if items.is_empty() { 1 } else { items[items.len() - 1].id + 1 }),
233 );
234
235 if min_items.is_some() || max_items.is_some() {
236 props.signal.update(|items| {
237 let num_items = items.len();
238 if let Some(min_items) = min_items {
239 if items.len() < min_items {
240 items.reserve(min_items - num_items);
241 while items.len() < min_items {
242 let id = next_id.get_untracked();
243 items.insert(
244 id,
245 VecSignalItem {
246 id,
247 signal: T::default_signal(&props.config.item, None),
248 },
249 );
250 next_id.update(|x| *x += 1);
251 }
252 }
253 }
254 if let Some(max_items) = max_items {
255 while max_items < items.len() {
256 items.pop();
257 }
258 }
259 });
260 }
261
262 let VecConfig {
263 item: item_config,
264 item_container_class,
265 item_class,
266 item_label,
267 item_style,
268 size,
269 add,
270 remove,
271 } = props.config;
272
273 let item_config_clone = item_config.clone();
274 view! {
275 <div id={props.id} class={props.class} style={props.style}>
276 <For
277 key=|(_, (key, _))| *key
278 each=move || props.signal.value.get().into_iter().enumerate()
279 children=move |(index, (key, item))| {
280 let id = || index.to_string();
281
282 let item_props = RenderProps::builder()
283 .id(Oco::Owned(id()))
284 .name(crate::format_form_name(props.name.as_ref(), id()))
285 .class(item_class.clone())
286 .style(item_style.clone())
287 .field_changed_class(props.field_changed_class.clone())
288 .signal(item.signal)
289 .config(item_config.clone())
290 .build();
291
292 VecConfig::<<T as FormField<El>>::Config>::wrap(
293 &size,
294 item_container_class.clone(),
295 item_label.as_ref(),
296 &remove,
297 props.signal,
298 key,
299 Oco::Owned(id()),
300 <T as FormComponent<El>>::render(item_props),
301 ).into_view()
302 }
303 />
304 {
305 let num_items_is_max = move || {
306 let num_items = props.signal.with(|items| items.len());
307 num_items >= max_items.unwrap_or(usize::MAX)
308 };
309
310 let cursor = move || if num_items_is_max() { None } else { Some("pointer") };
311 let opacity = move || if num_items_is_max() { Some("0.5") } else { None };
312
313 let on_add = move |_| {
314 if !num_items_is_max() {
315 props.signal.update(|items| {
316 let id = next_id.get_untracked();
317 items.insert(id, VecSignalItem { id, signal: T::default_signal(&item_config_clone, None) });
318 next_id.update(|x| *x = id + 1);
319 });
320 }
321 };
322
323 match (&size, &add) {
324 (VecConfigSize::Const(_), _)|(_, Adornment::None) => View::default(),
325 (_, Adornment::Component(component)) => component(Rc::new(on_add), Rc::new(opacity)),
326 (_, Adornment::Default) => view! {
327 <input
328 type="button"
329 on:click=on_add
330 style:cursor=cursor
331 style:margin-top="0.5 rem"
332 style:opacity=opacity
333 value="Add"
334 />
335 }
336 .into_view(),
337 (_, Adornment::Spec(adornment_spec)) => {
338 let style = (adornment_spec.class.is_none() && adornment_spec.style.is_none()).then_some("margin-top: 0.5rem;");
339 view! {
340 <input
341 type="button"
342 class={adornment_spec.class.clone()}
343 cursor=cursor
344 on:click=on_add
345 style:opacity=opacity
346 style=style
347 value={adornment_spec.text.clone().unwrap_or(Oco::Borrowed("Add"))}
348 />
349 }
350 .into_view()
351 }
352 }
353 }
354 </div>
355 }
356 }
357}
358
359impl From<usize> for VecConfigSize {
360 fn from(value: usize) -> Self {
361 Self::Const(value)
362 }
363}
364
365impl From<(usize, usize)> for VecConfigSize {
366 fn from(value: (usize, usize)) -> Self {
367 Self::Bounded {
368 min: Some(value.0),
369 max: Some(value.1),
370 }
371 }
372}
373
374static ASCII_LOWER: [char; 26] = [
375 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
376 'x', 'y', 'z',
377];
378
379static ASCII_UPPER: [char; 26] = [
380 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
381 'X', 'Y', 'Z',
382];
383
384impl<Config: Default> VecConfig<Config> {
385 #[allow(clippy::too_many_arguments)]
386 fn wrap<Signal: std::fmt::Debug>(
387 size: &VecConfigSize,
388 item_container_class: Option<Oco<'static, str>>,
389 item_label: Option<&VecItemLabel>,
390 remove_adornment: &Adornment,
391 signal: FormFieldSignal<IndexMap<usize, VecSignalItem<Signal>>>,
392 key: usize,
393 id: Oco<'static, str>,
394 item: impl IntoView,
395 ) -> impl IntoView {
396 let (min_items, _) = size.split();
400 let num_items_is_min = move || {
401 let num_items = signal.with(|items| items.len());
402 num_items <= min_items.unwrap_or_default()
403 };
404
405 let cursor: StyleSignal = Rc::new(move || if num_items_is_min() { None } else { Some("pointer") });
406 let opacity: StyleSignal = Rc::new(move || if num_items_is_min() { Some("0.5") } else { None });
407
408 let on_remove = move |_| {
409 if !num_items_is_min() {
410 signal.update(|items| {
411 items.remove(&key);
412 });
413 }
414 };
415
416 let remove_component = match (size, remove_adornment) {
417 (VecConfigSize::Const(_), _) | (_, Adornment::None) => View::default(),
418 (_, Adornment::Component(component)) => component(Rc::new(on_remove), opacity),
419 (_, Adornment::Default) => {
420 view! {
421 <MaterialClose
422 cursor=cursor
423 on:click=on_remove
424 opacity=opacity
425 style=Oco::Borrowed("margin-left: 0.5rem !important;")
426 />
427 }
428 }
429 (_, Adornment::Spec(adornment_spec)) => {
430 let adornment_style = (adornment_spec.class.is_none() && adornment_spec.style.is_none())
431 .then_some("margin-left: 0.5rem;");
432 view! {
433 <MaterialClose
434 class=adornment_spec.class.clone()
435 cursor=cursor
436 height=adornment_spec.height
437 on:click=on_remove
438 opacity=opacity
439 style=adornment_style.map(Oco::from)
440 width=adornment_spec.width
441 />
442 }
443 }
444 };
445
446 view! {
447 <div class={item_container_class} style="display: flex; flex-direction: row; align-items: center; margin-bottom: 0.5rem">
448 {match item_label {
449 Some(item_label) => item_label.wrap_label(key, id, item, signal),
450 None => item.into_view(),
451 }}
452 {remove_component}
453 </div>
454 }
455 }
456}
457
458impl VecItemLabel {
459 fn wrap_label<Signal>(
460 &self,
461 key: usize,
462 id: Oco<'static, str>,
463 item: impl IntoView,
464 signal: FormFieldSignal<IndexMap<usize, VecSignalItem<Signal>>>,
465 ) -> View {
466 let notation = self.notation;
467 let punctuation = self.punctuation;
468 let prefix = move || {
469 signal
470 .with(|items| items.get_index_of(&key))
471 .and_then(|index| match (notation, punctuation) {
472 (Some(notation), punctuation) => {
473 Some(notation.render(index) + punctuation.map(|x| x.render()).unwrap_or_default())
474 }
475 _ => None,
476 })
477 .map(|prefix| view! { <div>{prefix}</div> }.into_view())
478 .unwrap_or_default()
479 };
480 view! {
481 <label for={id} class={self.class.clone()} style={self.style.clone()}>
482 {prefix}
483 {item}
484 </label>
485 }
486 .into_view()
487 }
488}
489
490impl VecConfigSize {
491 fn split(&self) -> (Option<usize>, Option<usize>) {
492 match *self {
493 VecConfigSize::Bounded { min, max } => (min, max),
494 VecConfigSize::Const(num_items) => (Some(num_items), Some(num_items)),
495 VecConfigSize::Unbounded => (None, None),
496 }
497 }
498}
499
500impl VecItemLabelNotation {
501 fn render(&self, index: usize) -> String {
502 let display_index = index + 1;
503 let ascii_set = match self {
504 Self::CapitalLetter => &ASCII_UPPER,
505 Self::Letter => &ASCII_LOWER,
506 Self::Number => return display_index.to_string(),
507 };
508 let n = (display_index.ilog(ascii_set.len()) + 1) as usize;
509 let mut chars = vec![' '; n];
510 let mut num = index;
511 for j in 0..n {
512 chars[n - 1 - j] = ascii_set[num % ascii_set.len()];
513 num /= ascii_set.len();
514 }
515 chars.into_iter().collect()
516 }
517}
518
519impl VecItemLabelPunctuation {
520 fn render(&self) -> &'static str {
521 match self {
522 Self::Parenthesis => ")",
523 Self::Period => ".",
524 }
525 }
526}