thaw/back_top/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use crate::{ConfigInjection, Icon};
use leptos::{either::Either, ev, html, prelude::*};
use thaw_components::{CSSTransition, Teleport};
use thaw_utils::{
    add_event_listener, class_list, get_scroll_parent_element, mount_style, BoxCallback,
    EventListenerHandle,
};

#[component]
pub fn BackTop(
    #[prop(optional, into)] class: MaybeProp<String>,
    /// The width of BackTop from the right side of the page.
    #[prop(default=40.into(), into)]
    right: Signal<i32>,
    /// The height of BackTop from the bottom of the page.
    #[prop(default=40.into(), into)]
    bottom: Signal<i32>,
    /// BackTop's trigger scroll top.
    #[prop(default=180.into(), into)]
    visibility_height: Signal<i32>,
    #[prop(optional)] children: Option<Children>,
) -> impl IntoView {
    mount_style("back-top", include_str!("./back-top.css"));
    let config_provider = ConfigInjection::expect_context();
    let placeholder_ref = NodeRef::<html::Div>::new();
    let is_show_back_top = RwSignal::new(false);
    let scroll_top = RwSignal::new(0);

    Effect::new(move |prev: Option<()>| {
        scroll_top.track();
        if prev.is_some() {
            is_show_back_top.set(scroll_top.get() > visibility_height.get_untracked());
        }
    });

    let scroll_to_top = StoredValue::new(None::<BoxCallback>);
    let scroll_handle = StoredValue::new(None::<EventListenerHandle>);

    Effect::new(move |_| {
        let Some(placeholder_el) = placeholder_ref.get() else {
            return;
        };

        request_animation_frame(move || {
            let scroll_el = get_scroll_parent_element(&placeholder_el)
                .unwrap_or_else(|| document().document_element().unwrap());

            {
                let scroll_el = send_wrapper::SendWrapper::new(scroll_el.clone());
                scroll_to_top.set_value(Some(BoxCallback::new(move || {
                    let options = web_sys::ScrollToOptions::new();
                    options.set_top(0.0);
                    options.set_behavior(web_sys::ScrollBehavior::Smooth);
                    scroll_el.scroll_to_with_scroll_to_options(&options);
                })));
            }

            let handle = add_event_listener(scroll_el.clone(), ev::scroll, move |_| {
                scroll_top.set(scroll_el.scroll_top());
            });
            scroll_handle.set_value(Some(handle));
        });
    });

    on_cleanup(move || {
        scroll_handle.update_value(|handle| {
            if let Some(handle) = handle.take() {
                handle.remove();
            }
        });
    });

    let on_click = move |_| {
        scroll_to_top.with_value(|scroll_to_top| {
            if let Some(scroll_to_top) = scroll_to_top {
                scroll_to_top();
            }
        });
    };

    view! {
        <div style="display: none" class="thaw-back-top-placeholder" node_ref=placeholder_ref>
            <Teleport immediate=is_show_back_top>
                <CSSTransition
                    name="fade-in-scale-up-transition"
                    appear=is_show_back_top.get_untracked()
                    show=is_show_back_top
                    let:display
                >
                    <div
                        class=class_list!["thaw-config-provider thaw-back-top", class]
                        data-thaw-id=config_provider.id()
                        style=move || {
                            display
                                .get()
                                .map_or_else(
                                    || {
                                        format!(
                                            "right: {}px; bottom: {}px",
                                            right.get(),
                                            bottom.get(),
                                        )
                                    },
                                    |d| d.to_string(),
                                )
                        }
                        on:click=on_click
                    >
                        {if let Some(children) = children {
                            Either::Left(children())
                        } else {
                            Either::Right(
                                view! { <Icon icon=icondata_ai::AiVerticalAlignTopOutlined /> },
                            )
                        }}
                    </div>
                </CSSTransition>
            </Teleport>
        </div>
    }
}