b0nes
Version:
Zero-dependency component library and SSR/SSG framework
152 lines (142 loc) • 5.16 kB
JavaScript
import { normalizeClasses } from '../../utils/normalizeClasses.js';
import { validateProps, validatePropTypes, createComponentError } from '../../utils/componentError.js';
import { escapeAttr } from '../../utils/escapeAttr.js';
/**
* Source component - An HTML source element for media resources
*
* Renders an HTML source element used inside picture or video elements to specify
* multiple media resources. Allows browsers to choose the most appropriate source
* based on format support, screen size, or other factors. Essential for responsive
* images and cross-browser video compatibility.
*
* Two types:
* 1. Image sources (for picture element) - use 'src' attribute
* 2. Video sources (for video element) - use 'srcset' attribute
*
* @param {Object} props - Component properties
* @param {('image'|'video')} props.type - Source type: 'image' for picture, 'video' for video element
* @param {string} [props.src] - Image source URL (required for type='image')
* @param {string} [props.srcset] - Video source URL (required for type='video')
* @param {string} [props.attrs=''] - Additional HTML attributes (e.g., 'media="(min-width: 800px)" type="image/webp"')
* @param {string} [props.className=''] - Additional CSS classes to apply
*
* @returns {string} Rendered HTML source element (self-closing)
*
* @throws {ComponentError} If required props are missing or invalid
*
* @example
* // Image source for picture element (WebP format)
* source({
* type: 'image',
* src: '/images/photo.webp',
* attrs: 'type="image/webp"'
* })
* // Returns: '<source src="/images/photo.webp" class="img-src" type="image/webp" />'
*
* @example
* // Image source with media query (responsive)
* source({
* type: 'image',
* src: '/images/photo-large.jpg',
* attrs: 'media="(min-width: 1024px)" type="image/jpeg"'
* })
* // Returns: '<source src="/images/photo-large.jpg" class="img-src" media="(min-width: 1024px)" type="image/jpeg" />'
*
* @example
* // Image source with srcset and sizes (resolution switching)
* source({
* type: 'image',
* src: '/images/photo.jpg',
* attrs: 'srcset="/images/photo-400.jpg 400w, /images/photo-800.jpg 800w"\
* sizes="(max-width: 600px) 400px, 800px" type="image/jpeg"'
* })
*
* @example
* // Video source (MP4 format)
* source({
* type: 'video',
* srcset: '/videos/movie.mp4',
* attrs: 'type="video/mp4"'
* })
* // Returns: '<source src="/videos/movie.mp4" class="video-src" type="video/mp4" />'
*
* @example
* // Video source (WebM format)
* source({
* type: 'video',
* srcset: '/videos/movie.webm',
* attrs: 'type="video/webm"'
* })
* // Returns: '<source src="/videos/movie.webm" class="video-src" type="video/webm" />'
*
* @example
* // AVIF image source (modern format)
* source({
* type: 'image',
* src: '/images/hero.avif',
* attrs: 'type="image/avif"',
* className: 'modern-format'
* })
* // Returns: '<source src="/images/hero.avif" class="img-src modern-format" type="image/avif"/>'
*/
export const source = ({
type,
src,
srcset,
attrs = '',
className = ''
}) => {
// Validate required props
validateProps(
{ type },
['type'],
{ componentName: 'source', componentType: 'atom' }
);
// Validate prop types
validatePropTypes(
{ type, attrs, className },
{
type: 'string',
attrs: 'string',
className: 'string'
},
{ componentName: 'source', componentType: 'atom' }
);
// Validate type is either 'image' or 'video'
if (type !== 'image' && type !== 'video') {
throw createComponentError(
`Invalid type: "${type}". Must be either "image" or "video".`,
{ componentName: 'source', componentType: 'atom', props: { type } }
);
}
// Validate that appropriate source attribute is provided
if (type === 'image' && !src) {
throw createComponentError(
'Image sources require a "src" attribute.',
{ componentName: 'source', componentType: 'atom', props: { type, src } }
);
}
if (type === 'video' && !srcset) {
throw createComponentError(
'Video sources require a "srcset" attribute.',
{ componentName: 'source', componentType: 'atom', props: { type, srcset } }
);
}
// Process attributes
attrs = attrs ? ` ${attrs}` : '';
// Determine the source attribute based on type
// Note: HTML spec uses 'srcset' for picture sources, but for simplicity
// we use 'src' for images and 'srcset' for videos in our API
let sourceAttr = '';
if (type === 'video') {
sourceAttr = ` srcset="${srcset}"`;
} else {
// type === 'image'
sourceAttr = ` src="${src}"`;
}
// Determine base class based on type
const baseClass = type === 'video' ? 'video-src' : 'img-src';
// Normalize and escape classes
const classes = normalizeClasses([baseClass, className]);
return `<source${sourceAttr} class="${classes}"${attrs}/>`;
};