use crate::button::Button;
use crate::button::ButtonAppearance;
use crate::button_group::ButtonGroup;
use crate::button_group::First;
use crate::button_group::InGroupContext;
use crate::button_group::Last;
use crate::class_list;
use crate::form_input::FormInputContext;
use crate::form_input::Label;
use crate::form_input::MaybeLabelledFormInput;
use crate::icon::HideIcon;
use crate::icon::ShowIcon;
use crate::input_group::GroupItemClassContext;
use crate::util::callback::ArcOneCallback;
use crate::util::callback::BoxOneCallback;
use leptodon_proc_macros::generate_docs;
use leptos::either::Either;
use leptos::html;
use leptos::logging::debug_log;
use leptos::prelude::ClassAttribute;
use leptos::prelude::Effect;
use leptos::prelude::ElementChild;
use leptos::prelude::Get;
use leptos::prelude::GetUntracked;
use leptos::prelude::GlobalAttributes;
use leptos::prelude::IntoAny;
use leptos::prelude::MaybeProp;
use leptos::prelude::NodeRef;
use leptos::prelude::NodeRefAttribute;
use leptos::prelude::OnAttribute;
use leptos::prelude::RwSignal;
use leptos::prelude::Set;
use leptos::prelude::Signal;
use leptos::prelude::Update;
use leptos::prelude::use_context;
use leptos::{IntoView, component, view};
use leptos_use::math::use_or;
use std::fmt::Debug;
use web_sys::Event;
use web_sys::FocusEvent;
use web_sys::KeyboardEvent;
use zxcvbn::Score;
use zxcvbn::zxcvbn;
mod number;
pub use crate::input::number::*;
mod upload;
pub use crate::input::upload::*;
pub const OA_READONLY_INPUT_CLASSES: &str = "border-0 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500";
const OA_INPUT_CLASSES: &str = "shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500";
pub const PLACEHOLDER_TEXT_CLASS: &str = "text-gray-600 dark:text-gray-400";
#[generate_docs]
#[component]
#[allow(unused)] pub fn TextInputConfig(
#[prop(optional, into)] max_len: MaybeProp<u32>,
#[prop(optional, into)] min_len: MaybeProp<u32>,
#[prop(default = true)] trim: bool,
) -> impl IntoView {
}
#[generate_docs]
#[component]
pub fn TextInput(
#[prop(optional, into)]
id: MaybeProp<String>,
#[prop(optional, into)]
class: MaybeProp<String>,
#[prop(optional)]
input_ref: NodeRef<html::Input>,
#[prop(optional, into)]
label: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(optional, into)]
input_type: Signal<InputType>,
#[prop(optional, into)]
input_mode: Signal<InputMode>,
#[prop(default = TextInputConfigProps::builder().build())] text_config: TextInputConfigProps,
#[prop(optional)]
value: RwSignal<String>,
#[prop(optional, into)]
readonly: Signal<bool>,
#[prop(optional, into)]
required: Signal<bool>,
#[prop(optional, into)]
placeholder: MaybeProp<String>,
) -> impl IntoView {
let parser = move |input: String| {
let input = if text_config.trim {
input.trim()
} else {
input.as_str()
};
let input_len = input.chars().count() as u32;
if let Some(max_len) = text_config.max_len.get()
&& let Some(min_len) = text_config.min_len.get()
{
if input_len > max_len || input_len < min_len {
return Err(format!("Input Length must be >{min_len} and <{max_len}"));
}
} else if let Some(max_len) = text_config.max_len.get() {
if input_len > max_len {
return Err(format!("Input Length must be <{max_len}"));
}
} else if let Some(min_len) = text_config.min_len.get()
&& input_len < min_len
{
return Err(format!("Input Length must be >{min_len}"));
}
Ok(String::from(input))
};
let format = move |input: String| input;
view! {
<GenericInput<String, String>
id
class
input_ref
label
name
input_type
input_mode
value
readonly
required
placeholder
parser
format
/>
}
}
#[generate_docs]
#[component]
pub fn PasswordInput(
#[prop(optional, into)]
id: MaybeProp<String>,
#[prop(optional, into)]
class: MaybeProp<String>,
#[prop(optional)]
input_ref: NodeRef<html::Input>,
#[prop(optional, into)]
label: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(into)]
hazards: Vec<String>,
#[prop(default = false)]
show_eye: bool,
#[prop(optional, into)]
value: RwSignal<String>,
#[prop(optional, into)]
readonly: Signal<bool>,
#[prop(optional, into)]
required: Signal<bool>,
#[prop(optional, into)]
placeholder: MaybeProp<String>,
) -> impl IntoView {
let parser = ArcOneCallback::new(move |input: String| {
let hazard_strs: Vec<&str> = hazards.iter().map(|s| s.as_ref()).collect();
let entropy = zxcvbn(input.as_str(), hazard_strs.as_slice());
if let Some(feedback) = entropy.feedback() {
return Err(format!("{feedback}"));
}
if entropy.score() < Score::Four {
return Err("Almost strong enough, add another word or a couple symbols.".to_string());
}
Ok(input)
});
let password_vis = RwSignal::new(false);
if show_eye {
let form_context = use_context::<FormInputContext<String>>();
let form_required = Signal::from(
form_context
.clone()
.map(|ctx| ctx.required)
.unwrap_or_default(),
);
let required = use_or(required, form_required);
let label = if let Some(form_context) = form_context {
if form_context.label.get().is_some() || label.get().is_none() {
MaybeProp::default()
} else {
label
}
} else {
label
};
view! {
<MaybeLabelledFormInput<String> label required=required.get()>
<ButtonGroup>
<First slot:first>
<GenericInput<String, String>
id
class
input_ref
label
name
value
readonly
required
placeholder
input_type=Signal::derive(move || { if password_vis.get() { InputType::Text } else { InputType::Password } })
parser=parser.clone()
/>
</First>
<Last slot:last>
<Button
on_click=move |_| {
password_vis.update(|mut_vis| *mut_vis = !*mut_vis)
}
appearance=ButtonAppearance::Secondary
icon=Signal::derive(move || {
if password_vis.get() { HideIcon() } else { ShowIcon() }
})
></Button>
</Last>
</ButtonGroup>
</MaybeLabelledFormInput<String>>
}.into_any()
} else {
view! {
<GenericInput<String, String>
id
class
input_ref
label
name
value
readonly
required
placeholder
input_type=move || { if password_vis.get() { InputType::Text } else { InputType::Password } }
parser=parser.clone()
/>
}.into_any()
}
}
#[generate_docs]
#[component]
pub fn GenericInput<T, E>(
#[prop(optional, into)]
id: MaybeProp<String>,
#[prop(optional, into)]
class: MaybeProp<String>,
#[prop(optional)]
input_ref: NodeRef<html::Input>,
#[prop(optional, into)]
label: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(optional, into)]
input_type: Signal<InputType>,
#[prop(optional, into)]
input_mode: Signal<InputMode>,
#[prop(optional, into)]
value: RwSignal<T>,
#[prop(optional, into)]
parser: Option<ArcOneCallback<String, Result<T, E>>>,
#[prop(optional, into)]
format: Option<BoxOneCallback<T, String>>,
#[prop(optional, into)]
required: Signal<bool>,
#[prop(optional, into)]
on_focus: MaybeProp<ArcOneCallback<FocusEvent>>,
#[prop(optional, into)]
readonly: Signal<bool>,
#[prop(optional, into)]
placeholder: MaybeProp<String>,
#[prop(optional, into)]
step: MaybeProp<String>,
#[prop(optional, into)]
min: MaybeProp<String>,
#[prop(optional, into)]
max: MaybeProp<String>,
) -> impl IntoView
where
T: Clone + PartialEq + Debug + Default + Sync + Send + 'static,
E: Clone + Send + Sync + Debug + std::fmt::Display + 'static,
{
let group_context = use_context::<GroupItemClassContext>();
let group_classes = group_context.map(|item| item.class);
let in_group = use_context::<InGroupContext>().unwrap_or(InGroupContext { in_group: false });
let last_set_value = RwSignal::new(value.get_untracked());
let form_context = use_context::<FormInputContext<E>>();
let form_required = Signal::from(
form_context
.clone()
.map(|ctx| ctx.required)
.unwrap_or_default(),
);
let required = use_or(required, form_required);
let in_form = form_context.is_some();
let invalid_reason = RwSignal::new(None);
let try_parse = {
let parser = parser.clone();
move |should_format: bool| {
let Some(input) = input_ref.get_untracked() else {
return;
};
let internal_value = input.value();
debug_log!("Attempting to parse: {internal_value}, format({should_format})",);
if let Some(parser) = parser.as_ref()
&& (!internal_value.is_empty() || required.get())
{
let parsed_value = parser(internal_value);
debug_log!("Parse result: {parsed_value:?}");
match parsed_value {
Ok(parsed_success) => {
if !should_format {
debug_log!("Preventing internal format");
last_set_value.set(parsed_success.clone());
}
debug_log!("Updating value");
value.set(parsed_success);
invalid_reason.set(None);
}
Err(err) => {
invalid_reason.set(Some(err));
}
}
} else if internal_value.is_empty() && !required.get() {
if let Some(parser) = parser.as_ref()
&& let Ok(parsed_value) = parser(internal_value)
{
if !should_format {
debug_log!("Preventing internal format");
last_set_value.set(parsed_value.clone());
}
debug_log!("Updating value");
value.set(parsed_value);
}
invalid_reason.set(None);
}
}
};
if let Some(form_context) = form_context {
Effect::new(move || {
form_context.feedback.set(invalid_reason.get());
});
}
let on_blur = {
let try_parse = try_parse.clone();
move |_| {
try_parse(true);
}
};
let on_input = {
let try_parse = try_parse.clone();
move |_: Event| {
try_parse(false);
}
};
Effect::watch(
move || value.get(),
move |value, _, _| {
if let Some(format) = format.as_ref() {
if &(last_set_value.get_untracked()) != value {
let Some(input) = input_ref.get_untracked() else {
return;
};
input.set_value(format(value.clone()).as_str());
} else {
debug_log!("Prevented internal format");
}
} else {
debug_log!("No formatter to format {:?}", value)
}
},
true,
);
let standalone_input = view! {
<input id=id.get()
type=move || input_type.get().as_str()
inputmode=move || input_mode.get().as_str()
name={name.get()}
class=class_list![
("border-oa-red", move || invalid_reason.get().is_some()),
group_classes.unwrap_or_default(),
if in_group.in_group { "rounded-none border-r-0 !mr-0" } else { "" },
(OA_READONLY_INPUT_CLASSES, move || readonly.get()),
(OA_INPUT_CLASSES, move || !readonly.get()),
class
]
disabled={readonly.get()}
readonly={readonly.get()}
node_ref=input_ref
placeholder={placeholder.get()}
required={required.get()}
on:blur=on_blur
on:input=on_input
on:focus=move |e| {if let Some(on_focus) = on_focus.get() { on_focus(e) }}
on:keydown={
let try_parse = try_parse.clone();
move |key: KeyboardEvent| {
if key.code() == "Enter" {
try_parse(true);
}
}
}
step=step.get()
min=min.get()
max=max.get()
/>
{
move || {
if let Some(invalid_reason) = invalid_reason.get() && !in_form {
Either::Left(view!{
<div
id=id.get().map(|id| format!("{id}-invalid-reason"))
class="text-oa-red"
>{
invalid_reason.to_string()
}</div>
})
} else { Either::Right(()) }
}
}
};
if let Some(label) = label.get()
&& !in_form
{
Either::Left(view! {
<div>
<Label required=required.get() label>
{standalone_input}
</Label>
</div>
})
} else {
Either::Right(standalone_input)
}
}
#[derive(Clone)]
pub struct InputRef {
pub input_ref: NodeRef<html::Input>,
}
#[derive(Default, Clone)]
pub enum InputType {
#[default]
Text,
Password,
Search,
Tel,
Url,
Email,
Time,
Date,
DatetimeLocal,
Month,
Week,
Number,
}
impl InputType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Password => "password",
Self::Search => "search",
Self::Tel => "tel",
Self::Url => "url",
Self::Email => "email",
Self::Time => "time",
Self::Date => "date",
Self::DatetimeLocal => "datetime-local",
Self::Month => "month",
Self::Week => "week",
Self::Number => "number",
}
}
}
#[derive(Default, Clone)]
pub enum InputMode {
#[default]
Text,
Decimal,
Numeric,
Tel,
Search,
Email,
Url,
}
impl InputMode {
pub fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Decimal => "decimal",
Self::Numeric => "numeric",
Self::Search => "search",
Self::Tel => "tel",
Self::Url => "url",
Self::Email => "email",
}
}
}