use super::{util, websys_bridge};
use core::convert::AsRef;
use pulldown_cmark;
use serde::de::DeserializeOwned;
use std::{collections::HashMap, fmt};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys;
pub const UPDATE_TRIGGER_EVENT_ID: &str = "triggerupdate";
#[derive(Debug, Clone, PartialEq)]
pub enum Namespace {
Html,
Svg,
MathMl,
Xul,
Xbl,
Custom(String),
}
impl Namespace {
pub fn as_str(&self) -> &str {
use Namespace::*;
match self {
Html => "http://www.w3.org/1999/xhtml",
Svg => "http://www.w3.org/2000/svg",
MathMl => "http://www.w3.org/1998/mathml",
Xul => "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
Xbl => "http://www.mozilla.org/xbl",
Custom(s) => s,
}
}
}
impl From<String> for Namespace {
fn from(ns: String) -> Self {
match ns.as_ref() {
"http://www.w3.org/1999/xhtml" => Namespace::Html,
"http://www.w3.org/2000/svg" => Namespace::Svg,
"http://www.w3.org/1998/mathml" => Namespace::MathMl,
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" => Namespace::Xul,
"http://www.mozilla.org/xbl" => Namespace::Xbl,
_ => Namespace::Custom(ns),
}
}
}
pub fn simple_ev<Ms, T>(trigger: T, message: Ms) -> Listener<Ms>
where
Ms: Clone + 'static,
T: ToString,
{
let handler = || message;
let closure = move |_| handler.clone()();
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn input_ev<Ms, T: ToString>(
trigger: T,
mut handler: impl FnMut(String) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| {
if let Some(target) = event.target() {
return handler(util::get_value(&target));
}
handler(String::new())
};
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn raw_ev<Ms, T: ToString>(
trigger: T,
mut handler: impl FnMut(web_sys::Event) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| handler(event);
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn trigger_update_ev<Ms: Clone>(
mut handler: impl FnMut(web_sys::CustomEvent) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::CustomEvent>().unwrap().clone())
};
Listener::new(UPDATE_TRIGGER_EVENT_ID, Some(Box::new(closure)))
}
pub fn keyboard_ev<Ms: Clone, T: ToString>(
trigger: T,
mut handler: impl FnMut(web_sys::KeyboardEvent) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::KeyboardEvent>().unwrap().clone())
};
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn mouse_ev<Ms: Clone, T: ToString>(
trigger: T,
mut handler: impl FnMut(web_sys::MouseEvent) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::MouseEvent>().unwrap().clone())
};
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn pointer_ev<Ms, T: ToString>(
trigger: T,
mut handler: impl FnMut(web_sys::PointerEvent) -> Ms + 'static,
) -> Listener<Ms> {
let closure = move |event: web_sys::Event| {
handler(event.dyn_ref::<web_sys::PointerEvent>().unwrap().clone())
};
Listener::new(&trigger.to_string(), Some(Box::new(closure)))
}
pub fn trigger_update_handler<Ms: Clone + DeserializeOwned>() -> Listener<Ms> {
trigger_update_ev(|ev| {
ev.detail()
.into_serde()
.expect("trigger_update_handler: Deserialization failed!")
})
}
type EventHandler<Ms> = Box<FnMut(web_sys::Event) -> Ms>;
pub struct Listener<Ms> {
pub trigger: Ev,
pub handler: Option<EventHandler<Ms>>,
pub closure: Option<Closure<FnMut(web_sys::Event)>>,
pub control_val: Option<String>,
pub control_checked: Option<bool>,
}
impl<Ms> fmt::Debug for Listener<Ms> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Listener {{ trigger:{:?}, handler:{:?}, closure:{:?}, control:{:?}{:?} }}",
self.trigger,
fmt_hook_fn(&self.handler),
fmt_hook_fn(&self.closure),
self.control_val,
self.control_checked
)
}
}
impl<Ms> Listener<Ms> {
pub fn new(trigger: &str, handler: Option<EventHandler<Ms>>) -> Self {
Self {
trigger: trigger.into(),
handler,
closure: None,
control_val: None,
control_checked: None,
}
}
pub fn new_control(val: String) -> Self {
Self {
trigger: Ev::Input,
handler: None,
closure: None,
control_val: Some(val),
control_checked: None,
}
}
pub fn new_control_check(checked: bool) -> Self {
Self {
trigger: Ev::Click,
handler: None,
closure: None,
control_val: None,
control_checked: Some(checked),
}
}
pub fn attach<T>(&mut self, el_ws: &T, mailbox: crate::vdom::Mailbox<Ms>)
where
T: AsRef<web_sys::EventTarget>,
{
let mut handler = self.handler.take().expect("Can't find old handler");
let trigger = self.trigger.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
let msg = handler(event.clone());
mailbox.send(msg);
if trigger == Ev::Input {
let target = event.target().unwrap();
if let Some(input_el) = target.dyn_ref::<web_sys::HtmlInputElement>() {
let input_type = input_el.type_();
let should_trigger_rerender_with_set_value = {
(input_type == "number" || input_type == "text" || input_type == "password")
};
if should_trigger_rerender_with_set_value {
let value_set_by_seed_user = input_el.default_value();
let actual_value = input_el.value();
if value_set_by_seed_user != actual_value {
input_el.set_value(&value_set_by_seed_user);
}
}
}
}
}) as Box<FnMut(web_sys::Event) + 'static>);
(el_ws.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback(
self.trigger.as_str(),
closure.as_ref().unchecked_ref(),
)
.expect("problem adding listener to element");
if self.closure.replace(closure).is_some() {
panic!("self.closure already set in attach");
}
}
pub fn attach_control(&mut self, el_ws: &web_sys::Node) {
let val2 = self.control_val.clone();
let checked2 = self.control_checked;
let el_ws2 = el_ws.clone();
let closure = Closure::wrap(Box::new(move |_| {
if let Some(val) = val2.clone() {
if util::get_value(&el_ws2) != val {
util::set_value(&el_ws2, &val);
}
}
if let Some(checked) = checked2 {
let input_el = &el_ws2
.dyn_ref::<web_sys::HtmlInputElement>()
.expect("Problem casting as checkbox");
if input_el.checked() != checked {
input_el.set_checked(checked);
}
}
}) as Box<FnMut(web_sys::Event) + 'static>);
(el_ws.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback(
self.trigger.as_str(),
closure.as_ref().unchecked_ref(),
)
.expect("problem adding listener to element");
if self.closure.replace(closure).is_some() {
panic!("self.closure already set in attach_control");
}
}
pub fn detach<T>(&mut self, el_ws: &T)
where
T: AsRef<web_sys::EventTarget>,
{
let closure = self.closure.take().expect("Can't find closure to detach");
(el_ws.as_ref() as &web_sys::EventTarget)
.remove_event_listener_with_callback(
&self.trigger.as_str(),
closure.as_ref().unchecked_ref(),
)
.expect("problem removing listener from element");
}
}
impl<Ms: 'static> Listener<Ms> {
fn map_message<OtherMs, F>(self, f: F) -> Listener<OtherMs>
where
F: Fn(Ms) -> OtherMs + 'static,
{
Listener {
trigger: self.trigger,
handler: self.handler.map(|mut eh| {
Box::new(move |event| {
let m = (*eh)(event);
(f)(m)
}) as EventHandler<OtherMs>
}),
closure: self.closure,
control_val: self.control_val,
control_checked: self.control_checked,
}
}
}
impl<Ms> PartialEq for Listener<Ms> {
fn eq(&self, other: &Self) -> bool {
self.trigger == other.trigger
}
}
pub trait UpdateEl<T> {
fn update(self, el: &mut T);
}
impl<Ms> UpdateEl<El<Ms>> for Attrs {
fn update(self, el: &mut El<Ms>) {
el.attrs.merge(self);
}
}
impl<Ms> UpdateEl<El<Ms>> for &Attrs {
fn update(self, el: &mut El<Ms>) {
el.attrs.merge(self.clone());
}
}
impl<Ms> UpdateEl<El<Ms>> for Style {
fn update(self, el: &mut El<Ms>) {
el.style.merge(self);
}
}
impl<Ms> UpdateEl<El<Ms>> for &Style {
fn update(self, el: &mut El<Ms>) {
el.style.merge(self.clone());
}
}
impl<Ms> UpdateEl<El<Ms>> for Listener<Ms> {
fn update(self, el: &mut El<Ms>) {
el.listeners.push(self)
}
}
impl<Ms> UpdateEl<El<Ms>> for Vec<Listener<Ms>> {
fn update(mut self, el: &mut El<Ms>) {
el.listeners.append(&mut self);
}
}
impl<Ms> UpdateEl<El<Ms>> for DidMount<Ms> {
fn update(self, el: &mut El<Ms>) {
el.hooks.did_mount = Some(self)
}
}
impl<Ms> UpdateEl<El<Ms>> for DidUpdate<Ms> {
fn update(self, el: &mut El<Ms>) {
el.hooks.did_update = Some(self)
}
}
impl<Ms> UpdateEl<El<Ms>> for WillUnmount<Ms> {
fn update(self, el: &mut El<Ms>) {
el.hooks.will_unmount = Some(self)
}
}
impl<Ms> UpdateEl<El<Ms>> for &str {
fn update(self, el: &mut El<Ms>) {
el.children.push(El::new_text(self))
}
}
impl<Ms> UpdateEl<El<Ms>> for Vec<El<Ms>> {
fn update(mut self, el: &mut El<Ms>) {
el.children.append(&mut self);
}
}
impl<Ms> UpdateEl<El<Ms>> for El<Ms> {
fn update(self, el: &mut El<Ms>) {
el.children.push(self)
}
}
impl<Ms> UpdateEl<El<Ms>> for Tag {
fn update(self, el: &mut El<Ms>) {
el.tag = self;
}
}
impl<Ms> UpdateEl<El<Ms>> for Optimize {
fn update(self, el: &mut El<Ms>) {
el.optimizations.push(self)
}
}
macro_rules! make_attrs {
{ $($attr_camel:ident => $attr:expr),+ } => {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum At {
$(
$attr_camel,
)+
Custom(String)
}
impl At {
pub fn as_str(&self) -> &str {
match self {
$ (
At::$attr_camel => $attr,
) +
At::Custom(val) => &val
}
}
}
impl From<&str> for At {
fn from(attr: &str) -> Self {
match attr {
$ (
$attr => At::$attr_camel,
) +
_ => {
At::Custom(attr.to_owned())
}
}
}
}
impl From<String> for At {
fn from(attr: String) -> Self {
match attr.as_ref() {
$ (
$attr => At::$attr_camel,
) +
_ => {
At::Custom(attr)
}
}
}
}
}
}
make_attrs! {
Accept => "accept", AcceptCharset => "accept-charset", AccessKey => "accesskey", Action => "action",
Alt => "alt", Async => "async", AutoComplete => "autocomplete", AutoFocus => "autofocus",
AutoPlay => "autoplay", Charset => "charset", Checked => "checked", Cite => "cite", Class => "class",
Color => "color", Cols => "cols", ColSpan => "colspan", Content => "content", ContentEditable => "contenteditable",
Controls => "controls", Coords => "coords", Data => "data", DateTime => "datetime", Default => "default",
Defer => "defer", Dir => "dir", DirName => "dirname", Disabled => "disabled", Download => "download",
Draggable => "draggable", DropZone => "dropzone", EncType => "enctype", For => "for", Form => "form",
FormAction => "formaction", Headers => "headers", Height => "height", Hidden => "hidden", High => "high",
Href => "href", Hreflang => "hreflang", HttpEquiv => "http-equiv", Id => "id", IsMap => "ismap",
Kind => "kind", Label => "label", Lang => "lang", List => "list", Loop => "loop", Low => "low",
Max => "max", MaxLength => "maxlength", Media => "media", Method => "method", Min => "min", Multiple => "multiple",
Muted => "muted", Name => "name", NoValidate => "novalidate", OnAbort => "onabort", OnAfterPrint => "onafterprint",
OnBeforePrint => "onbeforeprint", OnBeforeUnload => "onbeforeunload", OnBlur => "onblur", OnCanPlay => "oncanplay",
OnCanPlayThrough => "oncanplaythrough", OnChange => "onchange", OnClick => "onclick", OnContextMenu => "oncontextmenu",
OnCopy => "oncopy", OnCueChange => "oncuechange", OnCut => "oncut", OnDblClick => "ondblclick",
OnDrag => "ondrag", OnDragend => "ondragend", OnDragEnter => "ondragenter", OnDragLeave => "ondragleave",
OnDragOver => "ondragover", OnDragStart => "ondragstart", OnDrop => "ondrop", OnDurationChange => "ondurationchange",
OnEmptied => "onemptied", OnEnded => "onended", OnError => "onerror", OnFocus => "onfocus",
OnHashChange => "onhashchange", OnInput => "oninput", OnInvalid => "oninvalid", OnKeyDown => "onkeydown",
OnKeyPress => "onkeypress", OnKeyUp => "onkeyup", OnLoad => "onload", OnLoadedData => "onloadeddata",
OnLoadedMetaData => "onloadedmetadata", OnLoadStart => "onloadstart", OnMouseDown => "onmousedown",
OnMouseMove => "onmousemove", OnMouseOut => "onmouseout", OnMouseOver => "onmouseover", OnMouseUp => "onmouseup",
OnMouseWheel => "onmousewheel", OnOffline => "onoffline", OnOnline => "ononline", OnPageHide => "onpagehide",
OnPageShow => "onpageshow", OnPaste => "onpaste", OnPause => "onpause", OnPlay => "onplay",
OnPlaying => "onplaying", OnPopstate => "onpopstate", OnProgress => "onprogress", OnRateChangen => "onratechange",
OnRest => "onreset", OnResize => "onresize", OnScroll => "onscroll", OnSearch => "onsearch",
OnSeeked => "onseeked", OnSeeking => "onseeking", OnSelect => "onselect", OnStalled => "onstalled",
OnStorage => "onstorage", OnSubmit => "onsubmit", Onsuspend => "onsuspend", OnTimeUpdate => "ontimeupdate",
OnToggle => "ontoggle", OnUnload => "onunload", OnVolumeChange => "onvolumechange", OnWaiting => "onwaiting",
OnWheel => "onwheel", Open => "open", Optimum => "optimum", Pattern => "pattern", PlaceHolder => "placeholder",
Poster => "poster", Preload => "preload", ReadOnly => "readonly", Rel => "rel", Required => "required",
Reversed => "reversed", Rows => "rows", RowSpan => "rowspan", Sandbox => "sandbox", Scope => "scope",
Selected => "selected", Shape => "shape", Size => "size", Span => "span", SpellCheck => "spellcheck",
Src => "src", SrcDoc => "srcdoc", SrcLang => "srclang", SrcSet => "srcset", Start => "start",
Step => "step", Style => "style", TabIndex => "tabindex", Target => "target", Title => "title",
Translate => "translate", Type => "type", UseMap => "usemap", Value => "value", Width => "width",
Wrap => "wrap",
AccentHeight => "accent-height", Accumulate => "accumulate", Additive => "additive",
AlignmentBaseline => "alignment-baseline", AllowReorder => "allowReorder", Amplitude => "amplitude",
ArabicForm => "arabic-form", Ascent => "ascent", AttributeName => "attributeName", AttributeType => "attributeType",
AutoReverse => "autoReverse", Azimuth => "azimumth", BaseFrequency => "baseFrequency", BaselineShift => "baseline-shift",
BaseProfile => "baseProfile", Bbox => "bbox", Begin => "begin", Bias => "bias", By => "by",
CalcMode => "calcMode", CapHeight => "cap-height", Clip => "clip",
Path => "path", D => "d", Xmlns => "xmlns", ViewBox => "viewBox", Fill => "fill"
}
macro_rules! make_styles {
{ $($st_camel:ident => $st:expr),+ } => {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum St {
$(
$st_camel,
)+
Custom(String)
}
impl St {
pub fn as_str(&self) -> &str {
match self {
$ (
St::$st_camel => $st,
) +
St::Custom(val) => &val
}
}
}
impl From<&str> for St {
fn from(st: &str) -> Self {
match st {
$ (
$st => St::$st_camel,
) +
_ => {
crate::log(&format!("Can't find this attribute: {}", st));
St::Background
}
}
}
}
impl From<String> for St {
fn from(st: String) -> Self {
match st.as_ref() {
$ (
$st => St::$st_camel,
) +
_ => {
crate::log(&format!("Can't find this attribute: {}", st));
St::Background
}
}
}
}
}
}
make_styles! {
AdditiveSymbols => "additive-symbols", AlignContent => "align-content", AlignItems => "align-items",
AlignSelf => "align-self", All => "all", Angle => "angle", Animation => "animation", AnimationDelay => "animation-delay",
AnimationDirection => "animation-direction", AnimationDuration => "animation-duration",
AnimationFillMode => "animation-fill-mode", AnimationIterationCount => "animation-iteration-count",
AnimationName => "animation-name", AnimationPlayState => "animation-play-state",
Background => "background", BackgroundAttachment => "background-attachment", BackgroundColor => "background-color",
BackgroundImage => "background-image", BackgroundPosition => "background-position", BackgroundRepeat => "background-repeat",
Border => "border", BorderBottom => "border-bottom", BorderBottomColor => "border-bottom-color",
BorderBottomStyle => "border-bottom-style", BorderBottomWidth => "border-bottom-width", BorderColor => "border-color"
}
#[derive(Clone, Debug, PartialEq)]
pub struct Attrs {
pub vals: HashMap<At, String>,
}
impl Attrs {
pub fn new(vals: HashMap<At, 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(At::Id, name);
result
}
pub fn to_string(&self) -> String {
self.vals
.iter()
.map(|(k, v)| format!("{}=\"{}\"", k.as_str(), v))
.collect::<Vec<_>>()
.join(" ")
}
pub fn add(&mut self, key: At, val: &str) {
self.vals.insert(key, val.to_string());
}
pub fn add_multiple(&mut self, key: At, items: Vec<&str>) {
self.add(key, &items.join(" "));
}
pub fn merge(&mut self, other: Self) {
for (other_key, other_value) in other.vals.into_iter() {
match self.vals.get_mut(&other_key) {
Some(original_value) => {
Self::merge_attribute_values(&other_key, original_value, other_value);
}
None => {
self.vals.insert(other_key, other_value);
}
}
}
}
fn merge_attribute_values(key: &At, original_value: &mut String, other_value: String) {
match key {
At::Class => {
original_value.push(' ');
original_value.push_str(&other_value);
}
_ => *original_value = other_value,
}
}
}
#[derive(Clone, Debug, 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 add(&mut self, key: &str, val: &str) {
self.vals.insert(key.to_string(), val.to_string());
}
pub fn merge(&mut self, other: Self) {
self.vals.extend(other.vals.into_iter());
}
}
impl ToString for Style {
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()
}
}
}
macro_rules! make_events {
{ $($event_camel:ident => $event:expr),+ } => {
#[derive(Clone, Debug, PartialEq)]
pub enum Ev {
$(
$event_camel,
)+
}
impl Ev {
pub fn as_str(&self) -> &str {
match self {
$ (
Ev::$event_camel => $event,
) +
}
}
}
impl From<&str> for Ev {
fn from(event: &str) -> Self {
match event {
$ (
$event => Ev::$event_camel,
) +
_ => {
crate::log(&format!("Can't find this event: {}", event));
Ev::Click
}
}
}
}
impl From<String> for Ev {
fn from(event: String) -> Self {
match event.as_ref(){
$ (
$event => Ev::$event_camel,
) +
_ => {
crate::log(&format!("Can't find this event: {}", event));
Ev::Click
}
}
}
}
impl ToString for Ev {
fn to_string( & self ) -> String {
match self {
$ (
Ev::$ event_camel => $ event.into(),
) +
}
}
}
}
}
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", KeyUp => "keyup",
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",
PointerOver => "pointerover", PointerEnter => "pointerenter",
PointerDown => "pointerdown", PointerMove => "pointermove", PointerUp => "pointerup",
PointerCancel => "pointercancel", PointerOut => "pointerout", PointerLeave => "pointerleave",
GotPointerCapture => "gotpointercapture", LostPointerCapture => "lostpointercapture",
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", RateChange => "ratechange",
Seeked => "seeked", Seeking => "seeking", Stalled => "stalled", Suspend => "suspend", TimeUpdate => "timeupdate",
VolumeChange => "volumechange",
Change => "change",
Input => "input",
TriggerUpdate => "triggerupdate"
}
macro_rules! make_tags {
{ $($tag_camel:ident => $tag:expr),+ } => {
#[derive(Clone, Debug, 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,
) +
}
}
}
impl From<String> for Tag {
fn from(tag: String) -> Self {
match tag.as_ref() {
$ (
$tag => Tag::$tag_camel,
) +
_ => {
Tag::Span
}
}
}
}
}
}
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"
}
#[derive(Copy, Clone, Debug)]
pub enum Optimize {
Key(u32),
Static,
}
pub trait ElContainer<Ms> {
fn els(self) -> Vec<El<Ms>>;
}
impl<Ms> ElContainer<Ms> for El<Ms> {
fn els(self) -> Vec<El<Ms>> {
vec![self]
}
}
impl<Ms> ElContainer<Ms> for Vec<El<Ms>> {
fn els(self) -> Vec<El<Ms>> {
self
}
}
#[derive(Debug)]
pub struct El<Ms: '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 el_ws: Option<web_sys::Node>,
pub namespace: Option<Namespace>,
pub ref_: Option<String>,
pub hooks: LifecycleHooks<Ms>,
pub empty: bool,
optimizations: Vec<Optimize>,
}
type _HookFn = Box<FnMut(&web_sys::Node)>;
pub struct LifecycleHooks<Ms> {
pub did_mount: Option<DidMount<Ms>>,
pub did_update: Option<DidUpdate<Ms>>,
pub will_unmount: Option<WillUnmount<Ms>>,
}
impl<Ms> LifecycleHooks<Ms> {
fn new() -> Self {
Self {
did_mount: None,
did_update: None,
will_unmount: None,
}
}
}
fn fmt_hook_fn<T>(h: &Option<T>) -> &'static str {
match h {
Some(_) => "Some(.. a dynamic handler ..)",
None => "None",
}
}
impl<Ms> fmt::Debug for LifecycleHooks<Ms> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"LifecycleHooks {{ did_mount:{:?}, did_update:{:?}, will_unmount:{} }}",
fmt_hook_fn(&self.did_mount),
fmt_hook_fn(&self.did_update),
fmt_hook_fn(&self.will_unmount)
)
}
}
impl<Ms> El<Ms> {
pub fn empty(tag: Tag) -> Self {
Self {
tag,
attrs: Attrs::empty(),
style: Style::empty(),
listeners: Vec::new(),
text: None,
children: Vec::new(),
el_ws: None,
namespace: None,
hooks: LifecycleHooks::new(),
empty: false,
optimizations: Vec::new(),
ref_: None,
}
}
pub fn map_message<OtherMs, F>(self, f: F) -> El<OtherMs>
where
F: Fn(Ms) -> OtherMs + Copy + 'static,
{
El {
tag: self.tag,
attrs: self.attrs,
style: self.style,
listeners: self
.listeners
.into_iter()
.map(|l| l.map_message(f))
.collect(),
text: self.text,
children: self
.children
.into_iter()
.map(|c| c.map_message(f))
.collect(),
el_ws: self.el_ws,
namespace: self.namespace,
hooks: LifecycleHooks::new(),
empty: self.empty,
optimizations: self.optimizations,
ref_: None,
}
}
pub fn new_text(text: &str) -> Self {
let mut result = Self::empty(Tag::Span);
result.text = Some(text.into());
result
}
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) -> Vec<Self> {
let parser = pulldown_cmark::Parser::new(markdown);
let mut html_text = String::new();
pulldown_cmark::html::push_html(&mut html_text, parser);
Self::from_html(&html_text)
}
pub fn from_html(html: &str) -> Vec<Self> {
let el_ws_wrapper = util::document()
.create_element("div")
.expect("Problem creating web-sys element");
el_ws_wrapper.set_inner_html(html);
let mut result = Vec::new();
let children = el_ws_wrapper.child_nodes();
for i in 0..children.length() {
let child = children
.get(i)
.expect("Can't find child in raw html element.");
if let Some(child_vdom) = websys_bridge::el_from_ws(&child) {
result.push(child_vdom)
}
}
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.into(), 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())
}
pub fn key(&self) -> Option<u32> {
for o in &self.optimizations {
if let Optimize::Key(key) = o {
return Some(*key);
}
}
None
}
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 get_text(&self) -> String {
let mut result = String::new();
for child in &self.children {
if let Some(text) = &child.text {
result += text;
}
}
result
}
pub fn walk_tree_mut<F>(&mut self, mut f: F)
where
F: FnMut(&mut Self),
{
fn walk_tree_mut_inner<Ms, F>(el: &mut El<Ms>, f: &mut F)
where
F: FnMut(&mut El<Ms>),
{
f(el);
for child in el.children.iter_mut() {
walk_tree_mut_inner(child, f);
}
}
walk_tree_mut_inner(self, &mut f);
}
pub fn ref_<S: ToString>(&mut self, ref_: S) {
self.ref_ = Some(ref_.to_string());
}
}
impl<Ms> 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(),
el_ws: self.el_ws.clone(),
listeners: Vec::new(),
namespace: self.namespace.clone(),
hooks: LifecycleHooks::new(),
empty: self.empty,
optimizations: self.optimizations.clone(),
ref_: self.ref_.clone(),
}
}
}
impl<Ms> 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.namespace == other.namespace
&& self.empty == other.empty
&& self.ref_ == other.ref_
}
}
pub struct DidMount<Ms> {
pub actions: Box<FnMut(&web_sys::Node)>,
pub message: Option<Ms>,
}
impl<Ms> DidMount<Ms> {
pub fn update2(mut self, message: Ms) -> Self {
self.message = Some(message);
self
}
}
pub struct DidUpdate<Ms> {
pub actions: Box<FnMut(&web_sys::Node)>,
pub message: Option<Ms>,
}
impl<Ms> DidUpdate<Ms> {
pub fn update2(mut self, message: Ms) -> Self {
self.message = Some(message);
self
}
}
pub struct WillUnmount<Ms> {
pub actions: Box<FnMut(&web_sys::Node)>,
pub message: Option<Ms>,
}
impl<Ms> WillUnmount<Ms> {
pub fn update2(mut self, message: Ms) -> Self {
self.message = Some(message);
self
}
}
pub fn did_mount<Ms>(mut actions: impl FnMut(&web_sys::Node) + 'static) -> DidMount<Ms> {
let closure = move |el: &web_sys::Node| actions(el);
DidMount {
actions: Box::new(closure),
message: None,
}
}
pub fn did_update<Ms>(mut actions: impl FnMut(&web_sys::Node) + 'static) -> DidUpdate<Ms> {
let closure = move |el: &web_sys::Node| actions(el);
DidUpdate {
actions: Box::new(closure),
message: None,
}
}
pub fn will_unmount<Ms>(mut actions: impl FnMut(&web_sys::Node) + 'static) -> WillUnmount<Ms> {
let closure = move |el: &web_sys::Node| actions(el);
WillUnmount {
actions: Box::new(closure),
message: None,
}
}
#[cfg(test)]
pub mod tests {
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use super::*;
use crate as seed;
use crate::vdom;
use std::collections::HashSet;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Element, Node};
#[derive(Debug)]
enum Msg {}
struct Model {}
fn create_app() -> seed::App<Msg, Model, El<Msg>> {
seed::App::build(Model {}, |_, _, _| (), |_| seed::empty())
.mount(util::body())
.finish()
}
fn el_to_websys(mut el: El<Msg>) -> Node {
let document = crate::util::document();
let parent = document.create_element("div").unwrap();
let app = create_app();
vdom::patch(
&document,
seed::empty(),
&mut el,
&parent,
None,
&vdom::Mailbox::new(|_: Msg| {}),
&app,
);
el.el_ws.unwrap()
}
fn get_node_html(node: &Node) -> String {
node.dyn_ref::<Element>().unwrap().outer_html()
}
fn get_node_attrs(node: &Node) -> HashMap<String, String> {
let element = node.dyn_ref::<Element>().unwrap();
element
.get_attribute_names()
.values()
.into_iter()
.map(|item_res| {
item_res.map(|item| {
let name = item.as_string().unwrap();
let value = element.get_attribute(&name).unwrap();
(name, value)
})
})
.collect::<Result<HashMap<String, String>, JsValue>>()
.unwrap()
}
#[wasm_bindgen_test]
pub fn single_div() {
let expected = "<div>test</div>";
let node = el_to_websys(div!["test"]);
assert_eq!(expected, get_node_html(&node));
}
#[wasm_bindgen_test]
pub fn nested_divs() {
let expected = "<section><div><div><h1>huge success</h1></div><p>\
I'm making a note here</p></div><span>This is a triumph</span></section>";
let node = el_to_websys(section![
div![div![h1!["huge success"]], p!["I'm making a note here"]],
span!["This is a triumph"]
]);
assert_eq!(expected, get_node_html(&node));
}
#[wasm_bindgen_test]
pub fn attrs_work() {
let expected = "<section src=\"https://seed-rs.org\" class=\"biochemistry\">ok</section>";
let expected2 = "<section class=\"biochemistry\" src=\"https://seed-rs.org\">ok</section>";
let node = el_to_websys(section![
attrs! {"class" => "biochemistry"; "src" => "https://seed-rs.org"},
"ok"
]);
let actual_html = get_node_html(&node);
assert!(expected == actual_html || expected2 == actual_html);
}
#[wasm_bindgen_test]
pub fn merge_different_attrs() {
let node = el_to_websys(a![
id!["my_id"],
style!["background-color" => "red"],
class!["my_class1"],
attrs![
At::Href => "#my_ref";
],
attrs![
At::Name => "whatever";
],
]);
let mut expected = HashMap::new();
expected.insert("id".to_string(), "my_id".to_string());
expected.insert("style".to_string(), "background-color:red".to_string());
expected.insert("class".to_string(), "my_class1".to_string());
expected.insert("href".to_string(), "#my_ref".to_string());
expected.insert("name".to_string(), "whatever".to_string());
assert_eq!(expected, get_node_attrs(&node));
}
#[wasm_bindgen_test]
pub fn merge_classes() {
let node = el_to_websys(a![
class!["my_class1", "my_class2"],
class!["my_class3"],
attrs![
At::Class => "my_class4 my_class5";
]
]);
let mut expected = HashMap::new();
expected.insert(
"class".to_string(),
"my_class1 my_class2 my_class3 my_class4 my_class5".to_string(),
);
assert_eq!(expected, get_node_attrs(&node));
}
#[wasm_bindgen_test]
pub fn merge_styles() {
let node = el_to_websys(a![
style!["border-top" => "1px"; "border-bottom" => "red"],
style!["background-color" => "blue"],
]);
let attrs = get_node_attrs(&node);
let actual_styles = attrs["style"]
.split(";")
.map(|x| x.to_string())
.collect::<HashSet<String>>();
let mut expected = HashSet::new();
expected.insert("border-top:1px".to_string());
expected.insert("border-bottom:red".to_string());
expected.insert("background-color:blue".to_string());
assert_eq!(expected, actual_styles);
}
#[wasm_bindgen_test]
pub fn merge_id() {
let node = el_to_websys(a![
id!["my_id1"],
attrs![
At::Id => "my_id2";
]
]);
let mut expected = HashMap::new();
expected.insert("id".to_string(), "my_id2".to_string());
assert_eq!(expected, get_node_attrs(&node));
}
}