Updated layout, added ability to make new sections, added new block types, increased layout size by 30%
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
class BlockRenderer {
|
||||
|
||||
static galleryUid = 0;
|
||||
|
||||
static render(blocks) {
|
||||
if (!Array.isArray(blocks)) return '';
|
||||
return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n');
|
||||
@@ -17,6 +19,8 @@ class BlockRenderer {
|
||||
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);
|
||||
@@ -48,15 +52,170 @@ class BlockRenderer {
|
||||
+ '</blockquote>';
|
||||
}
|
||||
|
||||
static renderGallery(block) {
|
||||
var images = block.images || [];
|
||||
var items = 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>';
|
||||
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 '<div class="block block-gallery"><div class="gallery-scroll">' + items + '</div></div>';
|
||||
|
||||
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) {
|
||||
|
||||
355
js/builder.js
355
js/builder.js
@@ -6,29 +6,75 @@
|
||||
class SectionBuilder {
|
||||
|
||||
constructor() {
|
||||
this.sections = {
|
||||
context: { title: 'Historical Context', icon: '🌍', color: '#d4a574', blocks: [] },
|
||||
rcaf: { title: 'RCAF Desert Cats', icon: '✈️', color: '#3498db', blocks: [] },
|
||||
rcn: { title: 'RCN Task Group', icon: '⚓', color: '#2c3e50', blocks: [] },
|
||||
army: { title: 'Canadian Army', icon: '🏥', color: '#27ae60', blocks: [] },
|
||||
legacy: { title: 'After the War', icon: '🍁', color: '#c0392b', blocks: [] }
|
||||
};
|
||||
this.sections = this.buildDefaultSections();
|
||||
this.sectionsStorageKey = 'pf-builder-sections';
|
||||
this.currentSection = 'context';
|
||||
this.editingIndex = null;
|
||||
this.defaultContent = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
buildDefaultSections() {
|
||||
return {
|
||||
context: {
|
||||
title: 'Historical Context',
|
||||
subtitle: 'Coalition Building & Diplomacy',
|
||||
icon: '🌍',
|
||||
color: '#d4a574',
|
||||
description: 'Understand how Iraq\'s invasion of Kuwait triggered the largest military coalition since WWII and Canada\'s diplomatic role.',
|
||||
stats: { articles: 7, storyMaps: 3, images: 45 },
|
||||
blocks: []
|
||||
},
|
||||
rcaf: {
|
||||
title: 'RCAF Desert Cats',
|
||||
subtitle: '409 Squadron Air Operations',
|
||||
icon: '✈️',
|
||||
color: '#3498db',
|
||||
description: 'Follow the Desert Cats from defensive patrols to strike missions, including the TNC-45 engagement and bombing runs.',
|
||||
stats: { articles: 8, storyMaps: 4, images: 60 },
|
||||
blocks: []
|
||||
},
|
||||
rcn: {
|
||||
title: 'RCN Task Group',
|
||||
subtitle: 'Naval Blockade Operations',
|
||||
icon: '⚓',
|
||||
color: '#2c3e50',
|
||||
description: 'The transformation of HMCS Terra Nova into a guided-missile destroyer and enforcement of UN sanctions.',
|
||||
stats: { articles: 7, storyMaps: 2, images: 35 },
|
||||
blocks: []
|
||||
},
|
||||
army: {
|
||||
title: 'Canadian Army',
|
||||
subtitle: 'Medical & Security Forces',
|
||||
icon: '🏥',
|
||||
color: '#27ae60',
|
||||
description: '1 Canadian Field Hospital\'s life-saving work and security operations protecting coalition installations.',
|
||||
stats: { articles: 7, storyMaps: 3, images: 40 },
|
||||
blocks: []
|
||||
},
|
||||
legacy: {
|
||||
title: 'After the War',
|
||||
subtitle: 'Veterans & Long-term Impact',
|
||||
icon: '🍁',
|
||||
color: '#c0392b',
|
||||
description: 'Gulf War Syndrome, environmental cleanup, peacekeeping missions, and ongoing support for veterans.',
|
||||
stats: { articles: 7, storyMaps: 2, images: 30 },
|
||||
blocks: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
var res = await fetch('data/sections-content.json');
|
||||
if (res.ok) this.defaultContent = await res.json();
|
||||
} catch (e) { console.warn('Could not load default content'); }
|
||||
|
||||
this.loadSectionsManifest();
|
||||
this.loadAllFromStorage();
|
||||
this.renderSectionTabs();
|
||||
this.bindGlobalEvents();
|
||||
this.loadSection('context');
|
||||
this.loadSection(this.sections.context ? 'context' : Object.keys(this.sections)[0]);
|
||||
}
|
||||
|
||||
// ── Storage ────────────────────────────────────────────────────────────
|
||||
@@ -45,8 +91,72 @@ class SectionBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
loadSectionsManifest() {
|
||||
var saved = localStorage.getItem(this.sectionsStorageKey);
|
||||
if (!saved) return;
|
||||
|
||||
try {
|
||||
var parsed = JSON.parse(saved);
|
||||
var existing = this.sections;
|
||||
var merged = {};
|
||||
var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {});
|
||||
var sectionMeta = parsed.sections || {};
|
||||
|
||||
order.forEach(function(id) {
|
||||
if (!sectionMeta[id]) return;
|
||||
merged[id] = {
|
||||
title: sectionMeta[id].title || id,
|
||||
subtitle: sectionMeta[id].subtitle || '',
|
||||
description: sectionMeta[id].description || '',
|
||||
icon: sectionMeta[id].icon || '📚',
|
||||
color: sectionMeta[id].color || '#7f8c8d',
|
||||
stats: {
|
||||
articles: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.articles, 10) || 0,
|
||||
storyMaps: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.storyMaps, 10) || 0,
|
||||
images: parseInt(sectionMeta[id].stats && sectionMeta[id].stats.images, 10) || 0
|
||||
},
|
||||
blocks: (existing[id] && existing[id].blocks) || []
|
||||
};
|
||||
});
|
||||
|
||||
Object.keys(existing).forEach(function(id) {
|
||||
if (!merged[id]) merged[id] = existing[id];
|
||||
});
|
||||
|
||||
this.sections = merged;
|
||||
} catch (e) {
|
||||
console.warn('Could not parse saved section metadata');
|
||||
}
|
||||
}
|
||||
|
||||
saveSectionsManifest() {
|
||||
var payload = { order: Object.keys(this.sections), sections: {} };
|
||||
var self = this;
|
||||
payload.order.forEach(function(id) {
|
||||
payload.sections[id] = self.sectionMeta(id);
|
||||
});
|
||||
localStorage.setItem(this.sectionsStorageKey, JSON.stringify(payload));
|
||||
}
|
||||
|
||||
sectionMeta(id) {
|
||||
var section = this.sections[id] || {};
|
||||
return {
|
||||
title: section.title || id,
|
||||
subtitle: section.subtitle || '',
|
||||
description: section.description || '',
|
||||
icon: section.icon || '📚',
|
||||
color: section.color || '#7f8c8d',
|
||||
stats: {
|
||||
articles: parseInt(section.stats && section.stats.articles, 10) || 0,
|
||||
storyMaps: parseInt(section.stats && section.stats.storyMaps, 10) || 0,
|
||||
images: parseInt(section.stats && section.stats.images, 10) || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
saveSection(id) {
|
||||
localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks));
|
||||
this.saveSectionsManifest();
|
||||
this.showToast(this.sections[id].title + ' saved!');
|
||||
}
|
||||
|
||||
@@ -92,6 +202,9 @@ class SectionBuilder {
|
||||
+ '</div>';
|
||||
|
||||
canvas.innerHTML = html;
|
||||
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
|
||||
BlockRenderer.initGalleries(canvas);
|
||||
}
|
||||
this.bindCanvasEvents();
|
||||
}
|
||||
|
||||
@@ -173,7 +286,33 @@ class SectionBuilder {
|
||||
case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' };
|
||||
case 'image': return { id: id, type: type, src: '', alt: '', caption: '' };
|
||||
case 'quote': return { id: id, type: type, text: 'Quote text here.', author: 'Source' };
|
||||
case 'gallery': return { id: id, type: type, images: [{ src: '', alt: '', caption: '' }] };
|
||||
case 'fact-panel': return {
|
||||
id: id,
|
||||
type: type,
|
||||
title: 'Fact Briefing',
|
||||
facts: [
|
||||
{ text: 'Day 1: 4,000 U.S. troops' },
|
||||
{ text: 'Day 30: 72,000 coalition troops' },
|
||||
{ text: 'Day 60: 185,000 coalition troops' }
|
||||
]
|
||||
};
|
||||
case 'timeline-card': return {
|
||||
id: id,
|
||||
type: type,
|
||||
date: 'Sept 24',
|
||||
title: 'Canadian Naval Task Group Arrives',
|
||||
text: 'HMCS Terra Nova, Athabaskan, and Protecteur begin patrol operations in the central Persian Gulf.'
|
||||
};
|
||||
case 'gallery': return {
|
||||
id: id,
|
||||
type: type,
|
||||
mode: 'carousel',
|
||||
autoplay: true,
|
||||
intervalSec: 5,
|
||||
showDots: true,
|
||||
showArrows: true,
|
||||
images: [{ src: '', alt: '', caption: '' }]
|
||||
};
|
||||
case 'grid4': return { id: id, type: type, images: [
|
||||
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' },
|
||||
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' }
|
||||
@@ -237,8 +376,46 @@ class SectionBuilder {
|
||||
return this.formGroup('Quote Text', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>')
|
||||
+ this.formGroup('Attribution (optional)', '<input type="text" name="author" value="' + this.esc(block.author || '') + '" placeholder="Name, Title, Date">');
|
||||
|
||||
case 'fact-panel':
|
||||
return this.formGroup('Panel Title', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="e.g. Desert Shield Force Buildup">')
|
||||
+ this.formGroup('Facts',
|
||||
'<div id="factItems">'
|
||||
+ ((block.facts || block.items || []).map(function(fact, i) {
|
||||
var text = (typeof fact === 'string') ? fact : (fact && fact.text) || '';
|
||||
return SectionBuilder.factRow(i, text);
|
||||
}).join('') || SectionBuilder.factRow(0, ''))
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="add-item-btn btn-add-fact">+ Add Fact</button>');
|
||||
|
||||
case 'timeline-card':
|
||||
return this.formGroup('Date Label', '<input type="text" name="date" value="' + this.esc(block.date || '') + '" placeholder="e.g. Sept 24">')
|
||||
+ this.formGroup('Heading', '<input type="text" name="title" value="' + this.esc(block.title || '') + '" placeholder="Timeline event title">')
|
||||
+ this.formGroup('Details', '<textarea name="text" rows="4">' + this.esc(block.text || '') + '</textarea>');
|
||||
|
||||
case 'gallery':
|
||||
return this.formGroup('Gallery Images',
|
||||
return this.formGroup('Display Mode',
|
||||
'<select name="mode">'
|
||||
+ '<option value="carousel"' + ((block.mode || 'carousel') === 'carousel' ? ' selected' : '') + '>Carousel (single image, rotates)</option>'
|
||||
+ '<option value="strip"' + ((block.mode || 'carousel') === 'strip' ? ' selected' : '') + '>Strip (horizontal scroll)</option>'
|
||||
+ '</select>')
|
||||
+ this.formGroup('Auto-rotate',
|
||||
'<select name="autoplay">'
|
||||
+ '<option value="true"' + (block.autoplay === false ? '' : ' selected') + '>On</option>'
|
||||
+ '<option value="false"' + (block.autoplay === false ? ' selected' : '') + '>Off</option>'
|
||||
+ '</select>')
|
||||
+ this.formGroup('Rotate Every (seconds)',
|
||||
'<input type="number" name="intervalSec" min="2" max="15" value="' + (parseInt(block.intervalSec, 10) || 5) + '">')
|
||||
+ this.formGroup('Show Dots',
|
||||
'<select name="showDots">'
|
||||
+ '<option value="true"' + (block.showDots === false ? '' : ' selected') + '>On</option>'
|
||||
+ '<option value="false"' + (block.showDots === false ? ' selected' : '') + '>Off</option>'
|
||||
+ '</select>')
|
||||
+ this.formGroup('Show Arrows',
|
||||
'<select name="showArrows">'
|
||||
+ '<option value="true"' + (block.showArrows === false ? '' : ' selected') + '>On</option>'
|
||||
+ '<option value="false"' + (block.showArrows === false ? ' selected' : '') + '>Off</option>'
|
||||
+ '</select>')
|
||||
+ this.formGroup('Gallery Images',
|
||||
'<div id="galleryItems">'
|
||||
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
|
||||
+ '</div>'
|
||||
@@ -289,6 +466,13 @@ class SectionBuilder {
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
static factRow(index, text) {
|
||||
return '<div class="item-row" data-index="' + index + '">'
|
||||
+ '<input type="text" data-field="text" data-index="' + index + '" value="' + (text || '') + '" placeholder="Fact line (e.g. Day 90: 320,000 coalition troops)">'
|
||||
+ '<button type="button" class="btn-remove-item">✕</button>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
bindDynamicFormEvents(container) {
|
||||
var self = this;
|
||||
|
||||
@@ -312,6 +496,16 @@ class SectionBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
var addFactBtn = container.querySelector('.btn-add-fact');
|
||||
if (addFactBtn) {
|
||||
addFactBtn.addEventListener('click', function() {
|
||||
var items = container.querySelector('#factItems');
|
||||
var newIdx = items.querySelectorAll('.item-row').length;
|
||||
items.insertAdjacentHTML('beforeend', SectionBuilder.factRow(newIdx, ''));
|
||||
self.bindRemoveBtns(container);
|
||||
});
|
||||
}
|
||||
|
||||
this.bindRemoveBtns(container);
|
||||
}
|
||||
|
||||
@@ -348,7 +542,23 @@ class SectionBuilder {
|
||||
block.text = body.querySelector('[name="text"]').value;
|
||||
block.author = body.querySelector('[name="author"]').value;
|
||||
break;
|
||||
case 'fact-panel':
|
||||
block.title = body.querySelector('[name="title"]').value;
|
||||
block.facts = this.readItemRows(body.querySelector('#factItems'), ['text'])
|
||||
.filter(function(f) { return (f.text || '').trim() !== ''; });
|
||||
break;
|
||||
case 'timeline-card':
|
||||
block.date = body.querySelector('[name="date"]').value;
|
||||
block.title = body.querySelector('[name="title"]').value;
|
||||
block.text = body.querySelector('[name="text"]').value;
|
||||
break;
|
||||
case 'gallery':
|
||||
block.mode = body.querySelector('[name="mode"]').value || 'carousel';
|
||||
block.autoplay = body.querySelector('[name="autoplay"]').value === 'true';
|
||||
block.intervalSec = parseInt(body.querySelector('[name="intervalSec"]').value, 10) || 5;
|
||||
block.intervalSec = Math.max(2, Math.min(block.intervalSec, 15));
|
||||
block.showDots = body.querySelector('[name="showDots"]').value === 'true';
|
||||
block.showArrows = body.querySelector('[name="showArrows"]').value === 'true';
|
||||
block.images = this.readItemRows(body.querySelector('#galleryItems'), ['src','alt','caption']);
|
||||
break;
|
||||
case 'grid4':
|
||||
@@ -437,12 +647,46 @@ class SectionBuilder {
|
||||
reader.onload = function(evt) {
|
||||
try {
|
||||
var data = JSON.parse(evt.target.result);
|
||||
Object.keys(self.sections).forEach(function(id) {
|
||||
if (data[id] && data[id].blocks) {
|
||||
self.sections[id].blocks = data[id].blocks;
|
||||
localStorage.setItem('pf-section-' + id, JSON.stringify(data[id].blocks));
|
||||
Object.keys(data).forEach(function(id) {
|
||||
var entry = data[id] || {};
|
||||
var blocks = Array.isArray(entry.blocks) ? entry.blocks : (Array.isArray(entry) ? entry : []);
|
||||
if (!self.sections[id]) {
|
||||
var meta = entry.meta || { title: id, icon: '📚', color: '#7f8c8d' };
|
||||
self.sections[id] = {
|
||||
title: meta.title || id,
|
||||
subtitle: meta.subtitle || '',
|
||||
description: meta.description || '',
|
||||
icon: meta.icon || '📚',
|
||||
color: meta.color || '#7f8c8d',
|
||||
stats: {
|
||||
articles: parseInt(meta.stats && meta.stats.articles, 10) || 0,
|
||||
storyMaps: parseInt(meta.stats && meta.stats.storyMaps, 10) || 0,
|
||||
images: parseInt(meta.stats && meta.stats.images, 10) || 0
|
||||
},
|
||||
blocks: []
|
||||
};
|
||||
} else if (entry.meta) {
|
||||
self.sections[id] = Object.assign({}, self.sections[id], {
|
||||
title: entry.meta.title || self.sections[id].title,
|
||||
subtitle: entry.meta.subtitle || self.sections[id].subtitle,
|
||||
description: entry.meta.description || self.sections[id].description,
|
||||
icon: entry.meta.icon || self.sections[id].icon,
|
||||
color: entry.meta.color || self.sections[id].color,
|
||||
stats: {
|
||||
articles: parseInt(entry.meta.stats && entry.meta.stats.articles, 10) || 0,
|
||||
storyMaps: parseInt(entry.meta.stats && entry.meta.stats.storyMaps, 10) || 0,
|
||||
images: parseInt(entry.meta.stats && entry.meta.stats.images, 10) || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.sections[id].blocks = blocks;
|
||||
localStorage.setItem('pf-section-' + id, JSON.stringify(blocks));
|
||||
});
|
||||
|
||||
self.saveSectionsManifest();
|
||||
self.renderSectionTabs();
|
||||
if (!self.sections[self.currentSection]) self.currentSection = Object.keys(self.sections)[0];
|
||||
self.loadSection(self.currentSection);
|
||||
self.showToast('Content imported!');
|
||||
} catch (err) {
|
||||
@@ -460,13 +704,25 @@ class SectionBuilder {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('newSectionBtn').addEventListener('click', function() {
|
||||
self.openSectionWizard();
|
||||
});
|
||||
|
||||
document.getElementById('modalSave').addEventListener('click', function() { self.saveModalBlock(); });
|
||||
document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); });
|
||||
document.getElementById('modalClose').addEventListener('click', function() { self.closeModal(); });
|
||||
document.querySelector('.modal-overlay').addEventListener('click', function() { self.closeModal(); });
|
||||
document.querySelector('#builderModal .modal-overlay').addEventListener('click', function() { self.closeModal(); });
|
||||
|
||||
document.getElementById('sectionWizardSave').addEventListener('click', function() { self.createSectionFromWizard(); });
|
||||
document.getElementById('sectionWizardCancel').addEventListener('click', function() { self.closeSectionWizard(); });
|
||||
document.getElementById('sectionWizardClose').addEventListener('click', function() { self.closeSectionWizard(); });
|
||||
document.querySelector('#sectionWizardModal .modal-overlay').addEventListener('click', function() { self.closeSectionWizard(); });
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') self.closeModal();
|
||||
if (e.key === 'Escape') {
|
||||
self.closeModal();
|
||||
self.closeSectionWizard();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
if (self.currentSection) self.saveSection(self.currentSection);
|
||||
@@ -474,10 +730,73 @@ class SectionBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
openSectionWizard() {
|
||||
document.getElementById('wizardTitle').value = '';
|
||||
document.getElementById('wizardSubtitle').value = '';
|
||||
document.getElementById('wizardDescription').value = '';
|
||||
document.getElementById('wizardIcon').value = '📚';
|
||||
document.getElementById('wizardColor').value = '#7f8c8d';
|
||||
document.getElementById('wizardArticles').value = '0';
|
||||
document.getElementById('wizardStoryMaps').value = '0';
|
||||
document.getElementById('wizardImages').value = '0';
|
||||
document.getElementById('sectionWizardModal').classList.remove('hidden');
|
||||
document.getElementById('wizardTitle').focus();
|
||||
}
|
||||
|
||||
closeSectionWizard() {
|
||||
document.getElementById('sectionWizardModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
createSectionFromWizard() {
|
||||
var title = (document.getElementById('wizardTitle').value || '').trim();
|
||||
if (!title) {
|
||||
alert('Please enter a title for the new section.');
|
||||
return;
|
||||
}
|
||||
|
||||
var idBase = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
if (!idBase) idBase = 'section';
|
||||
var id = idBase;
|
||||
var i = 2;
|
||||
while (this.sections[id]) {
|
||||
id = idBase + '-' + i;
|
||||
i++;
|
||||
}
|
||||
|
||||
var color = (document.getElementById('wizardColor').value || '').trim() || '#7f8c8d';
|
||||
if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
|
||||
alert('Please enter a valid hex color (for example: #3498db).');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sections[id] = {
|
||||
title: title,
|
||||
subtitle: (document.getElementById('wizardSubtitle').value || '').trim(),
|
||||
description: (document.getElementById('wizardDescription').value || '').trim(),
|
||||
icon: (document.getElementById('wizardIcon').value || '').trim() || '📚',
|
||||
color: color,
|
||||
stats: {
|
||||
articles: parseInt(document.getElementById('wizardArticles').value, 10) || 0,
|
||||
storyMaps: parseInt(document.getElementById('wizardStoryMaps').value, 10) || 0,
|
||||
images: parseInt(document.getElementById('wizardImages').value, 10) || 0
|
||||
},
|
||||
blocks: []
|
||||
};
|
||||
|
||||
this.saveSectionsManifest();
|
||||
this.renderSectionTabs();
|
||||
this.closeSectionWizard();
|
||||
this.loadSection(id);
|
||||
this.showToast('Section created. Add blocks and save when ready.');
|
||||
}
|
||||
|
||||
exportJSON() {
|
||||
var output = {};
|
||||
Object.keys(this.sections).forEach(function(id) {
|
||||
output[id] = { blocks: this.sections[id].blocks };
|
||||
output[id] = {
|
||||
meta: this.sectionMeta(id),
|
||||
blocks: this.sections[id].blocks
|
||||
};
|
||||
}, this);
|
||||
var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
@@ -503,7 +822,7 @@ class SectionBuilder {
|
||||
blockTypeLabel(type) {
|
||||
var labels = {
|
||||
heading: 'H Heading', paragraph: '¶ Paragraph', image: '🖼 Image',
|
||||
quote: '" Quote', gallery: '⊞ Gallery', grid4: '▦ Grid 4',
|
||||
quote: '" Quote', 'fact-panel': '▸ Fact Briefing Panel', 'timeline-card': '🗓 Timeline Card', gallery: '⊞ Gallery', grid4: '▦ Grid 4',
|
||||
statistics: '# Statistics', 'map-button': '📍 Map Button'
|
||||
};
|
||||
return labels[type] || type;
|
||||
|
||||
Reference in New Issue
Block a user