Skip to main content

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    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}