mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Dev/listener multiplexer (#1542)
* yew: partial event listener multiplexer web_sys implementaion compiles but the std_web implementation is unfinished. Keeping this only to commit curretn progress before reverting std_web. * yew: partial event listener multiplexer Feature parity with master, except for bubbling. * yew/listener: fix and test synchronous listeners * yew/listener: add placeholder comments * yew/listener: passive listener test * yew: extend and fix APIs and docs * yew/listener: event bubbling * clippy: ignore warning * Update yew/src/callback.rs Co-authored-by: Simon <simon@siku2.io> * Update yew/src/html/listener/listener_stdweb.rs Co-authored-by: Simon <simon@siku2.io> * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * yew/listner: remove redundant function * yew/listner: restore delibarate formatting * yew/callback: make Flags a newtype * yew/listener: use utility function * yew/listener: deferred listeners * yew/listner: input and change tests * yew/listener: optimize listener registration * yew/listener: remove benchmark placeholders Seems easybench-wasm does not support specifying a module path. * yew/callback: revert CallbackOnce -> Once * yew: convert listener_tests to a build flag * Apply suggestions from code review Co-authored-by: Simon <simon@siku2.io> * yew: fix doc comments * yew: simplify iteration * yew/remove unneeded default passive listeners * yew/listeners: DRY some more * yew/listener: fix clippy warnings * yew/listeners: remove legacy comment * yew/listeners: document stopping propagation * yew/listeners: update tests * ci: see how test run on stable * ci: let's find the new MSRV * ci: try to run integration tests only on stable * yew/test: clean up residual dirty state * yew/listeners: minor doc string and inline fixes * yew/listener: document reasonning for function Co-authored-by: Simon <simon@siku2.io>
This commit is contained in:
parent
60c08736f1
commit
68d2fdbc59
@ -269,7 +269,7 @@ impl ToTokens for HtmlElement {
|
||||
};
|
||||
|
||||
let listeners = if listeners.is_empty() {
|
||||
quote! { ::std::vec![] }
|
||||
quote! { ::yew::virtual_dom::listeners::Listeners::None }
|
||||
} else {
|
||||
let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
|
||||
let name = &label.name;
|
||||
@ -278,7 +278,11 @@ impl ToTokens for HtmlElement {
|
||||
}
|
||||
});
|
||||
|
||||
quote! { ::std::vec![#(#listeners_it),*].into_iter().flatten().collect() }
|
||||
quote! {
|
||||
::yew::virtual_dom::listeners::Listeners::Pending(
|
||||
::std::boxed::Box::new([#(#listeners_it),*])
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: if none of the children have possibly None expressions or literals as keys, we can
|
||||
|
||||
@ -302,69 +302,68 @@ error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<Cow<'stat
|
||||
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
|
||||
= note: required by `into_prop_value`
|
||||
|
||||
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `{integer}`
|
||||
error[E0277]: the trait bound `{integer}: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
|
||||
--> $DIR/element-fail.rs:51:28
|
||||
|
|
||||
51 | html! { <input onclick=1 /> };
|
||||
| ^ expected an `Fn<(MouseEvent,)>` closure, found `{integer}`
|
||||
| ^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not implemented for `{integer}`
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/html/listener/events.rs
|
||||
|
|
||||
| / impl_action! {
|
||||
3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
| / impl_short! {
|
||||
133 | | onauxclick(MouseEvent)
|
||||
134 | | onclick(MouseEvent)
|
||||
135 | |
|
||||
... |
|
||||
102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
103 | | }
|
||||
196 | | ontransitionstart(TransitionEvent)
|
||||
197 | | }
|
||||
| |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new`
|
||||
|
|
||||
= help: the trait `Fn<(MouseEvent,)>` is not implemented for `{integer}`
|
||||
= note: required because of the requirements on the impl of `IntoEventCallback<MouseEvent>` for `{integer}`
|
||||
= help: the following implementations were found:
|
||||
<&'static str as IntoPropValue<Cow<'static, str>>>
|
||||
<&'static str as IntoPropValue<Option<Cow<'static, str>>>>
|
||||
<&'static str as IntoPropValue<Option<String>>>
|
||||
<&'static str as IntoPropValue<String>>
|
||||
and 11 others
|
||||
|
||||
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback<String>`
|
||||
error[E0277]: the trait bound `yew::Callback<String>: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
|
||||
--> $DIR/element-fail.rs:52:29
|
||||
|
|
||||
52 | html! { <input onclick={Callback::from(|a: String| ())} /> };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| expected an implementor of trait `IntoEventCallback<MouseEvent>`
|
||||
| help: consider borrowing here: `&Callback::from(|a: String| ())`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not implemented for `yew::Callback<String>`
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/html/listener/events.rs
|
||||
|
|
||||
| / impl_action! {
|
||||
3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
| / impl_short! {
|
||||
133 | | onauxclick(MouseEvent)
|
||||
134 | | onclick(MouseEvent)
|
||||
135 | |
|
||||
... |
|
||||
102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
103 | | }
|
||||
196 | | ontransitionstart(TransitionEvent)
|
||||
197 | | }
|
||||
| |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new`
|
||||
|
|
||||
= note: the trait bound `yew::Callback<String>: IntoEventCallback<MouseEvent>` is not satisfied
|
||||
= note: required because of the requirements on the impl of `IntoEventCallback<MouseEvent>` for `yew::Callback<String>`
|
||||
|
||||
error[E0277]: the trait bound `Option<{integer}>: IntoEventCallback<FocusEvent>` is not satisfied
|
||||
error[E0277]: the trait bound `Option<{integer}>: IntoPropValue<Option<yew::Callback<FocusEvent>>>` is not satisfied
|
||||
--> $DIR/element-fail.rs:53:29
|
||||
|
|
||||
53 | html! { <input onfocus={Some(5)} /> };
|
||||
| ^^^^^^^ the trait `IntoEventCallback<FocusEvent>` is not implemented for `Option<{integer}>`
|
||||
| ^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<FocusEvent>>>` is not implemented for `Option<{integer}>`
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/html/listener/events.rs
|
||||
|
|
||||
| / impl_action! {
|
||||
3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
| / impl_short! {
|
||||
133 | | onauxclick(MouseEvent)
|
||||
134 | | onclick(MouseEvent)
|
||||
135 | |
|
||||
... |
|
||||
102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
103 | | }
|
||||
196 | | ontransitionstart(TransitionEvent)
|
||||
197 | | }
|
||||
| |_- required by this bound in `yew::html::onfocus::Wrapper::__macro_new`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
<Option<T> as IntoEventCallback<EVENT>>
|
||||
<Option<yew::Callback<EVENT>> as IntoEventCallback<EVENT>>
|
||||
<Option<&'static str> as IntoPropValue<Option<Cow<'static, str>>>>
|
||||
<Option<&'static str> as IntoPropValue<Option<String>>>
|
||||
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
|
||||
|
||||
error[E0277]: the trait bound `(): IntoPropValue<yew::NodeRef>` is not satisfied
|
||||
--> $DIR/element-fail.rs:56:25
|
||||
@ -386,28 +385,22 @@ error[E0277]: the trait bound `Option<yew::NodeRef>: IntoPropValue<yew::NodeRef>
|
||||
<Option<String> as IntoPropValue<Option<Cow<'static, str>>>>
|
||||
= note: required by `into_prop_value`
|
||||
|
||||
error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback<String>`
|
||||
error[E0277]: the trait bound `yew::Callback<String>: IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not satisfied
|
||||
--> $DIR/element-fail.rs:58:29
|
||||
|
|
||||
58 | html! { <input onclick={Callback::from(|a: String| ())} /> };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| expected an implementor of trait `IntoEventCallback<MouseEvent>`
|
||||
| help: consider borrowing here: `&Callback::from(|a: String| ())`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue<Option<yew::Callback<MouseEvent>>>` is not implemented for `yew::Callback<String>`
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/html/listener/events.rs
|
||||
|
|
||||
| / impl_action! {
|
||||
3 | | onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
4 | | onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
5 | | onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
| / impl_short! {
|
||||
133 | | onauxclick(MouseEvent)
|
||||
134 | | onclick(MouseEvent)
|
||||
135 | |
|
||||
... |
|
||||
102 | | ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
103 | | }
|
||||
196 | | ontransitionstart(TransitionEvent)
|
||||
197 | | }
|
||||
| |_- required by this bound in `yew::html::onclick::Wrapper::__macro_new`
|
||||
|
|
||||
= note: the trait bound `yew::Callback<String>: IntoEventCallback<MouseEvent>` is not satisfied
|
||||
= note: required because of the requirements on the impl of `IntoEventCallback<MouseEvent>` for `yew::Callback<String>`
|
||||
|
||||
error[E0277]: the trait bound `NotToString: IntoPropValue<Option<Cow<'static, str>>>` is not satisfied
|
||||
--> $DIR/element-fail.rs:60:28
|
||||
|
||||
@ -54,6 +54,7 @@ features = [
|
||||
"Element",
|
||||
"ErrorEvent",
|
||||
"Event",
|
||||
"EventInit",
|
||||
"EventTarget",
|
||||
"File",
|
||||
"FileList",
|
||||
@ -66,6 +67,7 @@ features = [
|
||||
"HtmlSelectElement",
|
||||
"HtmlTextAreaElement",
|
||||
"InputEvent",
|
||||
"InputEventInit",
|
||||
"KeyboardEvent",
|
||||
"Location",
|
||||
"MessageEvent",
|
||||
|
||||
@ -17,8 +17,18 @@ use std::rc::Rc;
|
||||
/// </aside>
|
||||
/// An `Rc` wrapper is used to make it cloneable.
|
||||
pub enum Callback<IN> {
|
||||
/// A callback which can be called multiple times
|
||||
Callback(Rc<dyn Fn(IN)>),
|
||||
/// A callback which can be called multiple times with optional modifier flags
|
||||
Callback {
|
||||
/// A callback which can be called multiple times
|
||||
cb: Rc<dyn Fn(IN)>,
|
||||
|
||||
/// Setting `passive` to [Some] explicitly makes the event listener passive or not.
|
||||
/// Yew sets sane defaults depending on the type of the listener.
|
||||
/// See
|
||||
/// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
|
||||
passive: Option<bool>,
|
||||
},
|
||||
|
||||
/// A callback which can only be called once. The callback will panic if it is
|
||||
/// called more than once.
|
||||
CallbackOnce(Rc<CallbackOnce<IN>>),
|
||||
@ -28,14 +38,20 @@ type CallbackOnce<IN> = RefCell<Option<Box<dyn FnOnce(IN)>>>;
|
||||
|
||||
impl<IN, F: Fn(IN) + 'static> From<F> for Callback<IN> {
|
||||
fn from(func: F) -> Self {
|
||||
Callback::Callback(Rc::new(func))
|
||||
Callback::Callback {
|
||||
cb: Rc::new(func),
|
||||
passive: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<IN> Clone for Callback<IN> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Callback::Callback(cb) => Callback::Callback(cb.clone()),
|
||||
Callback::Callback { cb, passive } => Callback::Callback {
|
||||
cb: cb.clone(),
|
||||
passive: *passive,
|
||||
},
|
||||
Callback::CallbackOnce(cb) => Callback::CallbackOnce(cb.clone()),
|
||||
}
|
||||
}
|
||||
@ -45,10 +61,16 @@ impl<IN> Clone for Callback<IN> {
|
||||
impl<IN> PartialEq for Callback<IN> {
|
||||
fn eq(&self, other: &Callback<IN>) -> bool {
|
||||
match (&self, &other) {
|
||||
(Callback::Callback(cb), Callback::Callback(other_cb)) => Rc::ptr_eq(cb, other_cb),
|
||||
(Callback::CallbackOnce(cb), Callback::CallbackOnce(other_cb)) => {
|
||||
Rc::ptr_eq(cb, other_cb)
|
||||
}
|
||||
(
|
||||
Callback::Callback { cb, passive },
|
||||
Callback::Callback {
|
||||
cb: rhs_cb,
|
||||
passive: rhs_passive,
|
||||
},
|
||||
) => Rc::ptr_eq(cb, rhs_cb) && passive == rhs_passive,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -57,7 +79,7 @@ impl<IN> PartialEq for Callback<IN> {
|
||||
impl<IN> fmt::Debug for Callback<IN> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let data = match self {
|
||||
Callback::Callback(_) => "Callback<_>",
|
||||
Callback::Callback { .. } => "Callback<_>",
|
||||
Callback::CallbackOnce(_) => "CallbackOnce<_>",
|
||||
};
|
||||
|
||||
@ -69,10 +91,10 @@ impl<IN> Callback<IN> {
|
||||
/// This method calls the callback's function.
|
||||
pub fn emit(&self, value: IN) {
|
||||
match self {
|
||||
Callback::Callback(cb) => cb(value),
|
||||
Callback::Callback { cb, .. } => cb(value),
|
||||
Callback::CallbackOnce(rc) => {
|
||||
let cb = rc.replace(None);
|
||||
let f = cb.expect("callback in CallbackOnce has already been used");
|
||||
let f = cb.expect("callback contains `FnOnce` which has already been used");
|
||||
f(value)
|
||||
}
|
||||
};
|
||||
|
||||
@ -17,7 +17,7 @@ pub struct ContextProviderProps<T: Clone + PartialEq> {
|
||||
/// The context provider component.
|
||||
///
|
||||
/// Every child (direct or indirect) of this component may access the context value.
|
||||
/// In order to consume contexts, [`ComponentLink::context`][Scope::context] method is used,
|
||||
/// In order to consume contexts, [`Scope::context`][Scope::context] method is used,
|
||||
/// In function components the `use_context` hook is used.
|
||||
#[derive(Debug)]
|
||||
pub struct ContextProvider<T: Clone + PartialEq + 'static> {
|
||||
|
||||
@ -255,6 +255,29 @@ impl<COMP: Component> Scope<COMP> {
|
||||
/// synchronously schedules a call to the [Component](Component)
|
||||
/// interface.
|
||||
pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
F: Fn(IN) -> M + 'static,
|
||||
{
|
||||
self.callback_with_passive(None, function)
|
||||
}
|
||||
|
||||
/// Creates a `Callback` which will send a message to the linked
|
||||
/// component's update method when invoked.
|
||||
///
|
||||
/// Setting `passive` to [Some] explicitly makes the event listener passive or not.
|
||||
/// Yew sets sane defaults depending on the type of the listener.
|
||||
/// See
|
||||
/// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
|
||||
///
|
||||
/// Please be aware that currently the result of this callback
|
||||
/// synchronously schedules a call to the [Component](Component)
|
||||
/// interface.
|
||||
pub fn callback_with_passive<F, IN, M>(
|
||||
&self,
|
||||
passive: impl Into<Option<bool>>,
|
||||
function: F,
|
||||
) -> Callback<IN>
|
||||
where
|
||||
M: Into<COMP::Message>,
|
||||
F: Fn(IN) -> M + 'static,
|
||||
@ -264,7 +287,10 @@ impl<COMP: Component> Scope<COMP> {
|
||||
let output = function(input);
|
||||
scope.send_message(output);
|
||||
};
|
||||
closure.into()
|
||||
Callback::Callback {
|
||||
passive: passive.into(),
|
||||
cb: Rc::new(closure),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Callback` from an `FnOnce` which will send a message
|
||||
|
||||
@ -1,103 +1,216 @@
|
||||
// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events
|
||||
impl_action! {
|
||||
onabort(name: "abort", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onauxclick(name: "auxclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onblur(name: "blur", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
oncancel(name: "cancel", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
oncanplay(name: "canplay", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
oncanplaythrough(name: "canplaythrough", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onchange(name: "change", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onclick(name: "click", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onclose(name: "close", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
oncontextmenu(name: "contextmenu", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
oncuechange(name: "cuechange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
ondblclick(name: "dblclick", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
ondrag(name: "drag", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragend(name: "dragend", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragenter(name: "dragenter", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragexit(name: "dragexit", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragleave(name: "dragleave", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragover(name: "dragover", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondragstart(name: "dragstart", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondrop(name: "drop", event: DragEvent) -> web_sys::DragEvent => |_, event| { event }
|
||||
ondurationchange(name: "durationchange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onemptied(name: "emptied", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onended(name: "ended", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onerror(name: "error", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onfocus(name: "focus", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
onfocusin(name: "focusin", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
onfocusout(name: "focusout", event: FocusEvent) -> web_sys::FocusEvent => |_, event| { event }
|
||||
// web_sys doesn't have a struct for `FormDataEvent`
|
||||
onformdata(name: "formdata", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
oninput(name: "input", event: InputEvent) -> web_sys::InputEvent => |_, event| { event }
|
||||
oninvalid(name: "invalid", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onkeydown(name: "keydown", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event }
|
||||
onkeypress(name: "keypress", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event }
|
||||
onkeyup(name: "keyup", event: KeyboardEvent) -> web_sys::KeyboardEvent => |_, event| { event }
|
||||
onload(name: "load", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onloadeddata(name: "loadeddata", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onloadedmetadata(name: "loadedmetadata", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onloadstart(name: "loadstart", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event }
|
||||
onmousedown(name: "mousedown", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmouseenter(name: "mouseenter", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmouseleave(name: "mouseleave", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmousemove(name: "mousemove", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmouseout(name: "mouseout", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmouseover(name: "mouseover", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onmouseup(name: "mouseup", event: MouseEvent) -> web_sys::MouseEvent => |_, event| { event }
|
||||
onpause(name: "pause", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onplay(name: "play", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onplaying(name: "playing", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onprogress(name: "progress", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event }
|
||||
onratechange(name: "ratechange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onreset(name: "reset", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onresize(name: "resize", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onscroll(name: "scroll", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onsecuritypolicyviolation(name: "securitypolicyviolation", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onseeked(name: "seeked", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onseeking(name: "seeking", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onselect(name: "select", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onslotchange(name: "slotchange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onstalled(name: "stalled", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
// web_sys doesn't have a struct for `SubmitEvent`
|
||||
onsubmit(name: "submit", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onsuspend(name: "suspend", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
ontimeupdate(name: "timeupdate", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
ontoggle(name: "toggle", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onvolumechange(name: "volumechange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onwaiting(name: "waiting", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onwheel(name: "wheel", event: WheelEvent) -> web_sys::WheelEvent => |_, event| { event }
|
||||
|
||||
oncopy(name: "copy", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
oncut(name: "cut", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onpaste(name: "paste", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
macro_rules! impl_action {
|
||||
($($action:ident($type:ident) -> $ret:path => $convert:path)*) => {$(
|
||||
impl_action!($action($type, false) -> $ret => $convert);
|
||||
)*};
|
||||
($($action:ident($type:ident, $passive:literal) -> $ret:path => $convert:path)*) => {$(
|
||||
/// An abstract implementation of a listener.
|
||||
#[doc(hidden)]
|
||||
pub mod $action {
|
||||
use crate::callback::Callback;
|
||||
use crate::virtual_dom::{Listener, ListenerKind};
|
||||
use std::rc::Rc;
|
||||
|
||||
onanimationcancel(name: "animationcancel", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event }
|
||||
onanimationend(name: "animationend", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event }
|
||||
onanimationiteration(name: "animationiteration", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event }
|
||||
onanimationstart(name: "animationstart", event: AnimationEvent) -> web_sys::AnimationEvent => |_, event| { event }
|
||||
ongotpointercapture(name: "gotpointercapture", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onloadend(name: "loadend", event: ProgressEvent) -> web_sys::ProgressEvent => |_, event| { event }
|
||||
onlostpointercapture(name: "lostpointercapture", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointercancel(name: "pointercancel", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerdown(name: "pointerdown", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerenter(name: "pointerenter", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerleave(name: "pointerleave", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerlockchange(name: "pointerlockchange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onpointerlockerror(name: "pointerlockerror", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onpointermove(name: "pointermove", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerout(name: "pointerout", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerover(name: "pointerover", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onpointerup(name: "pointerup", event: PointerEvent) -> web_sys::PointerEvent => |_, event| { event }
|
||||
onselectionchange(name: "selectionchange", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onselectstart(name: "selectstart", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
onshow(name: "show", event: Event) -> web_sys::Event => |_, event| { event }
|
||||
ontouchcancel(name: "touchcancel", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event }
|
||||
ontouchend(name: "touchend", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event }
|
||||
ontouchmove(name: "touchmove", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event }
|
||||
ontouchstart(name: "touchstart", event: TouchEvent) -> web_sys::TouchEvent => |_, event| { event }
|
||||
ontransitioncancel(name: "transitioncancel", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
ontransitionend(name: "transitionend", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
ontransitionrun(name: "transitionrun", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
ontransitionstart(name: "transitionstart", event: TransitionEvent) -> web_sys::TransitionEvent => |_, event| { event }
|
||||
/// A wrapper for a callback which attaches event listeners to elements.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Wrapper {
|
||||
callback: Callback<Event>,
|
||||
}
|
||||
|
||||
impl Wrapper {
|
||||
/// Create a wrapper for an event-typed callback
|
||||
pub fn new(callback: Callback<Event>) -> Self {
|
||||
Wrapper { callback }
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
pub fn __macro_new(
|
||||
callback: impl crate::html::IntoPropValue<Option<Callback<Event>>>,
|
||||
) -> Option<Rc<dyn Listener>> {
|
||||
let callback = callback.into_prop_value()?;
|
||||
Some(Rc::new(Self::new(callback)))
|
||||
}
|
||||
}
|
||||
|
||||
/// And event type which keeps the returned type.
|
||||
pub type Event = $ret;
|
||||
|
||||
impl Listener for Wrapper {
|
||||
fn kind(&self) -> ListenerKind {
|
||||
ListenerKind::$action
|
||||
}
|
||||
|
||||
fn handle(&self, event: web_sys::Event) {
|
||||
self.callback.emit($convert(event));
|
||||
}
|
||||
|
||||
fn passive(&self) -> bool {
|
||||
match &self.callback {
|
||||
Callback::Callback{passive, ..} => (*passive).unwrap_or($passive),
|
||||
_ => $passive,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
// Reduces repetition for common cases
|
||||
macro_rules! impl_short {
|
||||
($($action:ident)*) => {
|
||||
impl_action! {
|
||||
$(
|
||||
$action(Event) -> web_sys::Event => std::convert::identity
|
||||
)*
|
||||
}
|
||||
};
|
||||
($($action:ident($type:ident))*) => {
|
||||
impl_action! {
|
||||
$(
|
||||
$action($type) -> web_sys::$type => crate::html::listener::cast_event
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Unspecialized event type
|
||||
impl_short! {
|
||||
onabort
|
||||
oncancel
|
||||
oncanplay
|
||||
oncanplaythrough
|
||||
onclose
|
||||
oncuechange
|
||||
ondurationchange
|
||||
onemptied
|
||||
onended
|
||||
onerror
|
||||
onformdata // web_sys doesn't have a struct for `FormDataEvent`
|
||||
oninvalid
|
||||
|
||||
onload
|
||||
onloadeddata
|
||||
onloadedmetadata
|
||||
|
||||
onpause
|
||||
onplay
|
||||
onplaying
|
||||
|
||||
onratechange
|
||||
onreset
|
||||
onresize
|
||||
onsecuritypolicyviolation
|
||||
|
||||
onseeked
|
||||
onseeking
|
||||
|
||||
onselect
|
||||
onslotchange
|
||||
onstalled
|
||||
onsuspend
|
||||
ontimeupdate
|
||||
ontoggle
|
||||
onvolumechange
|
||||
onwaiting
|
||||
|
||||
onchange
|
||||
|
||||
oncopy
|
||||
oncut
|
||||
onpaste
|
||||
|
||||
onpointerlockchange
|
||||
onpointerlockerror
|
||||
onselectionchange
|
||||
onselectstart
|
||||
onshow
|
||||
}
|
||||
|
||||
// Specialized event type
|
||||
impl_short! {
|
||||
onauxclick(MouseEvent)
|
||||
onclick(MouseEvent)
|
||||
|
||||
oncontextmenu(MouseEvent)
|
||||
ondblclick(MouseEvent)
|
||||
|
||||
ondrag(DragEvent)
|
||||
ondragend(DragEvent)
|
||||
ondragenter(DragEvent)
|
||||
ondragexit(DragEvent)
|
||||
ondragleave(DragEvent)
|
||||
ondragover(DragEvent)
|
||||
ondragstart(DragEvent)
|
||||
ondrop(DragEvent)
|
||||
|
||||
onblur(FocusEvent)
|
||||
onfocus(FocusEvent)
|
||||
onfocusin(FocusEvent)
|
||||
onfocusout(FocusEvent)
|
||||
|
||||
onkeydown(KeyboardEvent)
|
||||
onkeypress(KeyboardEvent)
|
||||
onkeyup(KeyboardEvent)
|
||||
|
||||
onloadstart(ProgressEvent)
|
||||
onprogress(ProgressEvent)
|
||||
onloadend(ProgressEvent)
|
||||
|
||||
onmousedown(MouseEvent)
|
||||
onmouseenter(MouseEvent)
|
||||
onmouseleave(MouseEvent)
|
||||
onmousemove(MouseEvent)
|
||||
onmouseout(MouseEvent)
|
||||
onmouseover(MouseEvent)
|
||||
onmouseup(MouseEvent)
|
||||
onwheel(WheelEvent)
|
||||
|
||||
oninput(InputEvent)
|
||||
|
||||
onsubmit(FocusEvent)
|
||||
|
||||
onanimationcancel(AnimationEvent)
|
||||
onanimationend(AnimationEvent)
|
||||
onanimationiteration(AnimationEvent)
|
||||
onanimationstart(AnimationEvent)
|
||||
|
||||
ongotpointercapture(PointerEvent)
|
||||
onlostpointercapture(PointerEvent)
|
||||
onpointercancel(PointerEvent)
|
||||
onpointerdown(PointerEvent)
|
||||
onpointerenter(PointerEvent)
|
||||
onpointerleave(PointerEvent)
|
||||
onpointermove(PointerEvent)
|
||||
onpointerout(PointerEvent)
|
||||
onpointerover(PointerEvent)
|
||||
onpointerup(PointerEvent)
|
||||
|
||||
ontouchcancel(TouchEvent)
|
||||
ontouchend(TouchEvent)
|
||||
|
||||
ontransitioncancel(TransitionEvent)
|
||||
ontransitionend(TransitionEvent)
|
||||
ontransitionrun(TransitionEvent)
|
||||
ontransitionstart(TransitionEvent)
|
||||
}
|
||||
|
||||
macro_rules! impl_passive {
|
||||
($($action:ident($type:ident))*) => {
|
||||
impl_action! {
|
||||
$(
|
||||
$action($type, true) -> web_sys::$type
|
||||
=> crate::html::listener::cast_event
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Best used with passive listeners for responsiveness
|
||||
impl_passive! {
|
||||
onscroll(Event)
|
||||
|
||||
ontouchmove(TouchEvent)
|
||||
ontouchstart(TouchEvent)
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod events;
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
@ -8,6 +7,19 @@ use web_sys::{Event, EventTarget};
|
||||
use crate::Callback;
|
||||
pub use events::*;
|
||||
|
||||
/// Cast [Event] `e` into it's target `T`.
|
||||
///
|
||||
/// This function mainly exists to provide type inference in the [impl_action] macro to the compiler
|
||||
/// and avoid some verbosity by not having to type the signature over and over in closure
|
||||
/// definitions.
|
||||
#[inline]
|
||||
pub(crate) fn cast_event<T>(e: Event) -> T
|
||||
where
|
||||
T: JsCast,
|
||||
{
|
||||
e.unchecked_into()
|
||||
}
|
||||
|
||||
/// A trait to obtain a generic event target.
|
||||
///
|
||||
/// The methods in this trait are convenient helpers that use the [`JsCast`] trait internally
|
||||
|
||||
@ -271,6 +271,8 @@ pub use web_sys;
|
||||
pub mod events {
|
||||
pub use crate::html::TargetCast;
|
||||
|
||||
pub use crate::virtual_dom::listeners::set_event_bubbling;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use web_sys::{
|
||||
AnimationEvent, DragEvent, ErrorEvent, Event, FocusEvent, InputEvent, KeyboardEvent,
|
||||
|
||||
834
packages/yew/src/virtual_dom/listeners.rs
Normal file
834
packages/yew/src/virtual_dom/listeners.rs
Normal file
@ -0,0 +1,834 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
};
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use web_sys::{Element, Event};
|
||||
|
||||
thread_local! {
|
||||
/// Global event listener registry
|
||||
static REGISTRY: RefCell<Registry> = Default::default();
|
||||
|
||||
/// Key used to store listener id on element
|
||||
static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into();
|
||||
|
||||
/// Cached reference to the document body
|
||||
static BODY: web_sys::HtmlElement = crate::utils::document().body().unwrap();
|
||||
}
|
||||
|
||||
/// Bubble events during delegation
|
||||
static mut BUBBLE_EVENTS: bool = true;
|
||||
|
||||
/// Set, if events should bubble up the DOM tree, calling any matching callbacks.
|
||||
///
|
||||
/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event
|
||||
/// handling performance.
|
||||
///
|
||||
/// Note that yew uses event delegation and implements internal even bubbling for performance
|
||||
/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event
|
||||
/// handler has no effect.
|
||||
///
|
||||
/// This function should be called before any component is mounted.
|
||||
pub fn set_event_bubbling(bubble: bool) {
|
||||
unsafe {
|
||||
BUBBLE_EVENTS = bubble;
|
||||
}
|
||||
}
|
||||
|
||||
/// The [Listener] trait is an universal implementation of an event listener
|
||||
/// which is used to bind Rust-listener to JS-listener (DOM).
|
||||
pub trait Listener {
|
||||
/// Returns the name of the event
|
||||
fn kind(&self) -> ListenerKind;
|
||||
|
||||
/// Handles an event firing
|
||||
fn handle(&self, event: web_sys::Event);
|
||||
|
||||
/// Makes the event listener passive. See
|
||||
/// [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
|
||||
fn passive(&self) -> bool;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Listener {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Listener {{ kind: {}, passive: {:?} }}",
|
||||
self.kind().as_ref(),
|
||||
self.passive(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! gen_listener_kinds {
|
||||
($($kind:ident)*) => {
|
||||
/// Supported kinds of DOM event listeners
|
||||
// Using instead of strings to optimise registry collection performance by simplifying
|
||||
// hashmap hash calculation.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ListenerKind {
|
||||
$( $kind, )*
|
||||
}
|
||||
|
||||
impl AsRef<str> for ListenerKind {
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
$( Self::$kind => stringify!($kind), )*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gen_listener_kinds! {
|
||||
onabort
|
||||
onauxclick
|
||||
onblur
|
||||
oncancel
|
||||
oncanplay
|
||||
oncanplaythrough
|
||||
onchange
|
||||
onclick
|
||||
onclose
|
||||
oncontextmenu
|
||||
oncuechange
|
||||
ondblclick
|
||||
ondrag
|
||||
ondragend
|
||||
ondragenter
|
||||
ondragexit
|
||||
ondragleave
|
||||
ondragover
|
||||
ondragstart
|
||||
ondrop
|
||||
ondurationchange
|
||||
onemptied
|
||||
onended
|
||||
onerror
|
||||
onfocus
|
||||
onfocusin
|
||||
onfocusout
|
||||
onformdata
|
||||
oninput
|
||||
oninvalid
|
||||
onkeydown
|
||||
onkeypress
|
||||
onkeyup
|
||||
onload
|
||||
onloadeddata
|
||||
onloadedmetadata
|
||||
onloadstart
|
||||
onmousedown
|
||||
onmouseenter
|
||||
onmouseleave
|
||||
onmousemove
|
||||
onmouseout
|
||||
onmouseover
|
||||
onmouseup
|
||||
onpause
|
||||
onplay
|
||||
onplaying
|
||||
onprogress
|
||||
onratechange
|
||||
onreset
|
||||
onresize
|
||||
onscroll
|
||||
onsecuritypolicyviolation
|
||||
onseeked
|
||||
onseeking
|
||||
onselect
|
||||
onslotchange
|
||||
onstalled
|
||||
onsubmit
|
||||
onsuspend
|
||||
ontimeupdate
|
||||
ontoggle
|
||||
onvolumechange
|
||||
onwaiting
|
||||
onwheel
|
||||
oncopy
|
||||
oncut
|
||||
onpaste
|
||||
onanimationcancel
|
||||
onanimationend
|
||||
onanimationiteration
|
||||
onanimationstart
|
||||
ongotpointercapture
|
||||
onloadend
|
||||
onlostpointercapture
|
||||
onpointercancel
|
||||
onpointerdown
|
||||
onpointerenter
|
||||
onpointerleave
|
||||
onpointerlockchange
|
||||
onpointerlockerror
|
||||
onpointermove
|
||||
onpointerout
|
||||
onpointerover
|
||||
onpointerup
|
||||
onselectionchange
|
||||
onselectstart
|
||||
onshow
|
||||
ontouchcancel
|
||||
ontouchend
|
||||
ontouchmove
|
||||
ontouchstart
|
||||
ontransitioncancel
|
||||
ontransitionend
|
||||
ontransitionrun
|
||||
ontransitionstart
|
||||
}
|
||||
|
||||
/// A list of event listeners
|
||||
#[derive(Debug)]
|
||||
pub enum Listeners {
|
||||
/// No listeners registered or pending.
|
||||
/// Distinct from `Pending` with an empty slice to avoid an allocation.
|
||||
None,
|
||||
|
||||
/// Added to global registry by ID
|
||||
Registered(u32),
|
||||
|
||||
/// Not yet added to the element or registry
|
||||
Pending(Box<[Option<Rc<dyn Listener>>]>),
|
||||
}
|
||||
|
||||
impl Listeners {
|
||||
/// Register listeners and return their handle ID
|
||||
fn register(el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> Self {
|
||||
Self::Registered(Registry::with(|reg| {
|
||||
let id = reg.set_listener_id(el);
|
||||
reg.register(id, pending);
|
||||
id
|
||||
}))
|
||||
}
|
||||
|
||||
/// Remove any registered event listeners from the global registry
|
||||
pub(super) fn unregister(&self) {
|
||||
if let Self::Registered(id) = self {
|
||||
Registry::with(|r| r.unregister(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Apply for Listeners {
|
||||
type Element = Element;
|
||||
|
||||
fn apply(&mut self, el: &Self::Element) {
|
||||
if let Self::Pending(pending) = self {
|
||||
*self = Self::register(el, pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) {
|
||||
use Listeners::*;
|
||||
|
||||
match (std::mem::take(self), ancestor) {
|
||||
(Pending(pending), Registered(id)) => {
|
||||
// Reuse the ID
|
||||
Registry::with(|reg| reg.patch(&id, &*pending));
|
||||
*self = Registered(id);
|
||||
}
|
||||
(Pending(pending), None) => {
|
||||
*self = Self::register(el, &pending);
|
||||
}
|
||||
(None, Registered(id)) => {
|
||||
Registry::with(|reg| reg.unregister(&id));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Listeners {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
use Listeners::*;
|
||||
|
||||
match (self, rhs) {
|
||||
(None, None) => true,
|
||||
(Registered(lhs), Registered(rhs)) => lhs == rhs,
|
||||
(Registered(registered_id), Pending(pending))
|
||||
| (Pending(pending), Registered(registered_id)) => {
|
||||
use std::option::Option::None;
|
||||
|
||||
Registry::with(|reg| match reg.by_id.get(registered_id) {
|
||||
Some(reg) => {
|
||||
if reg.len() != pending.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
pending.iter().filter_map(|l| l.as_ref()).all(|l| {
|
||||
match reg.get(&EventDescriptor::from(l.deref())) {
|
||||
Some(reg) => reg.iter().any(|reg| {
|
||||
#[allow(clippy::vtable_address_comparisons)]
|
||||
Rc::ptr_eq(reg, l)
|
||||
}),
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
}
|
||||
(Pending(lhs), Pending(rhs)) => {
|
||||
if lhs.len() != rhs.len() {
|
||||
false
|
||||
} else {
|
||||
use std::option::Option::None;
|
||||
|
||||
lhs.iter()
|
||||
.zip(rhs.iter())
|
||||
.all(|(lhs, rhs)| match (lhs, rhs) {
|
||||
(Some(lhs), Some(rhs)) =>
|
||||
{
|
||||
#[allow(clippy::vtable_address_comparisons)]
|
||||
Rc::ptr_eq(lhs, rhs)
|
||||
}
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Listeners {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::None | Self::Registered(_) => Self::None,
|
||||
Self::Pending(v) => Self::Pending(v.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Listeners {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
|
||||
struct EventDescriptor {
|
||||
kind: ListenerKind,
|
||||
passive: bool,
|
||||
}
|
||||
|
||||
impl From<&dyn Listener> for EventDescriptor {
|
||||
fn from(l: &dyn Listener) -> Self {
|
||||
Self {
|
||||
kind: l.kind(),
|
||||
passive: l.passive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures global event handler registration.
|
||||
//
|
||||
// Separate struct to DRY, while avoiding partial struct mutability.
|
||||
#[derive(Default, Debug)]
|
||||
struct GlobalHandlers {
|
||||
/// Events with registered handlers that are possibly passive
|
||||
handling: HashSet<EventDescriptor>,
|
||||
|
||||
/// Keep track of all listeners to drop them on registry drop.
|
||||
/// The registry is never dropped in production.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
registered: Vec<(ListenerKind, Closure<dyn Fn(web_sys::Event)>)>,
|
||||
}
|
||||
|
||||
impl GlobalHandlers {
|
||||
/// Ensure a descriptor has a global event handler assigned
|
||||
fn ensure_handled(&mut self, desc: EventDescriptor) {
|
||||
if !self.handling.contains(&desc) {
|
||||
let cl = BODY.with(|body| {
|
||||
let cl = Closure::wrap(
|
||||
Box::new(move |e: Event| Registry::handle(desc, e)) as Box<dyn Fn(Event)>
|
||||
);
|
||||
AsRef::<web_sys::EventTarget>::as_ref(body)
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
&desc.kind.as_ref()[2..],
|
||||
cl.as_ref().unchecked_ref(),
|
||||
&{
|
||||
let mut opts = web_sys::AddEventListenerOptions::new();
|
||||
if desc.passive {
|
||||
opts.passive(true);
|
||||
}
|
||||
opts
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("could not register global listener: {:?}", e))
|
||||
.unwrap();
|
||||
cl
|
||||
});
|
||||
|
||||
// Never drop the closure as this event handler is static
|
||||
#[cfg(not(test))]
|
||||
cl.forget();
|
||||
#[cfg(test)]
|
||||
self.registered.push((desc.kind, cl));
|
||||
|
||||
self.handling.insert(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable resetting between tests
|
||||
#[cfg(test)]
|
||||
impl Drop for GlobalHandlers {
|
||||
fn drop(&mut self) {
|
||||
BODY.with(|body| {
|
||||
for (kind, cl) in std::mem::take(&mut self.registered) {
|
||||
AsRef::<web_sys::EventTarget>::as_ref(body)
|
||||
.remove_event_listener_with_callback(
|
||||
&kind.as_ref()[2..],
|
||||
cl.as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Global multiplexing event handler registry
|
||||
#[derive(Default, Debug)]
|
||||
struct Registry {
|
||||
/// Counter for assigning new IDs
|
||||
id_counter: u32,
|
||||
|
||||
/// Registered global event handlers
|
||||
global: GlobalHandlers,
|
||||
|
||||
/// Contains all registered event listeners by listener ID
|
||||
by_id: HashMap<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// Run f with access to global Registry
|
||||
#[inline]
|
||||
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> R {
|
||||
REGISTRY.with(|r| f(&mut *r.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Register all passed listeners under ID
|
||||
fn register(&mut self, id: u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
let mut by_desc =
|
||||
HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::with_capacity(listeners.len());
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc);
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
self.by_id.insert(id, by_desc);
|
||||
}
|
||||
|
||||
/// Patch an already registered set of handlers
|
||||
fn patch(&mut self, id: &u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
if let Some(by_desc) = self.by_id.get_mut(id) {
|
||||
// Keeping empty vectors is fine. Those don't do much and should happen rarely.
|
||||
for v in by_desc.values_mut() {
|
||||
v.clear()
|
||||
}
|
||||
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc);
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister any existing listeners for ID
|
||||
fn unregister(&mut self, id: &u32) {
|
||||
self.by_id.remove(id);
|
||||
}
|
||||
|
||||
/// Set unique listener ID onto element and return it
|
||||
fn set_listener_id(&mut self, el: &Element) -> u32 {
|
||||
let id = self.id_counter;
|
||||
self.id_counter += 1;
|
||||
|
||||
LISTENER_ID_PROP.with(|prop| {
|
||||
if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() {
|
||||
panic!("failed to set listener ID property");
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Handle a global event firing
|
||||
fn handle(desc: EventDescriptor, event: Event) {
|
||||
let target = match event
|
||||
.target()
|
||||
.map(|el| el.dyn_into::<web_sys::Element>().ok())
|
||||
.flatten()
|
||||
{
|
||||
Some(el) => el,
|
||||
None => return,
|
||||
};
|
||||
|
||||
Self::run_handlers(desc, event, target);
|
||||
}
|
||||
|
||||
fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) {
|
||||
let run_handler = |el: &web_sys::Element| {
|
||||
if let Some(l) = LISTENER_ID_PROP
|
||||
.with(|prop| js_sys::Reflect::get(el, prop).ok())
|
||||
.map(|v| v.dyn_into().ok())
|
||||
.flatten()
|
||||
.map(|num: js_sys::Number| {
|
||||
Registry::with(|r| {
|
||||
r.by_id
|
||||
.get(&(num.value_of() as u32))
|
||||
.map(|s| s.get(&desc))
|
||||
.flatten()
|
||||
.cloned()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
for l in l {
|
||||
l.handle(event.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_handler(&target);
|
||||
|
||||
if unsafe { BUBBLE_EVENTS } {
|
||||
let mut el = target;
|
||||
loop {
|
||||
el = match el.parent_element() {
|
||||
Some(el) => el,
|
||||
None => break,
|
||||
};
|
||||
// XXX: we have no way to detect, if the callback called `Event.stopPropagation()`
|
||||
// or `Event.stopImmediatePropagation()` without breaking the callback API.
|
||||
// It's arguably not worth the cost.
|
||||
run_handler(&el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test"))]
|
||||
mod tests {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
use crate::{html, html::TargetCast, utils::document, AppHandle, Component, Context, Html};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Click,
|
||||
StopListening,
|
||||
SetText(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
stop_listening: bool,
|
||||
clicked: u32,
|
||||
text: String,
|
||||
}
|
||||
|
||||
trait Mixin {
|
||||
fn passive() -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<a>{state.clicked}</a>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<a onclick={ctx.link().callback_with_passive(
|
||||
Self::passive(),
|
||||
|_| Message::Click,
|
||||
)}>
|
||||
{state.clicked}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
state: State,
|
||||
pd: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M> Component for Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
type Message = Message;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp {
|
||||
state: Default::default(),
|
||||
pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Message::Click => {
|
||||
self.state.clicked += 1;
|
||||
}
|
||||
Message::StopListening => {
|
||||
self.state.stop_listening = true;
|
||||
}
|
||||
Message::SetText(s) => {
|
||||
self.state.text = s;
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> crate::Html {
|
||||
M::view(ctx, &self.state)
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_count(el: &web_sys::HtmlElement, count: isize) {
|
||||
assert_eq!(el.text_content(), Some(count.to_string()))
|
||||
}
|
||||
|
||||
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
|
||||
document()
|
||||
.query_selector(tag)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement)
|
||||
where
|
||||
M: Mixin,
|
||||
{
|
||||
// Remove any existing listeners and elements
|
||||
super::Registry::with(|r| *r = Default::default());
|
||||
if let Some(el) = document().query_selector(tag).unwrap() {
|
||||
el.parent_element().unwrap().remove();
|
||||
}
|
||||
|
||||
let root = document().create_element("div").unwrap();
|
||||
document().body().unwrap().append_child(&root).unwrap();
|
||||
let app = crate::start_app_in_element::<Comp<M>>(root);
|
||||
|
||||
(app, get_el_by_tag(tag))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synchronous() {
|
||||
struct Synchronous;
|
||||
|
||||
impl Mixin for Synchronous {}
|
||||
|
||||
let (link, el) = init::<Synchronous>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
async fn await_animation_frame() {
|
||||
JsFuture::from(js_sys::Promise::new(&mut |resolve, _| {
|
||||
crate::utils::window()
|
||||
.request_animation_frame(&resolve)
|
||||
.unwrap();
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn passive() {
|
||||
struct Passive;
|
||||
|
||||
impl Mixin for Passive {
|
||||
fn passive() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
assert_async::<Passive>().await;
|
||||
}
|
||||
|
||||
async fn assert_async<M: Mixin + 'static>() {
|
||||
let (link, el) = init::<M>("a");
|
||||
|
||||
macro_rules! assert_after_click {
|
||||
($c:expr) => {
|
||||
el.click();
|
||||
await_animation_frame().await;
|
||||
assert_count(&el, $c);
|
||||
};
|
||||
}
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
assert_after_click!(1);
|
||||
|
||||
assert_after_click!(2);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
assert_after_click!(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubbling() {
|
||||
struct Bubbling;
|
||||
|
||||
impl Mixin for Bubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<a>
|
||||
{state.clicked}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
let cb = ctx.link().callback(|_| Message::Click);
|
||||
html! {
|
||||
<div onclick={cb.clone()}>
|
||||
<a onclick={cb}>
|
||||
{state.clicked}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, el) = init::<Bubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
}
|
||||
|
||||
fn test_input_listener<E>(make_event: impl Fn() -> E)
|
||||
where
|
||||
E: JsCast + std::fmt::Debug,
|
||||
{
|
||||
struct Input;
|
||||
|
||||
impl Mixin for Input {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<input type="text" />
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
onchange={ctx.link().callback(|e: web_sys::Event| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
Message::SetText(el.value())
|
||||
})}
|
||||
oninput={ctx.link().callback(|e: web_sys::InputEvent| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
Message::SetText(el.value())
|
||||
})}
|
||||
/>
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, input_el) = init::<Input>("input");
|
||||
let input_el = input_el.dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
let p_el = get_el_by_tag("p");
|
||||
|
||||
assert_eq!(&p_el.text_content().unwrap(), "");
|
||||
for mut s in ["foo", "bar", "baz"].iter() {
|
||||
input_el.set_value(s);
|
||||
if s == &"baz" {
|
||||
link.send_message(Message::StopListening);
|
||||
s = &"bar";
|
||||
}
|
||||
input_el
|
||||
.dyn_ref::<web_sys::EventTarget>()
|
||||
.unwrap()
|
||||
.dispatch_event(&make_event().dyn_into().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(&p_el.text_content().unwrap(), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oninput() {
|
||||
test_input_listener(|| {
|
||||
web_sys::InputEvent::new_with_event_init_dict(
|
||||
"input",
|
||||
&web_sys::InputEventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onchange() {
|
||||
test_input_listener(|| {
|
||||
web_sys::Event::new_with_event_init_dict(
|
||||
"change",
|
||||
&web_sys::EventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
#[doc(hidden)]
|
||||
pub mod key;
|
||||
#[doc(hidden)]
|
||||
pub mod listeners;
|
||||
#[doc(hidden)]
|
||||
pub mod vcomp;
|
||||
#[doc(hidden)]
|
||||
pub mod vlist;
|
||||
@ -14,14 +16,15 @@ pub mod vtag;
|
||||
pub mod vtext;
|
||||
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use gloo::events::EventListener;
|
||||
use indexmap::IndexMap;
|
||||
use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter};
|
||||
use std::{borrow::Cow, collections::HashMap, hint::unreachable_unchecked, iter};
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::key::Key;
|
||||
#[doc(inline)]
|
||||
pub use self::listeners::*;
|
||||
#[doc(inline)]
|
||||
pub use self::vcomp::{VChild, VComp};
|
||||
#[doc(inline)]
|
||||
pub use self::vlist::VList;
|
||||
@ -32,21 +35,6 @@ pub use self::vtag::VTag;
|
||||
#[doc(inline)]
|
||||
pub use self::vtext::VText;
|
||||
|
||||
/// The `Listener` trait is an universal implementation of an event listener
|
||||
/// which is used to bind Rust-listener to JS-listener (DOM).
|
||||
pub trait Listener {
|
||||
/// Returns the name of the event
|
||||
fn kind(&self) -> &'static str;
|
||||
/// Attaches a listener to the element.
|
||||
fn attach(&self, element: &Element) -> EventListener;
|
||||
}
|
||||
|
||||
impl fmt::Debug for dyn Listener {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Listener {{ kind: {} }}", self.kind())
|
||||
}
|
||||
}
|
||||
|
||||
/// Attribute value
|
||||
pub type AttrValue = Cow<'static, str>;
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
//! This module contains the implementation of a virtual element node [VTag].
|
||||
|
||||
use super::{Apply, AttrValue, Attributes, Key, Listener, VDiff, VList, VNode};
|
||||
use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VDiff, VList, VNode};
|
||||
use crate::html::{AnyScope, IntoPropValue, NodeRef};
|
||||
use crate::utils::document;
|
||||
use gloo::events::EventListener;
|
||||
use log::warn;
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::PartialEq;
|
||||
@ -145,67 +144,6 @@ enum VTagInner {
|
||||
},
|
||||
}
|
||||
|
||||
/// A list of event listeners, either registered or pending registration
|
||||
/// TODO(#943): Compare references of handler to do listeners update better
|
||||
#[derive(Debug)]
|
||||
enum Listeners {
|
||||
/// Listeners pending registration
|
||||
Pending(Vec<Rc<dyn Listener>>),
|
||||
|
||||
/// Already registered listeners.
|
||||
/// Keeps handlers for attached listeners to have an opportunity to drop them later
|
||||
Registered(Vec<EventListener>),
|
||||
}
|
||||
|
||||
impl Apply for Listeners {
|
||||
type Element = Element;
|
||||
|
||||
fn apply(&mut self, el: &Self::Element) {
|
||||
if let Self::Pending(v) = self {
|
||||
*self = Self::Registered(
|
||||
std::mem::take(v)
|
||||
.into_iter()
|
||||
.map(|l| l.attach(el))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff(&mut self, el: &Self::Element, _ancestor: Self) {
|
||||
// All we need to do with `_ancestor` is drop it
|
||||
|
||||
self.apply(el);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Listeners {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use Listeners::*;
|
||||
|
||||
match (self, other) {
|
||||
(Pending(s), Pending(o)) => {
|
||||
s.len() == o.len() && s.iter().map(|l| l.kind()).eq(o.iter().map(|l| l.kind()))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Listeners {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Pending(v) => Self::Pending(v.clone()),
|
||||
Self::Registered(_) => Self::Registered(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Rc<dyn Listener>>> for Listeners {
|
||||
fn from(v: Vec<Rc<dyn Listener>>) -> Self {
|
||||
Self::Pending(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for a virtual
|
||||
/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
|
||||
/// representation.
|
||||
@ -281,7 +219,7 @@ impl VTag {
|
||||
key: Option<Key>,
|
||||
// at bottom for more readable macro-expanded coded
|
||||
attributes: Attributes,
|
||||
listeners: Vec<Rc<dyn Listener>>,
|
||||
listeners: Listeners,
|
||||
) -> Self {
|
||||
VTag::new_base(
|
||||
VTagInner::Input(InputFields {
|
||||
@ -313,7 +251,7 @@ impl VTag {
|
||||
key: Option<Key>,
|
||||
// at bottom for more readable macro-expanded coded
|
||||
attributes: Attributes,
|
||||
listeners: Vec<Rc<dyn Listener>>,
|
||||
listeners: Listeners,
|
||||
) -> Self {
|
||||
VTag::new_base(
|
||||
VTagInner::Textarea {
|
||||
@ -340,7 +278,7 @@ impl VTag {
|
||||
key: Option<Key>,
|
||||
// at bottom for more readable macro-expanded coded
|
||||
attributes: Attributes,
|
||||
listeners: Vec<Rc<dyn Listener>>,
|
||||
listeners: Listeners,
|
||||
children: VList,
|
||||
) -> Self {
|
||||
VTag::new_base(
|
||||
@ -360,13 +298,13 @@ impl VTag {
|
||||
node_ref: NodeRef,
|
||||
key: Option<Key>,
|
||||
attributes: Attributes,
|
||||
listeners: Vec<Rc<dyn Listener>>,
|
||||
listeners: Listeners,
|
||||
) -> Self {
|
||||
VTag {
|
||||
inner,
|
||||
reference: None,
|
||||
attributes,
|
||||
listeners: listeners.into(),
|
||||
listeners,
|
||||
node_ref,
|
||||
key,
|
||||
}
|
||||
@ -497,22 +435,9 @@ impl VTag {
|
||||
.insert(key, value.into_prop_value());
|
||||
}
|
||||
|
||||
/// Adds new listener to the node.
|
||||
/// It's boxed because we want to keep it in a single list.
|
||||
/// Later `Listener::attach` will attach an actual listener to a DOM node.
|
||||
pub fn add_listener(&mut self, listener: Rc<dyn Listener>) {
|
||||
if let Listeners::Pending(v) = &mut self.listeners {
|
||||
v.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds new listeners to the node.
|
||||
/// They are boxed because we want to keep them in a single list.
|
||||
/// Later `Listener::attach` will attach an actual listener to a DOM node.
|
||||
pub fn add_listeners(&mut self, listeners: Vec<Rc<dyn Listener>>) {
|
||||
if let Listeners::Pending(v) = &mut self.listeners {
|
||||
v.extend(listeners);
|
||||
}
|
||||
/// Set event listeners on the [VTag]'s [Element]
|
||||
pub fn set_listener(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
|
||||
self.listeners = Listeners::Pending(listeners);
|
||||
}
|
||||
|
||||
fn create_element(&self, parent: &Element) -> Element {
|
||||
@ -542,6 +467,8 @@ impl VDiff for VTag {
|
||||
.take()
|
||||
.expect("tried to remove not rendered VTag from DOM");
|
||||
|
||||
self.listeners.unregister();
|
||||
|
||||
// recursively remove its children
|
||||
if let VTagInner::Other { children, .. } = &mut self.inner {
|
||||
children.detach(&node);
|
||||
@ -1101,6 +1028,10 @@ mod tests {
|
||||
|
||||
// check whether not changed virtual dom value has been set to the input element
|
||||
assert_eq!(current_value, "User input");
|
||||
|
||||
// Need to remove the element to clean up the dirty state of the DOM. Failing this causes
|
||||
// event listener tests to fail.
|
||||
parent.remove();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user