UNPKG

@fastkit/vue-scroller

Version:

A library for controlling the scrolling state of elements and components in Vue applications.

832 lines (687 loc) • 19.2 kB
# @fastkit/vue-scroller 🌐 English | [ę—„ęœ¬čŖž](https://github.com/dadajam4/fastkit/blob/main/packages/vue-scroller/README-ja.md) A comprehensive library for precisely controlling scroll functionality in Vue.js applications. Easily implement advanced scroll experiences including reactive scroll state monitoring, animated scrolling, guide display features, and element scrolling. ## Features - **Vue 3 Composition API**: Complete integration with Vue.js - **Reactive Scroll State**: Automatic updates for scrollTop, scrollLeft, scrollable, and more - **Animated Scrolling**: Smooth scroll animations - **Automatic Element Scrolling**: Precise scrolling to specified elements - **Scroll Guides**: Visual indicators for scrollable directions - **Scroll Stop Function**: Temporary scroll disabling - **Document Scroll Support**: Full-page scroll control - **Full TypeScript Support**: Type safety with strict type definitions - **SSR Compatible**: Safe operation in server-side rendering environments ## Installation ```bash npm install @fastkit/vue-scroller ``` ## Basic Usage ### Composition API ```vue <template> <div> <button @click="scrollToTop">To Top</button> <button @click="scrollToBottom">To Bottom</button> <div ref="scrollerRef" style="height: 300px; overflow: auto;" > <div style="height: 1000px; background: linear-gradient(to bottom, red, blue);"> Scroll Content </div> </div> <p>Scroll Position: {{ scroller.scrollTop }}px</p> <p>Scrollable: Y-axis {{ scroller.scrollableY ? 'Yes' : 'No' }}</p> </div> </template> <script setup lang="ts"> import { useScrollerControl } from '@fastkit/vue-scroller' const scroller = useScrollerControl({ el: 'self', duration: 300, // Animation duration easing: 'ease-out' }) const scrollerRef = scroller.elementRef const scrollToTop = () => { scroller.toTop({ duration: 500 }) } const scrollToBottom = () => { scroller.toBottom({ duration: 500 }) } </script> ``` ### VScroller Component ```vue <template> <VScroller ref="scrollerComponent" :guide="20" container-class="custom-container" :settings="{ duration: 400, easing: 'ease-in-out' }" style="height: 400px;" > <div class="content"> <h2>Section 1</h2> <p>Lorem ipsum dolor sit amet...</p> <h2 ref="section2">Section 2</h2> <p>Consectetur adipiscing elit...</p> <h2>Section 3</h2> <p>Sed do eiusmod tempor incididunt...</p> </div> </VScroller> <button @click="scrollToSection2">Go to Section 2</button> </template> <script setup lang="ts"> import { ref } from 'vue' import { VScroller } from '@fastkit/vue-scroller' const scrollerComponent = ref<InstanceType<typeof VScroller>>() const section2 = ref<HTMLElement>() const scrollToSection2 = () => { if (scrollerComponent.value && section2.value) { scrollerComponent.value.scroller.toElement(section2.value, { duration: 600, offsetTop: -20 // 20px margin at the top }) } } </script> <style> .content { padding: 20px; height: 1200px; /* Ensure scrollable height */ } .v-scroller { border: 1px solid #ddd; border-radius: 8px; } </style> ``` ## Advanced Usage Examples ### Document Scroll Control ```vue <template> <div> <nav class="floating-nav"> <button @click="scrollToSection('hero')">Hero</button> <button @click="scrollToSection('about')">About</button> <button @click="scrollToSection('contact')">Contact</button> </nav> <section id="hero" style="height: 100vh; background: #ff6b6b;"> <h1>Hero Section</h1> </section> <section id="about" style="height: 100vh; background: #4ecdc4;"> <h1>About Section</h1> </section> <section id="contact" style="height: 100vh; background: #45b7d1;"> <h1>Contact Section</h1> </section> <div class="scroll-info"> <p>Scroll Position: {{ documentScroller.scrollTop }}px</p> <p>Scrolling: {{ documentScroller.nowScrolling ? 'Yes' : 'No' }}</p> <p>Scroll Direction: {{ documentScroller.lastDirection }}</p> </div> </div> </template> <script setup lang="ts"> import { getDocumentScroller } from '@fastkit/vue-scroller' const documentScroller = getDocumentScroller() const scrollToSection = (sectionId: string) => { const element = document.getElementById(sectionId) if (element) { documentScroller.toElement(element, { duration: 800, easing: 'ease-in-out', offsetTop: -60 // Offset for navigation bar height }) } } </script> <style> .floating-nav { position: fixed; top: 20px; right: 20px; z-index: 1000; display: flex; gap: 10px; } .floating-nav button { padding: 8px 16px; background: white; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .scroll-info { position: fixed; bottom: 20px; left: 20px; background: rgba(0, 0, 0, 0.8); color: white; padding: 10px; border-radius: 8px; font-size: 12px; } </style> ``` ### Scroll Stopping and Filtering ```vue <template> <div> <div class="controls"> <button @click="toggleScrollLock"> Scroll {{ isScrollLocked ? 'Unlock' : 'Lock' }} </button> <button @click="addVerticalOnlyFilter">Vertical Only</button> <button @click="clearFilters">Clear Filters</button> </div> <VScroller ref="scroller" :guide="true" style="height: 300px; width: 500px; border: 1px solid #ccc;" > <div style="width: 800px; height: 600px; background: linear-gradient(45deg, red, blue);"> <p>Content scrollable in both directions</p> </div> </VScroller> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { VScroller, ScrollStopper } from '@fastkit/vue-scroller' const scroller = ref<InstanceType<typeof VScroller>>() const isScrollLocked = ref(false) const currentStoppers: ScrollStopper[] = [] const toggleScrollLock = () => { if (!scroller.value) return if (isScrollLocked.value) { // Unlock currentStoppers.forEach(stopper => { scroller.value!.scroller.removeScrollStopper(stopper) }) currentStoppers.length = 0 isScrollLocked.value = false } else { // Lock const stopper: ScrollStopper = () => true // Stop all scrolling scroller.value.scroller.pushScrollStopper(stopper) currentStoppers.push(stopper) isScrollLocked.value = true } } const addVerticalOnlyFilter = () => { if (!scroller.value) return const stopper: ScrollStopper = (axis) => { return axis === 'x' // Stop only X-axis scrolling (disable horizontal scroll) } scroller.value.scroller.pushScrollStopper(stopper) currentStoppers.push(stopper) } const clearFilters = () => { if (!scroller.value) return currentStoppers.forEach(stopper => { scroller.value!.scroller.removeScrollStopper(stopper) }) currentStoppers.length = 0 isScrollLocked.value = false } </script> <style> .controls { margin-bottom: 20px; display: flex; gap: 10px; } .controls button { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; background: white; } .controls button:hover { background: #f5f5f5; } </style> ``` ### Implementing Infinite Scroll ```vue <template> <VScroller ref="scrollerRef" style="height: 400px;" :settings="{ throttle: 16 }" > <div class="infinite-list"> <div v-for="item in items" :key="item.id" class="list-item" > {{ item.title }} </div> <div v-if="loading" class="loading"> Loading... </div> </div> </VScroller> </template> <script setup lang="ts"> import { ref, watch, onMounted } from 'vue' import { VScroller } from '@fastkit/vue-scroller' interface ListItem { id: number title: string } const scrollerRef = ref<InstanceType<typeof VScroller>>() const items = ref<ListItem[]>([]) const loading = ref(false) const currentPage = ref(1) // Load initial data onMounted(() => { loadMoreItems() }) // Monitor scroll position watch(() => scrollerRef.value?.scroller.scrollBottom, (scrollBottom) => { if (scrollBottom !== undefined && scrollBottom < 50 && !loading.value) { loadMoreItems() } }, { immediate: false }) const loadMoreItems = async () => { if (loading.value) return loading.value = true // Simulate API request await new Promise(resolve => setTimeout(resolve, 1000)) const newItems: ListItem[] = Array.from({ length: 20 }, (_, index) => ({ id: (currentPage.value - 1) * 20 + index + 1, title: `Item ${(currentPage.value - 1) * 20 + index + 1}` })) items.value.push(...newItems) currentPage.value++ loading.value = false } </script> <style> .infinite-list { padding: 20px; } .list-item { padding: 12px; border-bottom: 1px solid #eee; min-height: 50px; display: flex; align-items: center; } .list-item:hover { background: #f5f5f5; } .loading { padding: 20px; text-align: center; color: #666; } </style> ``` ### Saving and Restoring Scroll Position ```vue <template> <div> <div class="controls"> <button @click="savePosition">Save Position</button> <button @click="restorePosition">Restore Position</button> <button @click="clearSavedPosition">Clear Saved Data</button> </div> <VScroller ref="scrollerRef" style="height: 300px;" > <div class="content"> <div v-for="n in 100" :key="n" class="item"> Item {{ n }} </div> </div> </VScroller> <div v-if="savedPosition" class="saved-info"> Saved Position: Top={{ savedPosition.scrollTop }}px, Left={{ savedPosition.scrollLeft }}px </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { VScroller, ScrollPosition } from '@fastkit/vue-scroller' const scrollerRef = ref<InstanceType<typeof VScroller>>() const savedPosition = ref<ScrollPosition | null>(null) const savePosition = () => { if (!scrollerRef.value) return const scroller = scrollerRef.value.scroller savedPosition.value = { scrollTop: scroller.scrollTop, scrollLeft: scroller.scrollLeft } // Save to localStorage localStorage.setItem('scrollPosition', JSON.stringify(savedPosition.value)) } const restorePosition = () => { if (!scrollerRef.value || !savedPosition.value) return scrollerRef.value.scroller.to(savedPosition.value, { duration: 500, easing: 'ease-out' }) } const clearSavedPosition = () => { savedPosition.value = null localStorage.removeItem('scrollPosition') } // Restore saved position on page load onMounted(() => { const saved = localStorage.getItem('scrollPosition') if (saved) { try { savedPosition.value = JSON.parse(saved) } catch (e) { console.warn('Failed to parse saved scroll position') } } }) </script> <style> .controls { margin-bottom: 20px; display: flex; gap: 10px; } .content { padding: 20px; } .item { padding: 10px; border-bottom: 1px solid #eee; min-height: 40px; } .saved-info { margin-top: 10px; padding: 10px; background: #f0f0f0; border-radius: 4px; font-size: 14px; } </style> ``` ### Parallax Scroll Effects ```vue <template> <VScroller ref="scrollerRef" style="height: 100vh; position: relative; overflow: hidden;" > <div class="parallax-container"> <!-- Background layer (moves slowly) --> <div class="parallax-layer background" :style="{ transform: `translateY(${backgroundOffset}px)` }" > <div class="bg-pattern"></div> </div> <!-- Middle layer (medium speed) --> <div class="parallax-layer midground" :style="{ transform: `translateY(${midgroundOffset}px)` }" > <div class="floating-shapes"></div> </div> <!-- Foreground (normal speed) --> <div class="parallax-layer foreground"> <div class="content"> <h1>Parallax Effect</h1> <div v-for="n in 20" :key="n" class="content-block"> <h2>Section {{ n }}</h2> <p>When scrolling, the background and middle layers move at different speeds.</p> </div> </div> </div> </div> </VScroller> </template> <script setup lang="ts"> import { ref, computed } from 'vue' import { VScroller } from '@fastkit/vue-scroller' const scrollerRef = ref<InstanceType<typeof VScroller>>() // Calculate parallax effects const backgroundOffset = computed(() => { if (!scrollerRef.value) return 0 return scrollerRef.value.scroller.scrollTop * 0.5 // 50% speed }) const midgroundOffset = computed(() => { if (!scrollerRef.value) return 0 return scrollerRef.value.scroller.scrollTop * 0.7 // 70% speed }) </script> <style> .parallax-container { position: relative; height: 200vh; /* Scrollable height */ } .parallax-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .background { z-index: 1; } .midground { z-index: 2; } .foreground { z-index: 3; } .bg-pattern { width: 100%; height: 120%; background: linear-gradient(45deg, #ff6b6b, #4ecdc4); opacity: 0.3; } .floating-shapes { width: 100%; height: 120%; background-image: radial-gradient(circle, rgba(255,255,255,0.3) 20%, transparent 20%); background-size: 100px 100px; } .content { padding: 50px; background: rgba(255, 255, 255, 0.9); margin: 20px; border-radius: 10px; } .content-block { margin: 40px 0; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } </style> ``` ## API Reference ### useScrollerControl ```typescript function useScrollerControl(setting: UseScrollerSetting): UseScroller interface UseScrollerSetting extends Omit<Partial<ScrollerSetting>, 'el'> { el?: 'self' | 'body' duration?: number // Animation duration (milliseconds) easing?: string // Easing function throttle?: number // Scroll event throttle interval } interface UseScroller { // ćƒ—ćƒ­ćƒ‘ćƒ†ć‚£ readonly elementRef: Ref<HTMLElement | null> readonly scroller: ScrollerControl readonly containerHeight: number readonly containerWidth: number readonly isDestroyed: boolean readonly isPending: boolean readonly isReady: boolean readonly isRunning: boolean readonly lastAxis: ScrollAxis readonly lastDirection: ScrollDirection readonly lastXDirection: ScrollXDirection readonly lastYDirection: ScrollYDirection readonly nowScrolling: boolean readonly scrollEnabled: boolean readonly scrollHeight: number readonly scrollWidth: number readonly scrollableX: boolean readonly scrollableY: boolean readonly state: ScrollerState scrollBottom: number scrollLeft: number scrollRight: number scrollTop: number // ćƒ”ć‚½ćƒƒćƒ‰ ready(): Promise<void> element(): HTMLElement | null start(): void stop(): void destroy(): void update(): void cancel(): void // ć‚¹ć‚Æćƒ­ćƒ¼ćƒ«åˆ¶å¾” by(diffX: number, diffY: number, options?: ScrollerScrollOptions): Promise<void> to(scrollPosition: Partial<ScrollPosition>, options?: ScrollerScrollOptions): Promise<void> toElement(target: ScrollToElementTarget, options?: ScrollerScrollToElementOptions): Promise<void> toSide(targets: ScrollToSideTargets, options?: ScrollerScrollOptions): Promise<void> toTop(options?: ScrollerScrollOptions): Promise<void> toRight(options?: ScrollerScrollOptions): Promise<void> toBottom(options?: ScrollerScrollOptions): Promise<void> toLeft(options?: ScrollerScrollOptions): Promise<void> toLeftTop(options?: ScrollerScrollOptions): Promise<void> toLeftBottom(options?: ScrollerScrollOptions): Promise<void> toRightTop(options?: ScrollerScrollOptions): Promise<void> toRightBottom(options?: ScrollerScrollOptions): Promise<void> // ć‚¹ć‚Æćƒ­ćƒ¼ćƒ«åœę­¢ pushScrollStopper(stopper: ScrollStopper): void removeScrollStopper(stopper: ScrollStopper): void // ć‚Ŗćƒ•ć‚»ćƒƒćƒˆčØ­å®š setScrollToElementAdditionalOffset(offset: ScrollToElementAdditionalOffset): void deleteScrollToElementAdditionalOffset(): void } ``` ### VScroller Component ```typescript interface VScrollerProps { settings?: VScrollerSettings | null guide?: boolean | number // Display and offset for scroll guides containerClass?: string // Class name for container element } interface VScrollerSettings extends UseScrollerSetting {} interface ScrollerAPI { scroller: ScrollerControl scrollable: ScrollerCombinedScrollability } interface ScrollerCombinedScrollability { left: boolean // Scrollable to the left right: boolean // Scrollable to the right top: boolean // Scrollable upward bottom: boolean // Scrollable downward strict: ScrollerScrollability // Strict scrollability } ``` ### Scroll Options ```typescript interface ScrollerScrollOptions { duration?: number // Animation duration (milliseconds) easing?: string // Easing function cancelable?: boolean // Whether cancellable } interface ScrollerScrollToElementOptions extends ScrollerScrollOptions { offsetTop?: number // Top offset offsetLeft?: number // Left offset offsetRight?: number // Right offset offsetBottom?: number // Bottom offset } interface ScrollPosition { scrollTop?: number scrollLeft?: number } type ScrollStopper = (axis?: ScrollAxis, direction?: ScrollDirection) => boolean ``` ### getDocumentScroller ```typescript function getDocumentScroller(): UseScroller ``` Get a singleton instance for controlling page-wide scrolling. ## Style Customization ### CSS Classes ```css /* VScroller component styles */ .v-scroller { position: relative; overflow: hidden; } .v-scroller__container { height: 100%; overflow: auto; } .v-scroller__guide { position: absolute; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; } .v-scroller__guide--active { opacity: 1; } .v-scroller__guide--top { top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(to bottom, rgba(0,0,0,0.2), transparent); } .v-scroller__guide--bottom { bottom: 0; left: 0; right: 0; height: 4px; background: linear-gradient(to top, rgba(0,0,0,0.2), transparent); } .v-scroller__guide--left { top: 0; left: 0; bottom: 0; width: 4px; background: linear-gradient(to right, rgba(0,0,0,0.2), transparent); } .v-scroller__guide--right { top: 0; right: 0; bottom: 0; width: 4px; background: linear-gradient(to left, rgba(0,0,0,0.2), transparent); } ``` ### Custom Guide Styles ```css /* Custom guide style examples */ .v-scroller__guide--top { background: linear-gradient(to bottom, #4ECDC4, transparent); height: 6px; } .v-scroller__guide--bottom { background: linear-gradient(to top, #FF6B6B, transparent); height: 6px; } .v-scroller__guide--left, .v-scroller__guide--right { background: linear-gradient(to right, #FFD93D, transparent); width: 6px; } ``` ## Related Packages - `@fastkit/scroller` - Core scroll control library - `@fastkit/vue-utils` - Vue.js development utilities - `@fastkit/tiny-logger` - Logging system ## License MIT