255 lines
11 KiB
JavaScript
255 lines
11 KiB
JavaScript
/**
|
|
* Block Renderer - Project Friction
|
|
* Converts a block array into HTML strings.
|
|
* Used by both the main exhibit (app.js) and the builder preview.
|
|
*/
|
|
class BlockRenderer {
|
|
|
|
static galleryUid = 0;
|
|
|
|
static render(blocks) {
|
|
if (!Array.isArray(blocks)) return '';
|
|
return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n');
|
|
}
|
|
|
|
static renderBlock(block) {
|
|
if (!block || !block.type) return '';
|
|
switch (block.type) {
|
|
case 'heading': return BlockRenderer.renderHeading(block);
|
|
case 'paragraph': return BlockRenderer.renderParagraph(block);
|
|
case 'image': return BlockRenderer.renderImage(block);
|
|
case 'quote': return BlockRenderer.renderQuote(block);
|
|
case 'fact-panel': return BlockRenderer.renderFactPanel(block);
|
|
case 'timeline-card': return BlockRenderer.renderTimelineCard(block);
|
|
case 'gallery': return BlockRenderer.renderGallery(block);
|
|
case 'grid4': return BlockRenderer.renderGrid4(block);
|
|
case 'statistics': return BlockRenderer.renderStatistics(block);
|
|
case 'map-button': return BlockRenderer.renderMapButton(block);
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
static renderHeading(block) {
|
|
var level = Math.min(Math.max(parseInt(block.level) || 2, 1), 6);
|
|
return '<h' + level + ' class="block block-heading block-heading-' + level + '">' + (block.text || '') + '</h' + level + '>';
|
|
}
|
|
|
|
static renderParagraph(block) {
|
|
return '<p class="block block-paragraph">' + (block.text || '') + '</p>';
|
|
}
|
|
|
|
static renderImage(block) {
|
|
return '<figure class="block block-image">'
|
|
+ '<img src="' + (block.src || '') + '" alt="' + (block.alt || '') + '" loading="lazy">'
|
|
+ (block.caption ? '<figcaption>' + block.caption + '</figcaption>' : '')
|
|
+ '</figure>';
|
|
}
|
|
|
|
static renderQuote(block) {
|
|
return '<blockquote class="block block-quote quote-block">'
|
|
+ '<p class="quote-text">' + (block.text || '') + '</p>'
|
|
+ (block.author ? '<cite class="quote-attribution">— ' + block.author + '</cite>' : '')
|
|
+ '</blockquote>';
|
|
}
|
|
|
|
static renderFactPanel(block) {
|
|
var title = block.title || 'Fact Briefing';
|
|
var factItems = block.facts || block.items || [];
|
|
var facts = factItems.map(function(item) {
|
|
if (typeof item === 'string') return item;
|
|
return item && item.text ? item.text : '';
|
|
}).filter(function(text) {
|
|
return String(text).trim() !== '';
|
|
});
|
|
|
|
if (facts.length === 0) {
|
|
facts = ['Add fact entries in the builder to populate this panel.'];
|
|
}
|
|
|
|
var listHtml = facts.map(function(text) {
|
|
return '<li>' + text + '</li>';
|
|
}).join('');
|
|
|
|
return '<section class="block block-fact-panel fact-box">'
|
|
+ '<h3>' + title + '</h3>'
|
|
+ '<ul class="fact-panel-list">' + listHtml + '</ul>'
|
|
+ '</section>';
|
|
}
|
|
|
|
static renderTimelineCard(block) {
|
|
return '<article class="block block-timeline-card">'
|
|
+ '<header class="block-timeline-header">'
|
|
+ '<span class="block-timeline-date">' + (block.date || '') + '</span>'
|
|
+ '<h4 class="block-timeline-title">' + (block.title || '') + '</h4>'
|
|
+ '</header>'
|
|
+ '<p class="block-timeline-text">' + (block.text || '') + '</p>'
|
|
+ '</article>';
|
|
}
|
|
|
|
static renderGallery(block) {
|
|
var images = Array.isArray(block.images) ? block.images.filter(function(img) {
|
|
return img && img.src;
|
|
}) : [];
|
|
|
|
if (images.length === 0) {
|
|
return '<div class="block block-gallery"><div class="gallery-empty">No gallery images configured.</div></div>';
|
|
}
|
|
|
|
// Backward compatible: explicit strip mode keeps the original horizontal gallery.
|
|
var mode = (block.mode || 'carousel').toLowerCase();
|
|
if (mode === 'strip') {
|
|
var stripItems = images.map(function(img) {
|
|
return '<div class="gallery-item">'
|
|
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
|
|
+ (img.caption ? '<div class="gallery-item-caption">' + img.caption + '</div>' : '')
|
|
+ '</div>';
|
|
}).join('');
|
|
return '<div class="block block-gallery"><div class="gallery-scroll">' + stripItems + '</div></div>';
|
|
}
|
|
|
|
var id = 'gallery-' + (++BlockRenderer.galleryUid);
|
|
var autoplay = block.autoplay === false ? '0' : '1';
|
|
var intervalSec = parseInt(block.intervalSec, 10);
|
|
if (isNaN(intervalSec)) intervalSec = 5;
|
|
intervalSec = Math.max(2, Math.min(intervalSec, 15));
|
|
var showDots = block.showDots === false ? '0' : '1';
|
|
var showArrows = block.showArrows === false ? '0' : '1';
|
|
|
|
var slides = images.map(function(img, index) {
|
|
return '<figure class="gallery-slide' + (index === 0 ? ' is-active' : '') + '" data-slide-index="' + index + '">'
|
|
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
|
|
+ (img.caption ? '<figcaption class="gallery-slide-caption">' + img.caption + '</figcaption>' : '')
|
|
+ '</figure>';
|
|
}).join('');
|
|
|
|
var dots = images.map(function(_, index) {
|
|
return '<button type="button" class="gallery-dot' + (index === 0 ? ' is-active' : '') + '" data-gallery-dot="' + index + '" aria-label="Go to image ' + (index + 1) + '"></button>';
|
|
}).join('');
|
|
|
|
return '<div class="block block-gallery gallery-mode-carousel" id="' + id + '" data-gallery="1" data-autoplay="' + autoplay + '" data-interval-ms="' + (intervalSec * 1000) + '">'
|
|
+ '<div class="gallery-carousel">'
|
|
+ '<div class="gallery-slides">' + slides + '</div>'
|
|
+ (showArrows === '1' && images.length > 1
|
|
? '<button type="button" class="gallery-nav gallery-nav-prev" data-gallery-prev aria-label="Previous image">‹</button>'
|
|
+ '<button type="button" class="gallery-nav gallery-nav-next" data-gallery-next aria-label="Next image">›</button>'
|
|
: '')
|
|
+ '</div>'
|
|
+ (showDots === '1' && images.length > 1 ? '<div class="gallery-dots">' + dots + '</div>' : '')
|
|
+ '</div>';
|
|
}
|
|
|
|
static initGalleries(root) {
|
|
var scope = root || document;
|
|
if (!scope || !scope.querySelectorAll) return;
|
|
|
|
scope.querySelectorAll('[data-gallery="1"]').forEach(function(gallery) {
|
|
if (gallery.dataset.galleryInit === '1') return;
|
|
gallery.dataset.galleryInit = '1';
|
|
|
|
var slides = Array.from(gallery.querySelectorAll('.gallery-slide'));
|
|
if (slides.length <= 1) return;
|
|
|
|
var dots = Array.from(gallery.querySelectorAll('[data-gallery-dot]'));
|
|
var nextBtn = gallery.querySelector('[data-gallery-next]');
|
|
var prevBtn = gallery.querySelector('[data-gallery-prev]');
|
|
var index = 0;
|
|
var timerId = null;
|
|
var autoplay = gallery.dataset.autoplay !== '0';
|
|
var intervalMs = parseInt(gallery.dataset.intervalMs, 10);
|
|
if (isNaN(intervalMs)) intervalMs = 5000;
|
|
intervalMs = Math.max(2000, Math.min(intervalMs, 15000));
|
|
|
|
function showAt(newIndex) {
|
|
index = (newIndex + slides.length) % slides.length;
|
|
slides.forEach(function(slide, i) {
|
|
slide.classList.toggle('is-active', i === index);
|
|
});
|
|
dots.forEach(function(dot, i) {
|
|
dot.classList.toggle('is-active', i === index);
|
|
});
|
|
}
|
|
|
|
function stopTimer() {
|
|
if (timerId) {
|
|
clearTimeout(timerId);
|
|
timerId = null;
|
|
}
|
|
}
|
|
|
|
function scheduleNext() {
|
|
stopTimer();
|
|
if (!autoplay) return;
|
|
timerId = setTimeout(function tick() {
|
|
if (!document.body.contains(gallery)) {
|
|
stopTimer();
|
|
return;
|
|
}
|
|
showAt(index + 1);
|
|
timerId = setTimeout(tick, intervalMs);
|
|
}, intervalMs);
|
|
}
|
|
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener('click', function() {
|
|
showAt(index + 1);
|
|
scheduleNext();
|
|
});
|
|
}
|
|
|
|
if (prevBtn) {
|
|
prevBtn.addEventListener('click', function() {
|
|
showAt(index - 1);
|
|
scheduleNext();
|
|
});
|
|
}
|
|
|
|
dots.forEach(function(dot) {
|
|
dot.addEventListener('click', function() {
|
|
showAt(parseInt(dot.dataset.galleryDot, 10) || 0);
|
|
scheduleNext();
|
|
});
|
|
});
|
|
|
|
gallery.addEventListener('pointerenter', stopTimer);
|
|
gallery.addEventListener('pointerleave', scheduleNext);
|
|
|
|
showAt(0);
|
|
scheduleNext();
|
|
});
|
|
}
|
|
|
|
static renderGrid4(block) {
|
|
var images = (block.images || []).slice(0, 4);
|
|
var items = images.map(function(img) {
|
|
return '<div class="grid4-item">'
|
|
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
|
|
+ (img.caption ? '<div class="grid4-caption">' + img.caption + '</div>' : '')
|
|
+ '</div>';
|
|
}).join('');
|
|
return '<div class="block block-grid4"><div class="grid4-grid">' + items + '</div></div>';
|
|
}
|
|
|
|
static renderStatistics(block) {
|
|
var stats = block.stats || [];
|
|
var items = stats.map(function(s) {
|
|
return '<div class="stat-card">'
|
|
+ '<div class="stat-number">' + (s.number || '') + '</div>'
|
|
+ '<div class="stat-label">' + (s.label || '') + '</div>'
|
|
+ '</div>';
|
|
}).join('');
|
|
return '<div class="block block-statistics"><div class="block-stats-grid">' + items + '</div></div>';
|
|
}
|
|
|
|
static renderMapButton(block) {
|
|
return '<div class="block block-map-button-wrap">'
|
|
+ '<button class="map-button block-map-btn"'
|
|
+ ' data-lat="' + (block.lat || 0) + '"'
|
|
+ ' data-lng="' + (block.lng || 0) + '"'
|
|
+ ' data-zoom="' + (block.zoom || 6) + '">'
|
|
+ '<span class="map-button-icon">🗺</span> '
|
|
+ (block.label || 'View on Map')
|
|
+ '</button></div>';
|
|
}
|
|
}
|
|
|