mod rule;
mod types;
pub use rule::*;
pub use types::*;
use crate::{FieldInjection, Rule};
use leptos::prelude::*;
use num_traits::Bounded;
use std::ops::{Add, Sub};
use std::str::FromStr;
use thaw_utils::{class_list, mount_style, with, BoxOneCallback, Model, OptionalProp};
#[component]
pub fn SpinButton<T>(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] id: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(optional, into)]
rules: Vec<SpinButtonRule<T>>,
#[prop(optional, into)]
value: Model<T>,
#[prop(into)]
step_page: Signal<T>,
#[prop(default = T::min_value().into(), into)]
min: Signal<T>,
#[prop(default = T::max_value().into(), into)]
max: Signal<T>,
#[prop(optional, into)]
placeholder: MaybeProp<String>,
#[prop(optional, into)]
disabled: Signal<bool>,
#[prop(optional, into)]
size: Signal<SpinButtonSize>,
#[prop(optional, into)]
parser: OptionalProp<BoxOneCallback<String, Option<T>>>,
#[prop(optional, into)]
format: OptionalProp<BoxOneCallback<T, String>>,
) -> impl IntoView
where
T: Send + Sync,
T: Add<Output = T> + Sub<Output = T> + PartialOrd + Bounded,
T: Default + Clone + FromStr + ToString + 'static,
{
mount_style("spin-button", include_str!("./spin-button.css"));
let (id, name) = FieldInjection::use_id_and_name(id, name);
let validate = Rule::validate(rules, value, name);
let initialization_value = value.get_untracked().to_string();
let update_value = move |new_value| {
if with!(|value| value == &new_value) {
return;
}
let min = min.get_untracked();
let max = max.get_untracked();
if new_value < min {
value.set(min);
} else if new_value > max {
value.set(max);
} else {
value.set(new_value);
}
validate.run(Some(SpinButtonRuleTrigger::Change));
};
let increment_disabled = Memo::new(move |_| disabled.get() || value.get() >= max.get());
let decrement_disabled = Memo::new(move |_| disabled.get() || value.get() <= min.get());
let on_change = move |e| {
let target_value = event_target_value(&e);
let v = if let Some(parser) = parser.as_ref() {
parser(target_value)
} else {
target_value.parse::<T>().ok()
};
if let Some(value) = v {
update_value(value);
} else {
value.update(|_| {});
}
};
view! {
<span class=class_list![
"thaw-spin-button",
("thaw-spin-button--disabled", move || disabled.get()),
move || format!("thaw-spin-button--{}", size.get().as_str()),
class
]>
<input
autocomplete="off"
role="spinbutton"
aria-valuenow=move || value.get().to_string()
type="text"
disabled=move || disabled.get()
placeholder=move || placeholder.get()
value=initialization_value
prop:value=move || {
let value = value.get();
if let Some(format) = format.as_ref() {
format(value)
} else {
value.to_string()
}
}
class="thaw-spin-button__input"
id=id
name=name
on:change=on_change
/>
<button
tabindex="-1"
aria-label="Increment value"
type="button"
class="thaw-spin-button__increment-button"
class=(
"thaw-spin-button__increment-button--disabled",
move || increment_disabled.get(),
)
disabled=move || disabled.get()
on:click=move |_| {
if !increment_disabled.get_untracked() {
update_value(value.get_untracked() + step_page.get_untracked());
}
}
>
<svg
fill="currentColor"
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
d="M3.15 10.35c.2.2.5.2.7 0L8 6.21l4.15 4.14a.5.5 0 0 0 .7-.7l-4.5-4.5a.5.5 0 0 0-.7 0l-4.5 4.5a.5.5 0 0 0 0 .7Z"
fill="currentColor"
></path>
</svg>
</button>
<button
tabindex="-1"
aria-label="Decrement value"
type="button"
class="thaw-spin-button__decrement-button"
disabled=move || disabled.get()
class=(
"thaw-spin-button__decrement-button--disabled",
move || decrement_disabled.get(),
)
on:click=move |_| {
if !decrement_disabled.get_untracked() {
update_value(value.get_untracked() - step_page.get_untracked());
}
}
>
<svg
fill="currentColor"
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 16 16"
>
<path
d="M3.15 5.65c.2-.2.5-.2.7 0L8 9.79l4.15-4.14a.5.5 0 0 1 .7.7l-4.5 4.5a.5.5 0 0 1-.7 0l-4.5-4.5a.5.5 0 0 1 0-.7Z"
fill="currentColor"
></path>
</svg>
</button>
</span>
}
}