yew_limput/
lib.rs

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}