Skip to content

Content Extensions

Content extensions allow VistaView to display custom content types beyond images. They implement onInitializeImage to return custom VistaBox instances.

VistaBox is the base class that handles content display, sizing, and transitions. To support custom content:

  1. Extend VistaBox
  2. Set this.element to your custom DIV element
  3. Define dimensions (fullW, fullH, minW, maxW)
  4. Override methods as needed
import type { VistaExtension, VistaImageParams } from 'vistaview';
import { VistaBox } from 'vistaview';
class VistaCustomContent extends VistaBox {
element: HTMLDivElement;
constructor(par: VistaImageParams) {
super(par); // Always call super first
// Create your custom element
this.element = document.createElement('div');
this.element.classList.add('vvw-img-hi');
this.element.textContent = 'Custom Content';
// Set dimensions
const { width: fullWidth, height: fullHeight } = this.getFullSizeDim();
this.fullH = fullHeight;
this.fullW = fullWidth;
this.minW = this.fullW * 0.5; // Required: tells VistaView when to close (size threshold)
this.maxW = this.fullW;
this.element.style.width = `${fullWidth}px`;
this.element.style.height = `${fullHeight}px`;
// Initialize sizes
this.setSizes({ stableSize: false, initDimension: true });
// Mark as loaded
this.isLoadedResolved!(true);
}
protected getFullSizeDim(): { width: number; height: number } {
return { width: 800, height: 600 };
}
}
export function customContent(): VistaExtension {
return {
name: 'customContent',
onInitializeImage: (params: VistaImageParams) => {
// Check if this element should use custom content
if (params.elm.config.src.includes('custom')) {
return new VistaCustomContent(params);
}
},
};
}

Complete YouTube video extension:

import type { VistaData, VistaExtension, VistaImageParams } from 'vistaview';
import { VistaBox } from 'vistaview';
import type { VistaView } from 'vistaview';
export function parseYouTubeVideoId(url: string): string | null {
if (!url) return null;
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/live\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match?.[1]) return match[1];
}
return null;
}
export function getYouTubeThumbnail(videoUrl: string): string {
const videoId = parseYouTubeVideoId(videoUrl);
if (!videoId) throw new Error('Invalid YouTube video URL');
return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
}
export class VistaYoutubeVideo extends VistaBox {
element: HTMLDivElement;
url: string;
constructor(par: VistaImageParams) {
super(par);
const url = par.elm.config.src;
this.url = url;
// Create container with thumbnail
const div = document.createElement('div');
div.style.position = 'relative';
// youtube thumbnail view
// for initial iframe loading
const image = document.createElement('img');
div.appendChild(image);
// Get thumbnail source from:
// 1. Origin image (if lightbox opened from thumbnail click)
// 2. data-thumbnail attribute
// 3. Generated YouTube thumbnail URL
image.src = this.origin?.image.src || par.elm.elm.dataset.thumbnail || getYouTubeThumbnail(url);
image.style.width = '100%';
image.style.height = '100%';
image.style.objectFit = 'cover';
image.classList.add('vvw--pulsing');
// add vvw-img-hi clas to the div
// so the animation will run
this.element = div;
this.element.classList.add('vvw-img-hi');
// Set dimensions
const { width: fullWidth, height: fullHeight } = this.getFullSizeDim();
this.fullH = fullHeight;
this.fullW = fullWidth;
this.minW = this.fullW * 0.5; // Required: tells VistaView when to close (size threshold)
this.maxW = this.fullW;
this.element.style.width = `${fullWidth}px`;
this.element.style.height = `${fullHeight}px`;
// Initialize sizes
// always do this
this.setSizes({ stableSize: false, initDimension: true });
// Load iframe when in center position
if (this.pos === 0) {
const iframe = document.createElement('iframe');
iframe.frameBorder = '0';
iframe.allow =
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
iframe.allowFullscreen = true;
iframe.width = '100%';
iframe.height = '100%';
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
iframe.style.backgroundColor = 'transparent';
iframe.style.opacity = '0';
iframe.style.transition = 'opacity 1s ease';
iframe.src = `https://www.youtube.com/embed/${parseYouTubeVideoId(url)}?autoplay=1&rel=0`;
div.appendChild(iframe);
iframe.onload = () => {
iframe.style.opacity = '1';
image.classList.remove('vvw--pulsing');
};
}
// set as loaded
this.isLoadedResolved!(true);
}
// for videos, the full size have a max width
// so we extend this function
protected getFullSizeDim(): { width: number; height: number } {
const maxWidth = Math.min(window.innerWidth, 800);
return {
width: maxWidth,
height: (maxWidth * 9) / 16,
};
}
// final transform should not propagate events,
// since we don't need it for videos
setFinalTransform() {
return super.setFinalTransform({ propagateEvent: false });
}
}
export function youtubeVideo(): VistaExtension {
return {
name: 'ytVideo',
onInitializeImage: (params: VistaImageParams) => {
const url = params.elm.config.src;
const videoId = parseYouTubeVideoId(url);
if (!videoId) return;
return new VistaYoutubeVideo(params);
},
onImageView: async (data: VistaData, v: VistaView) => {
const mainData = data.images.to![Math.floor(data.images.to!.length / 2)];
if (mainData instanceof VistaYoutubeVideo) {
// deactivate these on display
v.deactivateUi(['download', 'zoomIn', 'zoomOut'], mainData);
}
},
};
}

Usage:

<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" data-thumbnail="thumb.jpg">
<img src="thumb.jpg" alt="Video" />
</a>
vistaView({
elements: '#gallery a',
extensions: [youtubeVideo()],
});
constructor(par: VistaImageParams) {
super(par); // ALWAYS call first
// Create your element
this.element = document.createElement('div');
this.element.classList.add('vvw-img-hi');
// Get dimensions
const { width: fullWidth, height: fullHeight } = this.getFullSizeDim();
// Set dimensions
this.fullW = fullWidth;
this.fullH = fullHeight;
this.minW = fullWidth * 0.5;
this.maxW = fullWidth;
// Set element size
this.element.style.width = `${fullWidth}px`;
this.element.style.height = `${fullHeight}px`;
// Initialize
this.setSizes({ stableSize: false, initDimension: true });
// Handle loading
this.isLoadedResolved!(true); // or wait for async load
}
// Required: Return full dimensions
protected getFullSizeDim(): { width: number; height: number } {
const maxWidth = Math.min(window.innerWidth, 800);
return {
width: maxWidth,
height: (maxWidth * 9) / 16,
};
}
// Optional: Custom transform behavior
setFinalTransform() {
return super.setFinalTransform({ propagateEvent: false });
}
constructor(par: VistaImageParams) {
super(par); // ALWAYS call first
// Create your element
this.element = document.createElement('div');
// Set dimensions
this.fullW = 800;
this.fullH = 600;
this.minW = 400;
this.maxW = 1200;
// Initialize
this.setSizes({ stableSize: false, initDimension: true });
// Handle loading
this.isLoadedResolved!(true); // or wait for async load
}

Key Points:

  • element must be set in constructor
  • Call getFullSizeDim() to get dimensions, then apply to element.style
  • Must call isLoadedResolved!(true) when ready
  • Can call isLoadedRejected!(error) on failure

Check attributes or URL patterns in onInitializeImage:

export function myPdfContent(): VistaExtension {
return {
name: 'myPdfContent',
onInitializeImage: (par: VistaImageParams) => {
const url = par.elm.config.src;
const type = par.elm.elm.getAttribute('data-type');
// Check by URL pattern (includes yourpdfsite.com and ends with .pdf)
if (/yourpdfsite\.com.*\.pdf$/i.test(url)) {
return new MyVistaPDF(par);
}
},
};
}
GitHubnpmllms.txtContext7

© 2026 • MIT License