UNPKG

@asciisd/vue-progressive-iframe

Version:

A Vue 3 component for progressive iframe loading with advanced content detection

425 lines (363 loc) 14.3 kB
import { computed, onMounted, onUnmounted, ref, type Ref } from 'vue' import type { ProgressiveIframeEvents, ProgressiveIframeOptions } from '../types' import { detectIframeContent, isOriginAllowed } from '../utils/content-detection' import { createFriendlyIframe } from '../utils/friendly-iframe' import { createAfterOnloadIframe, createDynamicAsyncIframe, createSetTimeoutIframe } from '../utils/loading-strategies' export interface UseProgressiveIframeReturn { // Refs iframeRef: Ref<HTMLIFrameElement | undefined> isLoading: Ref<boolean> isContentLoading: Ref<boolean> hasError: Ref<boolean> errorMessage: Ref<string> contentCheckCount: Ref<number> loadStartTime: Ref<number> consecutiveErrorCount: Ref<number> // Computed loadTime: Ref<number> isTimeout: Ref<boolean> // Methods forceLoad: () => void refresh: () => void startContentDetection: () => void handleMessage: (_event: MessageEvent) => void createIframeWithStrategy: (_container: HTMLElement) => HTMLIFrameElement | null } type EmitFunction = <K extends keyof ProgressiveIframeEvents>( event: K, payload: ProgressiveIframeEvents[K] ) => void export function useProgressiveIframe( options: ProgressiveIframeOptions, emit?: EmitFunction ): UseProgressiveIframeReturn { // Reactive state const iframeRef = ref<HTMLIFrameElement>() const isLoading = ref(true) const isContentLoading = ref(true) const hasError = ref(false) const errorMessage = ref('') const contentCheckCount = ref(0) const loadStartTime = ref(Date.now()) const consecutiveErrorCount = ref(0) // Configuration with defaults const config = { maxContentChecks: options.maxContentChecks || 30, contentDetectionTimeout: options.contentDetectionTimeout || 30000, crossOriginWaitTime: options.crossOriginWaitTime || 10000, allowedOrigins: options.allowedOrigins || [], loadingStrategy: options.loadingStrategy || 'progressive', preventOnloadBlocking: options.preventOnloadBlocking ?? true, minimizeBusyIndicators: options.minimizeBusyIndicators ?? true, detectionInterval: 1000 // 1 second } // Internal state const contentDetectionInterval = ref<number>() // Computed properties const loadTime = computed(() => Date.now() - loadStartTime.value) const isTimeout = computed(() => loadTime.value > config.contentDetectionTimeout) /** * Handle messages from iframe (for postMessage-based detection) */ const handleMessage = (event: MessageEvent) => { // Skip same-origin messages (usually browser extensions) if (event.origin === window.location.origin) { return } // Check if origin is allowed if (!isOriginAllowed(event.origin, config.allowedOrigins)) { if (options.showDebugInfo) { console.warn('[ProgressiveIframe] Message from disallowed origin:', event.origin) } return } if (options.showDebugInfo) { console.log('[ProgressiveIframe] Message received:', event.data) } // Handle standard iframe ready messages if (event.data?.type === 'iframe_ready' || event.data?.type === 'content_loaded') { handleContentLoaded('postmessage') } else if (event.data?.type === 'iframe_error') { handleLoadError(new Error(event.data.message || 'Iframe reported an error')) } } /** * Start content detection polling */ const startContentDetection = () => { contentCheckCount.value = 0 if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) } contentDetectionInterval.value = window.setInterval(() => { contentCheckCount.value++ if (options.showDebugInfo) { console.log(`[ProgressiveIframe] Content check ${contentCheckCount.value}/${config.maxContentChecks}`) } // Emit progress event emit?.('detection-progress', { checkCount: contentCheckCount.value, maxChecks: config.maxContentChecks, loadTime: loadTime.value }) const detectionResult = detectIframeContent( iframeRef.value, contentCheckCount.value, config.crossOriginWaitTime ) if (detectionResult.hasContent) { if (options.showDebugInfo) { console.log('[ProgressiveIframe] Content detected!', detectionResult) } handleContentLoaded(detectionResult.method) return } // Stop checking after max attempts if (contentCheckCount.value >= config.maxContentChecks) { if (options.showDebugInfo) { console.warn(`[ProgressiveIframe] Content detection timeout after ${config.maxContentChecks} checks`) } handleDetectionTimeout() } }, config.detectionInterval) } /** * Handle successful content loading */ const handleContentLoaded = (method: string = 'unknown') => { // Clear detection interval if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) contentDetectionInterval.value = undefined } // Update state isLoading.value = false isContentLoading.value = false hasError.value = false consecutiveErrorCount.value = 0 const currentLoadTime = loadTime.value if (options.showDebugInfo) { console.log(`[ProgressiveIframe] Content loaded successfully in ${currentLoadTime}ms using ${method}`) } // Emit success event emit?.('content-loaded', { loadTime: currentLoadTime, checkCount: contentCheckCount.value, method }) } /** * Handle loading error */ const handleLoadError = (error: Error) => { isLoading.value = false isContentLoading.value = false hasError.value = true consecutiveErrorCount.value++ errorMessage.value = consecutiveErrorCount.value > 2 ? 'Service is experiencing issues. Please contact support if this continues.' : error.message || 'Failed to load iframe content. Please try refreshing.' if (options.showDebugInfo) { console.error('[ProgressiveIframe] Load error:', error) } // Emit error event emit?.('load-error', { error, errorCount: consecutiveErrorCount.value }) } /** * Handle detection timeout */ const handleDetectionTimeout = () => { if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) contentDetectionInterval.value = undefined } // Don't set as error immediately - content might still be loading // Just stop active detection and let user decide if (options.showDebugInfo) { console.log('[ProgressiveIframe] Detection timeout - content may still be loading') } } /** * Force load - manually clear loading states */ const forceLoad = () => { if (options.showDebugInfo) { console.log('[ProgressiveIframe] Force loading content...') } // Clear any running detection if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) contentDetectionInterval.value = undefined } // Reset states isLoading.value = false isContentLoading.value = false hasError.value = false const currentLoadTime = loadTime.value // Emit force load event emit?.('force-loaded', { loadTime: currentLoadTime }) if (options.showDebugInfo) { console.log(`[ProgressiveIframe] Force loaded after ${currentLoadTime}ms`) } } /** * Refresh iframe */ const refresh = () => { if (!iframeRef.value) return if (options.showDebugInfo) { console.log('[ProgressiveIframe] Refreshing iframe...') } // Reset state loadStartTime.value = Date.now() isLoading.value = false // Show iframe immediately isContentLoading.value = true // But content is loading hasError.value = false // Clear existing detection if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) contentDetectionInterval.value = undefined } // Force reload iframe const currentSrc = iframeRef.value.src iframeRef.value.src = '' setTimeout(() => { if (iframeRef.value) { iframeRef.value.src = currentSrc // Start detection after iframe reloads setTimeout(() => { startContentDetection() }, 2000) } }, 100) } /** * Create iframe using specified loading strategy */ const createIframeWithStrategy = (container: HTMLElement) => { const strategy = config.loadingStrategy if (options.showDebugInfo) { console.log(`[ProgressiveIframe] Using ${strategy} loading strategy`) } switch (strategy) { case 'dynamic-async': return createDynamicAsyncIframe(options.src, container, { width: '100%', height: '100%', onReady: () => handleContentLoaded('dynamic-async'), onError: (error) => handleLoadError(error) }) case 'after-onload': createAfterOnloadIframe(options.src, container, { width: '100%', height: '100%', onReady: () => handleContentLoaded('after-onload') }) return null // Iframe created asynchronously case 'settimeout': return createSetTimeoutIframe(options.src, container, { width: '100%', height: '100%', delay: 5, onReady: () => handleContentLoaded('settimeout') }) case 'friendly': // Friendly iframe for same-domain content if (options.htmlContent) { return createFriendlyIframe(options.htmlContent, container, { width: '100%', height: '100%', styles: options.styles, onReady: () => handleContentLoaded('friendly') }) } else { // Fallback to traditional for external URLs console.warn('[ProgressiveIframe] Friendly iframe requires htmlContent, falling back to traditional') const iframe = document.createElement('iframe') iframe.src = options.src iframe.style.cssText = 'width: 100%; height: 100%; border: none;' iframe.onload = () => handleContentLoaded('friendly-fallback') container.appendChild(iframe) return iframe } case 'traditional': // Standard iframe - will block onload const iframe = document.createElement('iframe') iframe.src = options.src iframe.style.cssText = 'width: 100%; height: 100%; border: none;' iframe.onload = () => handleContentLoaded('traditional') container.appendChild(iframe) return iframe case 'progressive': default: // Our enhanced progressive approach const progressiveIframe = document.createElement('iframe') progressiveIframe.src = options.src progressiveIframe.style.cssText = 'width: 100%; height: 100%; border: none;' container.appendChild(progressiveIframe) // Start progressive loading isLoading.value = false isContentLoading.value = true setTimeout(() => { startContentDetection() }, 2000) return progressiveIframe } } // Lifecycle onMounted(() => { // Add message listener window.addEventListener('message', handleMessage) if (options.showDebugInfo) { console.log('[ProgressiveIframe] Starting loading with strategy:', config.loadingStrategy) console.log('[ProgressiveIframe] Prevent onload blocking:', config.preventOnloadBlocking) console.log('[ProgressiveIframe] Minimize busy indicators:', config.minimizeBusyIndicators) } // Set fallback timeout const fallbackTimeout = setTimeout(() => { if (isContentLoading.value) { if (options.showDebugInfo) { console.warn('[ProgressiveIframe] Fallback timeout reached') } handleDetectionTimeout() } }, config.contentDetectionTimeout) // Cleanup on unmount onUnmounted(() => { clearTimeout(fallbackTimeout) if (contentDetectionInterval.value) { clearInterval(contentDetectionInterval.value) } window.removeEventListener('message', handleMessage) }) }) return { // Refs iframeRef, isLoading, isContentLoading, hasError, errorMessage, contentCheckCount, loadStartTime, consecutiveErrorCount, // Computed loadTime, isTimeout, // Methods forceLoad, refresh, startContentDetection, handleMessage, createIframeWithStrategy } }