/**
* 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 '' + (block.text || '') + '';
}
static renderParagraph(block) {
return '
' + (block.text || '') + '
';
}
static renderImage(block) {
return ''
+ '
'
+ (block.caption ? '' + block.caption + '' : '')
+ '';
}
static renderQuote(block) {
return ''
+ '' + (block.text || '') + '
'
+ (block.author ? '— ' + block.author + '' : '')
+ '
';
}
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 '' + text + '';
}).join('');
return ''
+ '' + title + '
'
+ ''
+ '';
}
static renderTimelineCard(block) {
return ''
+ ''
+ '' + (block.text || '') + '
'
+ '';
}
static renderGallery(block) {
var images = Array.isArray(block.images) ? block.images.filter(function(img) {
return img && img.src;
}) : [];
if (images.length === 0) {
return 'No gallery images configured.
';
}
// 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 ''
+ '
 + ')
'
+ (img.caption ? '
' + img.caption + '
' : '')
+ '
';
}).join('');
return '';
}
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 ''
+ '
'
+ (img.caption ? '' + img.caption + '' : '')
+ '';
}).join('');
var dots = images.map(function(_, index) {
return '';
}).join('');
return ''
+ '
'
+ '
' + slides + '
'
+ (showArrows === '1' && images.length > 1
? '
'
+ '
'
: '')
+ '
'
+ (showDots === '1' && images.length > 1 ? '
' + dots + '
' : '')
+ '
';
}
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 ''
+ '
 + ')
'
+ (img.caption ? '
' + img.caption + '
' : '')
+ '
';
}).join('');
return '';
}
static renderStatistics(block) {
var stats = block.stats || [];
var items = stats.map(function(s) {
return ''
+ '
' + (s.number || '') + '
'
+ '
' + (s.label || '') + '
'
+ '
';
}).join('');
return '';
}
static renderMapButton(block) {
return ''
+ '
';
}
}