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