Skip to main content

dioxus_html/events/
before_input.rs

1use dioxus_core::Event;
2use std::fmt::{self, Debug, Display};
3
4pub type BeforeInputEvent = Event<BeforeInputData>;
5
6/// Define a string-backed enum from a single source-of-truth list that maps each variant
7/// to its canonical DOM string. The two conversion directions — `as_str` (variant → string)
8/// and `From<&str>` (string → variant) — are generated from that one list, so they can never
9/// drift out of sync: a new value only has to be added in one place. The trailing
10/// `_ => Variant(String)` arm declares a catch-all variant whose owned field preserves any
11/// value not covered by the known variants.
12macro_rules! string_enum {
13    (
14        $(#[$enum_meta:meta])*
15        $vis:vis enum $name:ident {
16            $(
17                $variant:ident => $value:literal,
18            )*
19            $(#[$unknown_meta:meta])*
20            _ => $unknown:ident($unknown_ty:ty),
21        }
22    ) => {
23        $(#[$enum_meta])*
24        $vis enum $name {
25            $(
26                $variant,
27            )*
28            $(#[$unknown_meta])*
29            $unknown($unknown_ty),
30        }
31
32        impl $name {
33            /// The canonical `inputType` string for this variant, exactly as emitted by the
34            /// DOM. For [`InputType::Unknown`] this returns the original value.
35            pub fn as_str(&self) -> &str {
36                match self {
37                    $( Self::$variant => $value, )*
38                    Self::$unknown(value) => value,
39                }
40            }
41        }
42
43        impl From<&str> for $name {
44            fn from(value: &str) -> Self {
45                match value {
46                    $( $value => Self::$variant, )*
47                    other => Self::$unknown(other.to_string()),
48                }
49            }
50        }
51    };
52}
53
54string_enum! {
55    /// The kind of mutation that is about to be applied to an editable element.
56    ///
57    /// These map to the `inputType` attribute of the underlying DOM `InputEvent`. The
58    /// full list of values is defined in the W3C Input Events spec at
59    /// <https://w3c.github.io/input-events/#interface-InputEvent-Attributes>. Any value
60    /// not covered by a known variant (e.g. one introduced by a newer user agent) is
61    /// preserved verbatim in [`InputType::Unknown`].
62    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
63    #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
64    #[cfg_attr(feature = "serialize", serde(from = "String", into = "String"))]
65    pub enum InputType {
66        InsertText => "insertText",
67        InsertReplacementText => "insertReplacementText",
68        InsertLineBreak => "insertLineBreak",
69        InsertParagraph => "insertParagraph",
70        InsertOrderedList => "insertOrderedList",
71        InsertUnorderedList => "insertUnorderedList",
72        InsertHorizontalRule => "insertHorizontalRule",
73        InsertFromYank => "insertFromYank",
74        InsertFromDrop => "insertFromDrop",
75        InsertFromPaste => "insertFromPaste",
76        InsertFromPasteAsQuotation => "insertFromPasteAsQuotation",
77        InsertTranspose => "insertTranspose",
78        InsertCompositionText => "insertCompositionText",
79        InsertLink => "insertLink",
80        DeleteWordBackward => "deleteWordBackward",
81        DeleteWordForward => "deleteWordForward",
82        DeleteSoftLineBackward => "deleteSoftLineBackward",
83        DeleteSoftLineForward => "deleteSoftLineForward",
84        DeleteEntireSoftLine => "deleteEntireSoftLine",
85        DeleteHardLineBackward => "deleteHardLineBackward",
86        DeleteHardLineForward => "deleteHardLineForward",
87        DeleteByDrag => "deleteByDrag",
88        DeleteByCut => "deleteByCut",
89        DeleteContent => "deleteContent",
90        DeleteContentBackward => "deleteContentBackward",
91        DeleteContentForward => "deleteContentForward",
92        HistoryUndo => "historyUndo",
93        HistoryRedo => "historyRedo",
94        FormatBold => "formatBold",
95        FormatItalic => "formatItalic",
96        FormatUnderline => "formatUnderline",
97        FormatStrikeThrough => "formatStrikeThrough",
98        FormatSuperscript => "formatSuperscript",
99        FormatSubscript => "formatSubscript",
100        FormatJustifyFull => "formatJustifyFull",
101        FormatJustifyCenter => "formatJustifyCenter",
102        FormatJustifyRight => "formatJustifyRight",
103        FormatJustifyLeft => "formatJustifyLeft",
104        FormatIndent => "formatIndent",
105        FormatOutdent => "formatOutdent",
106        FormatRemove => "formatRemove",
107        FormatSetBlockTextDirection => "formatSetBlockTextDirection",
108        FormatSetInlineTextDirection => "formatSetInlineTextDirection",
109        FormatBackColor => "formatBackColor",
110        FormatFontColor => "formatFontColor",
111        FormatFontName => "formatFontName",
112        /// An `inputType` value not covered by the known variants, preserved as-is.
113        _ => Unknown(String),
114    }
115}
116
117impl From<String> for InputType {
118    fn from(value: String) -> Self {
119        // Reuse the `&str` mapping; only `Unknown` needs to take ownership, and in
120        // that case the borrowed match already produced an owned copy.
121        InputType::from(value.as_str())
122    }
123}
124
125impl From<InputType> for String {
126    fn from(value: InputType) -> Self {
127        match value {
128            InputType::Unknown(value) => value,
129            other => other.as_str().to_string(),
130        }
131    }
132}
133
134impl Display for InputType {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.write_str(self.as_str())
137    }
138}
139
140/// Data fired alongside the `beforeinput` event.
141///
142/// The `beforeinput` event fires before an editable element (an `<input>`, `<textarea>`,
143/// or any element with `contenteditable="true"`) is about to be modified. It exposes
144/// the kind of mutation that is about to happen via [`BeforeInputData::input_type`]
145/// and the text that is about to be inserted (if any) via [`BeforeInputData::data`].
146///
147/// Unlike `input`, `beforeinput` is cancellable: calling `event.prevent_default()`
148/// from the handler will block the user-agent from applying the change.
149///
150/// See <https://developer.mozilla.org/en-US/docs/Web/API/Element/beforeinput_event>
151/// for the underlying DOM event.
152pub struct BeforeInputData {
153    inner: Box<dyn HasBeforeInputData>,
154}
155
156impl BeforeInputData {
157    /// Create a new `BeforeInputData` from any [`HasBeforeInputData`] implementation.
158    pub fn new(inner: impl HasBeforeInputData + 'static) -> Self {
159        Self {
160            inner: Box::new(inner),
161        }
162    }
163
164    /// The type of change that is about to be applied to the editable element.
165    ///
166    /// Returns an [`InputType`] so the common cases (e.g. [`InputType::InsertText`],
167    /// [`InputType::DeleteContentBackward`]) can be matched on directly. Values not
168    /// covered by a known variant are preserved in [`InputType::Unknown`]. The full
169    /// list is documented at
170    /// <https://w3c.github.io/input-events/#interface-InputEvent-Attributes>.
171    pub fn input_type(&self) -> InputType {
172        self.inner.input_type()
173    }
174
175    /// The characters that are about to be inserted by the user agent, if any.
176    ///
177    /// `None` is returned for input types that don't carry text data, e.g. deletions
178    /// or rich-text formatting changes.
179    pub fn data(&self) -> Option<String> {
180        self.inner.data()
181    }
182
183    /// Whether the event was fired while an IME composition session is active.
184    pub fn is_composing(&self) -> bool {
185        self.inner.is_composing()
186    }
187
188    /// The current value of the editable element, prior to the pending change.
189    pub fn value(&self) -> String {
190        self.inner.value()
191    }
192
193    /// Downcast this event to a concrete platform-specific event type.
194    #[inline(always)]
195    pub fn downcast<T: 'static>(&self) -> Option<&T> {
196        self.inner.as_any().downcast_ref::<T>()
197    }
198}
199
200impl<E: HasBeforeInputData> From<E> for BeforeInputData {
201    fn from(e: E) -> Self {
202        Self { inner: Box::new(e) }
203    }
204}
205
206impl PartialEq for BeforeInputData {
207    fn eq(&self, other: &Self) -> bool {
208        self.input_type() == other.input_type()
209            && self.data() == other.data()
210            && self.is_composing() == other.is_composing()
211            && self.value() == other.value()
212    }
213}
214
215impl Debug for BeforeInputData {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.debug_struct("BeforeInputData")
218            .field("input_type", &self.input_type())
219            .field("data", &self.data())
220            .field("is_composing", &self.is_composing())
221            .field("value", &self.value())
222            .finish()
223    }
224}
225
226/// An object exposing all the data backing a [`BeforeInputData`] event.
227pub trait HasBeforeInputData: std::any::Any {
228    /// The type of input change. See [`BeforeInputData::input_type`].
229    fn input_type(&self) -> InputType;
230
231    /// The text data being inserted, or `None` if the change carries no text.
232    fn data(&self) -> Option<String>;
233
234    /// Whether the event is fired during an active IME composition session.
235    fn is_composing(&self) -> bool;
236
237    /// The current value of the editable target, prior to the change.
238    fn value(&self) -> String;
239
240    /// Return self as `Any` so the event can be downcast to its concrete type.
241    fn as_any(&self) -> &dyn std::any::Any;
242}
243
244#[cfg(feature = "serialize")]
245pub use serialize::*;
246
247#[cfg(feature = "serialize")]
248mod serialize {
249    use super::*;
250
251    /// A serialized version of [`BeforeInputData`] used to transport the event
252    /// across the IPC boundary on non-web renderers.
253    #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone, Default)]
254    pub struct SerializedBeforeInputData {
255        pub input_type: String,
256
257        #[serde(default)]
258        pub data: Option<String>,
259
260        #[serde(default)]
261        pub is_composing: bool,
262
263        #[serde(default)]
264        pub value: String,
265    }
266
267    impl SerializedBeforeInputData {
268        pub fn new(
269            input_type: String,
270            data: Option<String>,
271            is_composing: bool,
272            value: String,
273        ) -> Self {
274            Self {
275                input_type,
276                data,
277                is_composing,
278                value,
279            }
280        }
281    }
282
283    impl HasBeforeInputData for SerializedBeforeInputData {
284        fn input_type(&self) -> InputType {
285            InputType::from(self.input_type.as_str())
286        }
287
288        fn data(&self) -> Option<String> {
289            self.data.clone()
290        }
291
292        fn is_composing(&self) -> bool {
293            self.is_composing
294        }
295
296        fn value(&self) -> String {
297            self.value.clone()
298        }
299
300        fn as_any(&self) -> &dyn std::any::Any {
301            self
302        }
303    }
304
305    impl From<&BeforeInputData> for SerializedBeforeInputData {
306        fn from(data: &BeforeInputData) -> Self {
307            Self {
308                input_type: data.input_type().to_string(),
309                data: data.data(),
310                is_composing: data.is_composing(),
311                value: data.value(),
312            }
313        }
314    }
315
316    impl serde::Serialize for BeforeInputData {
317        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
318            SerializedBeforeInputData::from(self).serialize(serializer)
319        }
320    }
321
322    impl<'de> serde::Deserialize<'de> for BeforeInputData {
323        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
324            let data = SerializedBeforeInputData::deserialize(deserializer)?;
325            Ok(Self {
326                inner: Box::new(data),
327            })
328        }
329    }
330}
331
332#[cfg(all(test, feature = "serialize"))]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn serialized_before_input_data_deserializes_missing_optional_fields() {
338        // `input_type` is required; it acts as the discriminator that keeps
339        // beforeinput payloads from being silently matched by other variants
340        // of `EventData` (see packages/html/src/transit.rs).
341        let data: SerializedBeforeInputData =
342            serde_json::from_str(r#"{"input_type": "insertText"}"#).unwrap();
343        assert_eq!(data.input_type, "insertText");
344        assert_eq!(data.data, None);
345        assert!(!data.is_composing);
346        assert_eq!(data.value, "");
347    }
348
349    #[test]
350    fn serialized_before_input_data_rejects_missing_input_type() {
351        assert!(serde_json::from_str::<SerializedBeforeInputData>("{}").is_err());
352    }
353
354    #[test]
355    fn before_input_data_exposes_serialized_fields() {
356        let event = BeforeInputData::new(SerializedBeforeInputData::new(
357            "insertText".to_string(),
358            Some("a".to_string()),
359            false,
360            "hello".to_string(),
361        ));
362
363        assert_eq!(event.input_type(), InputType::InsertText);
364        assert_eq!(event.data().as_deref(), Some("a"));
365        assert!(!event.is_composing());
366        assert_eq!(event.value(), "hello");
367    }
368
369    /// The browser-side serializer (packages/interpreter/src/ts/serialize.ts)
370    /// emits these exact field names for a `beforeinput` event. Pin the shape
371    /// so changes on either side break the test instead of silently degrading
372    /// the event payload.
373    #[test]
374    fn deserializes_payload_from_js_interpreter() {
375        let payload = r#"{
376            "input_type": "insertFromPaste",
377            "data": "pasted",
378            "is_composing": false,
379            "value": "hello pasted",
380            "values": [],
381            "valid": true
382        }"#;
383
384        let data: SerializedBeforeInputData = serde_json::from_str(payload).unwrap();
385        assert_eq!(data.input_type, "insertFromPaste");
386        assert_eq!(data.data.as_deref(), Some("pasted"));
387        assert!(!data.is_composing);
388        assert_eq!(data.value, "hello pasted");
389    }
390
391    #[test]
392    fn input_type_maps_known_values_and_preserves_unknown() {
393        assert_eq!(InputType::from("insertText"), InputType::InsertText);
394        assert_eq!(
395            InputType::from("deleteContentBackward"),
396            InputType::DeleteContentBackward
397        );
398
399        // Known variants round-trip back to their canonical DOM string.
400        assert_eq!(InputType::InsertFromPaste.as_str(), "insertFromPaste");
401        assert_eq!(InputType::InsertFromPaste.to_string(), "insertFromPaste");
402
403        // Unrecognized values are preserved verbatim through the round-trip.
404        let unknown = InputType::from("insertSomethingNew");
405        assert_eq!(
406            unknown,
407            InputType::Unknown("insertSomethingNew".to_string())
408        );
409        assert_eq!(unknown.as_str(), "insertSomethingNew");
410        assert_eq!(String::from(unknown), "insertSomethingNew");
411    }
412
413    #[test]
414    fn round_trips_through_serde() {
415        let original = SerializedBeforeInputData::new(
416            "deleteContentBackward".to_string(),
417            None,
418            true,
419            "hell".to_string(),
420        );
421
422        let json = serde_json::to_string(&original).unwrap();
423        let back: SerializedBeforeInputData = serde_json::from_str(&json).unwrap();
424        assert_eq!(original, back);
425    }
426}