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 on_code_change: Callback<String>,
30}
31
32impl PartialEq for LimitedInputProps {
33 fn eq(&self, other: &Self) -> bool {
34 self.input_type == other.input_type
35 && self.class == other.class
36 && Rc::ptr_eq(&self.filter, &other.filter)
37 && self.max_len == other.max_len
38 }
39}
40
41#[function_component]
42fn LimitedInput(props: &LimitedInputProps) -> Html {
43 let oninput = {
44 let filter = props.filter.clone();
45 let append_only = props.append_only;
46 let max_len = props.max_len.unwrap_or(usize::MAX);
47 let on_max_len = props.on_max_len.clone();
48 let on_code_change = props.on_code_change.clone();
49
50 move |event: InputEvent| {
51 let Some(target) = event.target() else {
52 error!("input event has no target");
53 return;
54 };
55
56 let Ok(input) = target.dyn_into::<HtmlInputElement>() else {
57 error!("input event target is not an input element");
58 return;
59 };
60
61 let value = input.value();
62
63 let filtered_value: String = value
64 .chars()
65 .filter(|c| (filter)(c))
66 .take(max_len)
67 .collect();
68
69 input.set_value(&filtered_value);
70
71 if append_only {
72 let len = filtered_value.len() as u32;
73 if let Err(err) = input.set_selection_range(len, len) {
74 error!("failed to set selection range: {:?}", err);
75 }
76 }
77
78 on_code_change.emit(filtered_value.clone());
79
80 if filtered_value.len() >= max_len {
81 on_max_len.emit(filtered_value);
82 }
83 }
84 };
85
86 let onkeydown = if props.append_only {
87 |event: KeyboardEvent| {
88 if matches!(
89 event.key().as_str(),
90 "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"
91 ) {
92 event.prevent_default();
93 }
94 }
95 } else {
96 |_event: KeyboardEvent| {}
97 };
98
99 let input_ref = &props.input_ref;
100 let id = &props.id;
101 let name = &props.name;
102 let autocomplete = if props.autocomplete { "on" } else { "off" };
103
104 html! {
105 <input
106 ref={input_ref}
107 {id}
108 {name}
109 {autocomplete}
110 type={props.input_type}
111 class={&props.class}
112 {oninput}
113 {onkeydown}
114 />
115 }
116}
117
118#[derive(Properties)]
119pub struct LimitedTextInputProps {
120 pub input_ref: Option<NodeRef>,
121 #[prop_or_default]
122 pub class: AttrValue,
123 #[prop_or_default]
124 pub id: AttrValue,
125 #[prop_or_default]
126 pub name: AttrValue,
127 #[prop_or(true)]
128 pub autocomplete: bool,
129 #[prop_or(false)]
130 pub append_only: bool,
131 pub filter: Rc<LimitedInputFilter>,
132 pub max_len: Option<usize>,
133 #[prop_or_else(|| Callback::noop())]
134 pub on_max_len: Callback<String>,
135 #[prop_or_else(|| Callback::noop())]
136 pub on_code_change: Callback<String>,
137}
138
139impl PartialEq for LimitedTextInputProps {
140 fn eq(&self, other: &Self) -> bool {
141 self.class == other.class
142 && Rc::ptr_eq(&self.filter, &other.filter)
143 && self.max_len == other.max_len
144 }
145}
146
147#[function_component]
148pub fn LimitedTextInput(props: &LimitedTextInputProps) -> Html {
149 let input_type = "text";
150 let input_ref = props.input_ref.clone().unwrap_or_default();
151 let class = &props.class;
152 let id = &props.id;
153 let name = &props.name;
154 let autocomplete = props.autocomplete;
155 let append_only = props.append_only;
156 let filter = props.filter.clone();
157 let max_len = props.max_len;
158 let on_max_len = props.on_max_len.clone();
159 let on_code_change = props.on_code_change.clone();
160
161 html! {
162 <LimitedInput
163 {input_type}
164 {input_ref}
165 {class}
166 {id}
167 {name}
168 {autocomplete}
169 {append_only}
170 {filter}
171 {max_len}
172 {on_max_len}
173 {on_code_change}
174 />
175 }
176}
177
178#[derive(PartialEq, Properties)]
179pub struct LimitedNumericInputProps {
180 pub input_ref: Option<NodeRef>,
181 #[prop_or_default]
182 pub class: AttrValue,
183 #[prop_or_default]
184 pub id: AttrValue,
185 #[prop_or_default]
186 pub name: AttrValue,
187 #[prop_or(true)]
188 pub autocomplete: bool,
189 #[prop_or(false)]
190 pub append_only: bool,
191 pub max_len: Option<usize>,
192 #[prop_or_else(|| Callback::noop())]
193 pub on_max_len: Callback<String>,
194 #[prop_or_else(|| Callback::noop())]
195 pub on_code_change: Callback<String>,
196}
197
198#[function_component]
199pub fn LimitedNumericInput(props: &LimitedNumericInputProps) -> Html {
200 let input_ref = props.input_ref.clone().unwrap_or_default();
201 let class = &props.class;
202 let id = &props.id;
203 let name = &props.name;
204 let autocomplete = props.autocomplete;
205 let append_only = props.append_only;
206 let filter = input_filter!(|c: &char| c.is_ascii_digit());
207 let max_len = props.max_len;
208 let on_max_len = props.on_max_len.clone();
209 let on_code_change = props.on_code_change.clone();
210
211 html! {
212 <LimitedTextInput
213 {input_ref}
214 {class}
215 {id}
216 {name}
217 {autocomplete}
218 {append_only}
219 {filter}
220 {max_len}
221 {on_max_len}
222 {on_code_change}
223 />
224 }
225}