1
0
Fork 0
This repository has been archived on 2025-05-26. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Checkpoint-Golang/develop/js/lv.js
2025-05-26 12:42:36 -05:00

949 lines
No EOL
28 KiB
JavaScript

// Shared resources
const THUMBNAIL_CACHE = new Map();
const THUMBNAIL_REGISTRY = new Map();
const VIDEO_SERVICES = new Map();
// Common constants
const DEFAULT_ALLOW = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen; web-share";
const DEFAULT_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-forms allow-presentation";
// Efficient image checking with modern fetch API
async function checkImage(url) {
if (THUMBNAIL_CACHE.has(url)) return THUMBNAIL_CACHE.get(url);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch(url, { method: 'HEAD', signal: controller.signal });
clearTimeout(timeoutId);
const valid = response.ok;
THUMBNAIL_CACHE.set(url, valid);
return valid;
} catch {
clearTimeout(timeoutId);
THUMBNAIL_CACHE.set(url, false);
return false;
}
}
// Helper for parsing URLs safely
function parseUrl(url) {
try { return new URL(url); } catch { return null; }
}
/**
* Service Provider base class - each video service extends this
*/
class VideoServiceProvider {
constructor() {
this.name = 'generic';
}
canHandle(url) { return false; }
getVideoId(url) { return null; }
getEmbedUrl(videoId, params, element) { return ''; }
getThumbnailUrls(videoId, quality, element) {
const customThumbnail = element.getAttribute("thumbnail");
return customThumbnail ? [customThumbnail] : [];
}
parseParams() { return {}; }
getIframeAttributes(element) {
return {
frameborder: element.getAttribute("frameborder") || "0",
allow: element.getAttribute("allow") || DEFAULT_ALLOW,
sandbox: element.getAttribute("sandbox") || DEFAULT_SANDBOX
};
}
getDefaults() { return { autoload: false }; }
}
/**
* YouTube service provider
*/
class YouTubeProvider extends VideoServiceProvider {
constructor() {
super();
this.name = 'youtube';
this.THUMBNAIL_QUALITIES = {
maxres: 'maxresdefault.jpg',
sd: 'sddefault.jpg',
hq: 'hqdefault.jpg',
mq: 'mqdefault.jpg',
default: 'default.jpg'
};
this.URL_PATTERNS = [
/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,
/youtu\.be\/([a-zA-Z0-9_-]{11})/
];
}
canHandle(url) {
return url && /youtube\.com|youtu\.be/.test(url);
}
getVideoId(url) {
if (!url) return null;
const parsedUrl = parseUrl(url);
if (parsedUrl) {
// Path-based ID extraction (/embed/ID or youtu.be/ID)
if (parsedUrl.pathname.startsWith("/embed/") || parsedUrl.hostname === "youtu.be") {
const parts = parsedUrl.pathname.split("/");
return parts[parts.length > 2 ? 2 : 1];
}
// Query-based ID extraction (?v=ID)
const videoId = parsedUrl.searchParams.get("v");
if (videoId) return videoId;
}
// Fallback to regex matching
for (const pattern of this.URL_PATTERNS) {
const match = url.match(pattern);
if (match?.[1]) return match[1];
}
return null;
}
getEmbedUrl(videoId, params = {}, element) {
// Determine domain based on cookie preference
const useNoCookie = element.getAttribute("no-cookie") !== "false";
const domain = useNoCookie ? "youtube-nocookie.com" : "youtube.com";
// Build URL with parameters
let url = `https://www.${domain}/embed/${videoId}?autoplay=1`;
// Add parameters
for (const [key, value] of Object.entries(params)) {
if (key !== 'autoplay' && key && value) {
url += `&${key}=${encodeURIComponent(value)}`;
}
}
return url;
}
getThumbnailUrls(videoId, quality, element) {
// Check for custom thumbnail first
const customThumbnail = element.getAttribute("thumbnail");
if (customThumbnail) return [customThumbnail];
const baseUrl = `https://img.youtube.com/vi/${videoId}`;
const urls = [];
// Choose quality based on device and user preference
if (quality && this.THUMBNAIL_QUALITIES[quality]) {
urls.push(`${baseUrl}/${this.THUMBNAIL_QUALITIES[quality]}`);
} else if (window.matchMedia("(max-width: 767px)").matches) {
urls.push(`${baseUrl}/${this.THUMBNAIL_QUALITIES.hq}`);
} else {
urls.push(`${baseUrl}/${this.THUMBNAIL_QUALITIES.maxres}`);
}
// Only add fallbacks if they're different from what we already have
if (!urls.includes(`${baseUrl}/${this.THUMBNAIL_QUALITIES.hq}`)) {
urls.push(`${baseUrl}/${this.THUMBNAIL_QUALITIES.hq}`);
}
if (!urls.includes(`${baseUrl}/${this.THUMBNAIL_QUALITIES.default}`)) {
urls.push(`${baseUrl}/${this.THUMBNAIL_QUALITIES.default}`);
}
return urls;
}
parseParams(url) {
const params = {};
const parsedUrl = parseUrl(url);
if (!parsedUrl) return params;
// Extract parameters from URL
for (const [key, value] of parsedUrl.searchParams.entries()) {
params[key] = value;
}
// Handle YouTube-specific parameters
if (params.t || params.start) params.start = params.t || params.start;
if (params.list) params.playlist = params.list;
return params;
}
}
/**
* Bitchute service provider
*/
class BitchuteProvider extends VideoServiceProvider {
constructor() {
super();
this.name = 'bitchute';
this.URL_PATTERNS = [
/bitchute\.com\/video\/([a-zA-Z0-9_-]+)/,
/bitchute\.com\/embed\/([a-zA-Z0-9_-]+)/
];
}
canHandle(url) {
return url && /bitchute\.com/.test(url);
}
getVideoId(url) {
if (!url) return null;
const parsedUrl = parseUrl(url);
if (parsedUrl) {
// Extract from path segments
const segments = parsedUrl.pathname.split('/').filter(Boolean);
for (let i = 0; i < segments.length - 1; i++) {
if ((segments[i] === "embed" || segments[i] === "video") && i + 1 < segments.length) {
return segments[i + 1];
}
}
}
// Fallback to regex matching
for (const pattern of this.URL_PATTERNS) {
const match = url.match(pattern);
if (match?.[1]) return match[1];
}
return null;
}
getEmbedUrl(videoId) {
return `https://www.bitchute.com/embed/${videoId}/`;
}
// Use parent class implementations for other methods
getDefaults() {
return { autoload: true };
}
}
// Register service providers
VIDEO_SERVICES.set('youtube', new YouTubeProvider());
VIDEO_SERVICES.set('bitchute', new BitchuteProvider());
class LazyVideo extends HTMLElement {
// Observable attributes
static get observedAttributes() {
return [
"src", "title", "width", "height", "thumbnail-quality",
"no-cookie", "autoload", "frameborder", "allow", "loading",
"hide-title", "thumbnail", "service", "align", "container-fit"
];
}
// CSS styles definition
static get styles() {
return `
:host {
--lv-aspect-ratio: 16 / 9;
display: var(--lv-display, block);
position: var(--lv-position, relative);
width: var(--lv-width, 100%);
max-width: var(--lv-max-width, 560px);
aspect-ratio: var(--lv-aspect-ratio);
background: var(--lv-background, #000);
overflow: var(--lv-overflow, hidden);
border-radius: var(--lv-border-radius, 0);
margin: var(--lv-margin, 0 auto);
}
:host([container-fit]) {
max-width: 100% !important;
max-height: auto !important;
width: 100%;
margin: 0;
}
/* Alignment control through attribute */
:host([align="left"]) { margin: var(--lv-margin-left, 0); }
:host([align="right"]) { margin: var(--lv-margin-right, 0 0 0 auto); }
:host([align="center"]) { margin: var(--lv-margin-center, 0 auto); }
/* Alignment classes for CSS variable-based alignment */
:host(.lv-align-left) { margin: var(--lv-margin-left, 0); }
:host(.lv-align-right) { margin: var(--lv-margin-right, 0 0 0 auto); }
:host(.lv-align-center) { margin: var(--lv-margin-center, 0 auto); }
:host([hide-title]), :host(:where(:not([hide-title]))) {
--lv-show-title: var(--lv-show-title, 1);
}
:host([hide-title]) [part="title-bar"] {
display: none;
}
:host([style*="height"]) { aspect-ratio: auto; }
[part="placeholder"], [part="iframe"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
[part="placeholder"] {
cursor: pointer;
background: var(--lv-placeholder-bg, #000);
}
[part="placeholder"]:focus {
outline: var(--lv-focus-outline, 2px solid #4285F4);
outline-offset: var(--lv-focus-outline-offset, 2px);
}
[part="thumbnail"] {
width: 100%;
height: 100%;
object-fit: var(--lv-thumbnail-object-fit, cover);
opacity: var(--lv-thumbnail-opacity, 0.85);
}
[part="placeholder"]:hover [part="thumbnail"],
[part="placeholder"]:focus [part="thumbnail"] {
opacity: var(--lv-thumbnail-hover-opacity, 1);
}
[part="title-bar"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: var(--lv-title-padding, 10px 12px);
background: var(--lv-title-bg, rgba(0, 0, 0, 0.75));
color: var(--lv-title-color, white);
font-family: var(--lv-title-font-family, Roboto, Arial, sans-serif);
font-size: var(--lv-title-font-size, 18px);
font-weight: var(--lv-title-font-weight, 500);
line-height: var(--lv-title-line-height, 1.2);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
z-index: 2;
box-sizing: border-box;
display: var(--lv-show-title, block);
}
[part="play-button"] {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: var(--lv-play-button-width, 68px);
height: var(--lv-play-button-height, 48px);
background: var(--lv-play-button-bg, rgba(33, 33, 33, 0.8));
border-radius: var(--lv-play-button-radius, 8px);
}
[part="play-button"]::after {
content: '';
position: absolute;
top: 50%;
left: 55%;
transform: translate(-50%, -50%);
border-style: solid;
border-width: var(--lv-play-button-arrow-size, 12px 0 12px 20px);
border-color: transparent transparent transparent var(--lv-play-button-color, rgba(255, 255, 255, 0.9));
}
[part="placeholder"]:hover [part="play-button"] {
background: var(--lv-play-button-bg-hover, rgba(230, 33, 23, 1));
}
[part="timestamp"] {
position: absolute;
right: var(--lv-timestamp-right, 10px);
bottom: var(--lv-timestamp-bottom, 10px);
background: var(--lv-timestamp-bg, rgba(0, 0, 0, 0.7));
color: var(--lv-timestamp-color, white);
padding: var(--lv-timestamp-padding, 2px 6px);
border-radius: var(--lv-timestamp-radius, 3px);
font-size: var(--lv-timestamp-font-size, 12px);
font-family: var(--lv-timestamp-font-family, system-ui, sans-serif);
}
[part="iframe"] {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn { to { opacity: 1; } }
[part="loading"], [part="fallback-thumbnail"] {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
}
[part="loading"] {
background: var(--lv-loading-bg, rgba(0,0,0,0.7));
color: var(--lv-loading-color, white);
font-family: var(--lv-loading-font-family, system-ui, sans-serif);
}
[part="fallback-thumbnail"] {
background: var(--lv-fallback-bg, #1a1a1a);
color: var(--lv-fallback-color, white);
font-family: var(--lv-fallback-font-family, system-ui, sans-serif);
font-size: var(--lv-fallback-font-size, 14px);
}
`;
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this._loaded = false;
this._placeholder = null;
this._observer = null;
this._handlers = new Map();
this._videoService = null;
}
connectedCallback() {
if (!this.isConnected) return;
if (!this._loaded && !this._placeholder) {
this._createPlaceholder();
}
// Setup autoloading if needed
if (this._getServiceOption('autoload')) {
this._setupObserver();
}
// Check for alignment from CSS variables
this._updateAlignmentFromCSS();
// Set up mutation observer for style changes
this._setupStyleObserver();
}
disconnectedCallback() {
this._cleanupObserver();
this._cleanupEventHandlers();
// Clean up style observer
if (this._styleObserver) {
this._styleObserver.disconnect();
this._styleObserver = null;
}
// Cancel any animation frames
if (this._styleFrameId) {
cancelAnimationFrame(this._styleFrameId);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (!this.isConnected) return;
switch (name) {
case "src":
if (oldValue !== newValue && newValue !== null) {
this._loaded = false;
this._createPlaceholder();
}
break;
case "width":
case "height":
this._updateStyles();
break;
case "autoload":
newValue === "true" || newValue === "" ? this._setupObserver() : this._cleanupObserver();
break;
case "thumbnail":
if (oldValue !== newValue) {
this._updateThumbnail();
}
break;
case "service":
if (oldValue !== newValue) {
this._loaded = false;
this._createPlaceholder();
}
break;
}
}
_getServiceProvider(url) {
// Check for explicit service attribute first
const serviceName = this.getAttribute("service");
if (serviceName && VIDEO_SERVICES.has(serviceName)) {
return VIDEO_SERVICES.get(serviceName);
}
// Auto-detect from URL
if (url) {
for (const provider of VIDEO_SERVICES.values()) {
if (provider.canHandle(url)) {
return provider;
}
}
}
// Default to YouTube if nothing else matches
return VIDEO_SERVICES.get('youtube');
}
_getServiceOption(option) {
// First check if attribute exists
if (this.hasAttribute(option)) {
const value = this.getAttribute(option);
// Handle boolean attributes
return value === "" || value === "true" || value !== "false";
}
// Then check service defaults
if (this._videoService?.getDefaults()[option] !== undefined) {
return this._videoService.getDefaults()[option];
}
return false;
}
_cleanupObserver() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
}
_cleanupEventHandlers() {
this._handlers.forEach((handler, key) => {
const [element, event] = key.split('|');
if (element && element.removeEventListener) {
element.removeEventListener(event, handler);
}
});
this._handlers.clear();
}
_setupObserver() {
if (!window.IntersectionObserver) return;
this._cleanupObserver();
this._observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && !this._loaded) {
this._loadVideo();
this._cleanupObserver();
}
}, {
rootMargin: "300px",
threshold: 0.1
});
this._observer.observe(this);
}
_updateThumbnail() {
const img = this._placeholder?.querySelector('[part="thumbnail"]');
if (!img) return;
const customThumbnail = this.getAttribute("thumbnail");
if (customThumbnail) {
img.src = customThumbnail;
// Remove any fallback thumbnail if present
const fallback = this._placeholder.querySelector('[part="fallback-thumbnail"]');
if (fallback) fallback.remove();
return;
}
// Get service thumbnails
const videoId = this._placeholder.dataset.videoId;
if (videoId && this._videoService) {
const thumbnailQuality = this.getAttribute("thumbnail-quality");
const thumbnailUrls = this._videoService.getThumbnailUrls(videoId, thumbnailQuality, this);
if (thumbnailUrls.length > 0) {
this._loadThumbnail(thumbnailUrls, img);
} else {
this._createFallbackThumbnail();
}
}
}
_createFallbackThumbnail() {
if (!this._placeholder || this._placeholder.querySelector('[part="fallback-thumbnail"]')) {
return; // Already exists or no placeholder
}
const fallback = document.createElement('div');
fallback.setAttribute('part', 'fallback-thumbnail');
// Service-specific branding
if (this._videoService) {
const serviceName = this._videoService.name;
fallback.innerHTML = `
<div style="text-align: center;">
<div style="font-size: 18px; margin-bottom: 8px;">${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}</div>
<div>Click to play video</div>
</div>
`;
} else {
fallback.textContent = 'No thumbnail available';
}
this._placeholder.appendChild(fallback);
}
async _createPlaceholder() {
const src = this.getAttribute("src");
// Determine service provider & video ID
this._videoService = this._getServiceProvider(src);
const videoId = this._videoService?.getVideoId(src);
if (!videoId) {
this.shadowRoot.innerHTML = `
<style>:host{display:block;padding:10px;color:red;background:#222;border-radius:var(--lv-border-radius,0)}</style>
<p>Error: Can't find video ID. Check the 'src' attribute.</p>
`;
return;
}
// Get parameters and create elements
this._videoParams = this._videoService.parseParams(src);
const title = this.getAttribute("title") || "Video";
// Build Shadow DOM
const style = document.createElement("style");
style.textContent = LazyVideo.styles;
const placeholder = this._buildPlaceholder(videoId, title);
this.shadowRoot.innerHTML = '';
this.shadowRoot.append(style, placeholder);
this._updateStyles();
}
_buildPlaceholder(videoId, title) {
// Create placeholder container
const placeholder = document.createElement("div");
placeholder.setAttribute("part", "placeholder");
placeholder.setAttribute("role", "button");
placeholder.setAttribute("aria-label", `Play: ${title}`);
placeholder.setAttribute("tabindex", "0");
placeholder.dataset.videoId = videoId;
placeholder.dataset.service = this._videoService.name;
this._placeholder = placeholder;
// Create thumbnail image
const thumbnailQuality = this.getAttribute("thumbnail-quality");
const thumbnailUrls = this._videoService.getThumbnailUrls(videoId, thumbnailQuality, this);
// Add thumbnail image
const img = document.createElement("img");
img.setAttribute("part", "thumbnail");
img.alt = `Thumbnail for ${title}`;
img.loading = "lazy";
img.decoding = "async";
img.fetchPriority = "low";
img.style.backgroundColor = "#111";
placeholder.appendChild(img);
// Start thumbnail loading process
if (thumbnailUrls.length > 0) {
this._setupThumbnailObserver(img, thumbnailUrls);
} else {
this._createFallbackThumbnail();
}
// Add title bar if not disabled
if (!this.hasAttribute("hide-title")) {
const titleBar = document.createElement("div");
titleBar.setAttribute("part", "title-bar");
titleBar.textContent = title;
placeholder.appendChild(titleBar);
}
// Add play button
const playButton = document.createElement("div");
playButton.setAttribute("part", "play-button");
placeholder.appendChild(playButton);
// Add timestamp if present in params
const startTime = parseInt(this._videoParams.start || this._videoParams.t, 10);
if (!isNaN(startTime) && startTime > 0) {
const timestamp = document.createElement("div");
timestamp.setAttribute("part", "timestamp");
timestamp.textContent = this._formatTime(startTime);
placeholder.appendChild(timestamp);
}
// Set up interaction handlers
const handleInteraction = (e) => {
if (e.type === "click" || e.key === "Enter" || e.key === " ") {
if (e.type !== "click") e.preventDefault();
this._loadVideo();
}
};
placeholder.addEventListener("click", handleInteraction);
placeholder.addEventListener("keydown", handleInteraction);
// Track handlers for cleanup
this._handlers.set(`${placeholder}|click`, handleInteraction);
this._handlers.set(`${placeholder}|keydown`, handleInteraction);
return placeholder;
}
_setupThumbnailObserver(imgElement, urls) {
if (!window.IntersectionObserver) {
this._loadThumbnail(urls, imgElement);
return;
}
this._thumbnailLoadAttempted = false;
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !this._thumbnailLoadAttempted) {
this._thumbnailLoadAttempted = true;
try {
await this._loadThumbnail(urls, imgElement);
} catch {
this._thumbnailLoadAttempted = false;
} finally {
observer.disconnect();
}
}
}, {
rootMargin: "300px",
threshold: 0.1
});
observer.observe(imgElement);
}
async _loadThumbnail(urls, imgElement) {
// Custom thumbnails bypass validation
if (urls.length === 1 && urls[0] === this.getAttribute("thumbnail")) {
imgElement.src = urls[0];
return true;
}
// Cache key for shared thumbnails
const videoId = this._placeholder?.dataset?.videoId;
const service = this._placeholder?.dataset?.service;
const cacheKey = videoId && service ? `${service}:${videoId}` : null;
// Try to use cached result
if (cacheKey && THUMBNAIL_REGISTRY.has(cacheKey)) {
try {
const bestUrl = await THUMBNAIL_REGISTRY.get(cacheKey);
if (bestUrl) {
imgElement.src = bestUrl;
return true;
}
} catch {
THUMBNAIL_REGISTRY.delete(cacheKey);
}
}
// Find best thumbnail
let bestUrl = null;
// Try parallel loading first
try {
const results = await Promise.all(
urls.map(url => checkImage(url)
.then(valid => ({ url, valid }))
.catch(() => ({ valid: false }))
)
);
const bestResult = results.find(result => result.valid);
if (bestResult) bestUrl = bestResult.url;
} catch {
// Try sequential loading if parallel fails
for (const url of urls) {
try {
if (await checkImage(url)) {
bestUrl = url;
break;
}
} catch {}
}
}
// Set the best URL or create fallback
if (bestUrl) {
imgElement.src = bestUrl;
if (cacheKey) THUMBNAIL_REGISTRY.set(cacheKey, Promise.resolve(bestUrl));
return true;
} else {
this._createFallbackThumbnail();
if (cacheKey) THUMBNAIL_REGISTRY.set(cacheKey, Promise.resolve(null));
return false;
}
}
_formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return hours > 0
? `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
: `${minutes}:${secs.toString().padStart(2, "0")}`;
}
_updateStyles() {
const width = this.getAttribute("width");
const height = this.getAttribute("height");
// Helper to check if a value already includes a CSS unit
const hasUnit = (value) => value && /[a-z%$]/.test(value);
if (width) {
this.style.setProperty("width", hasUnit(width) ? width : `${width}px`);
} else {
this.style.removeProperty("width");
}
if (height) {
this.style.setProperty("height", hasUnit(height) ? height : `${height}px`);
} else {
this.style.removeProperty("height");
}
// For aspect ratio, use numeric values from width/height if possible
if (width && height) {
const numericWidth = parseFloat(width);
const numericHeight = parseFloat(height);
if (!isNaN(numericWidth) && !isNaN(numericHeight)) {
this.style.setProperty("--lv-aspect-ratio", `${numericWidth} / ${numericHeight}`);
}
}
}
_loadVideo() {
if (this._loaded || !this._placeholder) return;
// Create loading indicator
const loading = document.createElement("div");
loading.setAttribute("part", "loading");
loading.textContent = "Loading...";
this.shadowRoot.appendChild(loading);
const videoId = this._placeholder.dataset.videoId;
const title = this.getAttribute("title") || "Video";
// Get the service if not already set
if (!this._videoService) {
const serviceName = this._placeholder.dataset.service;
this._videoService = VIDEO_SERVICES.get(serviceName) || VIDEO_SERVICES.get('youtube');
}
// Get embed URL and create iframe
const url = this._videoService.getEmbedUrl(videoId, this._videoParams, this);
// Create iframe
const iframe = document.createElement("iframe");
iframe.setAttribute("part", "iframe");
iframe.loading = "lazy";
iframe.src = url;
iframe.title = title;
// Add credentialless attribute for enhanced security
iframe.setAttribute("credentialless", "");
// Add service-specific attributes
const iframeAttrs = this._videoService.getIframeAttributes(this);
for (const [name, value] of Object.entries(iframeAttrs)) {
iframe.setAttribute(name, value);
}
// Handle loading indicator removal
const loadHandler = () => loading.parentNode?.removeChild(loading);
iframe.addEventListener("load", loadHandler, { once: true });
this._handlers.set(`${iframe}|load`, loadHandler);
// Replace placeholder with iframe
this._placeholder.replaceWith(iframe);
this._loaded = true;
this._placeholder = null;
// Notify that video is loaded
this.dispatchEvent(new CustomEvent("video-loaded", {
bubbles: true,
detail: { videoId, service: this._videoService.name }
}));
}
_setupStyleObserver() {
if (this._styleObserver) return;
// Create a MutationObserver to watch for style attribute changes
this._styleObserver = new MutationObserver(() => {
this._updateAlignmentFromCSS();
});
this._styleObserver.observe(this, {
attributes: true,
attributeFilter: ['style']
});
// Also observe document/body style changes that might affect CSS variables
if (window.getComputedStyle) {
// Use requestAnimationFrame to limit performance impact
let frameId;
const checkStyles = () => {
frameId = requestAnimationFrame(() => {
this._updateAlignmentFromCSS();
frameId = requestAnimationFrame(checkStyles);
});
};
checkStyles();
// Store the frame ID for cleanup
this._styleFrameId = frameId;
}
}
_updateAlignmentFromCSS() {
if (this.hasAttribute('container-fit')) return;
// Get computed style
const computedStyle = window.getComputedStyle(this);
const alignValue = computedStyle.getPropertyValue('--lv-align').trim();
// Remove existing alignment classes
this.classList.remove('lv-align-left', 'lv-align-right', 'lv-align-center');
// Add appropriate class based on the CSS variable
if (alignValue === 'left') {
this.classList.add('lv-align-left');
} else if (alignValue === 'right') {
this.classList.add('lv-align-right');
} else if (alignValue === 'center') {
this.classList.add('lv-align-center');
}
}
}
// Register the component
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => customElements.define("lazy-video", LazyVideo));
} else {
customElements.define("lazy-video", LazyVideo);
}