use core::convert::AsRef;
use std::collections::HashMap;
use pulldown_cmark;
use web_sys;
use wasm_bindgen::{closure::Closure, JsCast};
use crate::vdom::Mailbox;
#[derive(Debug,Clone,PartialEq)]
pub enum Namespace {
Svg,
Custom(String)
}
impl Namespace {
pub fn as_str(&self) -> &str {
use self::Namespace::*;
match self{
Svg => "http://www.w3.org/2000/svg",
Custom(s) => s
}
}
}
pub fn simple_ev<Ms>(trigger: &str, message: Ms) -> Listener<Ms>
where Ms: Clone + 'static
{
let handler = || message;
let closure = move |_| handler.clone()();
Listener::new(&trigger.into(), Some(Box::new(closure)))
}
pub fn input_ev<Ms>(trigger: &str, mut handler: impl FnMut(String) -> Ms + 'static) -> Listener<Ms>
where Ms: Clone + 'static
{
let closure = move |event: web_sys::Event| {
if let Some(target) = event.target() {
if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
return handler(input.value());
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
return handler(input.value());
}
if let Some(input) = target.dyn_ref::<web_sys::HtmlSelectElement>() {
return handler(input.value());
}
}
handler(String::new())
};
Listener::new(&trigger.into(), Some(Box::new(closure)))
}
pub fn raw_ev<Ms>(trigger: &str, mut handler: impl FnMut(web_sys::Event) -> Ms + 'static) -> Listener<Ms>
where Ms: Clone + 'static
{
let closure = move |event: web_sys::Event| handler(event);
Listener::new(&trigger.into(), Some(Box::new(closure)))
}
pub fn keyboard_ev<Ms>(trigger: &str, mut handler: impl FnMut(web_sys::KeyboardEvent) -> Ms + 'static) -> Listener<Ms>
where Ms: Clone + 'static
{
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::KeyboardEvent>().unwrap().clone())
};
Listener::new(&trigger.into(), Some(Box::new(closure)))
}
pub fn mouse_ev<Ms>(trigger: &str, mut handler: impl FnMut(web_sys::MouseEvent) -> Ms + 'static) -> Listener<Ms>
where Ms: Clone + 'static
{
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::MouseEvent>().unwrap().clone())
};
Listener::new(&trigger.into(), Some(Box::new(closure)))
}
pub struct Listener<Ms: Clone> {
pub trigger: String,
pub handler: Option<Box<FnMut(web_sys::Event) -> Ms>>,
pub closure: Option<Closure<FnMut(web_sys::Event)>>,
pub id: Option<u32>,
}
impl<Ms: Clone + 'static> Listener<Ms> {
pub fn new(event: &Event, handler: Option<Box<FnMut(web_sys::Event) -> Ms>>) -> Self {
Self {
trigger: String::from(event.as_str()),
handler,
closure: None,
id: None,
}
}
pub fn attach<T>(&mut self, el_ws: &T, mailbox: Mailbox<Ms>)
where T: AsRef<web_sys::EventTarget> {
let mut handler = self.handler.take().expect("Can't find old handler");
let closure = Closure::wrap(
Box::new(move |event: web_sys::Event| {
mailbox.send(handler(event))
})
as Box<FnMut(web_sys::Event) + 'static>,
);
(el_ws.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback(&self.trigger, closure.as_ref().unchecked_ref())
.expect("problem adding listener to element");
self.closure = Some(closure);
}
pub fn detach<T>(&self, el_ws: &T)
where T: AsRef<web_sys::EventTarget> {
let closure = self.closure.as_ref().unwrap();
(el_ws.as_ref() as &web_sys::EventTarget)
.remove_event_listener_with_callback(&self.trigger, closure.as_ref().unchecked_ref())
.expect("problem removing listener from element");
}
}
impl<Ms: Clone + 'static> PartialEq for Listener<Ms> {
fn eq(&self, other: &Self) -> bool {
self.trigger == other.trigger && self.id == other.id
}
}
pub trait UpdateEl<T> {
fn update(self, el: &mut T);
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Attrs {
fn update(self, el: &mut El<Ms>) {
el.attrs = self;
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for &Attrs {
fn update(self, el: &mut El<Ms>) {
el.attrs = self.clone();
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Style {
fn update(self, el: &mut El<Ms>) {
el.style = self;
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for &Style {
fn update(self, el: &mut El<Ms>) {
el.style = self.clone();
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Listener<Ms> {
fn update(self, el: &mut El<Ms>) {
el.listeners.push(self)
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Vec<Listener<Ms>> {
fn update(self, el: &mut El<Ms>) {
for listener in self.into_iter() {
el.listeners.push(listener)
}
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for DidMount {
fn update(self, el: &mut El<Ms>) {
el.did_mount = Some(self.actions)
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for DidUpdate {
fn update(self, el: &mut El<Ms>) {
el.did_update = Some(self.actions)
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for WillUnmount {
fn update(self, el: &mut El<Ms>) {
el.will_unmount = Some(self.actions)
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for &str {
fn update(self, el: &mut El<Ms>) {
el.text = Some(self.into());
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Vec<El<Ms>> {
fn update(self, el: &mut El<Ms>) {
for child in self.into_iter() {
el.children.push(child);
}
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for El<Ms> {
fn update(self, el: &mut El<Ms>) {
el.children.push(self)
}
}
impl<Ms: Clone> UpdateEl<El<Ms>> for Tag {
fn update(self, el: &mut El<Ms>) {
el.tag = self;
}
}
pub enum _Attr {
Action,
Alt,
Class,
Disabled,
Height,
Href,
Id,
Lang,
OnChange,
OnClick,
OnContextMenu,
OnDblClick,
OnMouseOver,
Src,
Title,
Width,
}
#[derive(Clone, PartialEq)]
pub struct Attrs {
pub vals: HashMap<String, String>
}
impl Attrs {
pub fn new(vals: HashMap<String, String>) -> Self {
Self { vals }
}
pub fn empty() -> Self {
Self { vals: HashMap::new() }
}
pub fn from_id(name: &str) -> Self {
let mut result = Self::empty();
result.add("id", name);
result
}
pub fn to_string(&self) -> String {
self.vals.iter()
.map(|(k,v)|format!("{}=\"{}\"", k, v))
.collect::<Vec<_>>()
.join(" ")
}
pub fn add(&mut self, key: &str, val: &str) {
self.vals.insert(key.to_string(), val.to_string());
}
pub fn add_multiple(&mut self, name: &str, items: Vec<&str>) {
self.add(name, &items.join(" "));
}
pub fn merge(&self, other: &Self) -> Self {
let mut result = self.clone();
for (key, val) in &other.vals {
result.vals.insert(key.clone(), val.clone());
}
result
}
}
#[derive(Clone, PartialEq)]
pub struct Style {
pub vals: HashMap<String, String>
}
impl Style {
pub fn new(vals: HashMap<String, String>) -> Self {
let mut new_vals = HashMap::new();
for (key, val) in vals.into_iter() {
let val_backup = val.clone();
match val.parse::<u32>() {
Ok(_) => new_vals.insert(key, val_backup + "px"),
Err(_) => new_vals.insert(key, val_backup),
};
}
Self { vals: new_vals }
}
pub fn empty() -> Self {
Self { vals: HashMap::new() }
}
pub fn to_string(&self) -> String {
if self.vals.keys().len() > 0 {
self.vals
.iter()
.map(|(k, v)| format!("{}:{}", k, v))
.collect::<Vec<_>>()
.join(";")
} else { String::new() }
}
pub fn add(&mut self, key: &str, val: &str) {
self.vals.insert(key.to_string(), val.to_string());
}
pub fn merge(&self, other: &Self) -> Self {
let mut result = self.clone();
for (key, val) in &other.vals {
result.vals.insert(key.clone(), val.clone());
}
result
}
}
macro_rules! make_events {
{ $($event_camel:ident => $event:expr),+ } => {
#[derive(Clone)]
pub enum Event {
$(
$event_camel,
)+
}
impl Event {
pub fn as_str(&self) -> &str {
match self {
$ (
Event::$event_camel => $event,
) +
}
}
}
impl From<&str> for Event {
fn from(event: &str) -> Self {
match event {
$ (
$event => Event::$event_camel,
) +
_ => {
crate::log(&format!("Can't find this event: {}", event));
Event::Click
}
}
}
}
}
}
make_events! {
Cached => "cached", Error => "error", Abort => "abort", Load => "load", BeforeUnload => "beforeunload",
Unload => "unload", Online => "online", Offline => "offline", Focus => "focus", Blur => "blur",
Open => "open", Message => "message", Close => "close", PageHide => "pagehide",
PageShow => "pageshow", PopState => "popstate", AnimationStart => "animationstart", AnimationEnd => "animationend",
AnimationIteration => "animationiteration", TransitionStart => "transtionstart", TransitionEnd => "transitionend",
TranstionRun => "transitionrun",
Rest => "rest", Submit => "submit", BeforePrint => "beforeprint", AfterPrint => "afterprint",
CompositionStart => "compositionstart", CompositionUpdate => "compositionupdate", CompositionEnd => "compositionend",
FullScreenChange => "fullscreenchange", FullScreenError => "fullscreenerror", Resize => "resize",
Scroll => "scroll", Cut => "cut", Copy => "copy", Paste => "paste",
KeyDown => "keydown",
KeyPress => "keypress", AuxClick => "auxclick", Click => "click", ContextMenu => "contextmenu", DblClick => "dblclick",
MouseDown => "mousedown", MouseEnter => "mouseenter", MouseLeave => "mouseleave",
MouseMove => "mousemove", MouseOver => "mouseover", MouseOut => "mouseout", MouseUp => "mouseup",
PointerLockChange => "pointerlockchange", PointerLockError => "pointerlockerror", Select => "select",
Wheel => "wheel",
Drag => "drag", DragEnd => "dragend", DragEnter => "dragenter", DragStart => "dragstart", DragLeave => "dragleave",
DragOver => "dragover", Drop => "drop",
AudioProcess => "audioprocess", CanPlay => "canplay", CanPlayThrough => "canplaythrough", Complete => "complete",
DurationChange => "durationchange", Emptied => "emptied", Ended => "ended", LoadedData => "loadeddata",
LoadedMetaData => "loadedmetadata", Pause => "pause", Play => "play", Playing => "playing", RateChagne => "ratechange",
Seeked => "seeked", Seeking => "seeking", Stalled => "stalled", Suspend => "suspend", TimeUpdate => "timeupdate",
VolumeChange => "volumechange",
Change => "change",
Input => "input"
}
macro_rules! make_tags {
{ $($tag_camel:ident => $tag:expr),+ } => {
#[derive(Clone, PartialEq)]
pub enum Tag {
Custom(String),
$(
$tag_camel,
)+
}
impl Tag {
pub fn as_str(&self) -> &str {
match self {
Tag::Custom(name) => &name,
$ (
Tag::$tag_camel => $tag,
) +
}
}
}
}
}
make_tags! {
Address => "address", Article => "article", Aside => "aside", Footer => "footer",
Header => "header", H1 => "h1",
H2 => "h2", H3 => "h3", H4 => "h4", H5 => "h5", H6 => "h6",
Hgroup => "hgroup", Main => "main", Nav => "nav", Section => "section",
BlockQuote => "blockquote",
Dd => "dd", Dir => "dir", Div => "div", Dl => "dl", Dt => "dt", FigCaption => "figcaption", Figure => "figure",
Hr => "hr", Li => "li", Ol => "ol", P => "p", Pre => "pre", Ul => "ul",
A => "a", Abbr => "abbr",
B => "b", Bdi => "bdi", Bdo => "bdo", Br => "br", Cite => "cite", Code => "code", Data => "data",
Dfn => "dfn", Em => "em", I => "i", Kbd => "kbd", Mark => "mark", Q => "q", Rb => "rb",
Rp => "rp", Rt => "rt", Rtc => "rtc", Ruby => "ruby", S => "s", Samp => "samp", Small => "small",
Span => "span", Strong => "strong", Sub => "sub", Sup => "sup", Time => "time", Tt => "tt",
U => "u", Var => "var", Wbr => "wbr",
Area => "area", Audio => "audio", Img => "img", Map => "map", Track => "track", Video => "video",
Applet => "applet", Embed => "embed", Iframe => "iframe",
NoEmbed => "noembed", Object => "object", Param => "param", Picture => "picture", Source => "source",
Canvas => "canvas", NoScript => "noscript", Script => "Script",
Del => "del", Ins => "ins",
Caption => "caption", Col => "col", ColGroup => "colgroup", Table => "table", Tbody => "tbody",
Td => "td", Tfoot =>"tfoot", Th => "th", Thead => "thead", Tr => "tr",
Button => "button", DataList => "datalist", FieldSet => "fieldset", Form => "form", Input => "input",
Label => "label", Legend => "legend", Meter => "meter", OptGroup => "optgroup", Option => "option",
Output => "output", Progress => "progress", Select => "select", TextArea => "textarea",
Details => "details", Dialog => "dialog", Menu => "menu", MenuItem => "menuitem", Summary => "summary",
Content => "content", Element => "element", Shadow => "shadow", Slot => "slot", Template => "template",
Animate => "animate", AnimateColor => "animateColor", AnimateMotion => "animateMotion",
AnimateTransform => "animateTransform", Discard => "discard", Mpath => "mpath", Set => "set",
Circle => "circle", Ellipse => "ellipse", Line => "line", Polygon => "polygon",
Polyline => "polyline", Rect => "rect", Mesh => "mesh", Path => "path",
Defs => "defs", G => "g", Marker => "marker", Mask => "mask", MissingGlyph => "missing-glyph",
Pattern => "pattern", Svg => "svg", Switch => "switch", Symbol => "symbol", Unknown => "unknown",
Desc => "desc", Metadata => "metadata", Title => "title",
FeBlend => "feBlend",
FeColorMatrix => "feColorMatrix",
FeComponentTransfer => "feComponentTransfer",
FeComposite => "feComposite",
FeConvolveMatrix => "feConvolveMatrix",
FeDiffuseLighting => "feDiffuseLighting",
FeDisplacementMap => "feDisplacementMap",
FeDropShadow => "feDropShadow",
FeFlood => "feFlood",
FeFuncA => "feFuncA",
FeFuncB => "feFuncB",
FeFuncG => "feFuncG",
FeFuncR => "feFuncR",
FeGaussianBlur => "feGaussianBlur",
FeImage => "feImage",
FeMerge => "feMerge",
FeMergeNode => "feMergeNode",
FeMorphology => "feMorphology",
FeOffset => "feOffset",
FeSpecularLighting => "feSpecularLighting",
FeTile => "feTile",
FeTurbulence => "feTurbulence",
FeDistantLight => "feDistantLight", FePointLight => "fePointLight", FeSpotLight => "feSpotLight",
Font => "font",
FontFace => "font-face",
FontFaceFormat => "font-face-format",
FontFaceName => "font-face-name",
FontFaceSrc => "font-face-src",
FontFaceUri => "font-face-uri",
HKern => "hkern",
VKern => "vkern",
LinearGradient => "linearGradient", MeshGradient => "meshGradient",
RadialGradient => "radialGradient", Stop => "stop",
Image => "image",
Use => "use",
Hatch => "hatch", SolidColor => "solidcolor",
AltGlyph => "altGlyph", AltGlyphDef => "altGlyphDef", AltGlyphItem => "altGlyphItem", Glyph => "glyph",
GlyphRef => "glyphRef", TextPath => "textPath", Text => "text", TRef => "tref", TSpan => "tspan",
ClipPath => "clipPath", ColorProfile => "color-profile", Cursor => "cursor", Filter => "filter",
ForeignObject => "foreignObject", HatchPath => "hatchpath", MeshPatch => "meshpatch", MeshRow => "meshrow",
Style => "style", View => "view"
}
pub struct El<Ms: Clone + 'static> {
pub tag: Tag,
pub attrs: Attrs,
pub style: Style,
pub listeners: Vec<Listener<Ms>>,
pub text: Option<String>,
pub children: Vec<El<Ms>>,
pub id: Option<u32>,
pub nest_level: Option<u32>,
pub el_ws: Option<web_sys::Element>,
pub raw_html: bool,
pub namespace: Option<Namespace>,
pub did_mount: Option<Box<FnMut(&web_sys::Element)>>,
pub did_update: Option<Box<FnMut(&web_sys::Element)>>,
pub will_unmount: Option<Box<FnMut(&web_sys::Element)>>,
}
impl<Ms: Clone + 'static> El<Ms> {
pub fn empty(tag: Tag) -> Self {
Self {
tag,
attrs: Attrs::empty(),
style: Style::empty(),
listeners: Vec::new(),
text: None,
children: Vec::new(),
id: None,
nest_level: None,
el_ws: None,
raw_html: false,
namespace: None,
did_mount: None,
did_update: None,
will_unmount: None,
}
}
pub fn empty_svg(tag: Tag) -> Self {
let mut el = El::empty(tag);
el.namespace = Some(Namespace::Svg);
el
}
pub fn from_markdown(markdown: &str) -> Self {
let parser = pulldown_cmark::Parser::new(markdown);
let mut html_text = String::new();
pulldown_cmark::html::push_html(&mut html_text, parser);
let mut result = Self::empty(Tag::Span);
result.raw_html = true;
result.text = Some(html_text);
result
}
pub fn from_html(html: &str) -> Self {
let mut result = Self::empty(Tag::Span);
result.raw_html = true;
result.text = Some(html.into());
result
}
pub fn add_child(&mut self, element: El<Ms>) {
self.children.push(element);
}
pub fn add_attr(&mut self, key: String, val: String) {
self.attrs.vals.insert(key, val);
}
pub fn add_style(&mut self, key: String, val: String) {
self.style.vals.insert(key, val);
}
pub fn set_text(&mut self, text: &str) {
self.text = Some(text.into())
}
fn _html(&self) -> String {
let text = self.text.clone().unwrap_or_default();
let opening = String::from("<") + self.tag.as_str() + &self.attrs.to_string() +
" style=\"" + &self.style.to_string() + ">\n";
let inner = self.children.iter().fold(String::new(), |result, child| result + &child._html());
let closing = String::from("\n</") + self.tag.as_str() + ">";
opening + &text + &inner + &closing
}
pub fn quick_clone(&self) -> Self {
Self {
tag: self.tag.clone(),
attrs: Attrs::empty(),
style: Style::empty(),
listeners: Vec::new(),
text: None,
children: Vec::new(),
id: None,
nest_level: None,
el_ws: self.el_ws.clone(),
raw_html: self.raw_html,
namespace: self.namespace.clone(),
did_mount: None,
did_update: None,
will_unmount: None,
}
}
pub fn is_dummy(&self) -> bool {
if let Tag::Del = self.tag {
if self.attrs.vals.get("dummy-element").is_some() {
return true;
}
}
false
}
}
impl<Ms: Clone + 'static> Clone for El<Ms> {
fn clone(&self) -> Self {
Self {
tag: self.tag.clone(),
attrs: self.attrs.clone(),
style: self.style.clone(),
text: self.text.clone(),
children: self.children.clone(),
id: self.id,
nest_level: self.nest_level,
el_ws: self.el_ws.clone(),
listeners: Vec::new(),
raw_html: self.raw_html,
namespace: self.namespace.clone(),
did_mount: None,
did_update: None,
will_unmount: None,
}
}
}
impl<Ms: Clone + 'static> PartialEq for El<Ms> {
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag &&
self.attrs == other.attrs &&
self.style == other.style &&
self.text == other.text &&
self.listeners == other.listeners &&
self.nest_level == other.nest_level
}
}
pub struct DidMount {
actions: Box<FnMut(&web_sys::Element)>
}
pub struct DidUpdate {
actions: Box<FnMut(&web_sys::Element)>
}
pub struct WillUnmount {
actions: Box<FnMut(&web_sys::Element)>
}
pub fn did_mount(mut actions: impl FnMut(&web_sys::Element) + 'static) -> DidMount {
let closure = move |el: &web_sys::Element| actions(el);
DidMount { actions: Box::new(closure) }
}
pub fn did_update(mut actions: impl FnMut(&web_sys::Element) + 'static) -> DidUpdate {
let closure = move |el: &web_sys::Element| actions(el);
DidUpdate { actions: Box::new(closure) }
}
pub fn will_unmount(mut actions: impl FnMut(&web_sys::Element) + 'static) -> WillUnmount {
let closure = move |el: &web_sys::Element| actions(el);
WillUnmount { actions: Box::new(closure) }
}
#[cfg(test)]
pub mod tests {
use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser);
use wasm_bindgen_test::*;
use super::*;
use crate as seed;
use crate::{div, section, span, attrs, h1, p};
#[derive(Clone)]
enum Msg {
Placeholder
}
#[wasm_bindgen_test]
pub fn single() {
let expected = "<div>test</div>";
let mut el: El<Msg> = div![ "test" ];
crate::vdom::setup_els(&crate::util::document(), &mut el, 0, 0);
assert_eq!(expected, el.el_ws.unwrap().outer_html());
}
#[wasm_bindgen_test]
pub fn attrs() {
let expected = "<section class=\"biochemistry\" src=\"https://seed-rs.org\">ok</section>";
let mut el: El<Msg> = section![
attrs!{"class" => "biochemistry"; "src" => "https://seed-rs.org"},
"ok"
];
crate::vdom::setup_els(&crate::util::document(), &mut el, 0, 0);
assert_eq!(expected, el.el_ws.unwrap().outer_html());
}
}