Event handlers!

This commit is contained in:
Bodil Stokke 2018-11-15 23:25:38 +00:00
parent ca77a12599
commit a9dc58da9c
12 changed files with 335 additions and 37 deletions

View File

@ -31,6 +31,75 @@ pub fn global_attrs(span: Span) -> StringyMap<Ident, TokenStream> {
insert("style", "String");
insert("tabindex", "isize");
insert("title", "String");
// FIXME ARIA and XML attrs missing
}
attrs
}
// This NEEDS to be a sorted list!
pub static ATTR_EVENTS: &[&str] = &[
"abort",
// "autocomplete",
// "autocompleteerror",
"blur",
// "cancel",
// "canplay",
// "canplaythrough",
"change",
"click",
// "close",
"contextmenu",
// "cuechange",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",
// "durationchange",
// "emptied",
// "ended",
"error",
"focus",
"input",
// "invalid",
"keydown",
"keypress",
"keyup",
"load",
// "loadeddata",
// "loadedmetadata",
"loadstart",
"mousedown",
"mouseenter",
"mouseleave",
"mousemove",
"mouseout",
"mouseover",
"mouseup",
"mousewheel",
// "pause",
// "play",
// "playing",
"progress",
// "ratechange",
// "reset",
"resize",
"scroll",
// "seeked",
// "seeking",
// "select",
// "show",
// "sort",
// "stalled",
"submit",
// "suspend",
// "timeupdate",
// "toggle",
// "volumechange",
// "waiting",
];

View File

@ -1,6 +1,6 @@
use proc_macro::{quote, Ident, Literal, TokenStream, TokenTree};
use proc_macro::{quote, Ident, Literal, Span, TokenStream, TokenTree};
use config::global_attrs;
use config::{global_attrs, ATTR_EVENTS};
use error::ParseError;
use lexer::{Lexer, Token};
use map::StringyMap;
@ -52,8 +52,7 @@ impl Declare {
self.req_children.iter().map(|child| {
let child_name: TokenTree =
Ident::new(&format!("child_{}", child.to_string()), child.span()).into();
let child_type: TokenTree =
Ident::new(&format!("{}", child.to_string()), child.span()).into();
let child_type: TokenTree = Ident::new(&child.to_string(), child.span()).into();
let child_str = Literal::string(&child.to_string()).into();
(child_name, child_type, child_str)
})
@ -103,7 +102,8 @@ impl Declare {
quote!(
pub struct $elem_name {
pub attrs: $attr_type_name,
pub data_attributes: Vec<(String, String)>,
pub data_attributes: Vec<(&'static str, String)>,
pub events: ::events::Events,
$body
}
)
@ -128,6 +128,7 @@ impl Declare {
attrs: $attr_type_name { $attrs },
));
body.extend(quote!(data_attributes: Vec::new(),));
for (child_name, _, _) in self.req_children() {
body.extend(quote!( $child_name, ));
}
@ -139,6 +140,7 @@ impl Declare {
impl $elem_name {
pub fn new($args) -> Self {
$elem_name {
events: ::events::Events::default(),
$body
}
}
@ -156,7 +158,7 @@ impl Declare {
}
let mut opt_children = TokenStream::new();
if self.opt_children.is_some() {
opt_children.extend(quote!(for child in &self.children {
opt_children.extend(quote!(for child in &mut self.children {
children.push(child.vnode());
}));
}
@ -165,7 +167,7 @@ impl Declare {
for (attr_name, _, attr_str) in self.attrs() {
push_attrs.extend(quote!(
if let Some(ref value) = self.attrs.$attr_name {
attributes.push(($attr_str.to_string(), value.to_string()));
attributes.push(($attr_str, value.to_string()));
}
));
}
@ -173,9 +175,7 @@ impl Declare {
quote!(
let mut attributes = Vec::new();
$push_attrs
for (key, value) in &self.data_attributes {
attributes.push((format!("data-{}", key), value.to_string()));
}
attributes.extend(self.data_attributes.clone());
let mut children = Vec::new();
$req_children
@ -184,6 +184,7 @@ impl Declare {
::dom::VNode::Element(::dom::VElement {
name: $elem_name,
attributes,
events: &mut self.events,
children
})
)
@ -194,7 +195,7 @@ impl Declare {
let vnode = self.impl_vnode();
quote!(
impl ::dom::Node for $elem_name {
fn vnode(&self) -> ::dom::VNode {
fn vnode<'a>(&'a mut self) -> ::dom::VNode<'a> {
$vnode
}
}
@ -215,7 +216,7 @@ impl Declare {
for (attr_name, _, attr_str) in self.attrs() {
push_attrs.extend(quote!(
if let Some(ref value) = self.attrs.$attr_name {
out.push(($attr_str.to_string(), value.to_string()));
out.push(($attr_str, value.to_string()));
}
));
}
@ -234,11 +235,11 @@ impl Declare {
&[ $reqs ]
}
fn attributes(&self) -> Vec<(String, String)> {
fn attributes(&self) -> Vec<(&'static str, String)> {
let mut out = Vec::new();
$push_attrs
for (key, value) in &self.data_attributes {
out.push((format!("data-{}", key), value.to_string()));
out.push((key, value.to_string()));
}
out
}
@ -308,6 +309,18 @@ impl Declare {
));
}
let mut print_events = TokenStream::new();
for event in ATTR_EVENTS {
let event_name = TokenTree::Ident(Ident::new(event, Span::call_site()));
let event_str = TokenTree::Literal(Literal::string(event));
print_events.extend(quote!(
if let Some(ref value) = self.events.$event_name {
write!(f, " on{}=\"{}\"", $event_str,
::htmlescape::encode_attribute(&value.render().unwrap()))?;
}
));
}
quote!(
impl std::fmt::Display for $elem_name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
@ -317,6 +330,7 @@ impl Declare {
write!(f, " data-{}=\"{}\"", key,
::htmlescape::encode_attribute(&value))?;
}
$print_events
$print_children
}
}

View File

@ -198,7 +198,28 @@ TypeArgList: Vec<Token> = "<" TypeArgs ">" => {
args
};
TypeSpec: Vec<Token> = Reference? TypePath TypeArgList? => {
FnReturnType: Vec<Token> = "-" ">" TypeSpec => {
let (dash, right, spec) = (<>);
let mut out = vec![dash, right];
out.extend(spec);
out
};
FnArgList: Vec<Token> = ParenGroupToken FnReturnType? => {
let (args, rt) = (<>);
let mut out = vec![args];
if let Some(rt) = rt {
out.extend(rt);
}
out
};
TypeArgSpec = {
TypeArgList,
FnArgList,
};
TypeSpec: Vec<Token> = Reference? TypePath TypeArgSpec? => {
let (reference, path, args) = (<>);
let mut out = Vec::new();
if let Some(reference) = reference {

View File

@ -2,7 +2,7 @@ use proc_macro::{
quote, Delimiter, Diagnostic, Group, Ident, Level, Literal, TokenStream, TokenTree,
};
use config::required_children;
use config::{required_children, ATTR_EVENTS};
use error::ParseError;
use lexer::{Lexer, Token};
use map::StringyMap;
@ -68,12 +68,31 @@ fn extract_data_attrs(attrs: &mut StringyMap<Ident, TokenTree>) -> StringyMap<St
let prefix = "data_";
if key_name.starts_with(prefix) {
let value = attrs.remove(&key).unwrap();
data.insert(key_name[prefix.len()..].to_string(), value);
data.insert(format!("data-{}", &key_name[prefix.len()..]), value);
}
}
data
}
fn extract_event_handlers(
attrs: &mut StringyMap<Ident, TokenTree>,
) -> StringyMap<Ident, TokenTree> {
let mut events = StringyMap::new();
let keys: Vec<Ident> = attrs.keys().cloned().collect();
for key in keys {
let key_name = key.to_string();
let prefix = "on";
if key_name.starts_with(prefix) {
let event_name = &key_name[prefix.len()..];
if ATTR_EVENTS.binary_search(&event_name).is_ok() {
let value = attrs.remove(&key).unwrap();
events.insert(Ident::new_raw(event_name, key.span()), value);
}
}
}
events
}
fn process_value(value: &TokenTree) -> TokenStream {
match value {
TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => {
@ -113,6 +132,7 @@ impl Element {
.emit();
panic!();
}
let events = extract_event_handlers(&mut self.attributes);
let data_attrs = extract_data_attrs(&mut self.attributes);
let attrs = self.attributes.iter().map(|(key, value)| {
(
@ -164,11 +184,19 @@ impl Element {
.map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone()))
{
body.extend(quote!(
element.data_attributes.push(($key.into(), $value.into()));
element.data_attributes.push(($key, $value.into()));
));
}
body.extend(opt_children);
for (key, value) in events.iter() {
let key = TokenTree::Ident(key.clone());
let value = process_value(value);
body.extend(quote!(
element.events.$key = Some(Box::new($value));
));
}
let mut args = TokenStream::new();
for arg in req_children {
args.extend(quote!( $arg, ));

View File

@ -117,7 +117,7 @@ pub fn unroll_stream(stream: TokenStream, deep: bool) -> Vec<Token> {
TokenTree::Ident(ident) => vec.push(ident.into()),
TokenTree::Literal(literal) => vec.push(literal.into()),
TokenTree::Punct(punct) => vec.push(punct.into()),
TokenTree::Group(ref group) if deep => {
TokenTree::Group(ref group) if deep && group.delimiter() != Delimiter::Parenthesis => {
vec.push(Token::GroupOpen(group.delimiter(), group.span()));
let sub = unroll_stream(group.stream(), deep);
vec.extend(sub);

View File

@ -31,6 +31,7 @@ fn index() -> Content<String> {
})
}
<p>"<img src=\"javascript:alert('pwned lol')\">"</p>
<button onclick="alert('She is not a cat.')">"Click me!"</button>
</body>
</html>
).to_string())

View File

@ -12,3 +12,4 @@ language-tags = "0.2.2"
enumset = "0.3.12"
http = "0.1.13"
htmlescape = "0.3.1"
stdweb = "0.4.10"

View File

@ -35,9 +35,9 @@ fn main() {
})
}
<p>"<img src=\"javascript:alert('pwned lol')\">"</p>
<button onclick="alert('lol')">"lol"</button>
</body>
</html>
);
println!("{}", doc.to_string());
println!("{:?}", doc.vnode());
}

View File

@ -1,11 +1,9 @@
//! DOM and virtual DOM types.
#![allow(non_camel_case_types)]
#![allow(dead_code)]
use std::fmt::Display;
use elements::{FlowContent, PhrasingContent};
use events::Events;
use htmlescape::encode_minimal;
/// An untyped representation of an HTML node.
@ -23,18 +21,17 @@ use htmlescape::encode_minimal;
/// ```
///
/// [Node]: trait.Node.html
#[derive(Clone, Debug)]
pub enum VNode {
Text(String),
Element(VElement),
pub enum VNode<'a> {
Text(&'a str),
Element(VElement<'a>),
}
/// An untyped representation of an HTML element.
#[derive(Clone, Debug)]
pub struct VElement {
pub struct VElement<'a> {
pub name: &'static str,
pub attributes: Vec<(String, String)>,
pub children: Vec<VNode>,
pub attributes: Vec<(&'static str, String)>,
pub events: &'a mut Events,
pub children: Vec<VNode<'a>>,
}
/// Trait for rendering a typed HTML node.
@ -53,7 +50,7 @@ pub trait Node: Display {
/// Render the node into a [`VNode`][VNode] tree.
///
/// [VNode]: enum.VNode.html
fn vnode(&self) -> VNode;
fn vnode<'a>(&'a mut self) -> VNode<'a>;
}
/// Trait for querying a typed HTML element.
@ -79,7 +76,7 @@ pub trait Element: Node {
///
/// This will convert attribute values into strings and return a vector of
/// key/value pairs.
fn attributes(&self) -> Vec<(String, String)>;
fn attributes(&self) -> Vec<(&'static str, String)>;
}
/// An HTML text node.
@ -134,10 +131,11 @@ impl Display for TextNode {
}
impl Node for TextNode {
fn vnode(&self) -> VNode {
VNode::Text(self.0.clone())
fn vnode<'a>(&'a mut self) -> VNode<'a> {
VNode::Text(&self.0)
}
}
impl IntoIterator for TextNode {
type Item = TextNode;
type IntoIter = std::vec::IntoIter<TextNode>;

150
typed-html/src/events.rs Normal file
View File

@ -0,0 +1,150 @@
use std::marker::PhantomData;
use stdweb::web::event::*;
use stdweb::web::{Element, EventListenerHandle, IEventTarget};
macro_rules! declare_events {
($($name:ident : $type:ty ,)*) => {
#[derive(Default)]
pub struct Events {
$(
pub $name: Option<Box<dyn EventHandler<$type>>>,
)*
}
#[macro_export]
macro_rules! for_events {
($event:ident in $events:expr => $body:block) => {
$(
if let Some(ref mut $event) = $events.$name $body
)*
}
}
}
}
// TODO? these are all the "on*" attributes defined in the HTML5 standard, with
// the ones I've been unable to match to stdweb event types commented out.
//
// This needs review.
declare_events! {
abort: ResourceAbortEvent,
// autocomplete: Event,
// autocompleteerror: Event,
blur: BlurEvent,
// cancel: Event,
// canplay: Event,
// canplaythrough: Event,
change: ChangeEvent,
click: ClickEvent,
// close: Event,
contextmenu: ContextMenuEvent,
// cuechange: Event,
dblclick: DoubleClickEvent,
drag: DragEvent,
dragend: DragEndEvent,
dragenter: DragEnterEvent,
dragexit: DragExitEvent,
dragleave: DragLeaveEvent,
dragover: DragOverEvent,
dragstart: DragStartEvent,
drop: DragDropEvent,
// durationchange: Event,
// emptied: Event,
// ended: Event,
error: ResourceErrorEvent,
focus: FocusEvent,
input: InputEvent,
// invalid: Event,
keydown: KeyDownEvent,
keypress: KeyPressEvent,
keyup: KeyUpEvent,
load: ResourceLoadEvent,
// loadeddata: Event,
// loadedmetadata: Event,
loadstart: LoadStartEvent,
mousedown: MouseDownEvent,
mouseenter: MouseEnterEvent,
mouseleave: MouseLeaveEvent,
mousemove: MouseMoveEvent,
mouseout: MouseOutEvent,
mouseover: MouseOverEvent,
mouseup: MouseUpEvent,
mousewheel: MouseWheelEvent,
// pause: Event,
// play: Event,
// playing: Event,
progress: ProgressEvent,
// ratechange: Event,
// reset: Event,
resize: ResizeEvent,
scroll: ScrollEvent,
// seeked: Event,
// seeking: Event,
// select: Event,
// show: Event,
// sort: Event,
// stalled: Event,
submit: SubmitEvent,
// suspend: Event,
// timeupdate: Event,
// toggle: Event,
// volumechange: Event,
// waiting: Event,
}
/// Trait for event handlers.
pub trait EventHandler<EventType> {
/// Build a callback function from this event handler.
///
/// Returns `None` is this event handler can't be used to build a callback
/// function. This is usually the case if the event handler is a string
/// intended for server side rendering.
// fn build(self) -> Option<Box<FnMut(EventType) + 'static>>;
fn attach(&mut self, target: &Element) -> EventListenerHandle;
/// Render this event handler as a string.
///
/// Returns `None` if this event handler cannot be rendered. Normally, the
/// only event handlers that can be rendered are string values intended for
/// server side rendering.
fn render(&self) -> Option<String>;
}
pub struct EFn<F, E>(Option<F>, PhantomData<E>);
impl<F, E> EFn<F, E>
where
F: FnMut(E) + 'static,
E: ConcreteEvent,
{
pub fn new(f: F) -> Self {
EFn(Some(f), PhantomData)
}
}
impl<F, E> EventHandler<E> for EFn<F, E>
where
F: FnMut(E) + 'static,
E: ConcreteEvent,
{
fn attach(&mut self, target: &Element) -> EventListenerHandle {
let handler = self.0.take().unwrap();
target.add_event_listener(handler)
}
fn render(&self) -> Option<String> {
None
}
}
impl<'a, EventType> EventHandler<EventType> for &'a str {
fn attach(&mut self, _target: &Element) -> EventListenerHandle {
panic!("Silly wabbit, strings as event handlers are only for printing.");
}
fn render(&self) -> Option<String> {
Some(self.to_string())
}
}

View File

@ -9,9 +9,11 @@ pub extern crate htmlescape;
extern crate http;
extern crate language_tags;
extern crate mime;
extern crate stdweb;
extern crate strum;
extern crate typed_html_macros;
pub mod dom;
pub mod elements;
pub mod events;
pub mod types;

View File

@ -5,10 +5,18 @@ extern crate stdweb;
extern crate typed_html;
extern crate typed_html_macros;
use stdweb::web::{self, IElement, INode};
use stdweb::web::{self, Element, IElement, INode};
use typed_html::for_events;
use typed_html::dom::{Node, VNode};
use typed_html::events::{EFn, Events};
use typed_html_macros::html;
fn install_handlers(target: &Element, handlers: &mut Events) {
for_events!(handler in handlers => {
handler.attach(target);
});
}
fn build(
document: &web::Document,
vnode: VNode,
@ -20,6 +28,7 @@ fn build(
for (key, value) in element.attributes {
node.set_attribute(&key, &value)?;
}
install_handlers(&node, element.events);
for child in element.children {
let child_node = build(document, child)?;
node.append_child(&child_node);
@ -30,13 +39,18 @@ fn build(
}
fn main() {
let doc = html!(
let mut doc = html!(
<div>
<h1>"Hello Kitty"</h1>
<p>
"She is not a "<em><a href="https://en.wikipedia.org/wiki/Cat">"cat"</a></em>
". She is a "<em>"human girl"</em>"."
</p>
<p>
<button onclick=EFn::new(|_event| web::alert("Hello Joe!"))>
"Call Joe"
</button>
</p>
</div>
);
let vdom = doc.vnode();