/** * 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.alt || '') + '' + (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.date || '') + '' + '

    ' + (block.title || '') + '

    ' + '
    ' + '

    ' + (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 ''; } // 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 ''; }).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 ''; }).join(''); var dots = images.map(function(_, index) { return ''; }).join(''); return ''; } 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.alt || '') + '' + (img.caption ? '
    ' + img.caption + '
    ' : '') + '
    '; }).join(''); return '
    ' + items + '
    '; } static renderStatistics(block) { var stats = block.stats || []; var items = stats.map(function(s) { return '
    ' + '
    ' + (s.number || '') + '
    ' + '
    ' + (s.label || '') + '
    ' + '
    '; }).join(''); return '
    ' + items + '
    '; } static renderMapButton(block) { return '
    ' + '
    '; } }