Updated layout, added ability to make new sections, added new block types, increased layout size by 30%
This commit is contained in:
71
app.js
71
app.js
@@ -60,6 +60,71 @@ class ProjectFriction {
|
|||||||
if (!Array.isArray(this.data.sections) || this.data.sections.length === 0) {
|
if (!Array.isArray(this.data.sections) || this.data.sections.length === 0) {
|
||||||
this.loadDefaultConfiguration();
|
this.loadDefaultConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mergeBuilderSectionsFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeBuilderSectionsFromStorage() {
|
||||||
|
if (typeof localStorage === 'undefined') return;
|
||||||
|
|
||||||
|
var raw = localStorage.getItem('pf-builder-sections');
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(raw);
|
||||||
|
var order = Array.isArray(parsed.order) ? parsed.order : Object.keys(parsed.sections || {});
|
||||||
|
var sectionMeta = parsed.sections || {};
|
||||||
|
if (!Array.isArray(this.data.sections)) this.data.sections = [];
|
||||||
|
|
||||||
|
var existingById = {};
|
||||||
|
this.data.sections.forEach(function(section) {
|
||||||
|
existingById[section.id] = section;
|
||||||
|
});
|
||||||
|
|
||||||
|
order.forEach(function(id) {
|
||||||
|
var meta = sectionMeta[id];
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
var 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
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingById[id]) {
|
||||||
|
existingById[id].title = meta.title || existingById[id].title;
|
||||||
|
existingById[id].subtitle = meta.subtitle || existingById[id].subtitle;
|
||||||
|
existingById[id].description = meta.description || existingById[id].description;
|
||||||
|
existingById[id].icon = meta.icon || existingById[id].icon;
|
||||||
|
existingById[id].color = meta.color || existingById[id].color;
|
||||||
|
existingById[id].stats = stats;
|
||||||
|
} else {
|
||||||
|
var newSection = {
|
||||||
|
id: id,
|
||||||
|
title: meta.title || id,
|
||||||
|
subtitle: meta.subtitle || '',
|
||||||
|
icon: meta.icon || '📚',
|
||||||
|
color: meta.color || '#7f8c8d',
|
||||||
|
description: meta.description || '',
|
||||||
|
stats: stats
|
||||||
|
};
|
||||||
|
this.data.sections.push(newSection);
|
||||||
|
existingById[id] = newSection;
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
// Keep display order aligned with builder tabs for kiosk consistency.
|
||||||
|
var reordered = [];
|
||||||
|
order.forEach(function(id) {
|
||||||
|
if (existingById[id]) reordered.push(existingById[id]);
|
||||||
|
});
|
||||||
|
this.data.sections.forEach(function(section) {
|
||||||
|
if (order.indexOf(section.id) === -1) reordered.push(section);
|
||||||
|
});
|
||||||
|
this.data.sections = reordered;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not parse builder section metadata from localStorage.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadJSON(url) {
|
async loadJSON(url) {
|
||||||
@@ -434,6 +499,10 @@ class ProjectFriction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeSectionInteractions(sectionId) {
|
initializeSectionInteractions(sectionId) {
|
||||||
|
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
|
||||||
|
BlockRenderer.initGalleries(document.getElementById('sectionContainer'));
|
||||||
|
}
|
||||||
|
|
||||||
// Wire map-button blocks to the Leaflet map
|
// Wire map-button blocks to the Leaflet map
|
||||||
const self = this;
|
const self = this;
|
||||||
document.querySelectorAll('.block-map-btn').forEach(function(btn) {
|
document.querySelectorAll('.block-map-btn').forEach(function(btn) {
|
||||||
@@ -526,7 +595,7 @@ class ProjectFriction {
|
|||||||
|
|
||||||
if (sidebar && toggleIcon) {
|
if (sidebar && toggleIcon) {
|
||||||
sidebar.classList.toggle('collapsed');
|
sidebar.classList.toggle('collapsed');
|
||||||
toggleIcon.textContent = sidebar.classList.contains('collapsed') ? '▶' : '◀';
|
toggleIcon.textContent = sidebar.classList.contains('collapsed') ? '◀' : '▶';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
blocks.css
160
blocks.css
@@ -77,11 +77,85 @@
|
|||||||
/* ── Quote Block ─────────────────────────────────────────────────────────── */
|
/* ── Quote Block ─────────────────────────────────────────────────────────── */
|
||||||
/* Uses existing .quote-block, .quote-text, .quote-attribution from components.css */
|
/* Uses existing .quote-block, .quote-text, .quote-attribution from components.css */
|
||||||
|
|
||||||
|
/* ── Fact Briefing Panel ─────────────────────────────────────────────────── */
|
||||||
|
.block-fact-panel {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-fact-panel .fact-panel-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Timeline Card ───────────────────────────────────────────────────────── */
|
||||||
|
.block-timeline-card {
|
||||||
|
background: var(--color-white);
|
||||||
|
border: 1px solid var(--color-gray);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-timeline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-timeline-date {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-white);
|
||||||
|
padding: 0.18rem 0.45rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-timeline-title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.55rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-timeline-text {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: calc(0.45rem + 0.55rem + 52px);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.block-timeline-text {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Gallery Block ───────────────────────────────────────────────────────── */
|
/* ── Gallery Block ───────────────────────────────────────────────────────── */
|
||||||
.block-gallery {
|
.block-gallery {
|
||||||
margin: 1.25rem 0;
|
margin: 1.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gallery-empty {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px dashed var(--color-gray);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-dark-gray);
|
||||||
|
font-family: var(--font-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-scroll {
|
.gallery-scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -127,6 +201,92 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Carousel Gallery Mode ────────────────────────────────────────────────── */
|
||||||
|
.gallery-mode-carousel .gallery-carousel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-mode-carousel .gallery-slides {
|
||||||
|
position: relative;
|
||||||
|
min-height: 220px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
background: #0f1720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-mode-carousel .gallery-slide {
|
||||||
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-mode-carousel .gallery-slide.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-mode-carousel .gallery-slide img {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-slide-caption {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0));
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-family: var(--font-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-nav:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-nav-prev { left: 0.5rem; }
|
||||||
|
.gallery-nav-next { right: 0.5rem; }
|
||||||
|
|
||||||
|
.gallery-dots {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
border: none;
|
||||||
|
background: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-dot.is-active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 4-Image Grid Block ──────────────────────────────────────────────────── */
|
/* ── 4-Image Grid Block ──────────────────────────────────────────────────── */
|
||||||
.block-grid4 {
|
.block-grid4 {
|
||||||
margin: 1.25rem 0;
|
margin: 1.25rem 0;
|
||||||
|
|||||||
103
builder.html
103
builder.html
@@ -147,6 +147,24 @@
|
|||||||
|
|
||||||
.tab-icon { font-size: 16px; }
|
.tab-icon { font-size: 16px; }
|
||||||
|
|
||||||
|
.new-section-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
border: 1px dashed #3498db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5fbff;
|
||||||
|
color: #2b6ea4;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-section-btn:hover {
|
||||||
|
background: #eaf4fb;
|
||||||
|
border-color: #2f8dd8;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Block Palette ───────────────────────────────────────────────── */
|
/* ── Block Palette ───────────────────────────────────────────────── */
|
||||||
.palette-blocks {
|
.palette-blocks {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -321,18 +339,33 @@
|
|||||||
|
|
||||||
.canvas-block-preview {
|
.canvas-block-preview {
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.canvas-block-preview button,
|
||||||
|
.canvas-block-preview a,
|
||||||
|
.canvas-block-preview input,
|
||||||
|
.canvas-block-preview select,
|
||||||
|
.canvas-block-preview textarea {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-block-preview [data-gallery-prev],
|
||||||
|
.canvas-block-preview [data-gallery-next],
|
||||||
|
.canvas-block-preview [data-gallery-dot] {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.canvas-block-preview::after {
|
.canvas-block-preview::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0; left: 0; right: 0;
|
bottom: 0; left: 0; right: 0;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.8));
|
background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.8));
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Insert Strip ────────────────────────────────────────────────── */
|
/* ── Insert Strip ────────────────────────────────────────────────── */
|
||||||
@@ -412,7 +445,8 @@
|
|||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modalClose {
|
#modalClose,
|
||||||
|
#sectionWizardClose {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -422,7 +456,8 @@
|
|||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#modalClose:hover { color: #2c3e50; }
|
#modalClose:hover,
|
||||||
|
#sectionWizardClose:hover { color: #2c3e50; }
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
@@ -482,6 +517,12 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-row-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Gallery / Stats item rows */
|
/* Gallery / Stats item rows */
|
||||||
.item-row {
|
.item-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -595,6 +636,7 @@
|
|||||||
<div class="palette-section">
|
<div class="palette-section">
|
||||||
<div class="palette-title">Sections</div>
|
<div class="palette-title">Sections</div>
|
||||||
<div id="sectionTabs"></div>
|
<div id="sectionTabs"></div>
|
||||||
|
<button id="newSectionBtn" class="new-section-btn">+ New Section</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Block Palette -->
|
<!-- Block Palette -->
|
||||||
@@ -605,6 +647,8 @@
|
|||||||
<button class="palette-block" data-type="paragraph"> ¶ Paragraph</button>
|
<button class="palette-block" data-type="paragraph"> ¶ Paragraph</button>
|
||||||
<button class="palette-block" data-type="image"> 🖼 Image</button>
|
<button class="palette-block" data-type="image"> 🖼 Image</button>
|
||||||
<button class="palette-block" data-type="quote"> " Quote</button>
|
<button class="palette-block" data-type="quote"> " Quote</button>
|
||||||
|
<button class="palette-block" data-type="fact-panel"> ▸ Fact Briefing Panel</button>
|
||||||
|
<button class="palette-block" data-type="timeline-card"> 🗓 Timeline Card</button>
|
||||||
<button class="palette-block" data-type="gallery"> ⊞ Gallery</button>
|
<button class="palette-block" data-type="gallery"> ⊞ Gallery</button>
|
||||||
<button class="palette-block" data-type="grid4"> ▦ 4-Image Grid</button>
|
<button class="palette-block" data-type="grid4"> ▦ 4-Image Grid</button>
|
||||||
<button class="palette-block" data-type="statistics"> # Statistics</button>
|
<button class="palette-block" data-type="statistics"> # Statistics</button>
|
||||||
@@ -646,6 +690,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── New Section Wizard ─────────────────────────────────────────────── -->
|
||||||
|
<div id="sectionWizardModal" class="builder-modal hidden">
|
||||||
|
<div class="modal-overlay"></div>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Create New Section</h3>
|
||||||
|
<button id="sectionWizardClose" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="sectionWizardBody">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Title</label>
|
||||||
|
<input type="text" id="wizardTitle" placeholder="e.g. Intelligence & Reconnaissance">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Subtitle</label>
|
||||||
|
<input type="text" id="wizardSubtitle" placeholder="Short subheading shown on the card">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea id="wizardDescription" rows="4" placeholder="Card preview description for visitors"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-row-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Icon (emoji)</label>
|
||||||
|
<input type="text" id="wizardIcon" value="📚" maxlength="4">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Theme Color</label>
|
||||||
|
<input type="text" id="wizardColor" value="#7f8c8d" placeholder="#3498db">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Articles</label>
|
||||||
|
<input type="number" id="wizardArticles" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Story Maps</label>
|
||||||
|
<input type="number" id="wizardStoryMaps" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Images</label>
|
||||||
|
<input type="number" id="wizardImages" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="sectionWizardSave" class="btn btn-primary">Create Section</button>
|
||||||
|
<button id="sectionWizardCancel" class="btn btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js/block-renderer.js"></script>
|
<script src="js/block-renderer.js"></script>
|
||||||
<script src="js/builder.js"></script>
|
<script src="js/builder.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="sidebar" id="sidebar">
|
<div class="sidebar" id="sidebar">
|
||||||
<!-- Toggle Button -->
|
<!-- Toggle Button -->
|
||||||
<div class="sidebar-toggle" id="sidebarToggle">
|
<div class="sidebar-toggle" id="sidebarToggle">
|
||||||
<span id="toggle-icon">◀</span>
|
<span id="toggle-icon">▶</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
class BlockRenderer {
|
class BlockRenderer {
|
||||||
|
|
||||||
|
static galleryUid = 0;
|
||||||
|
|
||||||
static render(blocks) {
|
static render(blocks) {
|
||||||
if (!Array.isArray(blocks)) return '';
|
if (!Array.isArray(blocks)) return '';
|
||||||
return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n');
|
return blocks.map(function(block) { return BlockRenderer.renderBlock(block); }).join('\n');
|
||||||
@@ -17,6 +19,8 @@ class BlockRenderer {
|
|||||||
case 'paragraph': return BlockRenderer.renderParagraph(block);
|
case 'paragraph': return BlockRenderer.renderParagraph(block);
|
||||||
case 'image': return BlockRenderer.renderImage(block);
|
case 'image': return BlockRenderer.renderImage(block);
|
||||||
case 'quote': return BlockRenderer.renderQuote(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 'gallery': return BlockRenderer.renderGallery(block);
|
||||||
case 'grid4': return BlockRenderer.renderGrid4(block);
|
case 'grid4': return BlockRenderer.renderGrid4(block);
|
||||||
case 'statistics': return BlockRenderer.renderStatistics(block);
|
case 'statistics': return BlockRenderer.renderStatistics(block);
|
||||||
@@ -48,15 +52,170 @@ class BlockRenderer {
|
|||||||
+ '</blockquote>';
|
+ '</blockquote>';
|
||||||
}
|
}
|
||||||
|
|
||||||
static renderGallery(block) {
|
static renderFactPanel(block) {
|
||||||
var images = block.images || [];
|
var title = block.title || 'Fact Briefing';
|
||||||
var items = images.map(function(img) {
|
var factItems = block.facts || block.items || [];
|
||||||
return '<div class="gallery-item">'
|
var facts = factItems.map(function(item) {
|
||||||
+ '<img src="' + (img.src || '') + '" alt="' + (img.alt || '') + '" loading="lazy">'
|
if (typeof item === 'string') return item;
|
||||||
+ (img.caption ? '<div class="gallery-item-caption">' + img.caption + '</div>' : '')
|
return item && item.text ? item.text : '';
|
||||||
+ '</div>';
|
}).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('');
|
}).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) {
|
static renderGrid4(block) {
|
||||||
|
|||||||
355
js/builder.js
355
js/builder.js
@@ -6,29 +6,75 @@
|
|||||||
class SectionBuilder {
|
class SectionBuilder {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sections = {
|
this.sections = this.buildDefaultSections();
|
||||||
context: { title: 'Historical Context', icon: '🌍', color: '#d4a574', blocks: [] },
|
this.sectionsStorageKey = 'pf-builder-sections';
|
||||||
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.currentSection = 'context';
|
this.currentSection = 'context';
|
||||||
this.editingIndex = null;
|
this.editingIndex = null;
|
||||||
this.defaultContent = null;
|
this.defaultContent = null;
|
||||||
this.init();
|
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() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
var res = await fetch('data/sections-content.json');
|
var res = await fetch('data/sections-content.json');
|
||||||
if (res.ok) this.defaultContent = await res.json();
|
if (res.ok) this.defaultContent = await res.json();
|
||||||
} catch (e) { console.warn('Could not load default content'); }
|
} catch (e) { console.warn('Could not load default content'); }
|
||||||
|
|
||||||
|
this.loadSectionsManifest();
|
||||||
this.loadAllFromStorage();
|
this.loadAllFromStorage();
|
||||||
this.renderSectionTabs();
|
this.renderSectionTabs();
|
||||||
this.bindGlobalEvents();
|
this.bindGlobalEvents();
|
||||||
this.loadSection('context');
|
this.loadSection(this.sections.context ? 'context' : Object.keys(this.sections)[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Storage ────────────────────────────────────────────────────────────
|
// ── 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) {
|
saveSection(id) {
|
||||||
localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks));
|
localStorage.setItem('pf-section-' + id, JSON.stringify(this.sections[id].blocks));
|
||||||
|
this.saveSectionsManifest();
|
||||||
this.showToast(this.sections[id].title + ' saved!');
|
this.showToast(this.sections[id].title + ' saved!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +202,9 @@ class SectionBuilder {
|
|||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|
||||||
canvas.innerHTML = html;
|
canvas.innerHTML = html;
|
||||||
|
if (typeof BlockRenderer !== 'undefined' && typeof BlockRenderer.initGalleries === 'function') {
|
||||||
|
BlockRenderer.initGalleries(canvas);
|
||||||
|
}
|
||||||
this.bindCanvasEvents();
|
this.bindCanvasEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +286,33 @@ class SectionBuilder {
|
|||||||
case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' };
|
case 'paragraph': return { id: id, type: type, text: 'Enter paragraph text here.' };
|
||||||
case 'image': return { id: id, type: type, src: '', alt: '', caption: '' };
|
case 'image': return { id: id, type: type, src: '', alt: '', caption: '' };
|
||||||
case 'quote': return { id: id, type: type, text: 'Quote text here.', author: 'Source' };
|
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: [
|
case 'grid4': return { id: id, type: type, images: [
|
||||||
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' },
|
{ src: '', alt: '', caption: '' }, { src: '', alt: '', caption: '' },
|
||||||
{ 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>')
|
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">');
|
+ 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':
|
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">'
|
'<div id="galleryItems">'
|
||||||
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
|
+ (block.images || []).map(function(img, i) { return SectionBuilder.imageRow(i, img, true); }).join('')
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
@@ -289,6 +466,13 @@ class SectionBuilder {
|
|||||||
+ '</div>';
|
+ '</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) {
|
bindDynamicFormEvents(container) {
|
||||||
var self = this;
|
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);
|
this.bindRemoveBtns(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +542,23 @@ class SectionBuilder {
|
|||||||
block.text = body.querySelector('[name="text"]').value;
|
block.text = body.querySelector('[name="text"]').value;
|
||||||
block.author = body.querySelector('[name="author"]').value;
|
block.author = body.querySelector('[name="author"]').value;
|
||||||
break;
|
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':
|
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']);
|
block.images = this.readItemRows(body.querySelector('#galleryItems'), ['src','alt','caption']);
|
||||||
break;
|
break;
|
||||||
case 'grid4':
|
case 'grid4':
|
||||||
@@ -437,12 +647,46 @@ class SectionBuilder {
|
|||||||
reader.onload = function(evt) {
|
reader.onload = function(evt) {
|
||||||
try {
|
try {
|
||||||
var data = JSON.parse(evt.target.result);
|
var data = JSON.parse(evt.target.result);
|
||||||
Object.keys(self.sections).forEach(function(id) {
|
Object.keys(data).forEach(function(id) {
|
||||||
if (data[id] && data[id].blocks) {
|
var entry = data[id] || {};
|
||||||
self.sections[id].blocks = data[id].blocks;
|
var blocks = Array.isArray(entry.blocks) ? entry.blocks : (Array.isArray(entry) ? entry : []);
|
||||||
localStorage.setItem('pf-section-' + id, JSON.stringify(data[id].blocks));
|
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.loadSection(self.currentSection);
|
||||||
self.showToast('Content imported!');
|
self.showToast('Content imported!');
|
||||||
} catch (err) {
|
} 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('modalSave').addEventListener('click', function() { self.saveModalBlock(); });
|
||||||
document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); });
|
document.getElementById('modalCancel').addEventListener('click', function() { self.closeModal(); });
|
||||||
document.getElementById('modalClose').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) {
|
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') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (self.currentSection) self.saveSection(self.currentSection);
|
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() {
|
exportJSON() {
|
||||||
var output = {};
|
var output = {};
|
||||||
Object.keys(this.sections).forEach(function(id) {
|
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);
|
}, this);
|
||||||
var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' });
|
var blob = new Blob([JSON.stringify(output, null, 2)], { type: 'application/json' });
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
@@ -503,7 +822,7 @@ class SectionBuilder {
|
|||||||
blockTypeLabel(type) {
|
blockTypeLabel(type) {
|
||||||
var labels = {
|
var labels = {
|
||||||
heading: 'H Heading', paragraph: '¶ Paragraph', image: '🖼 Image',
|
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'
|
statistics: '# Statistics', 'map-button': '📍 Map Button'
|
||||||
};
|
};
|
||||||
return labels[type] || type;
|
return labels[type] || type;
|
||||||
|
|||||||
10
main.css
10
main.css
@@ -77,7 +77,7 @@ body {
|
|||||||
|
|
||||||
/* Sidebar Styling */
|
/* Sidebar Styling */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 420px;
|
width: 35%;
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -85,20 +85,21 @@ body {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
transition: transform var(--transition-normal);
|
transition: transform var(--transition-normal);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.collapsed {
|
.sidebar.collapsed {
|
||||||
transform: translateX(-370px);
|
transform: translateX(370px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle {
|
.sidebar-toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -30px;
|
left: -30px;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -266,6 +267,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#map {
|
#map {
|
||||||
|
|||||||
Reference in New Issue
Block a user