1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#![deny(missing_docs)]

//! ![Crates.io](https://img.shields.io/crates/l/yew-component-size) ![Crates.io](https://img.shields.io/crates/v/yew-component-size)
//!
//! A Yew component that emits events when the parent component changes width/height.
//! Only compatible with Yew using web_sys.
//!
//! # Example:
//! ```rust
//! let onsize = self.link.callback(|size: ComponentSize| {
//!     // Access to `size.width` and `size.height`
//! });
//!
//! html! {
//!     // Parent that you're tracking the size of must be `position: relative`
//!     <div style="position: relative;">
//!         // ...
//!         <ComponentSizeObserver onsize=onsize />
//!     </div>   
//! }
//! ```
//!
//! # How it works
//!
//! This uses a trick borrowed from Svelte where we use an iframe that is positioned absolutely
//! to fill it's parent element, and then we listen to the resize event of iframe's window.
//!
//! _**Note:** This incurs a small cost and so should not be used on a large number of elements at the same time._
//!
//! # License
//!
//! Licensed under either of
//!
//!  * Apache License, Version 2.0
//!    ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
//!  * MIT license
//!    ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
//!
//! at your option.
//!
//! # Contribution
//!
//! Unless you explicitly state otherwise, any contribution intentionally submitted
//! for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
//! dual licensed as above, without any additional terms or conditions.

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::HtmlIFrameElement;
use yew::{html, Callback, Component, NodeRef, Properties};

const IFRAME_STYLE: &str = "display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;";

/// Yew component to observe changes to the size of the parent element.
///
/// See the crate documentation for an example and more information.
#[derive(Debug)]
pub struct ComponentSizeObserver {
    props: Props,
    iframe_ref: NodeRef,
    on_resize: Option<Closure<dyn Fn()>>,
}

/// ComponentSizeObserver properties
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct Props {
    /// A callback that is fired when the component size changes for any reason.
    pub onsize: Callback<ComponentSize>,
}

/// A struct containing the width and height of the component
#[derive(Default, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ComponentSize {
    /// Width of the component in pixels
    pub width: f64,

    /// Height of the component in pixels
    pub height: f64,
}

impl Component for ComponentSizeObserver {
    type Message = ();
    type Properties = Props;

    fn create(props: Self::Properties, _link: yew::ComponentLink<Self>) -> Self {
        Self {
            props,
            iframe_ref: Default::default(),
            on_resize: None,
        }
    }

    fn update(&mut self, _msg: Self::Message) -> yew::ShouldRender {
        false
    }

    fn change(&mut self, props: Self::Properties) -> yew::ShouldRender {
        if self.props != props {
            self.props = props;
            self.add_resize_listener();
            false
        } else {
            false
        }
    }

    fn view(&self) -> yew::Html {
        html! {
            <iframe style=IFRAME_STYLE ref=self.iframe_ref.clone() />
        }
    }

    fn rendered(&mut self, first_render: bool) {
        if first_render {
            self.add_resize_listener();
        }
    }
}

impl ComponentSizeObserver {
    fn add_resize_listener(&mut self) {
        let iframe = self.iframe_ref.cast::<HtmlIFrameElement>().unwrap();
        let window = iframe.content_window().unwrap();

        let iframe_ref = self.iframe_ref.clone();
        let size_callback = self.props.onsize.clone();
        let on_resize = Closure::wrap(Box::new(move || {
            let iframe = iframe_ref.cast::<HtmlIFrameElement>().unwrap();
            let bcr = iframe.get_bounding_client_rect();
            size_callback.emit(ComponentSize {
                width: bcr.width(),
                height: bcr.height(),
            });
        }) as Box<dyn Fn()>);
        window.set_onresize(Some(on_resize.as_ref().unchecked_ref()));
        self.on_resize = Some(on_resize);
    }
}