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
59#[derive(Clone, Copy, Debug, Default)]
61enum ModalAnchor {
62 BottomRightTopLeft,
63 BottomRightBottomLeft,
64 BottomRightTopRight,
65 BottomLeftTopLeft,
66 TopRightTopLeft,
67 TopRightBottomRight,
68
69 #[default]
70 TopLeftBottomLeft,
71}
72
73impl ModalAnchor {
74 const fn is_rev_vert(&self) -> bool {
75 matches!(
76 self,
77 Self::BottomLeftTopLeft
78 | Self::BottomRightBottomLeft
79 | Self::BottomRightTopLeft
80 | Self::BottomRightTopRight
81 )
82 }
83}
84
85fn calc_relative_position(
90 elem: &HtmlElement,
91 _top: f64,
92 left: f64,
93 height: f64,
94 width: f64,
95) -> ModalAnchor {
96 let window = global::window();
97 let rect = elem.get_bounding_client_rect();
98 let inner_width = window.inner_width().unwrap().as_f64().unwrap();
99 let inner_height = window.inner_height().unwrap().as_f64().unwrap();
100 let rect_top = rect.top();
101 let rect_height = rect.height();
102 let rect_width = rect.width();
103 let rect_left = rect.left();
104
105 let elem_over_y = inner_height < rect_top + rect_height;
106 let elem_over_x = inner_width < rect_left + rect_width;
107 let target_over_x = inner_width < rect_left + width;
108 let target_over_y = inner_height < rect_top + height;
109
110 match (elem_over_y, elem_over_x, target_over_x, target_over_y) {
112 (true, _, true, true) => ModalAnchor::BottomRightTopLeft,
113 (true, _, true, false) => ModalAnchor::BottomRightBottomLeft,
114 (true, true, false, _) => {
115 if left + width - rect_width > 0.0 {
116 ModalAnchor::BottomRightTopRight
117 } else {
118 ModalAnchor::BottomLeftTopLeft
119 }
120 },
121 (true, false, false, _) => ModalAnchor::BottomLeftTopLeft,
122 (false, true, true, _) => ModalAnchor::TopRightTopLeft,
123 (false, true, false, _) => {
124 if left + width - rect_width > 0.0 {
125 ModalAnchor::TopRightBottomRight
126 } else {
127 ModalAnchor::TopLeftBottomLeft
128 }
129 },
130 _ => ModalAnchor::TopLeftBottomLeft,
131 }
132}
133
134impl<T> ModalElement<T>
135where
136 T: Component,
137 T::Properties: ModalLink<T>,
138{
139 pub fn new(
140 custom_element: web_sys::HtmlElement,
141 props: T::Properties,
142 own_focus: bool,
143 on_blur: Option<Callback<()>>,
144 ) -> Self {
145 custom_element.set_attribute("tabindex", "0").unwrap();
146 let init = web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open);
147 let shadow_root = custom_element
148 .attach_shadow(&init)
149 .unwrap()
150 .unchecked_into::<web_sys::Element>();
151
152 let cprops = yew::props!(ModalProps<T> {
153 child: Some(html_nested! {
154 <T ..props />
155 }),
156 });
157
158 let root = Rc::new(RefCell::new(Some(
159 yew::Renderer::with_root_and_props(shadow_root, cprops).render(),
160 )));
161
162 let blurhandler = Rc::new(RefCell::new(None));
163 Self {
164 root,
165 custom_element,
166 target: Rc::new(RefCell::new(None)),
167 own_focus,
168 blurhandler,
169 resize_sub: Rc::new(RefCell::new(None)),
170 anchor: Default::default(),
171 on_blur,
172 }
173 }
174
175 fn calc_anchor_position(&self, target: &HtmlElement) -> (f64, f64) {
176 let elem = target.unchecked_ref::<HtmlElement>();
177 let rect = elem.get_bounding_client_rect();
178 let height = rect.height();
179 let width = rect.width();
180 let top = rect.top();
181 let left = rect.left();
182
183 let self_rect = self.custom_element.get_bounding_client_rect();
184 let rect_height = self_rect.height();
185 let rect_width = self_rect.width();
186
187 match self.anchor.get() {
188 ModalAnchor::BottomRightTopLeft => (top - rect_height, left - rect_width + 1.0),
189 ModalAnchor::BottomRightBottomLeft => {
190 (top - rect_height + height, left - rect_width + 1.0)
191 },
192 ModalAnchor::BottomRightTopRight => {
193 (top - rect_height + 1.0, left + width - rect_width)
194 },
195 ModalAnchor::BottomLeftTopLeft => (top - rect_height + 1.0, left),
196 ModalAnchor::TopRightTopLeft => (top, left - rect_width + 1.0),
197 ModalAnchor::TopRightBottomRight => (top + height - 1.0, left + width - rect_width),
198 ModalAnchor::TopLeftBottomLeft => ((top + height - 1.0), left),
199 }
200 }
201
202 async fn open_within_viewport(&self, target: HtmlElement) -> ApiResult<()> {
203 let elem = target.unchecked_ref::<HtmlElement>();
204 let rect = elem.get_bounding_client_rect();
205 let width = rect.width();
206 let height = rect.height();
207 let top = rect.top();
208 let left = rect.left();
209 *self.target.borrow_mut() = Some(target.clone());
210
211 let msg = ModalMsg::SetPos {
213 top: top + height - 1.0,
214 left,
215 visible: false,
216 rev_vert: false,
217 };
218
219 self.root.borrow().as_ref().unwrap().send_message(msg);
220 global::body().append_child(&self.custom_element)?;
221 request_animation_frame().await;
222
223 self.anchor.set(calc_relative_position(
225 &self.custom_element,
226 top,
227 left,
228 height,
229 width,
230 ));
231
232 let (top, left) = self.calc_anchor_position(&target);
233 let msg = ModalMsg::SetPos {
234 top,
235 left,
236 visible: true,
237 rev_vert: self.anchor.get().is_rev_vert(),
238 };
239
240 self.root.borrow().as_ref().unwrap().send_message(msg);
241
242 if self.own_focus {
243 let mut this = Some(self.clone());
244 *self.blurhandler.borrow_mut() = Some(Closure::new(move |_| {
245 this.take().and_then(|x| x.hide().ok()).unwrap_or(())
246 }));
247
248 self.custom_element
249 .dataset()
250 .set("poscorrected", "true")
251 .unwrap();
252
253 self.custom_element.add_event_listener_with_callback(
254 "blur",
255 self.blurhandler
256 .borrow()
257 .as_ref()
258 .unwrap()
259 .as_ref()
260 .unchecked_ref(),
261 )?;
262
263 Ok(self.custom_element.focus()?)
264 } else {
265 Ok(())
266 }
267 }
268
269 pub fn send_message(&self, msg: T::Message) {
270 self.root
271 .borrow()
272 .as_ref()
273 .unwrap()
274 .send_message(ModalMsg::SubMsg(msg))
275 }
276
277 pub fn send_message_batch(&self, msgs: Vec<T::Message>) {
278 self.root
279 .borrow()
280 .as_ref()
281 .unwrap()
282 .send_message_batch(msgs.into_iter().map(ModalMsg::SubMsg).collect())
283 }
284
285 pub fn send_future_batch<Fut>(&self, future: Fut)
286 where
287 Fut: Future + 'static,
288 Fut::Output: SendAsMessage<Modal<T>>,
289 {
290 self.root
291 .borrow()
292 .as_ref()
293 .unwrap()
294 .send_future_batch(future)
295 }
296
297 pub async fn open(
304 self,
305 target: web_sys::HtmlElement,
306 resize_pubsub: Option<&PubSub<()>>,
307 ) -> ApiResult<()> {
308 if let Some(resize) = resize_pubsub {
309 let this = self.clone();
310 let target = target.clone();
311 let anchor = self.anchor.clone();
312 *self.resize_sub.borrow_mut() = Some(resize.add_listener(move |()| {
313 let (top, left) = this.calc_anchor_position(&target);
314 let msg = ModalMsg::SetPos {
315 top,
316 left,
317 visible: true,
318 rev_vert: anchor.get().is_rev_vert(),
319 };
320
321 this.root.borrow().as_ref().unwrap().send_message(msg);
322 }));
323 };
324
325 target.class_list().add_1("modal-target").unwrap();
326 let theme = get_theme(&target);
327 self.open_within_viewport(target).await.unwrap();
328 if let Some(theme) = theme {
329 self.custom_element.set_attribute("theme", &theme).unwrap();
330 }
331
332 Ok(())
333 }
334
335 pub fn is_open(&self) -> bool {
336 self.custom_element.is_connected()
337 }
338
339 pub fn hide(&self) -> ApiResult<()> {
341 if self.is_open() {
342 if self.own_focus {
343 self.custom_element.remove_event_listener_with_callback(
344 "blur",
345 self.blurhandler
346 .borrow()
347 .as_ref()
348 .unwrap()
349 .as_ref()
350 .unchecked_ref(),
351 )?;
352
353 *self.blurhandler.borrow_mut() = None;
354 }
355
356 global::body().remove_child(&self.custom_element)?;
357 if let Some(blur) = &self.on_blur {
358 blur.emit(());
359 }
360
361 let target = self.target.borrow_mut().take().unwrap();
362 let event = web_sys::CustomEvent::new("-perspective-close-expression")?;
363 target.class_list().remove_1("modal-target").unwrap();
364 if get_theme(&target).is_some() {
365 self.custom_element.remove_attribute("theme")?;
366 }
367
368 target.dispatch_event(&event)?;
369 }
370
371 Ok(())
372 }
373
374 pub fn destroy(self) -> ApiResult<()> {
376 self.hide()?;
377 self.root.borrow_mut().take().unwrap().destroy();
378 Ok(())
379 }
380}
381
382fn get_theme(elem: &HtmlElement) -> Option<String> {
383 let styles = global::window().get_computed_style(elem).unwrap().unwrap();
384 styles
385 .get_property_value("--theme-name")
386 .ok()
387 .and_then(|x| {
388 let trimmed = x.trim();
389 if !trimmed.is_empty() {
390 Some(trimmed[1..trimmed.len() - 1].to_owned())
391 } else {
392 None
393 }
394 })
395}