Split out listener code from html module

This commit is contained in:
Justin Starry 2019-08-04 18:29:16 -04:00
parent 3ef8ee5b80
commit 2020944f49
2 changed files with 190 additions and 181 deletions

175
src/html/listener.rs Normal file
View File

@ -0,0 +1,175 @@
use super::*;
use crate::virtual_dom::Listener;
use log::debug;
use stdweb::web::html_element::SelectElement;
#[allow(unused_imports)]
use stdweb::web::{EventListenerHandle, FileList, INode};
#[allow(unused_imports)]
use stdweb::{_js_impl, js};
macro_rules! impl_action {
($($action:ident($event:ident : $type:ident) -> $ret:ty => $convert:expr)*) => {$(
/// An abstract implementation of a listener.
pub mod $action {
use stdweb::web::{IEventTarget, Element};
use stdweb::web::event::{IEvent, $type};
use super::*;
/// A wrapper for a callback.
/// Listener extracted from here when attached.
pub struct Wrapper<F>(Option<F>);
/// And event type which keeps the returned type.
pub type Event = $ret;
impl<F, MSG> From<F> for Wrapper<F>
where
MSG: 'static,
F: Fn($ret) -> MSG + 'static,
{
fn from(handler: F) -> Self {
Wrapper(Some(handler))
}
}
impl<T, COMP> Listener<COMP> for Wrapper<T>
where
T: Fn($ret) -> COMP::Message + 'static,
COMP: Component + Renderable<COMP>,
{
fn kind(&self) -> &'static str {
stringify!($action)
}
fn attach(&mut self, element: &Element, mut activator: Scope<COMP>)
-> EventListenerHandle {
let handler = self.0.take().expect("tried to attach listener twice");
let this = element.clone();
let listener = move |event: $type| {
debug!("Event handler: {}", stringify!($type));
event.stop_propagation();
let handy_event: $ret = $convert(&this, event);
let msg = handler(handy_event);
activator.send_message(msg);
};
element.add_event_listener(listener)
}
}
}
)*};
}
// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events
impl_action! {
onclick(event: ClickEvent) -> ClickEvent => |_, event| { event }
ondoubleclick(event: DoubleClickEvent) -> DoubleClickEvent => |_, event| { event }
onkeypress(event: KeyPressEvent) -> KeyPressEvent => |_, event| { event }
onkeydown(event: KeyDownEvent) -> KeyDownEvent => |_, event| { event }
onkeyup(event: KeyUpEvent) -> KeyUpEvent => |_, event| { event }
onmousemove(event: MouseMoveEvent) -> MouseMoveEvent => |_, event| { event }
onmousedown(event: MouseDownEvent) -> MouseDownEvent => |_, event| { event }
onmouseup(event: MouseUpEvent) -> MouseUpEvent => |_, event| { event }
onmouseover(event: MouseOverEvent) -> MouseOverEvent => |_, event| { event }
onmouseout(event: MouseOutEvent) -> MouseOutEvent => |_, event| { event }
onmouseenter(event: MouseEnterEvent) -> MouseEnterEvent => |_, event| { event }
onmouseleave(event: MouseLeaveEvent) -> MouseLeaveEvent => |_, event| { event }
onmousewheel(event: MouseWheelEvent) -> MouseWheelEvent => |_, event| { event }
ongotpointercapture(event: GotPointerCaptureEvent) -> GotPointerCaptureEvent => |_, event| { event }
onlostpointercapture(event: LostPointerCaptureEvent) -> LostPointerCaptureEvent => |_, event| { event }
onpointercancel(event: PointerCancelEvent) -> PointerCancelEvent => |_, event| { event }
onpointerdown(event: PointerDownEvent) -> PointerDownEvent => |_, event| { event }
onpointerenter(event: PointerEnterEvent) -> PointerEnterEvent => |_, event| { event }
onpointerleave(event: PointerLeaveEvent) -> PointerLeaveEvent => |_, event| { event }
onpointermove(event: PointerMoveEvent) -> PointerMoveEvent => |_, event| { event }
onpointerout(event: PointerOutEvent) -> PointerOutEvent => |_, event| { event }
onpointerover(event: PointerOverEvent) -> PointerOverEvent => |_, event| { event }
onpointerup(event: PointerUpEvent) -> PointerUpEvent => |_, event| { event }
onscroll(event: ScrollEvent) -> ScrollEvent => |_, event| { event }
onblur(event: BlurEvent) -> BlurEvent => |_, event| { event }
onfocus(event: FocusEvent) -> FocusEvent => |_, event| { event }
onsubmit(event: SubmitEvent) -> SubmitEvent => |_, event| { event }
ondragstart(event: DragStartEvent) -> DragStartEvent => |_, event| { event }
ondrag(event: DragEvent) -> DragEvent => |_, event| { event }
ondragend(event: DragEndEvent) -> DragEndEvent => |_, event| { event }
ondragenter(event: DragEnterEvent) -> DragEnterEvent => |_, event| { event }
ondragleave(event: DragLeaveEvent) -> DragLeaveEvent => |_, event| { event }
ondragover(event: DragOverEvent) -> DragOverEvent => |_, event| { event }
ondragexit(event: DragExitEvent) -> DragExitEvent => |_, event| { event }
ondrop(event: DragDropEvent) -> DragDropEvent => |_, event| { event }
oncontextmenu(event: ContextMenuEvent) -> ContextMenuEvent => |_, event| { event }
oninput(event: InputEvent) -> InputData => |this: &Element, _| {
use stdweb::web::html_element::{InputElement, TextAreaElement};
use stdweb::unstable::TryInto;
// Normally only InputElement or TextAreaElement can have an oninput event listener. In
// practice though any element with `contenteditable=true` may generate such events,
// therefore here we fall back to just returning the text content of the node.
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event.
let v1 = this.clone().try_into().map(|input: InputElement| input.raw_value()).ok();
let v2 = this.clone().try_into().map(|input: TextAreaElement| input.value()).ok();
let v3 = this.text_content();
let value = v1.or(v2).or(v3)
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
InputData { value }
}
onchange(event: ChangeEvent) -> ChangeData => |this: &Element, _| {
use stdweb::web::{FileList, IElement};
use stdweb::web::html_element::{InputElement, TextAreaElement, SelectElement};
use stdweb::unstable::TryInto;
match this.node_name().as_ref() {
"INPUT" => {
let input: InputElement = this.clone().try_into().unwrap();
let is_file = input.get_attribute("type").map(|value| {
value.eq_ignore_ascii_case("file")
})
.unwrap_or(false);
if is_file {
let files: FileList = js!( return @{input}.files; )
.try_into()
.unwrap();
ChangeData::Files(files)
} else {
ChangeData::Value(input.raw_value())
}
}
"TEXTAREA" => {
let tae: TextAreaElement = this.clone().try_into().unwrap();
ChangeData::Value(tae.value())
}
"SELECT" => {
let se: SelectElement = this.clone().try_into().unwrap();
ChangeData::Select(se)
}
_ => {
panic!("only an InputElement, TextAreaElement or SelectElement can have an onchange event listener");
}
}
}
}
/// A type representing data from `oninput` event.
#[derive(Debug)]
pub struct InputData {
/// Inserted characters. Contains value from
/// [InputEvent](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data).
pub value: String,
}
// There is no '.../Web/API/ChangeEvent/data' (for onchange) similar to
// https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data (for oninput).
// ChangeData actually contains the value of the InputElement/TextAreaElement
// after `change` event occured or contains the SelectElement (see more at the
// variant ChangeData::Select)
/// A type representing change of value(s) of an element after committed by user
/// ([onchange event](https://developer.mozilla.org/en-US/docs/Web/Events/change)).
#[derive(Debug)]
pub enum ChangeData {
/// Value of the element in cases of `<input>`, `<textarea>`
Value(String),
/// SelectElement in case of `<select>` element. You can use one of methods of SelectElement
/// to collect your required data such as: `value`, `selected_index`, `selected_indices` or
/// `selected_values`. You can also iterate throught `selected_options` yourself.
Select(SelectElement),
/// Files
Files(FileList),
}

View File

@ -3,19 +3,22 @@
//! Also this module contains declaration of `Component` trait which used
//! to create own UI-components.
mod listener;
use crate::callback::Callback;
use crate::scheduler::{scheduler, Runnable, Shared};
use crate::virtual_dom::{Listener, VDiff, VNode};
use log::debug;
use crate::virtual_dom::{VDiff, VNode};
pub use listener::*;
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use stdweb::web::html_element::SelectElement;
#[allow(unused_imports)]
use stdweb::web::{Element, EventListenerHandle, FileList, INode, Node};
use stdweb::web::{Element, Node};
#[allow(unused_imports)]
use stdweb::{_js_impl, js};
/// A type which expected as a result of `view` function implementation.
pub type Html<MSG> = VNode<MSG>;
/// This type indicates that component should be rendered again.
pub type ShouldRender = bool;
@ -42,6 +45,12 @@ pub trait Component: Sized + 'static {
fn destroy(&mut self) {} // TODO Replace with `Drop`
}
/// Should be rendered relative to context and component environment.
pub trait Renderable<COMP: Component> {
/// Called by rendering loop.
fn view(&self) -> Html<COMP>;
}
/// Trait for building properties for a component
pub trait Properties {
/// Builder that will be used to construct properties
@ -61,17 +70,12 @@ impl Properties for () {
EmptyBuilder
}
}
impl EmptyBuilder {
/// Build empty properties
pub fn build(self) {}
}
/// Should be rendered relative to context and component environment.
pub trait Renderable<COMP: Component> {
/// Called by rendering loop.
fn view(&self) -> Html<COMP>;
}
/// Updates for a `Components` instance. Used by scope sender.
pub(crate) enum ComponentUpdate<COMP: Component> {
/// Wraps messages for a component.
@ -349,176 +353,6 @@ where
}
}
/// A type which expected as a result of `view` function implementation.
pub type Html<MSG> = VNode<MSG>;
macro_rules! impl_action {
($($action:ident($event:ident : $type:ident) -> $ret:ty => $convert:expr)*) => {$(
/// An abstract implementation of a listener.
pub mod $action {
use stdweb::web::{IEventTarget, Element};
use stdweb::web::event::{IEvent, $type};
use super::*;
/// A wrapper for a callback.
/// Listener extracted from here when attached.
pub struct Wrapper<F>(Option<F>);
/// And event type which keeps the returned type.
pub type Event = $ret;
impl<F, MSG> From<F> for Wrapper<F>
where
MSG: 'static,
F: Fn($ret) -> MSG + 'static,
{
fn from(handler: F) -> Self {
Wrapper(Some(handler))
}
}
impl<T, COMP> Listener<COMP> for Wrapper<T>
where
T: Fn($ret) -> COMP::Message + 'static,
COMP: Component + Renderable<COMP>,
{
fn kind(&self) -> &'static str {
stringify!($action)
}
fn attach(&mut self, element: &Element, mut activator: Scope<COMP>)
-> EventListenerHandle {
let handler = self.0.take().expect("tried to attach listener twice");
let this = element.clone();
let listener = move |event: $type| {
debug!("Event handler: {}", stringify!($type));
event.stop_propagation();
let handy_event: $ret = $convert(&this, event);
let msg = handler(handy_event);
activator.send_message(msg);
};
element.add_event_listener(listener)
}
}
}
)*};
}
// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events
impl_action! {
onclick(event: ClickEvent) -> ClickEvent => |_, event| { event }
ondoubleclick(event: DoubleClickEvent) -> DoubleClickEvent => |_, event| { event }
onkeypress(event: KeyPressEvent) -> KeyPressEvent => |_, event| { event }
onkeydown(event: KeyDownEvent) -> KeyDownEvent => |_, event| { event }
onkeyup(event: KeyUpEvent) -> KeyUpEvent => |_, event| { event }
onmousemove(event: MouseMoveEvent) -> MouseMoveEvent => |_, event| { event }
onmousedown(event: MouseDownEvent) -> MouseDownEvent => |_, event| { event }
onmouseup(event: MouseUpEvent) -> MouseUpEvent => |_, event| { event }
onmouseover(event: MouseOverEvent) -> MouseOverEvent => |_, event| { event }
onmouseout(event: MouseOutEvent) -> MouseOutEvent => |_, event| { event }
onmouseenter(event: MouseEnterEvent) -> MouseEnterEvent => |_, event| { event }
onmouseleave(event: MouseLeaveEvent) -> MouseLeaveEvent => |_, event| { event }
onmousewheel(event: MouseWheelEvent) -> MouseWheelEvent => |_, event| { event }
ongotpointercapture(event: GotPointerCaptureEvent) -> GotPointerCaptureEvent => |_, event| { event }
onlostpointercapture(event: LostPointerCaptureEvent) -> LostPointerCaptureEvent => |_, event| { event }
onpointercancel(event: PointerCancelEvent) -> PointerCancelEvent => |_, event| { event }
onpointerdown(event: PointerDownEvent) -> PointerDownEvent => |_, event| { event }
onpointerenter(event: PointerEnterEvent) -> PointerEnterEvent => |_, event| { event }
onpointerleave(event: PointerLeaveEvent) -> PointerLeaveEvent => |_, event| { event }
onpointermove(event: PointerMoveEvent) -> PointerMoveEvent => |_, event| { event }
onpointerout(event: PointerOutEvent) -> PointerOutEvent => |_, event| { event }
onpointerover(event: PointerOverEvent) -> PointerOverEvent => |_, event| { event }
onpointerup(event: PointerUpEvent) -> PointerUpEvent => |_, event| { event }
onscroll(event: ScrollEvent) -> ScrollEvent => |_, event| { event }
onblur(event: BlurEvent) -> BlurEvent => |_, event| { event }
onfocus(event: FocusEvent) -> FocusEvent => |_, event| { event }
onsubmit(event: SubmitEvent) -> SubmitEvent => |_, event| { event }
ondragstart(event: DragStartEvent) -> DragStartEvent => |_, event| { event }
ondrag(event: DragEvent) -> DragEvent => |_, event| { event }
ondragend(event: DragEndEvent) -> DragEndEvent => |_, event| { event }
ondragenter(event: DragEnterEvent) -> DragEnterEvent => |_, event| { event }
ondragleave(event: DragLeaveEvent) -> DragLeaveEvent => |_, event| { event }
ondragover(event: DragOverEvent) -> DragOverEvent => |_, event| { event }
ondragexit(event: DragExitEvent) -> DragExitEvent => |_, event| { event }
ondrop(event: DragDropEvent) -> DragDropEvent => |_, event| { event }
oncontextmenu(event: ContextMenuEvent) -> ContextMenuEvent => |_, event| { event }
oninput(event: InputEvent) -> InputData => |this: &Element, _| {
use stdweb::web::html_element::{InputElement, TextAreaElement};
use stdweb::unstable::TryInto;
// Normally only InputElement or TextAreaElement can have an oninput event listener. In
// practice though any element with `contenteditable=true` may generate such events,
// therefore here we fall back to just returning the text content of the node.
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event.
let v1 = this.clone().try_into().map(|input: InputElement| input.raw_value()).ok();
let v2 = this.clone().try_into().map(|input: TextAreaElement| input.value()).ok();
let v3 = this.text_content();
let value = v1.or(v2).or(v3)
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
InputData { value }
}
onchange(event: ChangeEvent) -> ChangeData => |this: &Element, _| {
use stdweb::web::{FileList, IElement};
use stdweb::web::html_element::{InputElement, TextAreaElement, SelectElement};
use stdweb::unstable::TryInto;
match this.node_name().as_ref() {
"INPUT" => {
let input: InputElement = this.clone().try_into().unwrap();
let is_file = input.get_attribute("type").map(|value| {
value.eq_ignore_ascii_case("file")
})
.unwrap_or(false);
if is_file {
let files: FileList = js!( return @{input}.files; )
.try_into()
.unwrap();
ChangeData::Files(files)
} else {
ChangeData::Value(input.raw_value())
}
}
"TEXTAREA" => {
let tae: TextAreaElement = this.clone().try_into().unwrap();
ChangeData::Value(tae.value())
}
"SELECT" => {
let se: SelectElement = this.clone().try_into().unwrap();
ChangeData::Select(se)
}
_ => {
panic!("only an InputElement, TextAreaElement or SelectElement can have an onchange event listener");
}
}
}
}
/// A type representing data from `oninput` event.
#[derive(Debug)]
pub struct InputData {
/// Inserted characters. Contains value from
/// [InputEvent](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data).
pub value: String,
}
// There is no '.../Web/API/ChangeEvent/data' (for onchange) similar to
// https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data (for oninput).
// ChangeData actually contains the value of the InputElement/TextAreaElement
// after `change` event occured or contains the SelectElement (see more at the
// variant ChangeData::Select)
/// A type representing change of value(s) of an element after committed by user
/// ([onchange event](https://developer.mozilla.org/en-US/docs/Web/Events/change)).
#[derive(Debug)]
pub enum ChangeData {
/// Value of the element in cases of `<input>`, `<textarea>`
Value(String),
/// SelectElement in case of `<select>` element. You can use one of methods of SelectElement
/// to collect your required data such as: `value`, `selected_index`, `selected_indices` or
/// `selected_values`. You can also iterate throught `selected_options` yourself.
Select(SelectElement),
/// Files
Files(FileList),
}
/// A bridging type for checking `href` attribute value.
#[derive(Debug)]
pub struct Href {