perspective_viewer/custom_elements/
modal.rs1use std::cell::{Cell, RefCell};
14use std::rc::Rc;
15
16use derivative::Derivative;
17use futures::Future;
18use perspective_js::utils::{global, *};
19use wasm_bindgen::JsCast;
20use wasm_bindgen::prelude::*;
21use web_sys::*;
22use yew::html::SendAsMessage;
23use yew::prelude::*;
24
25use crate::components::modal::*;
26use crate::utils::*;
27
28type BlurHandlerType = Rc<RefCell<Option<Closure<dyn FnMut(FocusEvent)>>>>;
29
30#[derive(Derivative)]
43#[derivative(Clone(bound = ""))]
44pub struct ModalElement<T>
45where
46 T: Component,
47 T::Properties: ModalLink<T>,
48{
49 root: Rc<RefCell<Option<AppHandle<Modal<T>>>>>,
50 pub custom_element: HtmlElement,
51 target: Rc<RefCell<Option<HtmlElement>>>,
52 blurhandler: BlurHandlerType,
53 own_focus: bool,
54 resize_sub: Rc<RefCell<Option<Subscription>>>,
55 anchor: Rc<Cell<ModalAnchor>>,
56 on_blur: Option<Callback<()>>,
57}
58
59impl<T> ModalElement<T>
60where
61 T: Component,
62 T::Properties: ModalLink<T>,
63{
64 pub fn new(
65 custom_element: web_sys::HtmlElement,
66 props: T::Properties,
67 own_focus: bool,
68 on_blur: Option<Callback<()>>,
69 ) -> Self {
70 custom_element.set_attribute("tabindex", "0").unwrap();
71 let init = web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open);
72 let shadow_root = custom_element
73 .attach_shadow(&init)
74 .unwrap()
75 .unchecked_into::<web_sys::Element>();
76
77 let cprops = yew::props!(ModalProps<T> {
78 child: Some(html_nested! {
79 <T ..props />
80 }),
81 });
82
83 let root = Rc::new(RefCell::new(Some(
84 yew::Renderer::with_root_and_props(shadow_root, cprops).render(),
85 )));
86
87 let blurhandler = Rc::new(RefCell::new(None));
88 Self {
89 root,
90 custom_element,
91 target: Rc::new(RefCell::new(None)),
92 own_focus,
93 blurhandler,
94 resize_sub: Rc::new(RefCell::new(None)),
95 anchor: Default::default(),
96 on_blur,
97 }
98 }
99
100 fn calc_anchor_pos(&self, target: &HtmlElement) -> (f64, f64) {
101 let target_rect = target.get_bounding_client_rect();
102 let modal_rect = self.custom_element.get_bounding_client_rect();
103 calc_anchor_position(self.anchor.get(), &target_rect, &modal_rect)
104 }
105
106 async fn open_within_viewport(&self, target: HtmlElement) -> ApiResult<()> {
107 let elem = target.unchecked_ref::<HtmlElement>();
108 let rect = elem.get_bounding_client_rect();
109 let width = rect.width();
110 let height = rect.height();
111 let top = rect.top();
112 let left = rect.left();
113 *self.target.borrow_mut() = Some(target.clone());
114
115 let msg = ModalMsg::SetPos {
117 top: top + height - 1.0,
118 left,
119 visible: false,
120 rev_vert: false,
121 };
122
123 self.root.borrow().as_ref().unwrap().send_message(msg);
124 global::body().append_child(&self.custom_element)?;
125 request_animation_frame().await;
126
127 self.anchor.set(calc_relative_position(
129 &self.custom_element,
130 top,
131 left,
132 height,
133 width,
134 ));
135
136 let (top, left) = self.calc_anchor_pos(&target);
137 let msg = ModalMsg::SetPos {
138 top,
139 left,
140 visible: true,
141 rev_vert: self.anchor.get().is_rev_vert(),
142 };
143
144 self.root.borrow().as_ref().unwrap().send_message(msg);
145
146 if self.own_focus {
147 let mut this = Some(self.clone());
148 *self.blurhandler.borrow_mut() = Some(Closure::new(move |_| {
149 this.take().and_then(|x| x.hide().ok()).unwrap_or(())
150 }));
151
152 self.custom_element
153 .dataset()
154 .set("poscorrected", "true")
155 .unwrap();
156
157 self.custom_element.add_event_listener_with_callback(
158 "blur",
159 self.blurhandler
160 .borrow()
161 .as_ref()
162 .unwrap()
163 .as_ref()
164 .unchecked_ref(),
165 )?;
166
167 Ok(self.custom_element.focus()?)
168 } else {
169 Ok(())
170 }
171 }
172
173 pub fn send_message(&self, msg: T::Message) {
174 self.root
175 .borrow()
176 .as_ref()
177 .unwrap()
178 .send_message(ModalMsg::SubMsg(msg))
179 }
180
181 pub fn send_message_batch(&self, msgs: Vec<T::Message>) {
182 self.root
183 .borrow()
184 .as_ref()
185 .unwrap()
186 .send_message_batch(msgs.into_iter().map(ModalMsg::SubMsg).collect())
187 }
188
189 pub fn send_future_batch<Fut>(&self, future: Fut)
190 where
191 Fut: Future + 'static,
192 Fut::Output: SendAsMessage<Modal<T>>,
193 {
194 self.root
195 .borrow()
196 .as_ref()
197 .unwrap()
198 .send_future_batch(future)
199 }
200
201 pub async fn open(
208 self,
209 target: web_sys::HtmlElement,
210 resize_pubsub: Option<&PubSub<()>>,
211 ) -> ApiResult<()> {
212 if let Some(resize) = resize_pubsub {
213 let this = self.clone();
214 let target = target.clone();
215 let anchor = self.anchor.clone();
216 *self.resize_sub.borrow_mut() = Some(resize.add_listener(move |()| {
217 let (top, left) = this.calc_anchor_pos(&target);
218 let msg = ModalMsg::SetPos {
219 top,
220 left,
221 visible: true,
222 rev_vert: anchor.get().is_rev_vert(),
223 };
224
225 this.root.borrow().as_ref().unwrap().send_message(msg);
226 }));
227 };
228
229 target.class_list().add_1("modal-target").unwrap();
230 let theme = get_theme(&target);
231 self.open_within_viewport(target).await.unwrap();
232 if let Some(theme) = theme {
233 self.custom_element.set_attribute("theme", &theme).unwrap();
234 }
235
236 Ok(())
237 }
238
239 pub fn is_open(&self) -> bool {
240 self.custom_element.is_connected()
241 }
242
243 pub fn hide(&self) -> ApiResult<()> {
245 if self.is_open() {
246 if self.own_focus {
247 self.custom_element.remove_event_listener_with_callback(
248 "blur",
249 self.blurhandler
250 .borrow()
251 .as_ref()
252 .unwrap()
253 .as_ref()
254 .unchecked_ref(),
255 )?;
256
257 *self.blurhandler.borrow_mut() = None;
258 }
259
260 global::body().remove_child(&self.custom_element)?;
261 if let Some(blur) = &self.on_blur {
262 blur.emit(());
263 }
264
265 let target = self.target.borrow_mut().take().unwrap();
266 let event = web_sys::CustomEvent::new("-perspective-close-expression")?;
267 target.class_list().remove_1("modal-target").unwrap();
268 if get_theme(&target).is_some() {
269 self.custom_element.remove_attribute("theme")?;
270 }
271
272 target.dispatch_event(&event)?;
273 }
274
275 Ok(())
276 }
277
278 pub fn destroy(self) -> ApiResult<()> {
280 self.hide()?;
281 self.root.borrow_mut().take().unwrap().destroy();
282 Ok(())
283 }
284}
285
286fn get_theme(elem: &HtmlElement) -> Option<String> {
287 let styles = global::window().get_computed_style(elem).unwrap().unwrap();
288 styles
289 .get_property_value("--psp-theme-name")
290 .ok()
291 .and_then(|x| {
292 let trimmed = x.trim();
293 if !trimmed.is_empty() {
294 Some(trimmed[1..trimmed.len() - 1].to_owned())
295 } else {
296 None
297 }
298 })
299}