Skip to main content

leptos_struct_table/components/
table_content.rs

1// leptos-struct-table/src/components/table_content.rs
2
3#![allow(clippy::await_holding_refcell_ref)]
4
5use crate::components::renderer_fn::renderer_fn;
6use crate::loaded_rows::{LoadedRows, RowState};
7use crate::selection::Selection;
8use crate::table_row::TableRow;
9use crate::{
10    ChangeEvent, ColumnSort, DefaultErrorRowRenderer, DefaultLoadingRowRenderer,
11    DefaultRowPlaceholderRenderer, DefaultTableBodyRenderer, DefaultTableHeadRenderer,
12    DefaultTableHeadRowRenderer, DefaultTableRowRenderer, DisplayStrategy, EventHandler,
13    HeadDragHandler, ReloadController, RowReader, SelectionChangeEvent, SortingMode,
14    TableClassesProvider, TableDataProvider, TableHeadEvent,
15};
16use leptos::prelude::*;
17use leptos::tachys::view::any_view::AnyView;
18use leptos::task::spawn_local;
19use leptos_use::core::IntoElementMaybeSignal;
20use leptos_use::{
21    UseElementSizeOptions, UseElementSizeReturn, UseScrollOptions, UseScrollReturn,
22    use_debounce_fn, use_element_size_with_options, use_scroll_with_options,
23};
24use std::cell::RefCell;
25use std::collections::{HashSet, VecDeque};
26use std::fmt::Debug;
27use std::marker::PhantomData;
28use std::ops::Range;
29use std::rc::Rc;
30use std::sync::Arc;
31
32const MAX_DISPLAY_ROW_COUNT: usize = 500;
33
34renderer_fn!(
35    RowRendererFn<Row, Column>(
36        class: Signal<String>,
37        row: RwSignal<Row>,
38        index: usize,
39        selected: Signal<bool>,
40        on_select: EventHandler<web_sys::MouseEvent>,
41        columns: RwSignal<Vec<Column>>
42    )
43    default DefaultTableRowRenderer
44    where
45        Row: TableRow<Column> + 'static,
46        Column: Copy + Send + Sync + 'static
47);
48
49renderer_fn!(
50    RowPlaceholderRendererFn(height: Signal<f64>)
51    default DefaultRowPlaceholderRenderer
52);
53
54renderer_fn!(
55    WrapperRendererFn(view: AnyView, class: Signal<String>)
56);
57
58pub type BodyRef = Arc<dyn Fn(web_sys::Element, ())>;
59
60renderer_fn!(
61    TbodyRendererFn(view: AnyView, class: Signal<String>, body_ref: BodyRef)
62);
63
64renderer_fn!(
65    ErrorRowRendererFn(err: String, index: usize, col_count: usize)
66    default DefaultErrorRowRenderer
67);
68
69renderer_fn!(
70    LoadingRowRendererFn(class: Signal<String>, get_cell_class: Callback<(usize,), String>, get_cell_inner_class: Callback<(usize,), String>, index: usize, col_count: usize)
71    default DefaultLoadingRowRenderer
72);
73
74/// Render the content of a table. This is the main component of this crate.
75#[component]
76pub fn TableContent<Row, Column, DataP, Err, ClsP, ScrollEl, ScrollM>(
77    /// The data to be rendered in this table.
78    /// This must implement [`TableDataProvider`] or [`PaginatedTableDataProvider`].
79    rows: DataP,
80    /// The container element which has scrolling capabilities.
81    scroll_container: ScrollEl,
82    /// Event handler for when a row is edited.
83    /// Check out the [editable example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/editable/src/main.rs).
84    #[prop(optional, into)]
85    on_change: EventHandler<ChangeEvent<Row>>,
86    /// Selection mode together with the `RwSignal` to hold the selection. Available modes are
87    /// - `None` - No selection (default)
88    /// - `Single` - Single selection
89    /// - `Multiple` - Multiple selection
90    ///
91    /// Please see [`Selection`] for more information and check out the
92    /// [selectable example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/selectable/src/main.rs).
93    #[prop(optional, into)]
94    selection: Selection,
95    /// Event handler callback for when the selection changes.
96    /// See the [selectable example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/selectable/src/main.rs) for details.
97    #[prop(optional, into)]
98    on_selection_change: EventHandler<SelectionChangeEvent<Row>>,
99    /// Renderer function for the table head. Defaults to [`DefaultTableHeadRenderer`]. For a full example see the
100    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
101    #[prop(default = DefaultTableHeadRenderer.into(), into)]
102    thead_renderer: WrapperRendererFn,
103    /// Renderer function for the table body. Defaults to [`DefaultTableBodyRenderer`]. For a full example see the
104    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
105    #[prop(default = DefaultTableBodyRenderer.into(), into)]
106    tbody_renderer: TbodyRendererFn,
107    /// Renderer function for the table head row. Defaults to [`DefaultTableHeadRowRenderer`]. For a full example see the
108    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
109    #[prop(default = DefaultTableHeadRowRenderer.into(), into)]
110    thead_row_renderer: WrapperRendererFn,
111    /// The row renderer. Defaults to [`DefaultTableRowRenderer`]. For a full example see the
112    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
113    #[prop(optional, into)]
114    row_renderer: RowRendererFn<Row, Column>,
115    /// The row renderer for when that row is currently being loaded.
116    /// Defaults to [`DefaultLoadingRowRenderer`]. For a full example see the
117    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
118    #[prop(optional, into)]
119    loading_row_renderer: LoadingRowRendererFn,
120    /// The row renderer for when that row failed to load.
121    /// Defaults to [`DefaultErrorRowRenderer`]. For a full example see the
122    /// [custom_renderers_svg example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/custom_renderers_svg/src/main.rs).
123    #[prop(optional, into)]
124    error_row_renderer: ErrorRowRendererFn,
125    /// The row placeholder renderer. Defaults to [`DefaultRowPlaceholderRenderer`].
126    /// This is used in place of rows that are not shown
127    /// before and after the currently visible rows.
128    #[prop(optional, into)]
129    row_placeholder_renderer: RowPlaceholderRendererFn,
130    /// Additional classes to add to rows
131    #[prop(optional, into)]
132    row_class: Signal<String>,
133    /// Additional classes to add to the thead
134    #[prop(optional, into)]
135    thead_class: Signal<String>,
136    /// Additional classes to add to the row inside the thead
137    #[prop(optional, into)]
138    thead_row_class: Signal<String>,
139    /// Additional classes to add to the tbody
140    #[prop(optional, into)]
141    tbody_class: Signal<String>,
142    /// Additional classes to add to the cell inside a row that is being loaded
143    #[prop(optional, into)]
144    loading_cell_class: Signal<String>,
145    /// Additional classes to add to the inner element inside a cell that is inside a row that is being loaded
146    #[prop(optional, into)]
147    loading_cell_inner_class: Signal<String>,
148    /// The sorting to apply to the table.
149    /// For this to work you have add `#[table(sortable)]` to your struct.
150    /// Please see the [simple example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/simple/src/main.rs).
151    #[prop(default = RwSignal::new(VecDeque::new()), into)]
152    sorting: RwSignal<VecDeque<(Column, ColumnSort)>>,
153    /// The sorting mode to use. Defaults to `MultiColumn`. Please note that
154    /// this to have any effect you have to add the macro attribute `#[table(sortable)]`
155    /// to your struct.
156    #[prop(optional)]
157    sorting_mode: SortingMode,
158    /// The to-be rendered columns and their order.
159    /// Used for hiding and ordering columns.
160    #[prop(default = RwSignal::new(Row::columns().into()), into)]
161    columns: RwSignal<Vec<Column>>,
162    /// This is called once the number of rows is known.
163    /// It will only be executed if [`TableDataProvider::row_count`] returns `Some(...)`.
164    ///
165    /// See the [paginated_rest_datasource example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/paginated_rest_datasource/src/main.rs)
166    /// for how to use.
167    #[prop(optional, into)]
168    on_row_count: EventHandler<usize>,
169    /// Drag and drop handlers for head cells.
170    /// The main (and default) use-case is reordering columns.
171    ///
172    /// See the [column_order_and_visibility example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/column_order_and_visibility/src/main.rs)
173    /// for how to use.
174    #[prop(optional)]
175    drag_handler: HeadDragHandler<Column>,
176    /// Allows to manually trigger a reload.
177    ///
178    /// See the [paginated_rest_datasource example](https://github.com/Synphonyte/leptos-struct-table/blob/master/examples/paginated_rest_datasource/src/main.rs)
179    /// for how to use.
180    #[prop(optional)]
181    reload_controller: ReloadController,
182    /// The display strategy to use when rendering the table.
183    /// Can be one of
184    /// - `Virtualization`
185    /// - `InfiniteScroll`
186    /// - `Pagination`
187    ///
188    /// Please check [`DisplayStrategy`] to see explanations of all available options.
189    #[prop(optional)]
190    display_strategy: DisplayStrategy,
191    /// The maximum number of loading rows to display. Defaults to `None` which means unlimited.
192    /// Use this if you load a small number of rows and don't want the entire screen to be full of
193    /// loading rows.
194    #[prop(optional)]
195    loading_row_display_limit: Option<usize>,
196    /// Provides access to the data rows.
197    #[prop(optional)]
198    row_reader: RowReader<Row>,
199
200    #[prop(optional)] _marker: PhantomData<(Err, ScrollM)>,
201) -> impl IntoView
202where
203    Column: Eq + Ord + Copy + Clone + Send + Sync + 'static,
204    Row: TableRow<Column, ClassesProvider = ClsP> + Clone + Send + Sync + 'static,
205    DataP: TableDataProvider<Row, Column, Err> + 'static,
206    Err: Debug + 'static,
207    ClsP: TableClassesProvider + Send + Sync + Copy + 'static,
208    ScrollEl: IntoElementMaybeSignal<web_sys::Element, ScrollM> + 'static,
209    ScrollM: 'static,
210{
211    let on_change = StoredValue::new(on_change);
212    let rows = Rc::new(RefCell::new(rows));
213
214    let class_provider = ClsP::new();
215
216    let row_class = Signal::derive(move || row_class.get());
217    let loading_cell_inner_class = Signal::derive(move || loading_cell_inner_class.get());
218    let loading_cell_class = Signal::derive(move || loading_cell_class.get());
219    let thead_class = Signal::derive(move || class_provider.thead(&thead_class.get()));
220    let thead_row_class = Signal::derive(move || class_provider.thead_row(&thead_row_class.get()));
221    let tbody_class = Signal::derive(move || class_provider.tbody(&tbody_class.get()));
222
223    let loaded_rows = RwSignal::new(LoadedRows::<Row>::new());
224
225    let _ = row_reader
226        .get_loaded_rows
227        .replace(Box::new(move |index: usize| {
228            loaded_rows.read()[index].clone()
229        }));
230
231    let first_selected_index = RwSignal::new(None::<usize>);
232
233    let (row_count, set_row_count) = signal(None::<usize>);
234
235    let set_known_row_count = move |row_count: usize| {
236        set_row_count.set(Some(row_count));
237        loaded_rows.write().resize(row_count);
238        on_row_count.run(row_count);
239        display_strategy.set_row_count(row_count);
240    };
241
242    let load_row_count = {
243        let rows = Rc::clone(&rows);
244        let set_known_row_count = set_known_row_count.clone();
245
246        move || {
247            spawn_local({
248                let rows = Rc::clone(&rows);
249                let set_known_row_count = set_known_row_count.clone();
250
251                async move {
252                    // TODO: can we avoid this?
253                    let row_count = rows.borrow().row_count().await;
254
255                    // check if this component was disposed of
256                    if sorting.try_with_untracked(|_| {}).is_none() {
257                        return;
258                    }
259
260                    if let Some(row_count) = row_count {
261                        set_known_row_count(row_count);
262                    }
263
264                    // force update to trigger sorting effect below
265                    sorting.notify();
266                }
267            })
268        }
269    };
270
271    let (reload_count, set_reload_count) = signal(0_usize);
272    let clear = {
273        let load_row_count = load_row_count.clone();
274
275        move |clear_row_count: bool| {
276            selection.clear();
277            first_selected_index.set(None);
278            LoadedRows::<Row>::clear(&mut loaded_rows.write());
279
280            if clear_row_count {
281                let reload = row_count.get_untracked().is_some();
282                set_row_count.set(None);
283                if reload {
284                    load_row_count();
285                }
286            }
287
288            set_reload_count.set(reload_count.get_untracked().overflowing_add(1).0);
289        }
290    };
291
292    let on_head_click = move |event: TableHeadEvent<Column>| {
293        sorting_mode.update_sorting_from_event(&mut sorting.write(), event);
294    };
295
296    Effect::new({
297        let clear = clear.clone();
298        let rows = Rc::clone(&rows);
299
300        move || {
301            let sorting = sorting.read();
302            if let Ok(mut rows) = rows.try_borrow_mut() {
303                rows.set_sorting(&sorting);
304                clear(false);
305            };
306        }
307    });
308
309    Effect::new({
310        let rows = Rc::clone(&rows);
311
312        move || {
313            // triggered when `ReloadController::reload()` is called
314            reload_controller.track();
315            rows.borrow().track();
316            clear(true);
317        }
318    });
319
320    let selected_indices = match selection {
321        Selection::None => Signal::stored(HashSet::new()),
322        Selection::Single(selected_index) => Signal::derive(move || {
323            selected_index
324                .get()
325                .map(|i| HashSet::from([i]))
326                .unwrap_or_default()
327        }),
328        Selection::Multiple(selected_indices) => selected_indices.into(),
329    };
330
331    let scroll_container = scroll_container.into_element_maybe_signal();
332
333    let UseScrollReturn { y, set_y, .. } = use_scroll_with_options(
334        scroll_container,
335        UseScrollOptions::default().throttle(100.0),
336    );
337
338    let UseElementSizeReturn { height, .. } = use_element_size_with_options(
339        scroll_container,
340        UseElementSizeOptions::default().box_(web_sys::ResizeObserverBoxOptions::ContentBox),
341    );
342
343    Effect::new(move || {
344        if let DisplayStrategy::Virtualization | DisplayStrategy::Pagination { .. } =
345            display_strategy
346        {
347            load_row_count();
348        }
349    });
350
351    let (average_row_height, set_average_row_height) = signal(20.0);
352
353    let first_visible_row_index = if let DisplayStrategy::Pagination {
354        controller,
355        row_count,
356    } = display_strategy
357    {
358        Memo::new(move |_| controller.current_page.get() * row_count)
359    } else {
360        Memo::new(move |_| (y.get() / average_row_height.get()).floor() as usize)
361    };
362    let visible_row_count = match display_strategy {
363        DisplayStrategy::Pagination { row_count, .. } => Signal::derive(move || row_count),
364
365        DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => {
366            Memo::new(move |_| ((height.get() / average_row_height.get()).ceil() as usize).max(20))
367                .into()
368        }
369    };
370
371    let (display_range, set_display_range) = signal(0..0);
372
373    let placeholder_height_before =
374        if matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
375            Signal::derive(move || 0.0)
376        } else {
377            Memo::new(move |_| display_range.get().start as f64 * average_row_height.get()).into()
378        };
379
380    let placeholder_height_after = if matches!(display_strategy, DisplayStrategy::Pagination { .. })
381    {
382        Signal::derive(move || 0.0)
383    } else {
384        Memo::new(move |_| {
385            let row_count_after = if let Some(row_count) = row_count.get() {
386                (row_count.saturating_sub(display_range.get().end)) as f64
387            } else {
388                0.0
389            };
390
391            row_count_after * average_row_height.get()
392        })
393        .into()
394    };
395
396    let tbody_el = RwSignal::new_local(None::<web_sys::Element>);
397
398    let compute_average_row_height = use_debounce_fn(
399        move || {
400            compute_average_row_height_from_loaded(
401                tbody_el,
402                display_range,
403                y,
404                &set_y,
405                set_average_row_height,
406                placeholder_height_before,
407                loaded_rows,
408            );
409        },
410        50.0,
411    );
412
413    Effect::new(move || {
414        // with this a reload triggers this effect
415        reload_count.track();
416
417        // 1. Get all values *atomically* within a single .with() call
418        let (first_visible, visible_count, row_count_opt) = loaded_rows.with(|_| {
419            (
420                first_visible_row_index.get(),
421                visible_row_count.get(),
422                row_count.get(),
423            )
424        });
425
426        let visible_count = visible_count.min(MAX_DISPLAY_ROW_COUNT);
427
428        if visible_count == 0 {
429            return;
430        }
431
432        let mut start = first_visible.saturating_sub(visible_count * 2);
433        let mut end = start + visible_count * 5;
434
435        if let Some(row_count) = row_count_opt {
436            // Clamp end to row_count if we know it
437            end = end.min(row_count);
438
439            // Ensure start is within valid bounds *after* clamping end
440            start = start.min(end); // Crucial: prevent start > end
441        } else {
442            //If total number of rows is unknown, we don't clamp,
443            // but limit to MAX_DISPLAY_ROW_COUNT
444            if !matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
445                end = end.min(start + MAX_DISPLAY_ROW_COUNT);
446            }
447        }
448
449        if let Some(chunk_size) = DataP::CHUNK_SIZE {
450            start = (start / chunk_size) * chunk_size;
451            end = end.div_ceil(chunk_size) * chunk_size; // Round end *up* to nearest chunk size
452        }
453
454        let range = start..end;
455
456        set_display_range.set(match display_strategy {
457            DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => range.clone(),
458            DisplayStrategy::Pagination { row_count, .. } => {
459                first_visible..(first_visible + row_count).min(end)
460            }
461        });
462
463        loaded_rows.update_untracked(|loaded_rows| {
464            if end > loaded_rows.len() {
465                loaded_rows.resize(end);
466            }
467        });
468
469        let missing_range =
470            loaded_rows.with_untracked(|loaded_rows| loaded_rows.missing_range(range.clone()));
471
472        if let Some(missing_range) = missing_range {
473            // Ensure missing_range is valid *after* all calculations
474            let missing_start = missing_range.start.min(missing_range.end);
475            let missing_end = missing_range.end; // Already correct
476
477            let missing_range = missing_start..missing_end;
478
479            if missing_range.is_empty() {
480                // Don't proceed with empty ranges
481                return;
482            }
483
484            loaded_rows.write().write_loading(missing_range.clone());
485
486            let mut loading_ranges = vec![];
487            if let Some(chunk_size) = DataP::CHUNK_SIZE {
488                let start = missing_range.start / chunk_size * chunk_size;
489                let mut current_range = start..start + chunk_size;
490                while current_range.end <= missing_range.end {
491                    loading_ranges.push(current_range.clone());
492                    current_range = current_range.end..current_range.end + chunk_size;
493                }
494                // when we got a missing_range which size is less than the chunk_size, add current_range to loading_ranges
495                if current_range.end > missing_range.end && current_range.start < missing_range.end
496                {
497                    loading_ranges.push(current_range);
498                }
499            } else {
500                loading_ranges.push(missing_range);
501            }
502
503            // TODO : implement max concurrent requests
504            for missing_range in loading_ranges {
505                let compute_average_row_height = compute_average_row_height.clone();
506                spawn_local({
507                    let rows = Rc::clone(&rows);
508                    let set_known_row_count = set_known_row_count.clone();
509
510                    async move {
511                        let latest_reload_count = reload_count.get_untracked();
512
513                        // TODO: can we avoid this?
514                        let result = rows
515                            .borrow()
516                            .get_rows(missing_range.clone())
517                            .await
518                            .map_err(|err| format!("{err:?}"));
519
520                        if let Some(reload_count) = reload_count.try_get_untracked() {
521                            // make sure the loaded data is still valid
522                            if reload_count != latest_reload_count {
523                                return;
524                            }
525
526                            if let Ok((_, loaded_range)) = &result
527                                && loaded_range.end < missing_range.end
528                            {
529                                match row_count_opt {
530                                    // Use pre-fetched value!
531                                    Some(row_count) => {
532                                        if loaded_range.end < row_count {
533                                            set_known_row_count(loaded_range.end);
534                                        }
535                                    }
536                                    None => {
537                                        set_known_row_count(loaded_range.end);
538                                    }
539                                }
540                            }
541                            loaded_rows.write().write_loaded(result, missing_range);
542                            compute_average_row_height();
543                        }
544                    }
545                });
546            }
547        }
548    });
549
550    let thead_content =
551        Row::render_head_row(sorting.into(), on_head_click, drag_handler, columns).into_any();
552
553    fn clamp_range(range: Range<usize>, len: usize) -> Range<usize> {
554        let start = range.start.min(len);
555        let end = range.end.min(len);
556
557        if start > end {
558            return end..start;
559        }
560        start..end
561    }
562
563    let tbody_content = {
564        let row_renderer = row_renderer.clone();
565        let loading_row_renderer = loading_row_renderer.clone();
566        let error_row_renderer = error_row_renderer.clone();
567        let on_selection_change = on_selection_change.clone();
568
569        view! {
570            {row_placeholder_renderer.run(placeholder_height_before)}
571
572            <For
573                each=move || {
574                    let loaded_rows = loaded_rows.read();
575                    let display_range = display_range.read();
576
577                    let iter = loaded_rows[clamp_range(display_range.clone(), loaded_rows.len())]
578                        .iter()
579                        .cloned()
580                        .enumerate()
581                        .map(|(i, row)| (i + display_range.start, row));
582
583                    if let Some(loading_row_display_limit) = loading_row_display_limit {
584                        let mut loading_row_count = 0;
585                        iter.filter(|(_, row)| {
586                                if matches!(row, RowState::Loading | RowState::Placeholder) {
587                                    loading_row_count += 1;
588                                    loading_row_count <= loading_row_display_limit
589                                } else {
590                                    true
591                                }
592                            })
593                            .collect::<Vec<_>>()
594                    } else {
595                        iter.collect::<Vec<_>>()
596                    }
597                }
598
599                key=|(idx, row)| {
600                    match row {
601                        RowState::Loaded(_) => idx.to_string(),
602                        RowState::Error(_) => format!("error-{idx}"),
603                        RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
604                    }
605                }
606
607                children={
608                    let row_renderer = row_renderer.clone();
609                    let loading_row_renderer = loading_row_renderer.clone();
610                    let error_row_renderer = error_row_renderer.clone();
611                    let on_selection_change = on_selection_change.clone();
612                    move |(i, row)| {
613                        match row {
614                            RowState::Loaded(row) => {
615                                let selected_signal = Signal::derive(move || {
616                                    selected_indices.read().contains(&i)
617                                });
618
619                                let class_signal = Signal::derive(move || {
620                                    class_provider
621                                        .row(i, selected_signal.get(), row_class.read().as_str())
622                                });
623
624                                let on_select = {
625                                    let on_selection_change = on_selection_change.clone();
626
627                                    move |evt: web_sys::MouseEvent| {
628                                        update_selection(evt, selection, first_selected_index, i);
629
630                                        let selection_change_event = SelectionChangeEvent {
631                                            row: row.into(),
632                                            row_index: i,
633                                            selected: selected_signal.get_untracked(),
634                                        };
635
636                                        on_selection_change.run(selection_change_event);
637                                    }
638                                };
639
640                                Effect::watch(
641                                    move || { row.track() },
642                                    move |_, _, _| {
643                                        let on_change = on_change.get_value();
644
645                                        on_change
646                                            .run(ChangeEvent {
647                                                row_index: i,
648                                                changed_row: row.into(),
649                                            });
650                                    },
651                                    false,
652                                );
653                                row_renderer
654                                    .run(class_signal, row, i, selected_signal, on_select.into(), columns)
655                            }
656                            RowState::Error(err) => {
657                                error_row_renderer.run(err, i, Row::COLUMN_COUNT)
658                            }
659                            RowState::Loading | RowState::Placeholder => {
660                                loading_row_renderer
661                                    .run(
662                                        Signal::derive(move || {
663                                            class_provider.row(i, false, row_class.read().as_str())
664                                        }),
665                                        Callback::new(move |(col_index,): (usize,)| {
666                                            class_provider
667                                                .loading_cell(
668                                                    i,
669                                                    col_index,
670                                                    loading_cell_class.read().as_str(),
671                                                )
672                                        }),
673                                        Callback::new(move |(col_index,): (usize,)| {
674                                            class_provider
675                                                .loading_cell_inner(
676                                                    i,
677                                                    col_index,
678                                                    loading_cell_inner_class.read().as_str(),
679                                                )
680                                        }),
681                                        i,
682                                        Row::COLUMN_COUNT,
683                                    )
684                            }
685                        }
686                    }
687                }
688            />
689
690            {row_placeholder_renderer.run(placeholder_height_after)}
691        }
692        .into_any()
693    };
694
695    let tbody_directive = Arc::new(move |el: web_sys::Element, _: ()| {
696        tbody_el.set(Some(el));
697    });
698
699    let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_directive);
700
701    view! {
702        {thead_renderer.run(thead_row_renderer.run(thead_content, thead_row_class), thead_class)}
703
704        {tbody}
705    }
706}
707
708fn compute_average_row_height_from_loaded<Row, Column, ClsP>(
709    tbody_ref: RwSignal<Option<web_sys::Element>, LocalStorage>,
710    display_range: ReadSignal<Range<usize>>,
711    y: Signal<f64>,
712    set_y: &impl Fn(f64),
713    set_average_row_height: WriteSignal<f64>,
714    placeholder_height_before: Signal<f64>,
715    loaded_rows: RwSignal<LoadedRows<Row>>,
716) where
717    Row: TableRow<Column, ClassesProvider = ClsP> + Send + Sync + Clone + 'static,
718    Column: Copy + Send + Sync + 'static,
719{
720    if let Some(el) = tbody_ref.get_untracked() {
721        let el: &web_sys::Element = &el;
722        let display_range = display_range.get_untracked();
723        if display_range.end > 0 {
724            let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
725                let mut loading_row_start_index = None;
726                let mut loading_row_end_index = None;
727
728                for i in display_range.clone() {
729                    if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
730                        if loading_row_start_index.is_none() {
731                            loading_row_start_index = Some(i);
732                        }
733                        loading_row_end_index = Some(i);
734                    } else if loading_row_end_index.is_some() {
735                        break;
736                    }
737                }
738
739                if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
740                    (loading_row_start_index, loading_row_end_index)
741                {
742                    if loading_row_end_index == loading_row_start_index {
743                        return None;
744                    }
745
746                    let children = el.children();
747
748                    // skip first element, because it's the "before" placeholder
749                    let first_loading_row = children
750                        .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
751                    let last_loading_row = children
752                        .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
753
754                    if let (Some(first_loading_row), Some(last_loaded_row)) =
755                        (first_loading_row, last_loading_row)
756                    {
757                        return Some(
758                            (last_loaded_row.get_bounding_client_rect().top()
759                                - first_loading_row.get_bounding_client_rect().top())
760                                / (loading_row_end_index - loading_row_start_index) as f64,
761                        );
762                    }
763                }
764
765                None
766            });
767
768            if let Some(avg_row_height) = avg_row_height {
769                let prev_placeholder_height_before = placeholder_height_before.get_untracked();
770
771                set_average_row_height.set(avg_row_height);
772
773                let new_placeholder_height_before = placeholder_height_before.get_untracked();
774                set_y(
775                    y.get_untracked() - prev_placeholder_height_before
776                        + new_placeholder_height_before,
777                );
778            }
779        }
780    }
781}
782
783fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
784    let meta_pressed = evt.meta_key() || evt.ctrl_key();
785    let shift_pressed = evt.shift_key();
786    (meta_pressed, shift_pressed)
787}
788
789fn update_selection(
790    evt: web_sys::MouseEvent,
791    selection: Selection,
792    first_selected_index: RwSignal<Option<usize>>,
793    i: usize,
794) {
795    match selection {
796        Selection::None => {}
797        Selection::Single(selected_index) => {
798            if selected_index.get_untracked() == Some(i) {
799                selected_index.set(None);
800            } else {
801                selected_index.set(Some(i));
802            }
803        }
804        Selection::Multiple(selected_indices) => {
805            let mut indices = selected_indices.write();
806            let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
807
808            if meta_pressed {
809                if indices.contains(&i) {
810                    indices.remove(&i);
811                } else {
812                    indices.insert(i);
813                }
814                match indices.len() {
815                    0 => first_selected_index.set(None),
816                    1 => {
817                        first_selected_index.set(Some(i));
818                    }
819                    _ => {
820                        // do nothing
821                    }
822                }
823            } else if shift_pressed {
824                if let Some(first_selected_index) = first_selected_index.get() {
825                    let min = first_selected_index.min(i);
826                    let max = first_selected_index.max(i);
827                    for i in min..=max {
828                        indices.insert(i);
829                    }
830                } else {
831                    indices.insert(i);
832                    first_selected_index.set(Some(i));
833                }
834            } else {
835                HashSet::clear(&mut *indices);
836                indices.insert(i);
837                first_selected_index.set(Some(i));
838            }
839        }
840    }
841}