1use std::rc::Rc;
2
3use tracing::error;
4use web_sys::HtmlInputElement;
5use web_sys::wasm_bindgen::JsCast;
6use yew::prelude::*;
7
8pub type LimitedInputFilter = dyn for<'a> Fn(&'a char) -> bool;
9
10#[macro_export]
11macro_rules! input_filter {
12 ($filter:expr) => {
13 Rc::new($filter) as Rc<$crate::LimitedInputFilter>
14 };
15}
16
17#[derive(Properties)]
18struct LimitedInputProps {
19 input_type: &'static str,
20 input_ref: NodeRef,
21 class: AttrValue,
22 id: AttrValue,
23 name: AttrValue,
24 autocomplete: bool,
25 append_only: bool,
26 filter: Rc<LimitedInputFilter>,
27 max_len: Option<usize>,
28 on_max_len: Callback<String>,
29}
30
31impl PartialEq for LimitedInputProps {
32 fn eq(&self, other: &Self) -> bool {
33 self.input_type == other.input_type
34 && self.class == other.class
35 && Rc::ptr_eq(&self.filter, &other.filter)
36 && self.max_len == other.max_len
37 }
38}
39
40#[function_component]
41fn LimitedInput(props: &LimitedInputProps) -> Html {
42 let oninput = {
43 let filter = props.filter.clone();
44 let append_only = props.append_only;
45 let max_len = props.max_len.unwrap_or(usize::MAX);
46 let on_max_len = props.on_max_len.clone();
47
48 move |event: InputEvent| {
49 let Some(target) = event.target() else {
50 error!("input event has no target");
51 return;
52 };
53
54 let Ok(input) = target.dyn_into::<HtmlInputElement>() else {
55 error!("input event target is not an input element");
56 return;
57 };
58
59 let value = input.value();
60
61 let filtered_value: String = value
62 .chars()
63 .filter(|c| (filter)(c))
64 .take(max_len)
65 .collect();
66
67 input.set_value(&filtered_value);
68
69 if append_only {
70 let len = filtered_value.len() as u32;
71 if let Err(err) = input.set_selection_range(len, len) {
72 error!("failed to set selection range: {:?}", err);
73 }
74 }
75
76 if filtered_value.len() >= max_len {
77 on_max_len.emit(filtered_value);
78 }
79 }
80 };
81
82 let input_ref = &props.input_ref;
83 let id = &props.id;
84 let name = &props.name;
85 let autocomplete = if props.autocomplete { "on" } else { "off" };
86
87 html! {
88 <input
89 ref={input_ref}
90 {id}
91 {name}
92 {autocomplete}
93 type={props.input_type}
94 class={&props.class}
95 {oninput}
96 />
97 }
98}
99
100#[derive(Properties)]
101pub struct LimitedTextInputProps {
102 pub input_ref: Option<NodeRef>,
103 #[prop_or_default]
104 pub class: AttrValue,
105 #[prop_or_default]
106 pub id: AttrValue,
107 #[prop_or_default]
108 pub name: AttrValue,
109 #[prop_or(true)]
110 pub autocomplete: bool,
111 #[prop_or(false)]
112 pub append_only: bool,
113 pub filter: Rc<LimitedInputFilter>,
114 pub max_len: Option<usize>,
115 #[prop_or_else(|| Callback::noop())]
116 pub on_max_len: Callback<String>,
117}
118
119impl PartialEq for LimitedTextInputProps {
120 fn eq(&self, other: &Self) -> bool {
121 self.class == other.class
122 && Rc::ptr_eq(&self.filter, &other.filter)
123 && self.max_len == other.max_len
124 }
125}
126
127#[function_component]
128pub fn LimitedTextInput(props: &LimitedTextInputProps) -> Html {
129 let input_type = "text";
130 let input_ref = props.input_ref.clone().unwrap_or_default();
131 let class = &props.class;
132 let id = &props.id;
133 let name = &props.name;
134 let autocomplete = props.autocomplete;
135 let append_only = props.append_only;
136 let filter = props.filter.clone();
137 let max_len = props.max_len;
138 let on_max_len = props.on_max_len.clone();
139
140 html! {
141 <LimitedInput
142 {input_type}
143 {input_ref}
144 {class}
145 {id}
146 {name}
147 {autocomplete}
148 {append_only}
149 {filter}
150 {max_len}
151 {on_max_len}
152 />
153 }
154}
155
156#[derive(PartialEq, Properties)]
157pub struct LimitedNumericInputProps {
158 pub input_ref: Option<NodeRef>,
159 #[prop_or_default]
160 pub class: AttrValue,
161 #[prop_or_default]
162 pub id: AttrValue,
163 #[prop_or_default]
164 pub name: AttrValue,
165 #[prop_or(true)]
166 pub autocomplete: bool,
167 #[prop_or(false)]
168 pub append_only: bool,
169 pub max_len: Option<usize>,
170 #[prop_or_else(|| Callback::noop())]
171 pub on_max_len: Callback<String>,
172}
173
174#[function_component]
175pub fn LimitedNumericInput(props: &LimitedNumericInputProps) -> Html {
176 let input_ref = props.input_ref.clone().unwrap_or_default();
177 let class = &props.class;
178 let id = &props.id;
179 let name = &props.name;
180 let autocomplete = props.autocomplete;
181 let append_only = props.append_only;
182 let filter = Rc::new(|c: &char| c.is_ascii_digit()) as Rc<LimitedInputFilter>;
183 let max_len = props.max_len;
184 let on_max_len = props.on_max_len.clone();
185
186 html! {
187 <LimitedTextInput
188 {input_ref}
189 {class}
190 {id}
191 {name}
192 {autocomplete}
193 {append_only}
194 {filter}
195 {max_len}
196 {on_max_len}
197 />
198 }
199}