perspective_viewer/components/
dragdrop_list.rs1use std::collections::HashSet;
14use std::marker::PhantomData;
15
16use derivative::Derivative;
17use perspective_client::proto::ColumnType;
18use web_sys::*;
19use yew::html::Scope;
20use yew::prelude::*;
21
22use crate::components::column_dropdown::ColumnDropDownElement;
23use crate::components::column_selector::{EmptyColumn, InPlaceColumn, InvalidColumn};
24use crate::components::type_icon::TypeIcon;
25use crate::presentation::{DragDropContainer, Presentation};
26use crate::ui::intl_slug;
27use crate::utils::DragTarget;
28
29#[derive(Properties, Derivative)]
30#[derivative(Clone(bound = ""))]
31pub struct DragDropListProps<T, U>
32where
33 T: Component,
34 U: Component,
35 <U as Component>::Properties: DragDropListItemProps,
36{
37 pub parent: Scope<T>,
38
39 pub presentation: Presentation,
40 pub name: &'static str,
41 pub column_dropdown: ColumnDropDownElement,
42 pub exclude: HashSet<String>,
43 pub children: ChildrenWithProps<U>,
44
45 #[prop_or_default]
46 pub disabled: bool,
47
48 #[prop_or_default]
49 pub is_dragover: Option<(
50 usize,
51 <<U as Component>::Properties as DragDropListItemProps>::Item,
52 )>,
53
54 #[prop_or_default]
55 pub allow_duplicates: bool,
56
57 #[prop_or_default]
62 pub single_slot: bool,
63
64 #[prop_or_default]
72 pub role_label: Option<AttrValue>,
73
74 #[prop_or_default]
80 pub is_invalid: bool,
81}
82
83impl<T, U> PartialEq for DragDropListProps<T, U>
84where
85 T: Component,
86 U: Component,
87 <U as Component>::Properties: DragDropListItemProps,
88{
89 fn eq(&self, other: &Self) -> bool {
90 self.name == other.name
91 && self.children == other.children
92 && self.allow_duplicates == other.allow_duplicates
93 && self.is_dragover == other.is_dragover
94 && self.disabled == other.disabled
95 && self.single_slot == other.single_slot
96 && self.is_invalid == other.is_invalid
97 && self.role_label == other.role_label
98 }
99}
100
101pub enum DragDropListMsg {
102 Freeze(bool),
103}
104
105pub struct DragDropList<T, U, V>
116where
117 T: Component,
118 U: Component,
119 <U as Component>::Properties: DragDropListItemProps,
120 V: DragContext<T::Message> + 'static,
121{
122 parent_type: PhantomData<T>,
123 item_type: PhantomData<U>,
124 draggable_type: PhantomData<V>,
125 elem: NodeRef,
126 frozen_size: Option<f64>,
127}
128
129impl<T, U, V> Component for DragDropList<T, U, V>
130where
131 T: Component,
132 U: Component,
133 <U as Component>::Properties: DragDropListItemProps,
134 V: DragContext<T::Message> + 'static,
135{
136 type Message = DragDropListMsg;
137 type Properties = DragDropListProps<T, U>;
138
139 fn create(_ctx: &Context<Self>) -> Self {
140 Self {
141 parent_type: PhantomData,
142 item_type: PhantomData,
143 draggable_type: PhantomData,
144 elem: NodeRef::default(),
145 frozen_size: None,
146 }
147 }
148
149 fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
150 true
151 }
152
153 fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
154 match msg {
155 DragDropListMsg::Freeze(freeze) => {
161 if freeze && self.frozen_size.is_none() {
162 let elem = self.elem.cast::<HtmlElement>().unwrap();
163 self.frozen_size = Some({
164 let txt = window()
168 .unwrap()
169 .get_computed_style(&elem)
170 .unwrap()
171 .unwrap()
172 .get_property_value("width")
173 .unwrap();
174
175 let px = &txt[..txt.len() - 2];
177 px.parse::<f64>().unwrap()
178 });
179 true
180 } else if !freeze {
181 self.frozen_size = None;
183 false
184 } else {
185 false
186 }
187 },
188 }
189 }
190
191 fn view(&self, ctx: &Context<Self>) -> Html {
192 let dragover = Callback::from(|_event: DragEvent| _event.prevent_default());
193
194 let drag_container = DragDropContainer::new(
197 {
198 let total = ctx.props().children.len();
199 let parent = ctx.props().parent.clone();
200 let link = ctx.link().clone();
201 move || {
202 link.send_message(DragDropListMsg::Freeze(true));
203 parent.send_message(V::dragenter(total))
204 }
205 },
206 {
207 let parent = ctx.props().parent.clone();
208 let link = ctx.link().clone();
209 move || {
210 link.send_message(DragDropListMsg::Freeze(false));
211 parent.send_message(V::dragleave())
212 }
213 },
214 );
215
216 let drop = Callback::from({
217 let presentation = ctx.props().presentation.clone();
218 let link = ctx.link().clone();
219 move |event| {
220 link.send_message(DragDropListMsg::Freeze(false));
221 presentation.notify_drop(&event);
222 }
223 });
224
225 let container_noderef = drag_container.noderef.clone();
230
231 let invalid_drag: bool;
232 let mut valid_duplicate_drag = false;
233
234 let columns_html = if ctx.props().single_slot {
235 invalid_drag = ctx.props().is_invalid && ctx.props().is_dragover.is_some();
236
237 let is_self_move = ctx
241 .props()
242 .presentation
243 .get_drag_target()
244 .map(|x| V::is_self_move(x))
245 .unwrap_or_default();
246
247 let close = ctx.props().parent.callback(|_| V::close(0));
248 let dragenter = ctx.props().parent.callback({
249 let container_noderef = container_noderef.clone();
250 move |event: DragEvent| {
251 event.stop_propagation();
252 event.prevent_default();
253 if event.related_target().is_none()
254 && let Some(elem) = container_noderef.cast::<HtmlElement>()
255 {
256 let _ = elem.dataset().set("safaridragleave", "true");
257 }
258 V::dragenter(0)
259 }
260 });
261
262 if ctx.props().is_dragover.is_some() && !is_self_move && !invalid_drag {
263 html! {
264 <div class="pivot-column" ondragenter={dragenter}>
265 <div class="config-drop" />
266 </div>
267 }
268 } else if let Some(column) = ctx.props().children.iter().next() {
269 html! {
270 <div class="pivot-column" ondragenter={dragenter}>
271 { Html::from(column) }
272 <span class="row_close" onmousedown={close} />
273 </div>
274 }
275 } else {
276 html! {}
277 }
278 } else {
279 let mut columns = ctx
280 .props()
281 .children
282 .iter()
283 .map(|x| (true, Some(x)))
284 .enumerate()
285 .collect::<Vec<_>>();
286
287 invalid_drag = if ctx.props().is_invalid && ctx.props().is_dragover.is_some() {
288 true
290 } else if let Some((x, column)) = &ctx.props().is_dragover {
291 let index = *x;
292 let is_append = index == columns.len();
293 let is_self_move = ctx
294 .props()
295 .presentation
296 .get_drag_target()
297 .map(|x| V::is_self_move(x))
298 .unwrap_or_default();
299
300 let is_duplicate = columns
301 .iter()
302 .position(|x| x.1.1.as_ref().unwrap().props.get_item() == *column);
303
304 valid_duplicate_drag = is_duplicate.is_some() && !ctx.props().allow_duplicates;
305 if let Some(duplicate) = is_duplicate
306 && !is_append
307 && (!ctx.props().allow_duplicates || is_self_move)
308 {
309 columns.remove(duplicate);
310 }
311
312 if index < columns.len() {
316 columns.insert(index, (usize::MAX, (false, None)));
317 false
318 } else if (!is_append && !ctx.props().allow_duplicates)
319 || ((!is_append || !is_self_move)
320 && (is_duplicate.is_none() || ctx.props().allow_duplicates))
321 {
322 columns.push((usize::MAX, (false, None)));
323 false
324 } else {
325 true
326 }
327 } else {
328 false
329 };
330
331 columns
332 .into_iter()
333 .enumerate()
334 .map(|(idx, column)| {
335 let close = ctx.props().parent.callback(move |_| V::close(idx));
336 let dragenter = ctx.props().parent.callback({
337 let link = ctx.link().clone();
338 let container_noderef = container_noderef.clone();
339 move |event: DragEvent| {
340 event.stop_propagation();
341 event.prevent_default();
342 if event.related_target().is_none()
358 && let Some(elem) = container_noderef.cast::<HtmlElement>()
359 {
360 let _ = elem.dataset().set("safaridragleave", "true");
361 }
362 link.send_message(DragDropListMsg::Freeze(true));
363 V::dragenter(idx)
364 }
365 });
366
367 if let (key, (true, Some(column))) = column {
368 html! {
369 <div {key} class="pivot-column" ondragenter={dragenter}>
370 { Html::from(column) }
371 <span class="row_close" onmousedown={close} />
372 </div>
373 }
374 } else if let (key, (_, Some(column))) = column {
375 html! {
376 <div {key} class="pivot-column" ondragenter={dragenter}>
377 { Html::from(column) }
378 <span class="row_close" style="opacity: 0.3" />
379 </div>
380 }
381 } else {
382 let (key, _) = column;
383 html! {
384 <div {key} class="pivot-column" ondragenter={dragenter}>
385 <div class="config-drop" />
386 </div>
387 }
388 }
389 })
390 .collect::<Html>()
391 };
392
393 let show_empty = if ctx.props().single_slot {
394 ctx.props().children.is_empty() && ctx.props().is_dragover.is_none()
395 } else {
396 ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag)
397 };
398
399 let column_dropdown = ctx.props().column_dropdown.clone();
400 let exclude = ctx.props().exclude.clone();
401 let on_select = ctx.props().parent.callback(V::create);
402 let class = classes!("rrow");
403 let is_enabled = true;
404
405 let role_style = ctx.props().role_label.as_ref().map(|role| {
409 let slug = intl_slug(role);
410
411 format!(
416 "--psp-label--pivot--content: var(--psp-label--role--{slug}--content, \
417 \"{role}\"); --psp-label--pivot-secondary--display: inline-block"
418 )
419 });
420
421 html! {
422 <div ref={&self.elem} {class}>
423 <div
424 id={ctx.props().name}
425 style={role_style}
426 ondragover={is_enabled.then_some(dragover)}
427 ondragenter={is_enabled.then_some(drag_container.dragenter)}
428 ondragleave={is_enabled.then_some(drag_container.dragleave)}
429 ref={drag_container.noderef}
430 ondrop={is_enabled.then_some(drop)}
431 >
432 <div class="psp-text-field">
433 <ul class="psp-text-field__input" for={ctx.props().name}>
434 { columns_html }
435 if ctx.props().disabled && ctx.props().is_dragover.is_none() {
436 <div class="pivot-column">
437 <div class="pivot-column-border pivot-column-total">
438 <span class="drag-handle icon" />
439 <TypeIcon ty={ColumnType::Integer} />
440 <span class="column_name">{ "TOTAL" }</span>
441 </div>
442 <span
443 class="toggle-mode is_column_active"
444 onmousedown={ctx.props().parent.callback(move |_| V::close(0))}
445 />
446 </div>
447 } else if show_empty {
448 <EmptyColumn {column_dropdown} {exclude} {on_select} />
449 } else if invalid_drag {
450 <InvalidColumn />
451 }
452 </ul>
453 <label class="pivot-selector-label" for={ctx.props().name} />
454 </div>
455 </div>
456 </div>
457 }
458 }
459}
460
461pub trait DragDropListItemProps: Properties {
464 type Item: Clone + PartialEq;
465 fn get_item(&self) -> Self::Item;
466}
467
468pub trait DragContext<T> {
469 fn close(index: usize) -> T;
470 fn dragleave() -> T;
471 fn dragenter(index: usize) -> T;
472 fn create(col: InPlaceColumn) -> T;
473 fn is_self_move(effect: DragTarget) -> bool;
474}