1#![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#[component]
76pub fn TableContent<Row, Column, DataP, Err, ClsP, ScrollEl, ScrollM>(
77 rows: DataP,
80 scroll_container: ScrollEl,
82 #[prop(optional, into)]
85 on_change: EventHandler<ChangeEvent<Row>>,
86 #[prop(optional, into)]
94 selection: Selection,
95 #[prop(optional, into)]
98 on_selection_change: EventHandler<SelectionChangeEvent<Row>>,
99 #[prop(default = DefaultTableHeadRenderer.into(), into)]
102 thead_renderer: WrapperRendererFn,
103 #[prop(default = DefaultTableBodyRenderer.into(), into)]
106 tbody_renderer: TbodyRendererFn,
107 #[prop(default = DefaultTableHeadRowRenderer.into(), into)]
110 thead_row_renderer: WrapperRendererFn,
111 #[prop(optional, into)]
114 row_renderer: RowRendererFn<Row, Column>,
115 #[prop(optional, into)]
119 loading_row_renderer: LoadingRowRendererFn,
120 #[prop(optional, into)]
124 error_row_renderer: ErrorRowRendererFn,
125 #[prop(optional, into)]
129 row_placeholder_renderer: RowPlaceholderRendererFn,
130 #[prop(optional, into)]
132 row_class: Signal<String>,
133 #[prop(optional, into)]
135 thead_class: Signal<String>,
136 #[prop(optional, into)]
138 thead_row_class: Signal<String>,
139 #[prop(optional, into)]
141 tbody_class: Signal<String>,
142 #[prop(optional, into)]
144 loading_cell_class: Signal<String>,
145 #[prop(optional, into)]
147 loading_cell_inner_class: Signal<String>,
148 #[prop(default = RwSignal::new(VecDeque::new()), into)]
152 sorting: RwSignal<VecDeque<(Column, ColumnSort)>>,
153 #[prop(optional)]
157 sorting_mode: SortingMode,
158 #[prop(default = RwSignal::new(Row::columns().into()), into)]
161 columns: RwSignal<Vec<Column>>,
162 #[prop(optional, into)]
168 on_row_count: EventHandler<usize>,
169 #[prop(optional)]
175 drag_handler: HeadDragHandler<Column>,
176 #[prop(optional)]
181 reload_controller: ReloadController,
182 #[prop(optional)]
190 display_strategy: DisplayStrategy,
191 #[prop(optional)]
195 loading_row_display_limit: Option<usize>,
196 #[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 let row_count = rows.borrow().row_count().await;
254
255 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 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 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 reload_count.track();
416
417 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 end = end.min(row_count);
438
439 start = start.min(end); } else {
442 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; }
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 let missing_start = missing_range.start.min(missing_range.end);
475 let missing_end = missing_range.end; let missing_range = missing_start..missing_end;
478
479 if missing_range.is_empty() {
480 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 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 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 Some(latest_reload_count) = reload_count.try_get_untracked() else {
512 return;
513 };
514
515 let result = rows
517 .borrow()
518 .get_rows(missing_range.clone())
519 .await
520 .map_err(|err| format!("{err:?}"));
521
522 if let Some(reload_count) = reload_count.try_get_untracked() {
523 if reload_count != latest_reload_count {
525 return;
526 }
527
528 if let Ok((_, loaded_range)) = &result
529 && loaded_range.end < missing_range.end
530 {
531 match row_count_opt {
532 Some(row_count) => {
534 if loaded_range.end < row_count {
535 set_known_row_count(loaded_range.end);
536 }
537 }
538 None => {
539 set_known_row_count(loaded_range.end);
540 }
541 }
542 }
543 loaded_rows.write().write_loaded(result, missing_range);
544 compute_average_row_height();
545 }
546 }
547 });
548 }
549 }
550 });
551
552 let thead_content =
553 Row::render_head_row(sorting.into(), on_head_click, drag_handler, columns).into_any();
554
555 fn clamp_range(range: Range<usize>, len: usize) -> Range<usize> {
556 let start = range.start.min(len);
557 let end = range.end.min(len);
558
559 if start > end {
560 return end..start;
561 }
562 start..end
563 }
564
565 let tbody_content = {
566 let row_renderer = row_renderer.clone();
567 let loading_row_renderer = loading_row_renderer.clone();
568 let error_row_renderer = error_row_renderer.clone();
569 let on_selection_change = on_selection_change.clone();
570
571 view! {
572 {row_placeholder_renderer.run(placeholder_height_before)}
573
574 <For
575 each=move || {
576 let loaded_rows = loaded_rows.read();
577 let display_range = display_range.read();
578
579 let iter = loaded_rows[clamp_range(display_range.clone(), loaded_rows.len())]
580 .iter()
581 .cloned()
582 .enumerate()
583 .map(|(i, row)| (i + display_range.start, row));
584
585 if let Some(loading_row_display_limit) = loading_row_display_limit {
586 let mut loading_row_count = 0;
587 iter.filter(|(_, row)| {
588 if matches!(row, RowState::Loading | RowState::Placeholder) {
589 loading_row_count += 1;
590 loading_row_count <= loading_row_display_limit
591 } else {
592 true
593 }
594 })
595 .collect::<Vec<_>>()
596 } else {
597 iter.collect::<Vec<_>>()
598 }
599 }
600
601 key=|(idx, row)| {
602 match row {
603 RowState::Loaded(_) => idx.to_string(),
604 RowState::Error(_) => format!("error-{idx}"),
605 RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
606 }
607 }
608
609 children={
610 let row_renderer = row_renderer.clone();
611 let loading_row_renderer = loading_row_renderer.clone();
612 let error_row_renderer = error_row_renderer.clone();
613 let on_selection_change = on_selection_change.clone();
614 move |(i, row)| {
615 match row {
616 RowState::Loaded(row) => {
617 let selected_signal = Signal::derive(move || {
618 selected_indices.read().contains(&i)
619 });
620
621 let class_signal = Signal::derive(move || {
622 class_provider
623 .row(i, selected_signal.get(), row_class.read().as_str())
624 });
625
626 let on_select = {
627 let on_selection_change = on_selection_change.clone();
628
629 move |evt: web_sys::MouseEvent| {
630 update_selection(evt, selection, first_selected_index, i);
631
632 let selection_change_event = SelectionChangeEvent {
633 row: row.into(),
634 row_index: i,
635 selected: selected_signal.get_untracked(),
636 };
637
638 on_selection_change.run(selection_change_event);
639 }
640 };
641
642 Effect::watch(
643 move || { row.track() },
644 move |_, _, _| {
645 let on_change = on_change.get_value();
646
647 on_change
648 .run(ChangeEvent {
649 row_index: i,
650 changed_row: row.into(),
651 });
652 },
653 false,
654 );
655 row_renderer
656 .run(class_signal, row, i, selected_signal, on_select.into(), columns)
657 }
658 RowState::Error(err) => {
659 error_row_renderer.run(err, i, Row::COLUMN_COUNT)
660 }
661 RowState::Loading | RowState::Placeholder => {
662 loading_row_renderer
663 .run(
664 Signal::derive(move || {
665 class_provider.row(i, false, row_class.read().as_str())
666 }),
667 Callback::new(move |(col_index,): (usize,)| {
668 class_provider
669 .loading_cell(
670 i,
671 col_index,
672 loading_cell_class.read().as_str(),
673 )
674 }),
675 Callback::new(move |(col_index,): (usize,)| {
676 class_provider
677 .loading_cell_inner(
678 i,
679 col_index,
680 loading_cell_inner_class.read().as_str(),
681 )
682 }),
683 i,
684 Row::COLUMN_COUNT,
685 )
686 }
687 }
688 }
689 }
690 />
691
692 {row_placeholder_renderer.run(placeholder_height_after)}
693 }
694 .into_any()
695 };
696
697 let tbody_directive = Arc::new(move |el: web_sys::Element, _: ()| {
698 tbody_el.set(Some(el));
699 });
700
701 let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_directive);
702
703 view! {
704 {thead_renderer.run(thead_row_renderer.run(thead_content, thead_row_class), thead_class)}
705
706 {tbody}
707 }
708}
709
710fn compute_average_row_height_from_loaded<Row, Column, ClsP>(
711 tbody_ref: RwSignal<Option<web_sys::Element>, LocalStorage>,
712 display_range: ReadSignal<Range<usize>>,
713 y: Signal<f64>,
714 set_y: &impl Fn(f64),
715 set_average_row_height: WriteSignal<f64>,
716 placeholder_height_before: Signal<f64>,
717 loaded_rows: RwSignal<LoadedRows<Row>>,
718) where
719 Row: TableRow<Column, ClassesProvider = ClsP> + Send + Sync + Clone + 'static,
720 Column: Copy + Send + Sync + 'static,
721{
722 if let Some(el) = tbody_ref.get_untracked() {
723 let el: &web_sys::Element = ⪙
724 let display_range = display_range.get_untracked();
725 if display_range.end > 0 {
726 let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
727 let mut loading_row_start_index = None;
728 let mut loading_row_end_index = None;
729
730 for i in display_range.clone() {
731 if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
732 if loading_row_start_index.is_none() {
733 loading_row_start_index = Some(i);
734 }
735 loading_row_end_index = Some(i);
736 } else if loading_row_end_index.is_some() {
737 break;
738 }
739 }
740
741 if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
742 (loading_row_start_index, loading_row_end_index)
743 {
744 if loading_row_end_index == loading_row_start_index {
745 return None;
746 }
747
748 let children = el.children();
749
750 let first_loading_row = children
752 .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
753 let last_loading_row = children
754 .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
755
756 if let (Some(first_loading_row), Some(last_loaded_row)) =
757 (first_loading_row, last_loading_row)
758 {
759 return Some(
760 (last_loaded_row.get_bounding_client_rect().top()
761 - first_loading_row.get_bounding_client_rect().top())
762 / (loading_row_end_index - loading_row_start_index) as f64,
763 );
764 }
765 }
766
767 None
768 });
769
770 if let Some(avg_row_height) = avg_row_height {
771 let prev_placeholder_height_before = placeholder_height_before.get_untracked();
772
773 set_average_row_height.set(avg_row_height);
774
775 let new_placeholder_height_before = placeholder_height_before.get_untracked();
776 set_y(
777 y.get_untracked() - prev_placeholder_height_before
778 + new_placeholder_height_before,
779 );
780 }
781 }
782 }
783}
784
785fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
786 let meta_pressed = evt.meta_key() || evt.ctrl_key();
787 let shift_pressed = evt.shift_key();
788 (meta_pressed, shift_pressed)
789}
790
791fn update_selection(
792 evt: web_sys::MouseEvent,
793 selection: Selection,
794 first_selected_index: RwSignal<Option<usize>>,
795 i: usize,
796) {
797 match selection {
798 Selection::None => {}
799 Selection::Single(selected_index) => {
800 if selected_index.get_untracked() == Some(i) {
801 selected_index.set(None);
802 } else {
803 selected_index.set(Some(i));
804 }
805 }
806 Selection::Multiple(selected_indices) => {
807 let mut indices = selected_indices.write();
808 let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
809
810 if meta_pressed {
811 if indices.contains(&i) {
812 indices.remove(&i);
813 } else {
814 indices.insert(i);
815 }
816 match indices.len() {
817 0 => first_selected_index.set(None),
818 1 => {
819 first_selected_index.set(Some(i));
820 }
821 _ => {
822 }
824 }
825 } else if shift_pressed {
826 if let Some(first_selected_index) = first_selected_index.get() {
827 let min = first_selected_index.min(i);
828 let max = first_selected_index.max(i);
829 for i in min..=max {
830 indices.insert(i);
831 }
832 } else {
833 indices.insert(i);
834 first_selected_index.set(Some(i));
835 }
836 } else {
837 HashSet::clear(&mut *indices);
838 indices.insert(i);
839 first_selected_index.set(Some(i));
840 }
841 }
842 }
843}