147 lines
4.2 KiB
JavaScript
Raw Normal View History

2025-12-04 20:29:35 +01:00
/**
* @fileoverview Image Preview Component for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Displays images with lightbox functionality
* @keywords image, preview, lightbox, gallery, media
*/
import { BaseComponent } from './base-component.js';
import { buildDevrantImageUrl, isGifUrl } from '../utils/url.js';
class ImagePreview extends BaseComponent {
static get observedAttributes() {
return ['src', 'width', 'height', 'alt'];
}
init() {
this.render();
this.bindEvents();
}
render() {
const src = this.getAttr('src');
const width = this.getAttr('width');
const height = this.getAttr('height');
const alt = this.getAttr('alt') || 'Image';
if (!src) {
this.setHtml('');
return;
}
const imageUrl = buildDevrantImageUrl(src);
const isGif = isGifUrl(imageUrl);
const aspectRatio = width && height ? width / height : null;
this.addClass('image-preview');
this.setHtml(`
<div class="image-container" ${aspectRatio ? `style="aspect-ratio: ${aspectRatio}"` : ''}>
<img class="preview-image"
src="${imageUrl}"
alt="${alt}"
loading="lazy"
${width ? `width="${width}"` : ''}
${height ? `height="${height}"` : ''}>
${isGif ? '<span class="gif-badge">GIF</span>' : ''}
<button class="expand-btn" aria-label="View full size">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M21 11V3h-8l3.29 3.29-10 10L3 13v8h8l-3.29-3.29 10-10z"/>
</svg>
</button>
</div>
`);
}
bindEvents() {
this.on(this, 'click', this.handleClick);
}
handleClick(e) {
const expandBtn = e.target.closest('.expand-btn');
const img = e.target.closest('.preview-image');
if (expandBtn || img) {
e.preventDefault();
this.openLightbox();
}
}
openLightbox() {
const src = this.getAttr('src');
if (!src) return;
const imageUrl = buildDevrantImageUrl(src);
const lightbox = document.createElement('image-lightbox');
lightbox.setAttribute('src', imageUrl);
document.body.appendChild(lightbox);
}
onAttributeChanged(name, oldValue, newValue) {
this.render();
}
}
customElements.define('image-preview', ImagePreview);
class ImageLightbox extends BaseComponent {
static get observedAttributes() {
return ['src'];
}
init() {
this.keydownHandler = (e) => this.handleKeydown(e);
this.render();
this.bindEvents();
}
render() {
const src = this.getAttr('src');
this.addClass('lightbox');
this.setHtml(`
<div class="lightbox-backdrop"></div>
<div class="lightbox-content">
<button class="lightbox-close" aria-label="Close">
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
<img class="lightbox-image" src="${src}" alt="Full size image">
</div>
`);
requestAnimationFrame(() => this.addClass('lightbox-visible'));
}
bindEvents() {
this.on(this, 'click', this.handleClick);
document.addEventListener('keydown', this.keydownHandler);
}
handleClick(e) {
if (e.target.classList.contains('lightbox-backdrop') ||
e.target.closest('.lightbox-close')) {
this.close();
}
}
handleKeydown(e) {
if (e.key === 'Escape') {
this.close();
}
}
close() {
document.removeEventListener('keydown', this.keydownHandler);
this.removeClass('lightbox-visible');
setTimeout(() => this.remove(), 300);
}
}
customElements.define('image-lightbox', ImageLightbox);
export { ImagePreview, ImageLightbox };