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