Skip to main content

perspective_viewer/ui/
dragdrop.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::cell::Cell;
14use std::rc::Rc;
15
16use perspective_client::clone;
17use perspective_js::utils::*;
18use wasm_bindgen::prelude::*;
19use web_sys::*;
20use yew::prelude::*;
21
22use crate::js::{IntersectionObserver, IntersectionObserverEntry};
23
24pub type DragEndCallback = Closure<dyn FnMut(DragEvent)>;
25pub type PointerDownCallback = Closure<dyn FnMut(PointerEvent)>;
26
27pub fn closest_draggable(event: &Event) -> Option<HtmlElement> {
28    event
29        .composed_path()
30        .get(0)
31        .dyn_into::<Element>()
32        .ok()?
33        .closest("[draggable=\"true\"]")
34        .ok()
35        .flatten()?
36        .dyn_into::<HtmlElement>()
37        .ok()
38}
39
40pub fn clear_document_selection() -> ApiResult<()> {
41    if let Some(selection) = global::window().get_selection()?
42        && !selection.is_collapsed()
43    {
44        selection.remove_all_ranges()?;
45    }
46
47    Ok(())
48}
49
50pub fn dragenter_helper(callback: impl Fn() + 'static, target: NodeRef) -> Callback<DragEvent> {
51    Callback::from({
52        move |event: DragEvent| {
53            let r = (|| -> ApiResult<()> {
54                event.stop_propagation();
55                event.prevent_default();
56                if event.related_target().is_none() {
57                    target
58                        .cast::<HtmlElement>()
59                        .into_apierror()?
60                        .dataset()
61                        .set("safaridragleave", "true")?;
62                }
63                Ok(())
64            })();
65
66            if let Err(e) = r {
67                web_sys::console::warn_1(&e.into());
68            }
69
70            callback();
71        }
72    })
73}
74
75pub fn dragleave_helper(callback: impl Fn() + 'static, drag_ref: NodeRef) -> Callback<DragEvent> {
76    Callback::from({
77        clone!(drag_ref);
78        move |event: DragEvent| {
79            let r = (|| -> ApiResult<()> {
80                event.stop_propagation();
81                event.prevent_default();
82
83                let mut related_target = event
84                    .related_target()
85                    .or_else(|| Some(JsValue::UNDEFINED.unchecked_into::<EventTarget>()))
86                    .and_then(|x| x.dyn_into::<Element>().ok());
87
88                if related_target
89                    .as_ref()
90                    .map(|x| x.has_attribute("aria-hidden"))
91                    .unwrap_or_default()
92                {
93                    related_target = Some(
94                        related_target
95                            .into_apierror()?
96                            .parent_node()
97                            .into_apierror()?
98                            .dyn_ref::<ShadowRoot>()
99                            .ok_or_else(|| JsValue::from("Chrome drag/drop bug detection failed"))?
100                            .host()
101                            .unchecked_into::<Element>(),
102                    )
103                }
104
105                let current_target = drag_ref.cast::<HtmlElement>().unwrap();
106                match related_target {
107                    Some(ref related) => {
108                        if !current_target.contains(Some(related))
109                            && related.parent_element().is_some()
110                        {
111                            callback();
112                        }
113                    },
114                    None => {
115                        let dataset = current_target.dataset();
116                        if dataset.get("safaridragleave").is_some() {
117                            dataset.delete("safaridragleave");
118                        } else {
119                            callback();
120                        }
121                    },
122                };
123                Ok(())
124            })();
125
126            if let Err(e) = r {
127                web_sys::console::warn_1(&e.into());
128            }
129        }
130    })
131}
132
133#[derive(Clone)]
134pub struct DragDropContainer {
135    pub noderef: NodeRef,
136    pub dragenter: Callback<DragEvent>,
137    pub dragleave: Callback<DragEvent>,
138}
139
140impl DragDropContainer {
141    pub fn new<F: Fn() + 'static, G: Fn() + 'static>(ondragenter: F, ondragleave: G) -> Self {
142        let noderef = NodeRef::default();
143        Self {
144            dragenter: dragenter_helper(ondragenter, noderef.clone()),
145            dragleave: dragleave_helper(ondragleave, noderef.clone()),
146            noderef,
147        }
148    }
149}
150
151pub struct DragTargetState {
152    target: HtmlElement,
153    shadow_root: ShadowRoot,
154    alive: Rc<Cell<bool>>,
155    observer: IntersectionObserver,
156}
157
158impl DragTargetState {
159    pub fn new(host: HtmlElement, target: HtmlElement) -> Self {
160        let shadow_root = host.shadow_root().unwrap();
161        let alive = Rc::new(Cell::new(true));
162        let observer = IntersectionObserver::new(
163            &Closure::<dyn FnMut(js_sys::Array)>::new({
164                clone!(target, shadow_root, alive);
165                move |records: js_sys::Array| {
166                    if !alive.get() {
167                        return;
168                    }
169
170                    for record in records.iter() {
171                        let record: IntersectionObserverEntry = record.unchecked_into();
172                        if !record.is_intersecting() {
173                            shadow_root.append_child(&target).unwrap();
174                            return;
175                        }
176                    }
177                }
178            })
179            .into_js_value()
180            .unchecked_into(),
181        );
182
183        observer.observe(target.as_ref());
184        Self {
185            target,
186            shadow_root,
187            alive,
188            observer,
189        }
190    }
191}
192
193impl Drop for DragTargetState {
194    fn drop(&mut self) {
195        self.alive.set(false);
196        self.observer.unobserve(&self.target);
197        if self.target.is_connected() {
198            let _ = self.shadow_root.remove_child(&self.target);
199        }
200    }
201}