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