#![doc = include_str!("../YEW.md")]
use crate::common::{
AriaLive, AriaPressed, CrossOrigin, Decoding, FetchPriority, Layout, Loading, ObjectFit,
Position, ReferrerPolicy,
};
use gloo_net::http::Request;
use wasm_bindgen_futures::spawn_local;
use web_sys::IntersectionObserverEntry;
use web_sys::js_sys;
use web_sys::wasm_bindgen::JsCast;
use web_sys::wasm_bindgen::JsValue;
use web_sys::wasm_bindgen::prelude::*;
use web_sys::{IntersectionObserver, IntersectionObserverInit, RequestCache};
use yew::prelude::*;
#[derive(Properties, Clone, PartialEq)]
pub struct ImageProps {
#[prop_or_default]
pub src: &'static str,
#[prop_or_default]
pub alt: &'static str,
#[prop_or_default]
pub fallback_src: &'static str,
#[prop_or_default]
pub width: &'static str,
#[prop_or_default]
pub height: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub sizes: &'static str,
#[prop_or_default]
pub quality: &'static str,
#[prop_or_default]
pub loading: Loading,
#[prop_or_default]
pub placeholder: &'static str,
#[prop_or_default]
pub on_load: Callback<()>,
#[prop_or_default]
pub object_fit: ObjectFit,
#[prop_or_default]
pub object_position: Position,
#[prop_or_default]
pub on_error: Callback<String>,
#[prop_or_default]
pub decoding: Decoding,
#[prop_or_default]
pub blur_data_url: &'static str,
#[prop_or_default]
pub lazy_boundary: &'static str,
#[prop_or_default]
pub unoptimized: bool,
#[prop_or_default]
pub layout: Layout,
#[prop_or_default]
pub node_ref: NodeRef,
#[prop_or_default]
pub srcset: &'static str,
#[prop_or_default]
pub crossorigin: CrossOrigin,
#[prop_or_default]
pub referrerpolicy: ReferrerPolicy,
#[prop_or_default]
pub usemap: &'static str,
#[prop_or_default]
pub ismap: bool,
#[prop_or_default]
pub fetchpriority: FetchPriority,
#[prop_or_default]
pub elementtiming: &'static str,
#[prop_or_default]
pub attributionsrc: &'static str,
#[prop_or_default]
pub aria_current: &'static str,
#[prop_or_default]
pub aria_describedby: &'static str,
#[prop_or_default]
pub aria_expanded: &'static str,
#[prop_or_default]
pub aria_hidden: &'static str,
#[prop_or_default]
pub aria_live: AriaLive,
#[prop_or_default]
pub aria_pressed: AriaPressed,
#[prop_or_default]
pub aria_controls: &'static str,
#[prop_or_default]
pub aria_labelledby: &'static str,
}
impl Default for ImageProps {
fn default() -> Self {
ImageProps {
src: "",
alt: "Image",
width: "",
height: "",
style: "",
class: "",
sizes: "",
quality: "",
placeholder: "empty",
on_load: Callback::noop(),
object_fit: ObjectFit::default(),
object_position: Position::default(),
on_error: Callback::noop(),
decoding: Decoding::default(),
blur_data_url: "",
lazy_boundary: "100px",
unoptimized: false,
layout: Layout::default(),
node_ref: NodeRef::default(),
fallback_src: "",
srcset: "",
crossorigin: CrossOrigin::default(),
loading: Loading::default(),
referrerpolicy: ReferrerPolicy::default(),
usemap: "",
ismap: false,
fetchpriority: FetchPriority::default(),
elementtiming: "",
attributionsrc: "",
aria_current: "",
aria_describedby: "",
aria_expanded: "",
aria_hidden: "",
aria_live: AriaLive::default(),
aria_pressed: AriaPressed::default(),
aria_controls: "",
aria_labelledby: "",
}
}
}
#[function_component]
pub fn Image(props: &ImageProps) -> Html {
let mut props = props.clone();
let img_ref = props.node_ref.clone();
let img_ref_clone = img_ref.clone();
let on_load = props.on_load.clone();
let on_load_call = props.on_load.clone();
use_effect_with(JsValue::from(props.src), move |_deps| {
let callback = Closure::wrap(Box::new(
move |entries: js_sys::Array, _observer: IntersectionObserver| {
if let Some(entry) = entries.get(0).dyn_ref::<IntersectionObserverEntry>() {
if entry.is_intersecting() {
if let Some(img) = img_ref_clone.cast::<web_sys::HtmlImageElement>() {
img.set_src(props.src);
on_load.emit(());
}
}
}
},
)
as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
let options = IntersectionObserverInit::new();
options.set_threshold(&js_sys::Array::of1(&0.1.into()));
let observer =
IntersectionObserver::new_with_options(callback.as_ref().unchecked_ref(), &options)
.expect("Failed to create IntersectionObserver");
if let Some(img) = img_ref.cast::<web_sys::HtmlElement>() {
observer.observe(&img);
}
let observer_clone = observer.clone();
let _cleanup = move || {
observer_clone.disconnect();
};
callback.forget();
});
let fetch_data = {
Callback::from(move |_| {
let loading_complete_callback = props.on_load.clone();
let on_error_callback = props.on_error.clone();
spawn_local(async move {
match Request::get(props.fallback_src)
.cache(RequestCache::Reload)
.send()
.await
{
Ok(response) => {
if response.status() == 200 {
let json_result = response.json::<serde_json::Value>();
match json_result.await {
Ok(_data) => {
props.src = props.fallback_src;
loading_complete_callback.emit(());
}
Err(_err) => {
on_error_callback.emit("Image Not Found!".to_string());
}
}
} else {
let status = response.status();
let body = response.text().await.unwrap_or_else(|_| {
String::from("Failed to retrieve response body")
});
on_error_callback.emit(format!(
"Failed to load image. Status: {}, Body: {:?}",
status, body
));
}
}
Err(err) => {
on_error_callback.emit(format!("Network error: {}", err));
}
}
});
})
};
let img_style = {
let mut style = String::new();
style.push_str(&format!("object-fit: {};", props.object_fit.as_str()));
style.push_str(&format!(
"object-position: {};",
props.object_position.as_str()
));
if !props.style.is_empty() {
style.push_str(props.style);
}
style
};
let blur_style = if props.placeholder == "blur" {
format!(
"background-size: {}; background-position: {}; filter: blur(20px); background-image: url(\"{}\")",
props.sizes,
props.object_position.as_str(),
props.blur_data_url
)
} else {
String::new()
};
let onload = {
Callback::from(move |_| {
on_load_call.emit(());
})
};
let full_style = format!("{} {}", blur_style, img_style);
let layout = match props.layout {
Layout::Fill => {
html! {
<span style={"display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;"}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
loading={props.loading.as_str()}
sizes={props.sizes}
quality={props.quality}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
}
}
Layout::Responsive => {
let quotient: f64 =
props.height.parse::<f64>().unwrap() / props.width.parse::<f64>().unwrap();
let padding_top: String = if quotient.is_nan() {
"100%".to_string()
} else {
format!("{}%", quotient * 100.0)
};
html! {
<span style={"display: block; position: relative;"}>
<span style={"padding-top: ".to_owned() + &padding_top}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
sizes={props.sizes}
quality={props.quality}
loading={props.loading.as_str()}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
</span>
}
}
Layout::Intrinsic => {
html! {
<span style={"display: inline-block; position: relative; max-width: 100%;"}>
<span style={"max-width: 100%;"}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
sizes={props.sizes}
loading={props.loading.as_str()}
quality={props.quality}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
<img
src={props.blur_data_url}
style={"display: none;"}
alt={props.alt}
aria-hidden="true"
/>
</span>
}
}
Layout::Fixed => {
html! {
<span style={"display: inline-block; position: relative;"}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
sizes={props.sizes}
quality={props.quality}
loading={props.loading.as_str()}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
}
}
Layout::Auto => {
html! {
<span style={"display: inline-block; position: relative;"}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
sizes={props.sizes}
quality={props.quality}
placeholder={props.placeholder}
loading={props.loading.as_str()}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
}
}
Layout::Stretch => {
html! {
<span style={"display: block; width: 100%; height: 100%; position: relative;"}>
<img
src={props.src}
alt={props.alt}
width="100%"
height="100%"
style={full_style}
class={props.class}
loading={props.loading.as_str()}
sizes={props.sizes}
quality={props.quality}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
}
}
Layout::ScaleDown => {
html! {
<span style={"display: inline-block; position: relative; max-width: 100%; max-height: 100%;"}>
<img
src={props.src}
alt={props.alt}
width={props.width}
height={props.height}
style={full_style}
class={props.class}
loading={props.loading.as_str()}
sizes={props.sizes}
quality={props.quality}
placeholder={props.placeholder}
decoding={props.decoding.as_str()}
ref={props.node_ref}
role="img"
aria-label={props.alt}
aria-labelledby={props.aria_labelledby}
aria-describedby={props.aria_describedby}
aria-hidden={props.aria_hidden}
aria-current={props.aria_current}
aria-expanded={props.aria_expanded}
aria-live={props.aria_live.as_str()}
aria-pressed={props.aria_pressed.as_str()}
aria-controls={props.aria_controls}
onerror={fetch_data}
crossorigin={props.crossorigin.as_str()}
referrerpolicy={props.referrerpolicy.as_str()}
fetchpriority={props.fetchpriority.as_str()}
attributionsrc={props.attributionsrc}
onload={onload}
elementtiming={props.elementtiming}
srcset={props.srcset}
ismap={props.ismap}
usemap={props.usemap}
/>
</span>
}
}
};
html! {
{layout}
}
}