1use std::cell::{Cell, RefCell};
14use std::ops::Deref;
15use std::rc::Rc;
16
17use perspective_client::clone;
18use wasm_bindgen::prelude::*;
19use web_sys::*;
20use yew::html::ImplicitClone;
21use yew::prelude::*;
22
23use crate::js::{IntersectionObserver, IntersectionObserverEntry};
24use crate::utils::*;
25use crate::*;
26
27#[derive(Clone, Debug, PartialEq, Default)]
30pub struct DragDropProps {
31 pub column: Option<String>,
33}
34
35#[derive(Clone, Debug)]
36struct DragFrom {
37 column: String,
38 effect: DragEffect,
39}
40
41#[derive(Debug)]
42struct DragOver {
43 target: DragTarget,
44 index: usize,
45}
46
47#[derive(Debug, Default)]
48enum DragState {
49 #[default]
50 NoDrag,
51 DragInProgress(DragFrom),
52 DragOverInProgress(DragFrom, DragOver),
53}
54
55impl DragState {
56 const fn is_drag_in_progress(&self) -> bool {
57 !matches!(self, Self::NoDrag)
58 }
59}
60
61pub type DragEndCallback = Closure<dyn FnMut(DragEvent)>;
62
63pub struct DragDropState {
64 drag_state: RefCell<DragState>,
65 pub drop_received: PubSub<(String, DragTarget, DragEffect, usize)>,
66
67 pub on_dragstart: RefCell<Option<Callback<DragEffect>>>,
70
71 pub on_dragend: RefCell<Option<Callback<()>>>,
74
75 elem: HtmlElement,
78
79 host_dragend: RefCell<Option<DragEndCallback>>,
83
84 drag_target: RefCell<Option<DragTargetState>>,
85}
86
87#[derive(Clone)]
91pub struct DragDrop(Rc<DragDropState>);
92
93impl DragDrop {
94 pub fn new(elem: &HtmlElement) -> Self {
95 Self(Rc::new(DragDropState {
96 drag_state: Default::default(),
97 drop_received: Default::default(),
98 on_dragstart: Default::default(),
99 on_dragend: Default::default(),
100 elem: elem.clone(),
101 host_dragend: Default::default(),
102 drag_target: Default::default(),
103 }))
104 }
105}
106
107impl Deref for DragDrop {
108 type Target = Rc<DragDropState>;
109
110 fn deref(&self) -> &Self::Target {
111 &self.0
112 }
113}
114
115impl PartialEq for DragDrop {
116 fn eq(&self, other: &Self) -> bool {
117 Rc::ptr_eq(&self.0, &other.0)
118 }
119}
120
121impl ImplicitClone for DragDrop {}
122
123impl DragDrop {
124 pub fn to_props(&self) -> DragDropProps {
127 DragDropProps {
128 column: self.get_drag_column(),
129 }
130 }
131
132 pub fn get_drag_column(&self) -> Option<String> {
134 match *self.drag_state.borrow() {
135 DragState::DragInProgress(DragFrom { ref column, .. })
136 | DragState::DragOverInProgress(DragFrom { ref column, .. }, _) => Some(column.clone()),
137 _ => None,
138 }
139 }
140
141 pub fn get_drag_target(&self) -> Option<DragTarget> {
142 match *self.drag_state.borrow() {
143 DragState::DragInProgress(DragFrom {
144 effect: DragEffect::Move(target),
145 ..
146 })
147 | DragState::DragOverInProgress(
148 DragFrom {
149 effect: DragEffect::Move(target),
150 ..
151 },
152 _,
153 ) => Some(target),
154 _ => None,
155 }
156 }
157
158 pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
159 event.stop_propagation();
160 if let Some(dt) = event.data_transfer() {
161 dt.set_drop_effect("move");
162 }
164
165 let original: HtmlElement = event.target().into_apierror()?.unchecked_into();
166 let elem: HtmlElement = original
167 .children()
168 .get_with_index(0)
169 .unwrap()
170 .clone_node_with_deep(true)?
171 .unchecked_into();
172
173 elem.class_list().toggle("snap-drag-image")?;
174 original.append_child(&elem)?;
175 event.data_transfer().into_apierror()?.set_drag_image(
176 &elem,
177 event.offset_x(),
178 event.offset_y(),
179 );
180
181 *self.drag_target.borrow_mut() =
182 Some(DragTargetState::new(self.elem.clone(), original.clone()));
183
184 ApiFuture::spawn(async move {
186 request_animation_frame().await;
187 original.remove_child(&elem)?;
188 Ok(())
189 });
190
191 Ok(())
192 }
193
194 pub fn is_dragover(&self, drag_target: DragTarget) -> Option<(usize, String)> {
196 match *self.drag_state.borrow() {
197 DragState::DragOverInProgress(
198 DragFrom { ref column, .. },
199 DragOver { target, index },
200 ) if target == drag_target => Some((index, column.clone())),
201 _ => None,
202 }
203 }
204
205 pub fn notify_drop(&self, event: &DragEvent) {
206 event.prevent_default();
207 event.stop_propagation();
208
209 let action = match &*self.drag_state.borrow() {
210 DragState::DragOverInProgress(
211 DragFrom { column, effect },
212 DragOver { target, index },
213 ) => Some((column.to_string(), *target, *effect, *index)),
214 _ => None,
215 };
216
217 self.drag_target.borrow_mut().take();
218 *self.drag_state.borrow_mut() = DragState::NoDrag;
219 if let Some(action) = action {
220 self.drop_received.emit(action);
221 }
222 }
223
224 pub fn notify_drag_start(&self, column: String, effect: DragEffect) {
226 *self.drag_state.borrow_mut() = DragState::DragInProgress(DragFrom { column, effect });
227 self.register_host_dragend();
228 let emit = self.on_dragstart.borrow().clone();
229 ApiFuture::spawn(async move {
230 request_animation_frame().await;
231 if let Some(cb) = emit {
232 cb.emit(effect);
233 }
234
235 Ok(())
236 });
237 }
238
239 pub fn notify_drag_end(&self) {
241 if self.drag_state.borrow().is_drag_in_progress() {
242 self.drag_target.borrow_mut().take();
243 *self.drag_state.borrow_mut() = DragState::NoDrag;
244 if let Some(cb) = self.on_dragend.borrow().as_ref() {
245 cb.emit(());
246 }
247 }
248 }
249
250 fn register_host_dragend(&self) {
255 if let Some(prev) = self.host_dragend.borrow_mut().take() {
257 let _ = self
258 .elem
259 .remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
260 }
261
262 let this = self.clone();
263 let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
264 this.notify_drag_end();
265 }) as Box<dyn FnMut(DragEvent)>);
266
267 self.elem
268 .add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())
269 .unwrap();
270
271 *self.host_dragend.borrow_mut() = Some(closure);
272 }
273
274 pub fn notify_drag_leave(&self, drag_target: DragTarget) {
276 let reset = match *self.drag_state.borrow() {
277 DragState::DragOverInProgress(
278 DragFrom { ref column, effect },
279 DragOver { target, .. },
280 ) if target == drag_target => Some((column.clone(), effect)),
281 _ => None,
282 };
283
284 if let Some((column, effect)) = reset {
285 self.notify_drag_start(column, effect);
286 }
287 }
288
289 pub fn notify_drag_enter(&self, target: DragTarget, index: usize) -> bool {
292 let mut drag_state = self.drag_state.borrow_mut();
293 let should_render = match &*drag_state {
294 DragState::DragOverInProgress(_, drag_to) => {
295 drag_to.target != target || drag_to.index != index
296 },
297 _ => true,
298 };
299
300 *drag_state = match &*drag_state {
301 DragState::DragOverInProgress(drag_from, _) | DragState::DragInProgress(drag_from) => {
302 DragState::DragOverInProgress(drag_from.clone(), DragOver { target, index })
303 },
304 _ => DragState::NoDrag,
305 };
306
307 should_render
308 }
309}
310
311pub fn dragenter_helper(callback: impl Fn() + 'static, target: NodeRef) -> Callback<DragEvent> {
315 Callback::from({
316 move |event: DragEvent| {
317 maybe_log!({
318 event.stop_propagation();
319 event.prevent_default();
320 if event.related_target().is_none() {
321 target
322 .cast::<HtmlElement>()
323 .into_apierror()?
324 .dataset()
325 .set("safaridragleave", "true")?;
326 }
327 });
328
329 callback();
330 }
331 })
332}
333
334pub fn dragleave_helper(callback: impl Fn() + 'static, drag_ref: NodeRef) -> Callback<DragEvent> {
338 Callback::from({
339 clone!(drag_ref);
340 move |event: DragEvent| {
341 maybe_log!({
342 event.stop_propagation();
343 event.prevent_default();
344
345 let mut related_target = event
346 .related_target()
347 .or_else(|| Some(JsValue::UNDEFINED.unchecked_into::<EventTarget>()))
348 .and_then(|x| x.dyn_into::<Element>().ok());
349
350 if related_target
368 .as_ref()
369 .map(|x| x.has_attribute("aria-hidden"))
370 .unwrap_or_default()
371 {
372 related_target = Some(
373 related_target
374 .into_apierror()?
375 .parent_node()
376 .into_apierror()?
377 .dyn_ref::<ShadowRoot>()
378 .ok_or_else(|| JsValue::from("Chrome drag/drop bug detection failed"))?
379 .host()
380 .unchecked_into::<Element>(),
381 )
382 }
383
384 let current_target = drag_ref.cast::<HtmlElement>().unwrap();
385 match related_target {
386 Some(ref related) => {
387 if !current_target.contains(Some(related))
390 && related.parent_element().is_some()
391 {
392 callback();
393 }
394 },
395 None => {
396 let dataset = current_target.dataset();
399 if dataset.get("safaridragleave").is_some() {
400 dataset.delete("safaridragleave");
401 } else {
402 callback();
403 }
404 },
405 };
406 })
407 }
408 })
409}
410
411#[derive(Clone)]
412pub struct DragDropContainer {
413 pub noderef: NodeRef,
414 pub dragenter: Callback<DragEvent>,
415 pub dragleave: Callback<DragEvent>,
416}
417
418impl DragDropContainer {
419 pub fn new<F: Fn() + 'static, G: Fn() + 'static>(ondragenter: F, ondragleave: G) -> Self {
420 let noderef = NodeRef::default();
421 Self {
422 dragenter: dragenter_helper(ondragenter, noderef.clone()),
423 dragleave: dragleave_helper(ondragleave, noderef.clone()),
424 noderef,
425 }
426 }
427}
428
429struct DragTargetState {
432 target: HtmlElement,
433 shadow_root: ShadowRoot,
434 alive: Rc<Cell<bool>>,
435 observer: IntersectionObserver,
436}
437
438impl DragTargetState {
439 fn new(host: HtmlElement, target: HtmlElement) -> Self {
440 let shadow_root = host.shadow_root().unwrap();
441 let alive = Rc::new(Cell::new(true));
442 let observer = IntersectionObserver::new(
443 &Closure::<dyn FnMut(js_sys::Array)>::new({
444 clone!(target, shadow_root, alive);
445 move |records: js_sys::Array| {
446 if !alive.get() {
447 return;
448 }
449
450 for record in records.iter() {
451 let record: IntersectionObserverEntry = record.unchecked_into();
452 if !record.is_intersecting() {
453 shadow_root.append_child(&target).unwrap();
454 return;
455 }
456 }
457 }
458 })
459 .into_js_value()
460 .unchecked_into(),
461 );
462
463 observer.observe(target.as_ref());
464 Self {
465 target,
466 shadow_root,
467 alive,
468 observer,
469 }
470 }
471}
472
473impl Drop for DragTargetState {
474 fn drop(&mut self) {
475 self.alive.set(false);
476 self.observer.unobserve(&self.target);
477 if self.target.is_connected() {
478 let _ = self.shadow_root.remove_child(&self.target);
479 }
480 }
481}