mirror of
https://github.com/sendevia/website.git
synced 2026-03-05 23:32:45 +08:00
feat: add scroll-to-top component with improved scroll detection
This commit is contained in:
122
.vitepress/theme/components/ScrollToTop.vue
Normal file
122
.vitepress/theme/components/ScrollToTop.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
const visible = ref(false);
|
||||
let container: HTMLElement | Window = window;
|
||||
|
||||
function isScrollable(el: HTMLElement) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const overflowY = style.overflowY;
|
||||
return overflowY === "auto" || overflowY === "scroll" || el.scrollHeight > el.clientHeight;
|
||||
}
|
||||
|
||||
function detectContainer() {
|
||||
const el = document.getElementById("layout-content-flow");
|
||||
if (el && isScrollable(el)) return el;
|
||||
return window;
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
try {
|
||||
const y = container === window ? window.scrollY || window.pageYOffset : (container as HTMLElement).scrollTop;
|
||||
visible.value = y > 240;
|
||||
} catch (e) {
|
||||
visible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
if (container === window) {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else {
|
||||
(container as HTMLElement).scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
container = detectContainer();
|
||||
const target: any = container;
|
||||
target.addEventListener("scroll", onScroll, { passive: true });
|
||||
if (container !== window) window.addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const target: any = container;
|
||||
target.removeEventListener("scroll", onScroll);
|
||||
if (container !== window) window.removeEventListener("scroll", onScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="layout-scrolltop" :class="{ visible: visible }" @click="scrollToTop">
|
||||
<span id="scrolltop-button" role="button" aria-label="Scroll to top"> arrow_upward </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../styles/mixin";
|
||||
|
||||
#layout-scrolltop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 11/13;
|
||||
justify-content: center;
|
||||
|
||||
position: sticky;
|
||||
bottom: 72px;
|
||||
right: 0px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
-moz-user-select: none;
|
||||
opacity: 0;
|
||||
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
|
||||
user-select: none;
|
||||
visibility: hidden;
|
||||
z-index: 21;
|
||||
|
||||
#scrolltop-button {
|
||||
@include mixin.material-symbols($size: 24, $line-height: 84);
|
||||
|
||||
position: relative;
|
||||
|
||||
height: 84px;
|
||||
width: 84px;
|
||||
|
||||
color: var(--md-sys-color-outline);
|
||||
text-align: center;
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: var(--md-sys-motion-spring-fast-effect-duration) var(--md-sys-motion-spring-fast-effect);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import NotFoundLayout from "./NotFound.vue";
|
||||
import SearchPostsLayout from "./SearchPosts.vue";
|
||||
import Footer from "../components/Footer.vue";
|
||||
import Sidebar from "../components/Sidebar.vue";
|
||||
import ScrollToTop from "../components/ScrollToTop.vue";
|
||||
import { argbFromHex } from "@material/material-color-utilities";
|
||||
import { generateColorPalette } from "../utils/colorPalette";
|
||||
import { onMounted, nextTick, computed } from "vue";
|
||||
@@ -107,6 +108,7 @@ function onAfterEnter() {
|
||||
</div>
|
||||
</div>
|
||||
<component v-else :is="currentLayout" />
|
||||
<ScrollToTop />
|
||||
<Footer />
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -193,45 +195,6 @@ function onAfterEnter() {
|
||||
mask: var(--via-svg-mask) no-repeat 0 / 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#layout-scrolltop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-column: 11/13;
|
||||
|
||||
position: sticky;
|
||||
bottom: 72px;
|
||||
right: 0px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
opacity: 0;
|
||||
transition: var(--md-sys-motion-duration-medium4) var(--md-sys-motion-easing-standard);
|
||||
visibility: hidden;
|
||||
z-index: 21;
|
||||
|
||||
#layout-scrolltop-desktop {
|
||||
@include mixin.material-symbols($size: 24);
|
||||
|
||||
position: relative;
|
||||
|
||||
height: 84px;
|
||||
min-width: 84px;
|
||||
width: 84px;
|
||||
|
||||
color: var(--md-sys-color-outline);
|
||||
|
||||
border-radius: var(--md-sys-shape-corner-full);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
@@ -246,10 +209,6 @@ function onAfterEnter() {
|
||||
grid-template-columns: minmax(50vw, 70%) minmax(300px, 30%);
|
||||
}
|
||||
|
||||
#layout-scrolltop {
|
||||
grid-column: $columns;
|
||||
}
|
||||
|
||||
hr {
|
||||
grid-column: span $columns;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user