#![warn(
missing_docs,
clippy::all,
clippy::missing_errors_doc,
clippy::style,
clippy::unseparated_literal_suffix,
clippy::pedantic,
clippy::nursery
)]
use web_sys::wasm_bindgen::JsCast;
use yew::prelude::*;
#[derive(Clone, PartialEq, Copy, Default, Eq)]
pub enum Gravity {
#[default]
Top,
Bottom,
}
impl AsRef<str> for Gravity {
fn as_ref(&self) -> &str {
match self {
Self::Top => "top",
Self::Bottom => "bottom",
}
}
}
#[derive(Clone, PartialEq, Copy, Default, Eq)]
pub enum Position {
Left,
#[default]
Right,
Center,
}
impl AsRef<str> for Position {
fn as_ref(&self) -> &str {
match self {
Self::Left => "left",
Self::Right => "right",
Self::Center => "center",
}
}
}
#[derive(Clone, PartialEq)]
pub struct ToastData {
pub element: Html,
pub duration: u32,
pub close: bool,
pub gravity: Gravity,
pub position: Position,
pub onclick: Callback<()>,
}
impl Default for ToastData {
fn default() -> Self {
Self {
duration: 3000,
close: false,
gravity: Gravity::Top,
position: Position::Right,
element: html!(<div></div>),
onclick: Callback::noop(),
}
}
}
#[derive(Clone, PartialEq, Eq, Properties)]
pub struct ToastCornerContainerProps {
pub gravity: Gravity,
pub position: Position,
}
#[function_component(ToastCornerContainer)]
pub fn toast_corner_container(props: &ToastCornerContainerProps) -> Html {
const fn container_style(gravity: Gravity, position: Position) -> &'static str {
match (gravity, position) {
(Gravity::Top, Position::Left) => {
"position: fixed; top: 1rem; left: 1rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9999;"
}
(Gravity::Top, Position::Right) => {
"position: fixed; top: 1rem; right: 1rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 9999;"
}
(Gravity::Top, Position::Center) => {
"position: fixed; top: 1rem; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 0.5rem; z-index: 9999;"
}
(Gravity::Bottom, Position::Left) => {
"position: fixed; bottom: 1rem; left: 1rem; display: flex; flex-direction: column-reverse; gap: 0.5rem; z-index: 9999;"
}
(Gravity::Bottom, Position::Right) => {
"position: fixed; bottom: 1rem; right: 1rem; display: flex; flex-direction: column-reverse; gap: 0.5rem; z-index: 9999;"
}
(Gravity::Bottom, Position::Center) => {
"position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column-reverse; gap: 0.5rem; z-index: 9999;"
}
}
}
let style = container_style(props.gravity, props.position);
html! {
<div {style} id={format!("toast-container-{}-{}", props.gravity.as_ref(), props.position.as_ref())} />
}
}
#[function_component(ToastProvider)]
pub fn toast_provider() -> Html {
html! {
<>
<style>
{"
@keyframes slideRight {
0% { opacity: 0; transform: translateX(100%); }
10% { opacity: 1; transform: translateX(0); } /* finished entering */
90% { opacity: 1; transform: translateX(0); } /* hold */
100% { opacity: 0; transform: translateX(100%); }/* leave */
}
@keyframes slideLeft {
0% { opacity: 0; transform: translateX(-100%); }
10% { opacity: 1; transform: translateX(0); }
90% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(-100%); }
}
@keyframes slideTop {
0% { opacity: 0; transform: translateY(-100%); }
10% { opacity: 1; transform: translateY(0); }
90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-100%); }
}
@keyframes slideBottom {
0% { opacity: 0; transform: translateY(100%); }
10% { opacity: 1; transform: translateY(0); }
90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(100%); }
}
"}
</style>
<ToastCornerContainer gravity={Gravity::Top} position={Position::Left}/>
<ToastCornerContainer gravity={Gravity::Top} position={Position::Right}/>
<ToastCornerContainer gravity={Gravity::Top} position={Position::Center}/>
<ToastCornerContainer gravity={Gravity::Bottom} position={Position::Left}/>
<ToastCornerContainer gravity={Gravity::Bottom} position={Position::Right}/>
<ToastCornerContainer gravity={Gravity::Bottom} position={Position::Center}/>
</>
}
}
#[derive(Clone, PartialEq, Properties)]
struct ToastHolderProps {
duration: u32,
children: Html,
gravity: Gravity,
position: Position,
}
#[function_component(ToastHolder)]
fn toast_holder(props: &ToastHolderProps) -> Html {
let animation = match (props.gravity, props.position) {
(_, Position::Left) => "slideLeft",
(_, Position::Right) => "slideRight",
(Gravity::Top, Position::Center) => "slideTop",
(Gravity::Bottom, Position::Center) => "slideBottom",
};
html! {
<div style={format!("animation: {} {}ms ease-in-out;", animation, props.duration)}>
{props.children.clone()}
</div>
}
}
pub fn show_toast(toast: &ToastData) -> Result<(), web_sys::wasm_bindgen::JsValue> {
let Some(doc) = web_sys::window().and_then(|win| win.document()) else {
return Err(web_sys::wasm_bindgen::JsValue::from_str(
"document not found",
));
};
let container_id = format!(
"toast-container-{}-{}",
toast.gravity.as_ref(),
toast.position.as_ref()
);
let Some(container) = doc.get_element_by_id(&container_id) else {
return Err(web_sys::wasm_bindgen::JsValue::from_str(
"container not found",
));
};
let div = doc
.create_element("div")?
.dyn_into::<web_sys::HtmlElement>()?;
let div_clone = div.clone();
if toast.close {
div.set_onclick(Some(
web_sys::wasm_bindgen::closure::Closure::once_into_js(
move |_: web_sys::HtmlElement| {
div_clone.remove();
},
)
.unchecked_ref(),
));
} else {
let cb = toast.onclick.clone();
let closure = web_sys::wasm_bindgen::closure::Closure::wrap(Box::new(move || {
cb.emit(());
}) as Box<dyn Fn()>);
let function = closure.as_ref().clone().unchecked_into();
closure.forget();
div.set_onclick(Some(&function));
}
let div_clone = div.clone();
div.set_onanimationend(Some(
web_sys::wasm_bindgen::closure::Closure::once_into_js(move |_: web_sys::HtmlElement| {
div_clone.remove();
})
.unchecked_ref(),
));
container.append_child(&div)?;
yew::Renderer::<ToastHolder>::with_root_and_props(
div.unchecked_into(),
ToastHolderProps {
children: toast.element.clone(),
duration: toast.duration,
gravity: toast.gravity,
position: toast.position,
},
)
.render();
Ok(())
}
#[must_use]
pub fn show_toast_cb() -> Callback<ToastData> {
Callback::from(move |toast: ToastData| {
if let Err(e) = show_toast(&toast) {
web_sys::console::error_1(&e);
}
})
}