leptos_struct_table/components/
table_content.rs

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