UNPKG

@opuu/inview-vue

Version:

Vue.js directives for viewport detection - v-inview and v-outview directives powered by @opuu/inview. Perfect for Vue 3 lazy loading, scroll animations, infinite scroll, analytics tracking with TypeScript support.

218 lines (200 loc) 7.36 kB
import type { DirectiveBinding, ObjectDirective } from "vue"; import InView, { type InViewConfig, type InViewEvent } from "@opuu/inview"; /** * Extended HTMLElement interface to attach InView instance properties. */ interface HTMLElementWithInView extends HTMLElement { __inviewInstance?: InView; __inviewUniqueClass?: string; __inviewListenerCount?: number; } /** * Check if we're running in a browser environment (not SSR) */ function isBrowser(): boolean { return typeof window !== "undefined" && typeof document !== "undefined"; } /** * Ensure that an InView instance exists on the element. * * @param {HTMLElementWithInView} el - The HTML element to observe. * @param {Partial<InViewConfig>} config - Global configuration for InView. * @returns {InView | null} The InView instance associated with the element, or null if not in browser environment. * * Algorithm: Underlying Intersection Observer API; time complexity is O(n) per observed element. */ function ensureInViewInstance(el: HTMLElementWithInView, config: Partial<InViewConfig>): InView | null { // Skip during SSR if (!isBrowser()) { return null; } if (!el.__inviewInstance) { // Generate a unique class for the element if not already present. if (!el.__inviewUniqueClass) { el.__inviewUniqueClass = "inview-" + Math.random().toString(36).substr(2, 9); el.classList.add(el.__inviewUniqueClass); } // Build the instance configuration. const instanceConfig: InViewConfig = { selector: `.${el.__inviewUniqueClass}`, delay: config.delay ?? 0, precision: config.precision ?? "medium", single: config.single ?? true, }; el.__inviewInstance = new InView(instanceConfig); el.__inviewListenerCount = 0; } return el.__inviewInstance; } /** * Factory function to create the v-inview directive. * This directive triggers the provided callback when the element enters the viewport. * * @param {Partial<InViewConfig>} globalConfig - Global configuration for InView. * @returns {ObjectDirective} A Vue directive definition for handling "enter" events. * * @example * // In main.ts * import { createInViewDirective } from './inviewDirectives'; * app.directive('inview', createInViewDirective({ delay: 100, precision: 'high' })); * * @example * // In component template: * <div v-inview="handleEnter">...</div> */ export function createInViewDirective(globalConfig: Partial<InViewConfig> = {}): ObjectDirective { return { /** * Called when the directive is mounted on the element. * * @param {HTMLElement} el - The element the directive is bound to. * @param {DirectiveBinding} binding - The binding object (expects a callback function as value). */ mounted(el: HTMLElement, binding: DirectiveBinding) { const callback = binding.value; if (typeof callback !== "function") { console.warn("[InView]: v-inview expects a function as its value."); return; } // Skip during SSR if (!isBrowser()) { return; } // Get or create the InView instance. const instance = ensureInViewInstance(el as HTMLElementWithInView, globalConfig); if (!instance) { return; } instance.on("enter", (event: InViewEvent) => { callback(event); }); (el as HTMLElementWithInView).__inviewListenerCount = ((el as HTMLElementWithInView).__inviewListenerCount || 0) + 1; }, /** * Called when the directive is unmounted from the element. * * @param {HTMLElement} el - The element the directive was bound to. */ unmounted(el: HTMLElement) { const element = el as HTMLElementWithInView; if (element.__inviewInstance) { element.__inviewListenerCount = (element.__inviewListenerCount || 1) - 1; if (element.__inviewListenerCount <= 0) { element.__inviewInstance.destroy(); delete element.__inviewInstance; delete element.__inviewListenerCount; } } }, /** * SSR-specific hook that returns props to be rendered during server-side rendering. * This prevents the "getSSRProps" error in Nuxt and other SSR frameworks. * * @param {DirectiveBinding} _binding - The binding object (unused). * @returns {Record<string, any>} Empty object since we don't need to add any attributes during SSR. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getSSRProps(_binding: DirectiveBinding) { // Return empty object since InView directives don't need to add any attributes during SSR // The directive will initialize properly on the client-side during hydration return {}; }, }; } /** * Factory function to create the v-outview directive. * This directive triggers the provided callback when the element exits the viewport. * * @param {Partial<InViewConfig>} globalConfig - Global configuration for InView. * @returns {ObjectDirective} A Vue directive definition for handling "exit" events. * * @example * // In main.ts * import { createOutViewDirective } from './inviewDirectives'; * app.directive('outview', createOutViewDirective({ delay: 100, precision: 'high' })); * * @example * // In component template: * // <div v-outview="handleExit">...</div> */ export function createOutViewDirective(globalConfig: Partial<InViewConfig> = {}): ObjectDirective { return { /** * Called when the directive is mounted on the element. * * @param {HTMLElement} el - The element the directive is bound to. * @param {DirectiveBinding} binding - The binding object (expects a callback function as value). */ mounted(el: HTMLElement, binding: DirectiveBinding) { const callback = binding.value; if (typeof callback !== "function") { console.warn("[InView]: v-outview expects a function as its value."); return; } // Skip during SSR if (!isBrowser()) { return; } const instance = ensureInViewInstance(el as HTMLElementWithInView, globalConfig); if (!instance) { return; } instance.on("exit", (event: InViewEvent) => { callback(event); }); (el as HTMLElementWithInView).__inviewListenerCount = ((el as HTMLElementWithInView).__inviewListenerCount || 0) + 1; }, /** * Called when the directive is unmounted from the element. * * @param {HTMLElement} el - The element the directive was bound to. */ unmounted(el: HTMLElement) { const element = el as HTMLElementWithInView; if (element.__inviewInstance) { element.__inviewListenerCount = (element.__inviewListenerCount || 1) - 1; if (element.__inviewListenerCount <= 0) { element.__inviewInstance.destroy(); delete element.__inviewInstance; delete element.__inviewListenerCount; } } }, /** * SSR-specific hook that returns props to be rendered during server-side rendering. * This prevents the "getSSRProps" error in Nuxt and other SSR frameworks. * * @param {DirectiveBinding} _binding - The binding object (unused). * @returns {Record<string, any>} Empty object since we don't need to add any attributes during SSR. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getSSRProps(_binding: DirectiveBinding) { // Return empty object since OutView directives don't need to add any attributes during SSR // The directive will initialize properly on the client-side during hydration return {}; }, }; } export type { InViewConfig, InViewEvent } from "@opuu/inview";