-
Notifications
You must be signed in to change notification settings - Fork 184
add class and style interfaces #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8f31522
c2ca085
a00b158
c4faf53
cbeef67
76bb994
6240b7f
f29a402
92f26a1
678ced5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| use std::{borrow::Cow, marker::PhantomData}; | ||
|
|
||
| use xilem_core::{Id, MessageResult}; | ||
|
|
||
| use crate::{ | ||
| interfaces::{sealed::Sealed, Element}, | ||
| ChangeFlags, Cx, View, ViewMarker, | ||
| }; | ||
|
|
||
| /// Applies a class to the underlying element. | ||
| pub struct Class<E, T, A> { | ||
| pub(crate) element: E, | ||
| pub(crate) class_name: Option<Cow<'static, str>>, | ||
| pub(crate) phantom: PhantomData<fn() -> (T, A)>, | ||
| } | ||
|
|
||
| impl<E, T, A> ViewMarker for Class<E, T, A> {} | ||
| impl<E, T, A> Sealed for Class<E, T, A> {} | ||
|
|
||
| impl<E: Element<T, A>, T, A> View<T, A> for Class<E, T, A> { | ||
| type State = E::State; | ||
| type Element = E::Element; | ||
|
|
||
| fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { | ||
| if let Some(class_name) = &self.class_name { | ||
| cx.add_class_to_element(class_name); | ||
| } | ||
| self.element.build(cx) | ||
| } | ||
|
|
||
| fn rebuild( | ||
| &self, | ||
| cx: &mut Cx, | ||
| prev: &Self, | ||
| id: &mut Id, | ||
| state: &mut Self::State, | ||
| element: &mut Self::Element, | ||
| ) -> ChangeFlags { | ||
| if let Some(class_name) = &self.class_name { | ||
| cx.add_class_to_element(class_name); | ||
| } | ||
| self.element.rebuild(cx, &prev.element, id, state, element) | ||
| } | ||
|
|
||
| fn message( | ||
| &self, | ||
| id_path: &[Id], | ||
| state: &mut Self::State, | ||
| message: Box<dyn std::any::Any>, | ||
| app_state: &mut T, | ||
| ) -> MessageResult<A> { | ||
| self.element.message(id_path, state, message, app_state) | ||
| } | ||
| } | ||
|
|
||
| crate::interfaces::impl_dom_interfaces_for_ty!(Element, Class); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,12 +41,52 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { | |
| } | ||
| } | ||
|
|
||
| fn set_class(element: &web_sys::Element, class_name: &str) { | ||
| #[cfg(debug_assertions)] | ||
| if class_name.is_empty() { | ||
| panic!("class names cannot be the empty string"); | ||
| } | ||
| #[cfg(debug_assertions)] | ||
| if class_name.contains(' ') { | ||
| panic!("class names cannot contain the ascii space character"); | ||
| } | ||
| element.class_list().add_1(class_name).unwrap_throw() | ||
richard-uk1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| fn remove_class(element: &web_sys::Element, class_name: &str) { | ||
| #[cfg(debug_assertions)] | ||
| if class_name.is_empty() { | ||
| panic!("class names cannot be the empty string"); | ||
| } | ||
| #[cfg(debug_assertions)] | ||
| if class_name.contains(' ') { | ||
| panic!("class names cannot contain the ascii space character"); | ||
| } | ||
| element.class_list().remove_1(class_name).unwrap_throw() | ||
| } | ||
|
|
||
| fn set_style(element: &web_sys::Element, name: &str, value: &str) { | ||
| // styles will be ignored for non-html elements (e.g. SVG) | ||
| if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() { | ||
| el.style().set_property(name, value).unwrap_throw() | ||
| } | ||
| } | ||
|
|
||
| fn remove_style(element: &web_sys::Element, name: &str) { | ||
| // styles will be ignored for non-html elements (e.g. SVG) | ||
| if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() { | ||
| el.style().remove_property(name).unwrap_throw(); | ||
| } | ||
richard-uk1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Note: xilem has derive Clone here. Not sure. | ||
| pub struct Cx { | ||
| id_path: IdPath, | ||
| document: Document, | ||
| // TODO There's likely a cleaner more robust way to propagate the attributes to an element | ||
| pub(crate) current_element_attributes: VecMap<CowStr, AttributeValue>, | ||
| pub(crate) current_element_classes: VecMap<CowStr, ()>, | ||
| pub(crate) current_element_styles: VecMap<CowStr, CowStr>, | ||
|
||
| app_ref: Option<Box<dyn AppRunner>>, | ||
| } | ||
|
|
||
|
|
@@ -70,6 +110,8 @@ impl Cx { | |
| document: crate::document(), | ||
| app_ref: None, | ||
| current_element_attributes: Default::default(), | ||
| current_element_classes: Default::default(), | ||
| current_element_styles: Default::default(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -145,26 +187,77 @@ impl Cx { | |
| &mut self, | ||
| ns: &str, | ||
| name: &str, | ||
| ) -> (web_sys::Element, VecMap<CowStr, AttributeValue>) { | ||
| ) -> ( | ||
| web_sys::Element, | ||
| VecMap<CowStr, AttributeValue>, | ||
| VecMap<CowStr, ()>, | ||
| VecMap<CowStr, CowStr>, | ||
| ) { | ||
| let el = self | ||
| .document | ||
| .create_element_ns(Some(ns), name) | ||
| .expect("could not create element"); | ||
| let attributes = self.apply_attributes(&el); | ||
| (el, attributes) | ||
| let classes = self.apply_classes(&el); | ||
| let styles = self.apply_styles(&el); | ||
| (el, attributes, classes, styles) | ||
| } | ||
|
|
||
| pub(crate) fn rebuild_element( | ||
| &mut self, | ||
| element: &web_sys::Element, | ||
| attributes: &mut VecMap<CowStr, AttributeValue>, | ||
| classes: &mut VecMap<CowStr, ()>, | ||
| styles: &mut VecMap<CowStr, CowStr>, | ||
| ) -> ChangeFlags { | ||
| self.apply_attribute_changes(element, attributes) | ||
| | self.apply_class_changes(element, classes) | ||
| | self.apply_style_changes(element, styles) | ||
| } | ||
|
|
||
| // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) | ||
| // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) | ||
| pub(crate) fn add_attr_to_element(&mut self, name: &CowStr, value: &Option<AttributeValue>) { | ||
| // Special-case class so it works with the `class` method | ||
richard-uk1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if name == "class" { | ||
| if let Some(value) = value { | ||
| let value = value.serialize(); | ||
| for class_name in value.split_ascii_whitespace() { | ||
| if !class_name.is_empty() | ||
| && !self.current_element_classes.contains_key(class_name) | ||
| { | ||
| self.current_element_classes | ||
| .insert(class_name.to_string().into(), ()); | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // parse styles | ||
| if name == "style" { | ||
richard-uk1 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if let Some(value) = value { | ||
| let value = value.serialize(); | ||
| for pair in value.split(';') { | ||
| let mut iter = pair.splitn(2, ':'); | ||
| let Some(name) = iter.next() else { | ||
| continue; | ||
| }; | ||
| let Some(value) = iter.next() else { | ||
| continue; | ||
| }; | ||
| if name.is_empty() || value.is_empty() { | ||
| continue; | ||
| } | ||
| if !self.current_element_styles.contains_key(name) { | ||
| self.current_element_styles | ||
| .insert(name.to_string().into(), value.to_string().into()); | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if let Some(value) = value { | ||
| // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` | ||
| if !self.current_element_attributes.contains_key(name) { | ||
|
|
@@ -174,6 +267,20 @@ impl Cx { | |
| } | ||
| } | ||
|
|
||
| pub(crate) fn add_class_to_element(&mut self, class_name: &CowStr) { | ||
| // Don't strictly need this check but I assume its better for perf (might not be though) | ||
| if !self.current_element_classes.contains_key(class_name) { | ||
| self.current_element_classes.insert(class_name.clone(), ()); | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) { | ||
| if !self.current_element_styles.contains_key(name) { | ||
| self.current_element_styles | ||
| .insert(name.clone(), value.clone()); | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn apply_attributes( | ||
| &mut self, | ||
| element: &web_sys::Element, | ||
|
|
@@ -210,6 +317,72 @@ impl Cx { | |
| changed | ||
| } | ||
|
|
||
| pub(crate) fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap<CowStr, ()> { | ||
| let mut classes = VecMap::default(); | ||
| std::mem::swap(&mut classes, &mut self.current_element_classes); | ||
| for (class_name, ()) in classes.iter() { | ||
| set_class(element, class_name); | ||
| } | ||
| classes | ||
| } | ||
|
|
||
| pub(crate) fn apply_class_changes( | ||
| &mut self, | ||
| element: &web_sys::Element, | ||
| classes: &mut VecMap<CowStr, ()>, | ||
| ) -> ChangeFlags { | ||
| let mut changed = ChangeFlags::empty(); | ||
| // update attributes | ||
| for itm in diff_kv_iterables(&*classes, &self.current_element_classes) { | ||
| match itm { | ||
| Diff::Add(class_name, ()) | Diff::Change(class_name, ()) => { | ||
| set_class(element, class_name); | ||
| changed |= ChangeFlags::OTHER_CHANGE; | ||
| } | ||
| Diff::Remove(class_name) => { | ||
| remove_class(element, class_name); | ||
| changed |= ChangeFlags::OTHER_CHANGE; | ||
| } | ||
| } | ||
| } | ||
| std::mem::swap(classes, &mut self.current_element_classes); | ||
| self.current_element_classes.clear(); | ||
| changed | ||
| } | ||
|
|
||
| pub(crate) fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap<CowStr, CowStr> { | ||
| let mut styles = VecMap::default(); | ||
| std::mem::swap(&mut styles, &mut self.current_element_styles); | ||
| for (name, value) in styles.iter() { | ||
| set_style(element, name, value); | ||
| } | ||
| styles | ||
| } | ||
|
|
||
| pub(crate) fn apply_style_changes( | ||
| &mut self, | ||
| element: &web_sys::Element, | ||
| styles: &mut VecMap<CowStr, CowStr>, | ||
| ) -> ChangeFlags { | ||
| let mut changed = ChangeFlags::empty(); | ||
| // update attributes | ||
| for itm in diff_kv_iterables(&*styles, &self.current_element_styles) { | ||
| match itm { | ||
| Diff::Add(name, value) | Diff::Change(name, value) => { | ||
| set_style(element, name, value); | ||
| changed |= ChangeFlags::OTHER_CHANGE; | ||
| } | ||
| Diff::Remove(name) => { | ||
| remove_style(element, name); | ||
| changed |= ChangeFlags::OTHER_CHANGE; | ||
| } | ||
| } | ||
| } | ||
| std::mem::swap(styles, &mut self.current_element_styles); | ||
| self.current_element_styles.clear(); | ||
| changed | ||
| } | ||
|
|
||
| pub fn message_thunk(&self) -> MessageThunk { | ||
| MessageThunk { | ||
| id_path: self.id_path.clone(), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.