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    let tbody_content = {
554        let row_renderer = row_renderer.clone();
555        let loading_row_renderer = loading_row_renderer.clone();
556        let error_row_renderer = error_row_renderer.clone();
557        let on_selection_change = on_selection_change.clone();
558
559        view! {
560            {row_placeholder_renderer.run(placeholder_height_before)}
561
562            <For
563                each=move || {
564                    let loaded_rows = loaded_rows.read();
565                    let display_range = display_range.read();
566
567                    let iter = loaded_rows[display_range.clone()]
568                        .iter()
569                        .cloned()
570                        .enumerate()
571                        .map(|(i, row)| (i + display_range.start, row));
572
573                    if let Some(loading_row_display_limit) = loading_row_display_limit {
574                        let mut loading_row_count = 0;
575                        iter.filter(|(_, row)| {
576                                if matches!(row, RowState::Loading | RowState::Placeholder) {
577                                    loading_row_count += 1;
578                                    loading_row_count <= loading_row_display_limit
579                                } else {
580                                    true
581                                }
582                            })
583                            .collect::<Vec<_>>()
584                    } else {
585                        iter.collect::<Vec<_>>()
586                    }
587                }
588
589                key=|(idx, row)| {
590                    match row {
591                        RowState::Loaded(_) => idx.to_string(),
592                        RowState::Error(_) => format!("error-{idx}"),
593                        RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
594                    }
595                }
596
597                children={
598                    let row_renderer = row_renderer.clone();
599                    let loading_row_renderer = loading_row_renderer.clone();
600                    let error_row_renderer = error_row_renderer.clone();
601                    let on_selection_change = on_selection_change.clone();
602                    move |(i, row)| {
603                        match row {
604                            RowState::Loaded(row) => {
605                                let selected_signal = Signal::derive(move || {
606                                    selected_indices.read().contains(&i)
607                                });
608
609                                let class_signal = Signal::derive(move || {
610                                    class_provider
611                                        .row(i, selected_signal.get(), row_class.read().as_str())
612                                });
613
614                                let on_select = {
615                                    let on_selection_change = on_selection_change.clone();
616
617                                    move |evt: web_sys::MouseEvent| {
618                                        update_selection(evt, selection, first_selected_index, i);
619
620                                        let selection_change_event = SelectionChangeEvent {
621                                            row: row.into(),
622                                            row_index: i,
623                                            selected: selected_signal.get_untracked(),
624                                        };
625
626                                        on_selection_change.run(selection_change_event);
627                                    }
628                                };
629
630                                Effect::watch(
631                                    move || { row.track() },
632                                    move |_, _, _| {
633                                        let on_change = on_change.get_value();
634
635                                        on_change
636                                            .run(ChangeEvent {
637                                                row_index: i,
638                                                changed_row: row.into(),
639                                            });
640                                    },
641                                    false,
642                                );
643                                row_renderer
644                                    .run(class_signal, row, i, selected_signal, on_select.into(), columns)
645                            }
646                            RowState::Error(err) => {
647                                error_row_renderer.run(err, i, Row::COLUMN_COUNT)
648                            }
649                            RowState::Loading | RowState::Placeholder => {
650                                loading_row_renderer
651                                    .run(
652                                        Signal::derive(move || {
653                                            class_provider.row(i, false, row_class.read().as_str())
654                                        }),
655                                        Callback::new(move |(col_index,): (usize,)| {
656                                            class_provider
657                                                .loading_cell(
658                                                    i,
659                                                    col_index,
660                                                    loading_cell_class.read().as_str(),
661                                                )
662                                        }),
663                                        Callback::new(move |(col_index,): (usize,)| {
664                                            class_provider
665                                                .loading_cell_inner(
666                                                    i,
667                                                    col_index,
668                                                    loading_cell_inner_class.read().as_str(),
669                                                )
670                                        }),
671                                        i,
672                                        Row::COLUMN_COUNT,
673                                    )
674                            }
675                        }
676                    }
677                }
678            />
679
680            {row_placeholder_renderer.run(placeholder_height_after)}
681        }
682        .into_any()
683    };
684
685    let tbody_directive = Arc::new(move |el: web_sys::Element, _: ()| {
686        tbody_el.set(Some(el));
687    });
688
689    let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_directive);
690
691    view! {
692        {thead_renderer.run(thead_row_renderer.run(thead_content, thead_row_class), thead_class)}
693
694        {tbody}
695    }
696}
697
698fn compute_average_row_height_from_loaded<Row, Column, ClsP>(
699    tbody_ref: RwSignal<Option<web_sys::Element>, LocalStorage>,
700    display_range: ReadSignal<Range<usize>>,
701    y: Signal<f64>,
702    set_y: &impl Fn(f64),
703    set_average_row_height: WriteSignal<f64>,
704    placeholder_height_before: Signal<f64>,
705    loaded_rows: RwSignal<LoadedRows<Row>>,
706) where
707    Row: TableRow<Column, ClassesProvider = ClsP> + Send + Sync + Clone + 'static,
708    Column: Copy + Send + Sync + 'static,
709{
710    if let Some(el) = tbody_ref.get_untracked() {
711        let el: &web_sys::Element = &el;
712        let display_range = display_range.get_untracked();
713        if display_range.end > 0 {
714            let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
715                let mut loading_row_start_index = None;
716                let mut loading_row_end_index = None;
717
718                for i in display_range.clone() {
719                    if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
720                        if loading_row_start_index.is_none() {
721                            loading_row_start_index = Some(i);
722                        }
723                        loading_row_end_index = Some(i);
724                    } else if loading_row_end_index.is_some() {
725                        break;
726                    }
727                }
728
729                if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
730                    (loading_row_start_index, loading_row_end_index)
731                {
732                    if loading_row_end_index == loading_row_start_index {
733                        return None;
734                    }
735
736                    let children = el.children();
737
738                    // skip first element, because it's the "before" placeholder
739                    let first_loading_row = children
740                        .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
741                    let last_loading_row = children
742                        .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
743
744                    if let (Some(first_loading_row), Some(last_loaded_row)) =
745                        (first_loading_row, last_loading_row)
746                    {
747                        return Some(
748                            (last_loaded_row.get_bounding_client_rect().top()
749                                - first_loading_row.get_bounding_client_rect().top())
750                                / (loading_row_end_index - loading_row_start_index) as f64,
751                        );
752                    }
753                }
754
755                None
756            });
757
758            if let Some(avg_row_height) = avg_row_height {
759                let prev_placeholder_height_before = placeholder_height_before.get_untracked();
760
761                set_average_row_height.set(avg_row_height);
762
763                let new_placeholder_height_before = placeholder_height_before.get_untracked();
764                set_y(
765                    y.get_untracked() - prev_placeholder_height_before
766                        + new_placeholder_height_before,
767                );
768            }
769        }
770    }
771}
772
773fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
774    let meta_pressed = evt.meta_key() || evt.ctrl_key();
775    let shift_pressed = evt.shift_key();
776    (meta_pressed, shift_pressed)
777}
778
779fn update_selection(
780    evt: web_sys::MouseEvent,
781    selection: Selection,
782    first_selected_index: RwSignal<Option<usize>>,
783    i: usize,
784) {
785    match selection {
786        Selection::None => {}
787        Selection::Single(selected_index) => {
788            if selected_index.get_untracked() == Some(i) {
789                selected_index.set(None);
790            } else {
791                selected_index.set(Some(i));
792            }
793        }
794        Selection::Multiple(selected_indices) => {
795            let mut indices = selected_indices.write();
796            let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
797
798            if meta_pressed {
799                if indices.contains(&i) {
800                    indices.remove(&i);
801                } else {
802                    indices.insert(i);
803                }
804                match indices.len() {
805                    0 => first_selected_index.set(None),
806                    1 => {
807                        first_selected_index.set(Some(i));
808                    }
809                    _ => {
810                        // do nothing
811                    }
812                }
813            } else if shift_pressed {
814                if let Some(first_selected_index) = first_selected_index.get() {
815                    let min = first_selected_index.min(i);
816                    let max = first_selected_index.max(i);
817                    for i in min..=max {
818                        indices.insert(i);
819                    }
820                } else {
821                    indices.insert(i);
822                    first_selected_index.set(Some(i));
823                }
824            } else {
825                HashSet::clear(&mut *indices);
826                indices.insert(i);
827                first_selected_index.set(Some(i));
828            }
829        }
830    }
831}