Skip to main content

orbital_base_components/table/header_cell/
mod.rs

1mod resize;
2
3use leptos::{either::Either, ev, html, leptos_dom::helpers::WindowListenerHandle, prelude::*};
4
5use resize::{clamp_column_width, column_width_style};
6
7#[component]
8pub fn BaseTableHeaderCell(
9    #[prop(optional, into)] class: MaybeProp<String>,
10    #[prop(optional, into)] style: MaybeProp<String>,
11    #[prop(optional, default = false)] resizable: bool,
12    #[prop(default = None)] min_width: Option<f64>,
13    #[prop(default = None)] max_width: Option<f64>,
14    #[prop(default = None)] colspan: Option<u32>,
15    #[prop(default = None)] rowspan: Option<u32>,
16    #[prop(default = None)] on_resize_end: Option<Callback<f64, ()>>,
17    #[prop(default = None)] on_autosize: Option<Callback<(), ()>>,
18    #[prop(optional)] children: Option<Children>,
19) -> impl IntoView {
20    let th_ref = NodeRef::<html::Th>::new();
21    let pointer_down_x = RwSignal::new(0.0);
22    let pointer_down_th_width = RwSignal::new(0.0);
23    let last_width = RwSignal::new(0.0);
24    let resize_width = RwSignal::new(None::<f64>);
25    let listeners = StoredValue::new(Vec::<WindowListenerHandle>::new());
26
27    let th_style = Signal::derive(move || {
28        if let Some(width) = resize_width.get() {
29            column_width_style(width, min_width, max_width)
30        } else {
31            style.get().unwrap_or_default()
32        }
33    });
34
35    let begin_resize = move |client_x: f64| {
36        pointer_down_x.set(client_x);
37        let Some(th_el) = th_ref.get_untracked() else {
38            return;
39        };
40        let width = th_el.get_bounding_client_rect().width();
41        let width = clamp_column_width(width, min_width, max_width);
42        if width <= 0.0 {
43            return;
44        }
45        pointer_down_th_width.set(width);
46        last_width.set(width);
47        resize_width.set(Some(width));
48
49        let on_pointer_move = window_event_listener(ev::pointermove, move |e: ev::PointerEvent| {
50            let delta = f64::from(e.client_x()) - pointer_down_x.get_untracked();
51            let new_width = clamp_column_width(
52                pointer_down_th_width.get_untracked() + delta,
53                min_width,
54                max_width,
55            );
56            last_width.set(new_width);
57            resize_width.set(Some(new_width));
58        });
59        let on_mouse_move = window_event_listener(ev::mousemove, move |e: ev::MouseEvent| {
60            let delta = f64::from(e.client_x()) - pointer_down_x.get_untracked();
61            let new_width = clamp_column_width(
62                pointer_down_th_width.get_untracked() + delta,
63                min_width,
64                max_width,
65            );
66            last_width.set(new_width);
67            resize_width.set(Some(new_width));
68        });
69        let on_pointer_up = window_event_listener(ev::pointerup, move |_| {
70            let width = last_width.get_untracked();
71            if let Some(cb) = on_resize_end {
72                cb.run(width);
73            }
74            resize_width.set(Some(width));
75            listeners.update_value(|value| {
76                for handle in value.drain(..) {
77                    handle.remove();
78                }
79            });
80        });
81        let on_mouse_up = window_event_listener(ev::mouseup, move |_| {
82            let width = last_width.get_untracked();
83            if let Some(cb) = on_resize_end {
84                cb.run(width);
85            }
86            resize_width.set(Some(width));
87            listeners.update_value(|value| {
88                for handle in value.drain(..) {
89                    handle.remove();
90                }
91            });
92        });
93        listeners.update_value(|value| {
94            value.push(on_pointer_move);
95            value.push(on_mouse_move);
96            value.push(on_pointer_up);
97            value.push(on_mouse_up);
98        });
99    };
100
101    let on_pointer_down = move |e: ev::PointerEvent| {
102        if !resizable {
103            return;
104        }
105        e.prevent_default();
106        e.stop_propagation();
107        begin_resize(f64::from(e.client_x()));
108    };
109
110    let on_mouse_down = move |e: ev::MouseEvent| {
111        if !resizable {
112            return;
113        }
114        e.prevent_default();
115        e.stop_propagation();
116        begin_resize(f64::from(e.client_x()));
117    };
118
119    let on_dblclick = move |_| {
120        if let Some(cb) = on_autosize {
121            cb.run(());
122        }
123    };
124
125    on_cleanup(move || {
126        listeners.update_value(|value| {
127            for handle in value.drain(..) {
128                handle.remove();
129            }
130        });
131    });
132
133    view! {
134        <th
135            class=move || {
136                let extra = class.get().unwrap_or_default();
137                if extra.is_empty() {
138                    "orbital-table-header-cell".to_string()
139                } else {
140                    format!("orbital-table-header-cell {extra}")
141                }
142            }
143            style=move || th_style.get()
144            colspan=colspan
145            rowspan=rowspan
146            node_ref=th_ref
147        >
148            <div class="orbital-table-header-cell__button" role="presentation">
149                {if let Some(children) = children {
150                    Either::Left(children())
151                } else {
152                    Either::Right(())
153                }}
154            </div>
155            {if resizable {
156                Either::Left(view! {
157                    <span
158                        class="orbital-table-header-cell__aside"
159                        on:pointerdown=on_pointer_down
160                        on:mousedown=on_mouse_down
161                        on:dblclick=on_dblclick
162                    >
163                        <div
164                            class="orbital-table-resize-handle"
165                            role="separator"
166                            aria-hidden="true"
167                            on:pointerdown=on_pointer_down
168                            on:mousedown=on_mouse_down
169                            on:dblclick=on_dblclick
170                        ></div>
171                    </span>
172                })
173            } else {
174                Either::Right(())
175            }}
176        </th>
177    }
178}