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 ReloadController, RowReader, SelectionChangeEvent, SortingMode, TableClassesProvider,
14 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 use_debounce_fn, use_element_size_with_options, use_scroll_with_options, UseElementSizeOptions,
22 UseElementSizeReturn, UseScrollOptions, UseScrollReturn,
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>(
36 class: Signal<String>,
37 row: RwSignal<Row>,
38 index: usize,
39 selected: Signal<bool>,
40 on_select: EventHandler<web_sys::MouseEvent>
41 )
42 default DefaultTableRowRenderer
43 where Row: TableRow + 'static
44);
45
46renderer_fn!(
47 RowPlaceholderRendererFn(height: Signal<f64>)
48 default DefaultRowPlaceholderRenderer
49);
50
51renderer_fn!(
52 WrapperRendererFn(view: AnyView, class: Signal<String>)
53);
54
55pub type BodyRef = Arc<dyn Fn(web_sys::Element, ())>;
56
57renderer_fn!(
58 TbodyRendererFn(view: AnyView, class: Signal<String>, body_ref: BodyRef)
59);
60
61renderer_fn!(
62 ErrorRowRendererFn(err: String, index: usize, col_count: usize)
63 default DefaultErrorRowRenderer
64);
65
66renderer_fn!(
67 LoadingRowRendererFn(class: Signal<String>, get_cell_class: Callback<(usize,), String>, get_cell_inner_class: Callback<(usize,), String>, index: usize, col_count: usize)
68 default DefaultLoadingRowRenderer
69);
70
71#[component]
73pub fn TableContent<Row, DataP, Err, ClsP, ScrollEl, ScrollM>(
74 rows: DataP,
77 scroll_container: ScrollEl,
79 #[prop(optional, into)]
82 on_change: EventHandler<ChangeEvent<Row>>,
83 #[prop(optional, into)]
91 selection: Selection,
92 #[prop(optional, into)]
95 on_selection_change: EventHandler<SelectionChangeEvent<Row>>,
96 #[prop(default = DefaultTableHeadRenderer.into(), into)]
99 thead_renderer: WrapperRendererFn,
100 #[prop(default = DefaultTableBodyRenderer.into(), into)]
103 tbody_renderer: TbodyRendererFn,
104 #[prop(default = DefaultTableHeadRowRenderer.into(), into)]
107 thead_row_renderer: WrapperRendererFn,
108 #[prop(optional, into)]
111 row_renderer: RowRendererFn<Row>,
112 #[prop(optional, into)]
116 loading_row_renderer: LoadingRowRendererFn,
117 #[prop(optional, into)]
121 error_row_renderer: ErrorRowRendererFn,
122 #[prop(optional, into)]
126 row_placeholder_renderer: RowPlaceholderRendererFn,
127 #[prop(optional, into)]
129 row_class: Signal<String>,
130 #[prop(optional, into)]
132 thead_class: Signal<String>,
133 #[prop(optional, into)]
135 thead_row_class: Signal<String>,
136 #[prop(optional, into)]
138 tbody_class: Signal<String>,
139 #[prop(optional, into)]
141 loading_cell_class: Signal<String>,
142 #[prop(optional, into)]
144 loading_cell_inner_class: Signal<String>,
145 #[prop(default = RwSignal::new(VecDeque::new()), into)]
149 sorting: RwSignal<VecDeque<(usize, ColumnSort)>>,
150 #[prop(optional)]
154 sorting_mode: SortingMode,
155 #[prop(optional, into)]
161 on_row_count: EventHandler<usize>,
162 #[prop(optional)]
167 reload_controller: ReloadController,
168 #[prop(optional)]
176 display_strategy: DisplayStrategy,
177 #[prop(optional)]
181 loading_row_display_limit: Option<usize>,
182 #[prop(optional)]
184 row_reader: RowReader<Row>,
185
186 #[prop(optional)] _marker: PhantomData<(Err, ScrollM)>,
187) -> impl IntoView
188where
189 Row: TableRow<ClassesProvider = ClsP> + Clone + Send + Sync + 'static,
190 DataP: TableDataProvider<Row, Err> + 'static,
191 Err: Debug + 'static,
192 ClsP: TableClassesProvider + Send + Sync + Copy + 'static,
193 ScrollEl: IntoElementMaybeSignal<web_sys::Element, ScrollM> + 'static,
194 ScrollM: ?Sized + 'static,
195{
196 let on_change = StoredValue::new(on_change);
197 let rows = Rc::new(RefCell::new(rows));
198
199 let class_provider = ClsP::new();
200
201 let row_class = Signal::derive(move || row_class.get());
202 let loading_cell_inner_class = Signal::derive(move || loading_cell_inner_class.get());
203 let loading_cell_class = Signal::derive(move || loading_cell_class.get());
204 let thead_class = Signal::derive(move || class_provider.thead(&thead_class.get()));
205 let thead_row_class = Signal::derive(move || class_provider.thead_row(&thead_row_class.get()));
206 let tbody_class = Signal::derive(move || class_provider.tbody(&tbody_class.get()));
207
208 let loaded_rows = RwSignal::new(LoadedRows::<Row>::new());
209
210 let _ = row_reader
211 .get_loaded_rows
212 .replace(Box::new(move |index: usize| {
213 loaded_rows.read()[index].clone()
214 }));
215
216 let first_selected_index = RwSignal::new(None::<usize>);
217
218 let (row_count, set_row_count) = signal(None::<usize>);
219
220 let set_known_row_count = move |row_count: usize| {
221 set_row_count.set(Some(row_count));
222 loaded_rows.write().resize(row_count);
223 on_row_count.run(row_count);
224 display_strategy.set_row_count(row_count);
225 };
226
227 let load_row_count = {
228 let rows = Rc::clone(&rows);
229 let set_known_row_count = set_known_row_count.clone();
230
231 move || {
232 spawn_local({
233 let rows = Rc::clone(&rows);
234 let set_known_row_count = set_known_row_count.clone();
235
236 async move {
237 let row_count = rows.borrow().row_count().await;
239
240 if sorting.try_with_untracked(|_| {}).is_none() {
242 return;
243 }
244
245 if let Some(row_count) = row_count {
246 set_known_row_count(row_count);
247 }
248
249 sorting.notify();
251 }
252 })
253 }
254 };
255
256 let (reload_count, set_reload_count) = signal(0_usize);
257 let clear = {
258 let load_row_count = load_row_count.clone();
259
260 move |clear_row_count: bool| {
261 selection.clear();
262 first_selected_index.set(None);
263 LoadedRows::<Row>::clear(&mut loaded_rows.write());
264
265 if clear_row_count {
266 let reload = row_count.get_untracked().is_some();
267 set_row_count.set(None);
268 if reload {
269 load_row_count();
270 }
271 }
272
273 set_reload_count.set(reload_count.get_untracked().overflowing_add(1).0);
274 }
275 };
276
277 let on_head_click = move |event: TableHeadEvent| {
278 sorting_mode.update_sorting_from_event(&mut sorting.write(), event);
279 };
280
281 Effect::new({
282 let clear = clear.clone();
283 let rows = Rc::clone(&rows);
284
285 move || {
286 let sorting = sorting.read();
287 if let Ok(mut rows) = rows.try_borrow_mut() {
288 rows.set_sorting(&sorting);
289 clear(false);
290 };
291 }
292 });
293
294 Effect::new({
295 let rows = Rc::clone(&rows);
296
297 move || {
298 reload_controller.track();
300 rows.borrow().track();
301 clear(true);
302 }
303 });
304
305 let selected_indices = match selection {
306 Selection::None => Signal::stored(HashSet::new()),
307 Selection::Single(selected_index) => Signal::derive(move || {
308 selected_index
309 .get()
310 .map(|i| HashSet::from([i]))
311 .unwrap_or_default()
312 }),
313 Selection::Multiple(selected_indices) => selected_indices.into(),
314 };
315
316 let scroll_container = scroll_container.into_element_maybe_signal();
317
318 let UseScrollReturn { y, set_y, .. } = use_scroll_with_options(
319 scroll_container,
320 UseScrollOptions::default().throttle(100.0),
321 );
322
323 let UseElementSizeReturn { height, .. } = use_element_size_with_options(
324 scroll_container,
325 UseElementSizeOptions::default().box_(web_sys::ResizeObserverBoxOptions::ContentBox),
326 );
327
328 Effect::new(move || {
329 if let DisplayStrategy::Virtualization | DisplayStrategy::Pagination { .. } =
330 display_strategy
331 {
332 load_row_count();
333 }
334 });
335
336 let (average_row_height, set_average_row_height) = signal(20.0);
337
338 let first_visible_row_index = if let DisplayStrategy::Pagination {
339 controller,
340 row_count,
341 } = display_strategy
342 {
343 Memo::new(move |_| controller.current_page.get() * row_count)
344 } else {
345 Memo::new(move |_| (y.get() / average_row_height.get()).floor() as usize)
346 };
347 let visible_row_count = match display_strategy {
348 DisplayStrategy::Pagination { row_count, .. } => Signal::derive(move || row_count),
349
350 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => {
351 Memo::new(move |_| ((height.get() / average_row_height.get()).ceil() as usize).max(20))
352 .into()
353 }
354 };
355
356 let (display_range, set_display_range) = signal(0..0);
357
358 let placeholder_height_before =
359 if matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
360 Signal::derive(move || 0.0)
361 } else {
362 Memo::new(move |_| display_range.get().start as f64 * average_row_height.get()).into()
363 };
364
365 let placeholder_height_after = if matches!(display_strategy, DisplayStrategy::Pagination { .. })
366 {
367 Signal::derive(move || 0.0)
368 } else {
369 Memo::new(move |_| {
370 let row_count_after = if let Some(row_count) = row_count.get() {
371 (row_count.saturating_sub(display_range.get().end)) as f64
372 } else {
373 0.0
374 };
375
376 row_count_after * average_row_height.get()
377 })
378 .into()
379 };
380
381 let tbody_el = RwSignal::new_local(None::<web_sys::Element>);
382
383 let compute_average_row_height = use_debounce_fn(
384 move || {
385 compute_average_row_height_from_loaded(
386 tbody_el,
387 display_range,
388 y,
389 &set_y,
390 set_average_row_height,
391 placeholder_height_before,
392 loaded_rows,
393 );
394 },
395 50.0,
396 );
397
398 Effect::new(move || {
399 reload_count.track();
401
402 let (first_visible, visible_count, row_count_opt) = loaded_rows.with(|_| {
404 (
405 first_visible_row_index.get(),
406 visible_row_count.get(),
407 row_count.get(),
408 )
409 });
410
411 let visible_count = visible_count.min(MAX_DISPLAY_ROW_COUNT);
412
413 if visible_count == 0 {
414 return;
415 }
416
417 let mut start = first_visible.saturating_sub(visible_count * 2);
418 let mut end = start + visible_count * 5;
419
420 if let Some(row_count) = row_count_opt {
421 end = end.min(row_count);
423
424 start = start.min(end); } else {
427 if !matches!(display_strategy, DisplayStrategy::Pagination { .. }) {
430 end = end.min(start + MAX_DISPLAY_ROW_COUNT);
431 }
432 }
433
434 if let Some(chunk_size) = DataP::CHUNK_SIZE {
435 start = (start / chunk_size) * chunk_size;
436 end = end.div_ceil(chunk_size) * chunk_size; }
438
439 let range = start..end;
440
441 set_display_range.set(match display_strategy {
442 DisplayStrategy::Virtualization | DisplayStrategy::InfiniteScroll => range.clone(),
443 DisplayStrategy::Pagination { row_count, .. } => {
444 first_visible..(first_visible + row_count).min(end)
445 }
446 });
447
448 loaded_rows.update_untracked(|loaded_rows| {
449 if end > loaded_rows.len() {
450 loaded_rows.resize(end);
451 }
452 });
453
454 let missing_range =
455 loaded_rows.with_untracked(|loaded_rows| loaded_rows.missing_range(range.clone()));
456
457 if let Some(missing_range) = missing_range {
458 let missing_start = missing_range.start.min(missing_range.end);
460 let missing_end = missing_range.end; let missing_range = missing_start..missing_end;
463
464 if missing_range.is_empty() {
465 return;
467 }
468
469 loaded_rows.write().write_loading(missing_range.clone());
470
471 let mut loading_ranges = vec![];
472 if let Some(chunk_size) = DataP::CHUNK_SIZE {
473 let start = missing_range.start / chunk_size * chunk_size;
474 let mut current_range = start..start + chunk_size;
475 while current_range.end <= missing_range.end {
476 loading_ranges.push(current_range.clone());
477 current_range = current_range.end..current_range.end + chunk_size;
478 }
479 if current_range.end > missing_range.end && current_range.start < missing_range.end
481 {
482 loading_ranges.push(current_range);
483 }
484 } else {
485 loading_ranges.push(missing_range);
486 }
487
488 for missing_range in loading_ranges {
490 let compute_average_row_height = compute_average_row_height.clone();
491 spawn_local({
492 let rows = Rc::clone(&rows);
493 let set_known_row_count = set_known_row_count.clone();
494
495 async move {
496 let latest_reload_count = reload_count.get_untracked();
497
498 let result = rows
500 .borrow()
501 .get_rows(missing_range.clone())
502 .await
503 .map_err(|err| format!("{err:?}"));
504
505 if let Some(reload_count) = reload_count.try_get_untracked() {
506 if reload_count != latest_reload_count {
508 return;
509 }
510
511 if let Ok((_, loaded_range)) = &result {
512 if loaded_range.end < missing_range.end {
513 match row_count_opt {
514 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 }
526 loaded_rows.write().write_loaded(result, missing_range);
527 compute_average_row_height();
528 }
529 }
530 });
531 }
532 }
533 });
534
535 let thead_content = Row::render_head_row(sorting.into(), on_head_click).into_any();
536
537 let tbody_content = {
538 let row_renderer = row_renderer.clone();
539 let loading_row_renderer = loading_row_renderer.clone();
540 let error_row_renderer = error_row_renderer.clone();
541 let on_selection_change = on_selection_change.clone();
542
543 view! {
544 {row_placeholder_renderer.run(placeholder_height_before)}
545
546 <For
547 each=move || {
548 let loaded_rows = loaded_rows.read();
549 let display_range = display_range.read();
550
551 let iter = loaded_rows[display_range.clone()]
552 .iter()
553 .cloned()
554 .enumerate()
555 .map(|(i, row)| (i + display_range.start, row));
556
557 if let Some(loading_row_display_limit) = loading_row_display_limit {
558 let mut loading_row_count = 0;
559 iter.filter(|(_, row)| {
560 if matches!(row, RowState::Loading | RowState::Placeholder) {
561 loading_row_count += 1;
562 loading_row_count <= loading_row_display_limit
563 } else {
564 true
565 }
566 })
567 .collect::<Vec<_>>()
568 } else {
569 iter.collect::<Vec<_>>()
570 }
571 }
572
573 key=|(idx, row)| {
574 match row {
575 RowState::Loaded(_) => idx.to_string(),
576 RowState::Error(_) => format!("error-{idx}"),
577 RowState::Loading | RowState::Placeholder => format!("loading-{idx}"),
578 }
579 }
580
581 children={
582 let row_renderer = row_renderer.clone();
583 let loading_row_renderer = loading_row_renderer.clone();
584 let error_row_renderer = error_row_renderer.clone();
585 let on_selection_change = on_selection_change.clone();
586 move |(i, row)| {
587 match row {
588 RowState::Loaded(row) => {
589 let selected_signal = Signal::derive(move || {
590 selected_indices.read().contains(&i)
591 });
592
593 let class_signal = Signal::derive(move || {
594 class_provider
595 .row(i, selected_signal.get(), row_class.read().as_str())
596 });
597
598 let on_select = {
599 let on_selection_change = on_selection_change.clone();
600
601 move |evt: web_sys::MouseEvent| {
602 update_selection(evt, selection, first_selected_index, i);
603
604 let selection_change_event = SelectionChangeEvent {
605 row: row.into(),
606 row_index: i,
607 selected: selected_signal.get_untracked(),
608 };
609
610 on_selection_change.run(selection_change_event);
611 }
612 };
613
614 Effect::watch(
615 move || { row.track() },
616 move |_, _, _| {
617 let on_change = on_change.get_value();
618
619 on_change
620 .run(ChangeEvent {
621 row_index: i,
622 changed_row: row.into(),
623 });
624 },
625 false,
626 );
627 row_renderer
628 .run(class_signal, row, i, selected_signal, on_select.into())
629 }
630 RowState::Error(err) => {
631 error_row_renderer.run(err, i, Row::COLUMN_COUNT)
632 }
633 RowState::Loading | RowState::Placeholder => {
634 loading_row_renderer
635 .run(
636 Signal::derive(move || {
637 class_provider.row(i, false, row_class.read().as_str())
638 }),
639 Callback::new(move |(col_index,): (usize,)| {
640 class_provider
641 .loading_cell(
642 i,
643 col_index,
644 loading_cell_class.read().as_str(),
645 )
646 }),
647 Callback::new(move |(col_index,): (usize,)| {
648 class_provider
649 .loading_cell_inner(
650 i,
651 col_index,
652 loading_cell_inner_class.read().as_str(),
653 )
654 }),
655 i,
656 Row::COLUMN_COUNT,
657 )
658 }
659 }
660 }
661 }
662 />
663
664 {row_placeholder_renderer.run(placeholder_height_after)}
665 }
666 .into_any()
667 };
668
669 let tbody_directive = Arc::new(move |el: web_sys::Element, _: ()| {
670 tbody_el.set(Some(el));
671 });
672
673 let tbody = tbody_renderer.run(tbody_content, tbody_class, tbody_directive);
674
675 view! {
676 {thead_renderer.run(thead_row_renderer.run(thead_content, thead_row_class), thead_class)}
677
678 {tbody}
679 }
680}
681
682fn compute_average_row_height_from_loaded<Row, ClsP>(
683 tbody_ref: RwSignal<Option<web_sys::Element>, LocalStorage>,
684 display_range: ReadSignal<Range<usize>>,
685 y: Signal<f64>,
686 set_y: &impl Fn(f64),
687 set_average_row_height: WriteSignal<f64>,
688 placeholder_height_before: Signal<f64>,
689 loaded_rows: RwSignal<LoadedRows<Row>>,
690) where
691 Row: TableRow<ClassesProvider = ClsP> + Send + Sync + Clone + 'static,
692{
693 if let Some(el) = tbody_ref.get_untracked() {
694 let el: &web_sys::Element = ⪙
695 let display_range = display_range.get_untracked();
696 if display_range.end > 0 {
697 let avg_row_height = loaded_rows.with_untracked(|loaded_rows| {
698 let mut loading_row_start_index = None;
699 let mut loading_row_end_index = None;
700
701 for i in display_range.clone() {
702 if matches!(loaded_rows[i], RowState::Loaded(_) | RowState::Loading) {
703 if loading_row_start_index.is_none() {
704 loading_row_start_index = Some(i);
705 }
706 loading_row_end_index = Some(i);
707 } else if loading_row_end_index.is_some() {
708 break;
709 }
710 }
711
712 if let (Some(loading_row_start_index), Some(loading_row_end_index)) =
713 (loading_row_start_index, loading_row_end_index)
714 {
715 if loading_row_end_index == loading_row_start_index {
716 return None;
717 }
718
719 let children = el.children();
720
721 let first_loading_row = children
723 .get_with_index((loading_row_start_index + 1 - display_range.start) as u32);
724 let last_loading_row = children
725 .get_with_index((loading_row_end_index + 1 - display_range.start) as u32);
726
727 if let (Some(first_loading_row), Some(last_loaded_row)) =
728 (first_loading_row, last_loading_row)
729 {
730 return Some(
731 (last_loaded_row.get_bounding_client_rect().top()
732 - first_loading_row.get_bounding_client_rect().top())
733 / (loading_row_end_index - loading_row_start_index) as f64,
734 );
735 }
736 }
737
738 None
739 });
740
741 if let Some(avg_row_height) = avg_row_height {
742 let prev_placeholder_height_before = placeholder_height_before.get_untracked();
743
744 set_average_row_height.set(avg_row_height);
745
746 let new_placeholder_height_before = placeholder_height_before.get_untracked();
747 set_y(
748 y.get_untracked() - prev_placeholder_height_before
749 + new_placeholder_height_before,
750 );
751 }
752 }
753 }
754}
755
756fn get_keyboard_modifiers(evt: &web_sys::MouseEvent) -> (bool, bool) {
757 let meta_pressed = evt.meta_key() || evt.ctrl_key();
758 let shift_pressed = evt.shift_key();
759 (meta_pressed, shift_pressed)
760}
761
762fn update_selection(
763 evt: web_sys::MouseEvent,
764 selection: Selection,
765 first_selected_index: RwSignal<Option<usize>>,
766 i: usize,
767) {
768 match selection {
769 Selection::None => {}
770 Selection::Single(selected_index) => {
771 if selected_index.get_untracked() == Some(i) {
772 selected_index.set(None);
773 } else {
774 selected_index.set(Some(i));
775 }
776 }
777 Selection::Multiple(selected_indices) => {
778 let mut indices = selected_indices.write();
779 let (meta_pressed, shift_pressed) = get_keyboard_modifiers(&evt);
780
781 if meta_pressed {
782 if indices.contains(&i) {
783 indices.remove(&i);
784 } else {
785 indices.insert(i);
786 }
787 match indices.len() {
788 0 => first_selected_index.set(None),
789 1 => {
790 first_selected_index.set(Some(i));
791 }
792 _ => {
793 }
795 }
796 } else if shift_pressed {
797 if let Some(first_selected_index) = first_selected_index.get() {
798 let min = first_selected_index.min(i);
799 let max = first_selected_index.max(i);
800 for i in min..=max {
801 indices.insert(i);
802 }
803 } else {
804 indices.insert(i);
805 first_selected_index.set(Some(i));
806 }
807 } else {
808 HashSet::clear(&mut *indices);
809 indices.insert(i);
810 first_selected_index.set(Some(i));
811 }
812 }
813 }
814}