use std::cell::Cell;
use std::rc::Rc;
use perspective_js::utils::global;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::*;
use yew::prelude::*;
use crate::components::modal::ModalOrientation;
use crate::components::style::StyleProvider;
use crate::utils::*;
#[derive(Properties, PartialEq)]
pub struct PortalModalProps {
pub children: Children,
pub target: Option<HtmlElement>,
#[prop_or(true)]
pub own_focus: bool,
#[prop_or_default]
pub on_close: Callback<()>,
pub tag_name: &'static str,
pub theme: String,
}
pub enum PortalModalMsg {
Reposition,
}
pub struct PortalModal {
host: HtmlElement,
shadow_root: Element,
top: f64,
left: f64,
visible: bool,
rev_vert: ModalOrientation,
anchor: Rc<Cell<ModalAnchor>>,
_blur_closure: Option<Closure<dyn FnMut(FocusEvent)>>,
}
impl PortalModal {
fn attach_to_body(&self) {
if !self.host.is_connected() {
let _ = global::body().append_child(&self.host);
}
}
fn detach_from_body(&mut self) {
if self.host.is_connected() {
let _ = global::body().remove_child(&self.host);
}
if let Some(closure) = self._blur_closure.as_ref() {
self.host
.remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
.unwrap()
}
self._blur_closure = None;
}
fn position_against_target(&mut self, target: &HtmlElement) {
let target_rect = target.get_bounding_client_rect();
let height = target_rect.height();
let width = target_rect.width();
let top = target_rect.top();
let left = target_rect.left();
if !self.visible {
self.top = top + height - 1.0;
self.left = left;
self.visible = false;
} else {
let anchor = calc_relative_position(&self.host, top, left, height, width);
self.anchor.set(anchor);
let modal_rect = self.host.get_bounding_client_rect();
let (new_top, new_left) = calc_anchor_position(anchor, &target_rect, &modal_rect);
self.top = new_top;
self.left = new_left;
self.rev_vert.set(anchor.is_rev_vert());
}
}
fn setup_blur_handler(&mut self, ctx: &Context<Self>) {
let on_close = {
let target = ctx.props().target.clone();
ctx.props().on_close.reform(move |_| {
if let Some(target) = &target {
target.class_list().remove_1("modal-target").unwrap();
}
})
};
let closure = Closure::wrap(Box::new(move |_: FocusEvent| {
on_close.emit(());
}) as Box<dyn FnMut(FocusEvent)>);
let _ = self
.host
.add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref());
self._blur_closure = Some(closure);
}
}
impl Component for PortalModal {
type Message = PortalModalMsg;
type Properties = PortalModalProps;
fn create(ctx: &Context<Self>) -> Self {
let host: HtmlElement = global::document()
.create_element(ctx.props().tag_name)
.unwrap()
.unchecked_into();
host.style().set_property("position", "fixed").unwrap();
host.style().set_property("z-index", "10000").unwrap();
let init = ShadowRootInit::new(ShadowRootMode::Open);
let shadow_root = if let Some(elem) = host.shadow_root() {
elem
} else {
host.attach_shadow(&init).unwrap()
}
.unchecked_into::<Element>();
Self {
host,
shadow_root,
top: 0.0,
left: 0.0,
visible: false,
rev_vert: Default::default(),
anchor: Default::default(),
_blur_closure: None,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PortalModalMsg::Reposition => {
self.visible = true;
true
},
}
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
let new_target = &ctx.props().target;
let old_target = &old_props.target;
match (old_target, new_target, self._blur_closure.as_ref()) {
(None, Some(_), Some(closure)) => {
self.visible = false;
self.host
.remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
.unwrap();
self._blur_closure = None;
},
(None, Some(_), None) => {
self.visible = false;
self._blur_closure = None;
},
(Some(_), None, _) => {
self.detach_from_body();
return true;
},
_ => {},
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let target = &ctx.props().target;
if target.is_none() {
return html! {};
}
let opacity = if self.visible { "" } else { ";opacity:0" };
let css = format!(
":host{{top:{}px;left:{}px{}}}",
self.top, self.left, opacity
);
let portal_content = html! {
<>
<style>{ css }</style>
<ContextProvider<ModalOrientation> context={self.rev_vert.clone()}>
<StyleProvider root={self.host.clone()}>
{ for ctx.props().children.iter() }
</StyleProvider>
</ContextProvider<ModalOrientation>>
</>
};
yew::create_portal(portal_content, self.shadow_root.clone())
}
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
if let Some(target) = &ctx.props().target {
if !self.host.is_connected() {
let theme = ctx.props().theme.as_str();
self.host.set_attribute("theme", theme).unwrap();
self.position_against_target(target);
self.attach_to_body();
if let Some(theme) = target.get_attribute("theme") {
let _ = self.host.set_attribute("theme", &theme);
}
target.class_list().add_1("modal-target").unwrap();
if ctx.props().own_focus {
self.host.set_attribute("tabindex", "0").unwrap();
self.setup_blur_handler(ctx);
}
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
request_animation_frame().await;
link.send_message(PortalModalMsg::Reposition);
});
} else if self.visible {
self.position_against_target(target);
if ctx.props().own_focus && self._blur_closure.is_some() {
let _ = self.host.focus();
}
}
}
}
fn destroy(&mut self, ctx: &Context<Self>) {
if let Some(target) = &ctx.props().target {
target.class_list().remove_1("modal-target").unwrap();
if target.get_attribute("theme").is_some() {
let _ = self.host.remove_attribute("theme");
}
let event = CustomEvent::new("-perspective-close-expression").unwrap();
let _ = target.dispatch_event(&event);
}
self.detach_from_body();
}
}