Add the onvisible event handler to Element (#2911)

* Add the `onvisible` event using the IntersectionObserver

* Add a visible example

This example is based on https://codepen.io/ryanfinni/pen/VwZeGxN

* Remove web-sys CustomEvent feature for html package

* update js bindings and hash

* Use dioxus doc instead of lorem ipsum

* Fix clippy warning

* Add get_target method to the event data

* Make `get_time` return an Instant instead of a f64

* fix merge conflicts, compilinga

* remove target

* small nits

* serialzie as time_ms

* fix timing, works on desktop

* remove latent nits, fix hash

* box size clippy

* fix browserlist cargo.lock

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-12-02 13:51:27 -08:00
1 parent b114ed39f3
commit 79a2f023e7
20 files changed
+961 -289

No files matched your search

Generated
+325 -271
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -436,6 +436,10 @@ doc-scrape-examples = true
name = "streams"
doc-scrape-examples = true
[[example]]
name = "visible"
doc-scrape-examples = true
[[example]]
name = "window_event"
required-features = ["desktop"]
+27
View File
@@ -0,0 +1,27 @@
* {
box-sizing: border-box;
}
.container {
height: 500px;
width: 500px;
margin: 100px auto;
}
p {
line-height: 30px;
}
.animated-text {
font-size: 30px;
text-align: center;
opacity: 0;
transform: translateY(100px);
transition: opacity 1s ease, transform 1s ease;
}
.animated-text.visible {
opacity: 1;
transform: translateX(0);
}
-1
View File
@@ -1,5 +1,4 @@
//! A simple little clock that updates the time every few milliseconds.
//!
use async_std::task::sleep;
use dioxus::prelude::*;
+86
View File
@@ -0,0 +1,86 @@
//! Port of the https://codepen.io/ryanfinni/pen/VwZeGxN example
use dioxus::prelude::*;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
let mut animated_classes = use_signal(|| ["animated-text", ""]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("./examples/assets/visible.css") }
div {
class: "container",
p {
"Scroll to the bottom of the page. The text will transition in when it becomes visible in the viewport."
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
p {
"First, let's create a new project for our hacker news app. We can use the CLI to create a new
project. You can select a platform of your choice or view the getting started guide for more information
on each option. If you aren't sure what platform to try out, we recommend getting started with web or
desktop:"
}
p {
"The template contains some boilerplate to help you get started. For this guide, we will be rebuilding some of the code
from scratch for learning purposes. You can clear the src/main.rs file. We will be adding new code in the next
sections."
}
p {
"Next, let's setup our dependencies. We need to set up a few dependencies to work with the hacker news API: "
}
h2 {
class: animated_classes().join(" "),
onvisible: move |evt| {
let data = evt.data();
if let Ok(is_intersecting) = data.is_intersecting() {
animated_classes.write()[1] = if is_intersecting { "visible" } else { "" };
}
},
"Animated Text"
}
}
}
}
+1
View File
@@ -95,6 +95,7 @@ pub fn event_bubbles(evt: &str) -> bool {
"transitionend" => true,
"toggle" => true,
"mounted" => false,
"visible" => false,
_ => true,
}
}
+8
View File
@@ -161,6 +161,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
.into()
}
fn convert_visible_data(&self, event: &PlatformEventData) -> VisibleData {
event
.downcast::<SerializedVisibleData>()
.cloned()
.unwrap()
.into()
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
event
.downcast::<SerializedWheelData>()
+10
View File
@@ -146,6 +146,8 @@ pub trait HtmlEventConverter: Send + Sync {
fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData;
/// Convert a general event to a transition data event
fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData;
/// Convert a general event to a visible data event
fn convert_visible_data(&self, event: &PlatformEventData) -> VisibleData;
/// Convert a general event to a wheel data event
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData;
}
@@ -258,6 +260,12 @@ impl From<&PlatformEventData> for TransitionData {
}
}
impl From<&PlatformEventData> for VisibleData {
fn from(val: &PlatformEventData) -> Self {
with_event_converter(|c| c.convert_visible_data(val))
}
}
impl From<&PlatformEventData> for WheelData {
fn from(val: &PlatformEventData) -> Self {
with_event_converter(|c| c.convert_wheel_data(val))
@@ -282,6 +290,7 @@ mod selection;
mod toggle;
mod touch;
mod transition;
mod visible;
mod wheel;
pub use animation::*;
@@ -302,4 +311,5 @@ pub use selection::*;
pub use toggle::*;
pub use touch::*;
pub use transition::*;
pub use visible::*;
pub use wheel::*;
+307
View File
@@ -0,0 +1,307 @@
use std::{
fmt::{Display, Formatter},
time::SystemTime,
};
pub struct VisibleData {
inner: Box<dyn HasVisibleData>,
}
impl<E: HasVisibleData> From<E> for VisibleData {
fn from(e: E) -> Self {
Self { inner: Box::new(e) }
}
}
impl VisibleData {
/// Create a new VisibleData
pub fn new(inner: impl HasVisibleData + 'static) -> Self {
Self {
inner: Box::new(inner),
}
}
/// Get the bounds rectangle of the target element
pub fn get_bounding_client_rect(&self) -> VisibleResult<PixelsRect> {
self.inner.get_bounding_client_rect()
}
/// Get the ratio of the intersectionRect to the boundingClientRect
pub fn get_intersection_ratio(&self) -> VisibleResult<f64> {
self.inner.get_intersection_ratio()
}
/// Get the rect representing the target's visible area
pub fn get_intersection_rect(&self) -> VisibleResult<PixelsRect> {
self.inner.get_intersection_rect()
}
/// Get if the target element intersects with the intersection observer's root
pub fn is_intersecting(&self) -> VisibleResult<bool> {
self.inner.is_intersecting()
}
/// Get the rect for the intersection observer's root
pub fn get_root_bounds(&self) -> VisibleResult<PixelsRect> {
self.inner.get_root_bounds()
}
/// Get a timestamp indicating the time at which the intersection was recorded
pub fn get_time(&self) -> VisibleResult<SystemTime> {
self.inner.get_time()
}
/// Downcast this event to a concrete event type
pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.inner.as_any().downcast_ref::<T>()
}
}
impl std::fmt::Debug for VisibleData {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VisibleData")
.field(
"bounding_client_rect",
&self.inner.get_bounding_client_rect(),
)
.field("intersection_ratio", &self.inner.get_intersection_ratio())
.field("intersection_rect", &self.inner.get_intersection_rect())
.field("is_intersecting", &self.inner.is_intersecting())
.field("root_bounds", &self.inner.get_root_bounds())
.field("time", &self.inner.get_time())
.finish()
}
}
impl PartialEq for VisibleData {
fn eq(&self, _: &Self) -> bool {
true
}
}
#[cfg(feature = "serialize")]
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct DOMRect {
bottom: f64, // The bottom coordinate value of the DOMRect (usually the same as y + height)
height: f64, // The height of the DOMRect
left: f64, // The left coordinate value of the DOMRect (usually the same as x)
right: f64, // The right coordinate value of the DOMRect (usually the same as x + width)
top: f64, // The top coordinate value of the DOMRect (usually the same as y)
width: f64, // The width of the DOMRect
x: f64, // The x coordinate of the DOMRect's origin
y: f64, // The y coordinate of the DOMRect's origin
}
#[cfg(feature = "serialize")]
impl From<PixelsRect> for DOMRect {
fn from(rect: PixelsRect) -> Self {
let x = rect.origin.x;
let y = rect.origin.y;
let height = rect.height();
let width = rect.width();
Self {
bottom: y + height,
height,
left: x,
right: x + width,
top: y,
width,
x,
y,
}
}
}
#[cfg(feature = "serialize")]
impl From<&DOMRect> for PixelsRect {
fn from(rect: &DOMRect) -> Self {
PixelsRect::new(
euclid::Point2D::new(rect.x, rect.y),
euclid::Size2D::new(rect.width, rect.height),
)
}
}
#[cfg(feature = "serialize")]
/// A serialized version of VisibleData
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedVisibleData {
pub bounding_client_rect: DOMRect,
pub intersection_ratio: f64,
pub intersection_rect: DOMRect,
pub is_intersecting: bool,
pub root_bounds: DOMRect,
pub time_ms: u128,
}
#[cfg(feature = "serialize")]
impl SerializedVisibleData {
/// Create a new SerializedVisibleData
pub fn new(
bounding_client_rect: DOMRect,
intersection_ratio: f64,
intersection_rect: DOMRect,
is_intersecting: bool,
root_bounds: DOMRect,
time_ms: u128,
) -> Self {
Self {
bounding_client_rect,
intersection_ratio,
intersection_rect,
is_intersecting,
root_bounds,
time_ms,
}
}
}
#[cfg(feature = "serialize")]
impl From<&VisibleData> for SerializedVisibleData {
fn from(data: &VisibleData) -> Self {
Self::new(
data.get_bounding_client_rect().unwrap().into(),
data.get_intersection_ratio().unwrap(),
data.get_intersection_rect().unwrap().into(),
data.is_intersecting().unwrap(),
data.get_root_bounds().unwrap().into(),
data.get_time().unwrap().elapsed().unwrap().as_millis(),
)
}
}
#[cfg(feature = "serialize")]
impl HasVisibleData for SerializedVisibleData {
/// Get the bounds rectangle of the target element
fn get_bounding_client_rect(&self) -> VisibleResult<PixelsRect> {
Ok((&self.bounding_client_rect).into())
}
/// Get the ratio of the intersectionRect to the boundingClientRect
fn get_intersection_ratio(&self) -> VisibleResult<f64> {
Ok(self.intersection_ratio)
}
/// Get the rect representing the target's visible area
fn get_intersection_rect(&self) -> VisibleResult<PixelsRect> {
Ok((&self.intersection_rect).into())
}
/// Get if the target element intersects with the intersection observer's root
fn is_intersecting(&self) -> VisibleResult<bool> {
Ok(self.is_intersecting)
}
/// Get the rect for the intersection observer's root
fn get_root_bounds(&self) -> VisibleResult<PixelsRect> {
Ok((&self.root_bounds).into())
}
/// Get a timestamp indicating the time at which the intersection was recorded
fn get_time(&self) -> VisibleResult<SystemTime> {
Ok(SystemTime::UNIX_EPOCH + std::time::Duration::from_millis(self.time_ms as u64))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl serde::Serialize for VisibleData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedVisibleData::from(self).serialize(serializer)
}
}
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for VisibleData {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = SerializedVisibleData::deserialize(deserializer)?;
Ok(Self {
inner: Box::new(data),
})
}
}
pub trait HasVisibleData: std::any::Any {
/// Get the bounds rectangle of the target element
fn get_bounding_client_rect(&self) -> VisibleResult<PixelsRect> {
Err(VisibleError::NotSupported)
}
/// Get the ratio of the intersectionRect to the boundingClientRect
fn get_intersection_ratio(&self) -> VisibleResult<f64> {
Err(VisibleError::NotSupported)
}
/// Get the rect representing the target's visible area
fn get_intersection_rect(&self) -> VisibleResult<PixelsRect> {
Err(VisibleError::NotSupported)
}
/// Get if the target element intersects with the intersection observer's root
fn is_intersecting(&self) -> VisibleResult<bool> {
Err(VisibleError::NotSupported)
}
/// Get the rect for the intersection observer's root
fn get_root_bounds(&self) -> VisibleResult<PixelsRect> {
Err(VisibleError::NotSupported)
}
/// Get a timestamp indicating the time at which the intersection was recorded
fn get_time(&self) -> VisibleResult<SystemTime> {
Err(VisibleError::NotSupported)
}
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
use dioxus_core::Event;
use crate::geometry::PixelsRect;
pub type VisibleEvent = Event<VisibleData>;
impl_event! {
VisibleData;
/// onvisible
onvisible
}
/// The VisibleResult type for the VisibleData
pub type VisibleResult<T> = Result<T, VisibleError>;
#[derive(Debug)]
/// The error type for the VisibleData
#[non_exhaustive]
pub enum VisibleError {
/// The renderer does not support the requested operation
NotSupported,
/// The element was not found
OperationFailed(Box<dyn std::error::Error>),
/// The target element had no associated ElementId
NoElementId,
}
impl Display for VisibleError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
VisibleError::NotSupported => {
write!(f, "The renderer does not support the requested operation")
}
VisibleError::OperationFailed(e) => {
write!(f, "The operation failed: {}", e)
}
VisibleError::NoElementId => {
write!(f, "The target had no associated ElementId")
}
}
}
}
impl std::error::Error for VisibleError {}
+15
View File
@@ -105,6 +105,9 @@ fn deserialize_raw(name: &str, data: &serde_json::Value) -> Result<EventData, se
// Scroll
"scroll" => Scroll(de(data)?),
// Visible
"visible" => Visible(de(data)?),
// Wheel
"wheel" => Wheel(de(data)?),
@@ -163,6 +166,7 @@ pub enum EventData {
Touch(SerializedTouchData),
Resize(SerializedResizeData),
Scroll(SerializedScrollData),
Visible(SerializedVisibleData),
Wheel(SerializedWheelData),
Media(SerializedMediaData),
Animation(SerializedAnimationData),
@@ -207,6 +211,9 @@ impl EventData {
EventData::Scroll(data) => {
Rc::new(PlatformEventData::new(Box::new(data))) as Rc<dyn Any>
}
EventData::Visible(data) => {
Rc::new(PlatformEventData::new(Box::new(data))) as Rc<dyn Any>
}
EventData::Wheel(data) => {
Rc::new(PlatformEventData::new(Box::new(data))) as Rc<dyn Any>
}
@@ -415,6 +422,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
.into()
}
fn convert_visible_data(&self, event: &PlatformEventData) -> VisibleData {
event
.downcast::<SerializedVisibleData>()
.cloned()
.unwrap()
.into()
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
event
.downcast::<SerializedWheelData>()
+1 -1
View File
@@ -1 +1 @@
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;resizeObserver;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},this.handler=handler}handleResizeEvent(entry){const target=entry.target;let event=new CustomEvent("resize",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createObserver(element){if(!this.resizeObserver)this.resizeObserver=new ResizeObserver((entries)=>{for(let entry of entries)this.handleResizeEvent(entry)});this.resizeObserver.observe(element)}removeObserver(element){if(this.resizeObserver)this.resizeObserver.unobserve(element)}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}if(event_name=="resize")this.createObserver(element)}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}pushRoot(node){this.stack.push(node)}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate_node(hydrateNode,ids){const split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}hydrate(ids,underNodes){for(let i=0;i<underNodes.length;i++){const under=underNodes[i];if(under instanceof HTMLElement){if(under.getAttribute("data-node-hydration"))this.hydrate_node(under,ids);const hydrateNodes=under.querySelectorAll("[data-node-hydration]");for(let i2=0;i2<hydrateNodes.length;i2++)this.hydrate_node(hydrateNodes[i2],ids)}const treeWalker=document.createTreeWalker(under,NodeFilter.SHOW_COMMENT);while(treeWalker.currentNode){const currentNode=treeWalker.currentNode;if(currentNode.nodeType===Node.COMMENT_NODE){const id=currentNode.textContent,placeholderSplit=id.split("placeholder");if(placeholderSplit.length>1){if(this.nodes[ids[parseInt(placeholderSplit[1])]]=currentNode,!treeWalker.nextNode())break;continue}const textNodeSplit=id.split("node-id");if(textNodeSplit.length>1){let next=currentNode.nextSibling;currentNode.remove();let commentAfterText,textNode;if(next.nodeType===Node.COMMENT_NODE){const newText=next.parentElement.insertBefore(document.createTextNode(""),next);commentAfterText=next,textNode=newText}else textNode=next,commLine truncated
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;resizeObserver;intersectionObserver;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},this.handler=handler}handleResizeEvent(entry){const target=entry.target;let event=new CustomEvent("resize",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createResizeObserver(element){if(!this.resizeObserver)this.resizeObserver=new ResizeObserver((entries)=>{for(let entry of entries)this.handleResizeEvent(entry)});this.resizeObserver.observe(element)}removeResizeObserver(element){if(this.resizeObserver)this.resizeObserver.unobserve(element)}handleIntersectionEvent(entry){const target=entry.target;let event=new CustomEvent("visible",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createIntersectionObserver(element){if(!this.intersectionObserver)this.intersectionObserver=new IntersectionObserver((entries)=>{for(let entry of entries)this.handleIntersectionEvent(entry)});this.intersectionObserver.observe(element)}removeIntersectionObserver(element){if(this.intersectionObserver)this.intersectionObserver.unobserve(element)}createListener(event_name,element,bubbles){if(event_name=="resize")this.createResizeObserver(element);else if(event_name=="visible")this.createIntersectionObserver(element);if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(event_name=="resize")this.removeResizeObserver(element);else if(event_name=="visible")this.removeIntersectionObserver(element);else if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}pushRoot(node){this.stack.push(node)}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate_node(hydrateNode,ids){const split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}hydrate(ids,underNodes){for(let i=0;i<underNodes.length;i++){const under=underNodes[i];if(under instanceof HTMLElement){if(under.getAttribute("data-node-hydration"))this.hydrate_node(under,ids);const hydrateNodes=under.querySelectorAll("[data-node-hydration]");for(let i2=0;i2<hydrateNodes.length;i2++)this.hydrate_node(hydrateNLine truncated
+1 -1
View File
@@ -1 +1 @@
[6449103750905854967, 4461869229701639737, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 7422899642446454418, 5052021921702764563, 17534315583914394253, 5638004933879392817]
[6449103750905854967, 5429983586723452597, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 7422899642446454418, 5052021921702764563, 12925655762638175824, 5638004933879392817]
+1 -1
View File
@@ -1 +1 @@
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){const detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var toSerializableResizeObserverSize=function(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]};function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationELine truncated
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){const detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var toSerializableResizeObserverSize=function(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]};function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,Line truncated
+43 -7
View File
@@ -20,6 +20,7 @@ export class BaseInterpreter {
root: HTMLElement;
handler: EventListener;
resizeObserver: ResizeObserver;
intersectionObserver: IntersectionObserver;
nodes: Node[];
stack: Node[];
@@ -55,7 +56,7 @@ export class BaseInterpreter {
target.dispatchEvent(event);
}
createObserver(element: HTMLElement) {
createResizeObserver(element: HTMLElement) {
// Lazily create the resize observer
if (!this.resizeObserver) {
this.resizeObserver = new ResizeObserver((entries) => {
@@ -67,13 +68,48 @@ export class BaseInterpreter {
this.resizeObserver.observe(element);
}
removeObserver(element: HTMLElement) {
removeResizeObserver(element: HTMLElement) {
if (this.resizeObserver) {
this.resizeObserver.unobserve(element);
}
}
handleIntersectionEvent(entry: IntersectionObserverEntry) {
const target = entry.target;
let event = new CustomEvent<IntersectionObserverEntry>("visible", {
bubbles: false,
detail: entry,
});
target.dispatchEvent(event);
}
createIntersectionObserver(element: HTMLElement) {
/// Lazily create the intersection observer
if (!this.intersectionObserver) {
this.intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
this.handleIntersectionEvent(entry);
}
});
}
this.intersectionObserver.observe(element);
}
removeIntersectionObserver(element: HTMLElement) {
if (this.intersectionObserver) {
this.intersectionObserver.unobserve(element);
}
}
createListener(event_name: string, element: HTMLElement, bubbles: boolean) {
if (event_name == "resize") {
this.createResizeObserver(element);
} else if (event_name == "visible") {
this.createIntersectionObserver(element);
}
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = { active: 1, callback: this.handler };
@@ -88,14 +124,14 @@ export class BaseInterpreter {
}
element.addEventListener(event_name, this.handler);
}
if (event_name == "resize") {
this.createObserver(element);
}
}
removeListener(element: HTMLElement, event_name: string, bubbles: boolean) {
if (bubbles) {
if (event_name == "resize") {
this.removeResizeObserver(element);
} else if (event_name == "visible") {
this.removeIntersectionObserver(element);
} else if (bubbles) {
this.removeBubblingListener(event_name);
} else {
this.removeNonBubblingListener(element, event_name);
+23 -6
View File
@@ -62,6 +62,8 @@ export function serializeEvent(
const detail = event.detail;
if (detail instanceof ResizeObserverEntry) {
extend(serializeResizeEventDetail(detail));
} else if (detail instanceof IntersectionObserverEntry) {
extend(serializeIntersectionEventDetail(detail));
}
}
@@ -129,21 +131,36 @@ export function serializeResizeEventDetail(
border_box_size:
detail.borderBoxSize !== undefined
? toSerializableResizeObserverSize(
detail.borderBoxSize[0],
is_inline_width
)
detail.borderBoxSize[0],
is_inline_width
)
: detail.contentRect,
content_box_size:
detail.contentBoxSize !== undefined
? toSerializableResizeObserverSize(
detail.contentBoxSize[0],
is_inline_width
)
detail.contentBoxSize[0],
is_inline_width
)
: detail.contentRect,
content_rect: detail.contentRect,
};
}
export function serializeIntersectionEventDetail(
detail: IntersectionObserverEntry): SerializedEvent {
return {
bounding_client_rect: detail.boundingClientRect,
intersection_ratio: detail.intersectionRatio,
intersection_rect: detail.intersectionRect,
is_intersecting: detail.isIntersecting,
root_bounds: detail.rootBounds,
// math.floor removes the floating point extra such that we serialize into integer
// this *might* become an issue longer term
time_ms: Math.floor(Date.now() + detail.time),
}
}
function serializeInputEvent(
event: InputEvent,
target: EventTarget
+8
View File
@@ -147,6 +147,14 @@ impl HtmlEventConverter for SerializedHtmlEventConverter {
.into()
}
fn convert_visible_data(&self, event: &PlatformEventData) -> VisibleData {
event
.downcast::<SerializedVisibleData>()
.cloned()
.unwrap()
.into()
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
event
.downcast::<SerializedWheelData>()
+1 -1
View File
@@ -152,7 +152,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
#[serde(tag = "method", content = "params")]
enum IpcMessage {
#[serde(rename = "user_event")]
Event(HtmlEvent),
Event(Box<HtmlEvent>),
#[serde(rename = "query")]
Query(QueryResult),
}
+1
View File
@@ -63,6 +63,7 @@ features = [
"HtmlInputElement",
"HtmlSelectElement",
"HtmlTextAreaElement",
"IntersectionObserverEntry",
"InputEvent",
"KeyboardEvent",
"MouseEvent",
+10
View File
@@ -25,6 +25,7 @@ mod selection;
mod toggle;
mod touch;
mod transition;
mod visible;
mod wheel;
/// A wrapper for the websys event that allows us to give it the impls from dioxus-html
@@ -190,6 +191,15 @@ impl HtmlEventConverter for WebEventConverter {
Synthetic::<web_sys::TransitionEvent>::from(downcast_event(event).raw.clone()).into()
}
#[inline(always)]
fn convert_visible_data(
&self,
event: &dioxus_html::PlatformEventData,
) -> dioxus_html::VisibleData {
Synthetic::<web_sys::IntersectionObserverEntry>::from(downcast_event(event).raw.clone())
.into()
}
#[inline(always)]
fn convert_wheel_data(&self, event: &dioxus_html::PlatformEventData) -> dioxus_html::WheelData {
Synthetic::<web_sys::WheelEvent>::from(downcast_event(event).raw.clone()).into()
+89
View File
@@ -0,0 +1,89 @@
use std::time::SystemTime;
use dioxus_html::{
geometry::{
euclid::{Point2D, Size2D},
PixelsRect,
},
HasVisibleData, VisibleData, VisibleError, VisibleResult,
};
use wasm_bindgen::JsCast;
use web_sys::{CustomEvent, DomRectReadOnly, Event, IntersectionObserverEntry};
use super::{Synthetic, WebEventExt};
impl From<Event> for Synthetic<IntersectionObserverEntry> {
#[inline]
fn from(e: Event) -> Self {
<Synthetic<IntersectionObserverEntry> as From<&Event>>::from(&e)
}
}
impl From<&Event> for Synthetic<IntersectionObserverEntry> {
#[inline]
fn from(e: &Event) -> Self {
let e: &CustomEvent = e.unchecked_ref();
let value = e.detail();
Self::new(value.unchecked_into::<IntersectionObserverEntry>())
}
}
fn dom_rect_ro_to_pixel_rect(dom_rect: &DomRectReadOnly) -> PixelsRect {
PixelsRect::new(
Point2D::new(dom_rect.x(), dom_rect.y()),
Size2D::new(dom_rect.width(), dom_rect.height()),
)
}
impl HasVisibleData for Synthetic<IntersectionObserverEntry> {
/// Get the bounds rectangle of the target element
fn get_bounding_client_rect(&self) -> VisibleResult<PixelsRect> {
Ok(dom_rect_ro_to_pixel_rect(
&self.event.bounding_client_rect(),
))
}
/// Get the ratio of the intersectionRect to the boundingClientRect
fn get_intersection_ratio(&self) -> VisibleResult<f64> {
Ok(self.event.intersection_ratio())
}
/// Get the rect representing the target's visible area
fn get_intersection_rect(&self) -> VisibleResult<PixelsRect> {
Ok(dom_rect_ro_to_pixel_rect(&self.event.intersection_rect()))
}
/// Get if the target element intersects with the intersection observer's root
fn is_intersecting(&self) -> VisibleResult<bool> {
Ok(self.event.is_intersecting())
}
/// Get the rect for the intersection observer's root
fn get_root_bounds(&self) -> VisibleResult<PixelsRect> {
match self.event.root_bounds() {
Some(root_bounds) => Ok(dom_rect_ro_to_pixel_rect(&root_bounds)),
None => Err(VisibleError::NotSupported),
}
}
/// Get a timestamp indicating the time at which the intersection was recorded
fn get_time(&self) -> VisibleResult<SystemTime> {
let ms_since_epoch = self.event.time();
let rounded = ms_since_epoch.round() as u64;
let duration = std::time::Duration::from_millis(rounded);
Ok(SystemTime::UNIX_EPOCH + duration)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl WebEventExt for VisibleData {
type WebEvent = IntersectionObserverEntry;
#[inline(always)]
fn try_as_web_event(&self) -> Option<IntersectionObserverEntry> {
self.downcast::<CustomEvent>()
.and_then(|e| e.detail().dyn_into::<IntersectionObserverEntry>().ok())
}
}