Replace Google Docs viewer with download card for non-PDF file embeds

PDF files continue to embed in an iframe. Non-PDF files (DOC, DOCX, XLS,
etc.) now show a download card with the file name and download icon instead
of relying on Google Docs Viewer, which often fails with "No preview
available."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 14:13:02 -08:00
parent a71b58c2c7
commit 03f573b451
2 changed files with 257 additions and 113 deletions

View File

@@ -478,19 +478,47 @@
const fileInput = modal.querySelector('#asset-browser-file'); const fileInput = modal.querySelector('#asset-browser-file');
modal.querySelector('#asset-browser-upload-btn').addEventListener('click', () => fileInput.click()); modal.querySelector('#asset-browser-upload-btn').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async (e) => { fileInput.addEventListener('change', async (e) => {
this._handleFiles(e.target.files); const files = Array.from(e.target.files);
fileInput.value = ''; fileInput.value = '';
// Wait a moment for uploads to complete, then re-render // Upload files and re-render after all complete
setTimeout(() => { const uploads = files.map(file => {
const activeTab = modal.querySelector('.asset-tab.active'); let type = 'file';
this.renderBrowserGrid(activeTab?.dataset.type || 'image'); if (file.type.startsWith('image/')) type = 'image';
}, 500); else if (file.type.startsWith('video/')) type = 'video';
else if (file.name.endsWith('.css')) type = 'css';
else if (file.name.endsWith('.js')) type = 'js';
if (this.serverAvailable) {
return this._uploadFileToServer(file, type);
} else {
this._showServerRequiredError();
return Promise.resolve(null);
}
});
await Promise.all(uploads);
const activeTab = modal.querySelector('.asset-tab.active');
this.renderBrowserGrid(activeTab?.dataset.type || 'image');
}); });
} }
openBrowser(filterType) { async openBrowser(filterType) {
if (!this.modal) return Promise.reject('No modal'); if (!this.modal) return Promise.reject('No modal');
// Refresh assets from server to pick up uploads from other code paths
if (this.serverAvailable) {
try {
const resp = await fetch(API_BASE + '/api/assets');
if (resp.ok) {
const data = await resp.json();
if (data.success && Array.isArray(data.assets)) {
this.assets = data.assets;
this._saveMetadataIndex();
}
}
} catch (e) {
// Use existing cached assets on failure
}
}
// Set active tab // Set active tab
const tabs = this.modal.querySelectorAll('.asset-tab'); const tabs = this.modal.querySelectorAll('.asset-tab');
tabs.forEach(t => t.classList.remove('active')); tabs.forEach(t => t.classList.remove('active'));
@@ -603,66 +631,10 @@
}); });
} }
// -- Patch Video Block Traits -- // -- Video traits are defined directly in editor.js component types --
// Browse Video Assets buttons reference window.assetManager.openBrowser()
patchVideoTraits() { patchVideoTraits() {
const editor = this.editor; // No-op: browse buttons are now built into video-wrapper and video-section types
const self = this;
const browseVideoTrait = {
type: 'button',
label: '',
text: '📁 Browse Video Assets',
full: true,
command: () => {
self.openBrowser('video').then(asset => {
if (!asset) return;
const selected = editor.getSelected();
if (selected) {
selected.addAttributes({ videoUrl: asset.url });
// Trigger the video URL change handler
const event = 'change:attributes:videoUrl';
selected.trigger(event);
}
});
}
};
// Patch video-wrapper if it exists
const videoWrapperType = editor.DomComponents.getType('video-wrapper');
if (videoWrapperType) {
const origDefaults = videoWrapperType.model.prototype.defaults;
const origTraits = Array.isArray(origDefaults.traits) ? origDefaults.traits : [];
editor.DomComponents.addType('video-wrapper', {
model: {
defaults: {
traits: [
...origTraits.filter(t => t.type !== 'button'),
browseVideoTrait,
...origTraits.filter(t => t.type === 'button')
]
}
}
});
}
// Also patch the 'video' type
const videoType = editor.DomComponents.getType('video');
if (videoType) {
const origDefaults = videoType.model.prototype.defaults;
const origTraits = Array.isArray(origDefaults.traits) ? origDefaults.traits : [];
editor.DomComponents.addType('video', {
model: {
defaults: {
traits: [
...origTraits.filter(t => t.type !== 'button'),
browseVideoTrait
]
}
}
});
}
} }
// -- Patch File/PDF Block Traits -- // -- Patch File/PDF Block Traits --
@@ -683,22 +655,46 @@
selected.addAttributes({ fileUrl: asset.url }); selected.addAttributes({ fileUrl: asset.url });
// Apply the file: show iframe, hide placeholder
const url = asset.url; const url = asset.url;
const height = selected.getAttributes().frameHeight || 600; const height = selected.getAttributes().frameHeight || 600;
let embedUrl = url; const isPdf = url.match(/\.pdf(\?.*)?$/i);
if (!url.match(/\.pdf(\?.*)?$/i) && !url.includes('docs.google.com')) {
embedUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
}
const iframe = selected.components().find(c => c.getClasses().includes('file-embed-frame')); const iframe = selected.components().find(c => c.getClasses().includes('file-embed-frame'));
const placeholder = selected.components().find(c => c.getClasses().includes('file-embed-placeholder')); const placeholder = selected.components().find(c => c.getClasses().includes('file-embed-placeholder'));
const downloadCard = selected.components().find(c => c.getClasses().includes('file-download-card'));
if (iframe) { if (isPdf) {
iframe.addAttributes({ src: embedUrl }); // PDF: embed in iframe
iframe.addStyle({ display: 'block', height: height + 'px' }); if (iframe) {
const el = iframe.getEl(); iframe.addAttributes({ src: url });
if (el) { el.src = embedUrl; el.style.display = 'block'; el.style.height = height + 'px'; } iframe.addStyle({ display: 'block', height: height + 'px' });
const el = iframe.getEl();
if (el) { el.src = url; el.style.display = 'block'; el.style.height = height + 'px'; }
}
if (downloadCard) {
downloadCard.addStyle({ display: 'none' });
const el = downloadCard.getEl();
if (el) el.style.display = 'none';
}
} else {
// Non-PDF: show download card
if (iframe) {
iframe.addStyle({ display: 'none' });
const el = iframe.getEl();
if (el) el.style.display = 'none';
}
if (downloadCard) {
const fileName = decodeURIComponent(url.split('/').pop().split('?')[0]) || 'File';
downloadCard.addAttributes({ href: url });
downloadCard.addStyle({ display: 'flex' });
const cardEl = downloadCard.getEl();
if (cardEl) {
cardEl.href = url;
cardEl.style.display = 'flex';
const nameNode = cardEl.querySelector('.file-download-name');
if (nameNode) nameNode.textContent = fileName;
}
}
} }
if (placeholder) { if (placeholder) {
placeholder.addStyle({ display: 'none' }); placeholder.addStyle({ display: 'none' });

View File

@@ -297,6 +297,11 @@
const blockManager = editor.BlockManager; const blockManager = editor.BlockManager;
// Remove plugin-provided Image/Video blocks that duplicate the Media section's
// custom blocks (which have browse-assets support and proper wrappers)
blockManager.remove('image');
blockManager.remove('video');
// Section Block // Section Block
blockManager.add('section', { blockManager.add('section', {
label: 'Section', label: 'Section',
@@ -643,12 +648,19 @@
blockManager.add('hero-video', { blockManager.add('hero-video', {
label: 'Hero (Video)', label: 'Hero (Video)',
category: 'Sections', category: 'Sections',
content: `<section style="min-height:500px;display:flex;align-items:center;justify-content:center;position:relative;padding:60px 20px;text-align:center;overflow:hidden;"> content: `<section class="section-with-video-bg" data-video-section="true" style="position:relative;min-height:500px;display:flex;align-items:center;justify-content:center;padding:60px 20px;overflow:hidden;">
<video autoplay muted loop playsinline style="position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;transform:translate(-50%,-50%);z-index:0;"> <div class="bg-video-wrapper" data-bg-video="true" style="position:absolute;top:0;left:0;right:0;bottom:0;z-index:0;overflow:hidden;">
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4"> <iframe class="bg-video-frame" style="position:absolute;top:50%;left:50%;width:100vw;height:56.25vw;min-height:100%;min-width:177.77vh;transform:translate(-50%,-50%);border:none;display:none;" src="" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</video> <video class="bg-video-player" style="position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;transform:translate(-50%,-50%);object-fit:cover;display:none;" autoplay muted loop playsinline></video>
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1;"></div> <div class="bg-video-placeholder" style="position:absolute;top:0;left:0;right:0;bottom:0;background:#1a1a2e;display:flex;align-items:center;justify-content:center;color:#fff;font-family:Inter,sans-serif;">
<div style="position:relative;z-index:2;max-width:800px;"> <div style="text-align:center;">
<div style="font-size:32px;margin-bottom:8px;">▶</div>
<div style="font-size:12px;opacity:0.7;">Click this section, then add Video URL in Settings →</div>
</div>
</div>
</div>
<div class="bg-overlay" style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1;"></div>
<div class="bg-content" style="position:relative;z-index:2;max-width:800px;text-align:center;">
<h1 style="color:#fff;font-size:48px;font-weight:700;margin-bottom:20px;font-family:Inter,sans-serif;">Video Background Hero</h1> <h1 style="color:#fff;font-size:48px;font-weight:700;margin-bottom:20px;font-family:Inter,sans-serif;">Video Background Hero</h1>
<p style="color:rgba(255,255,255,0.9);font-size:20px;line-height:1.6;margin-bottom:30px;font-family:Inter,sans-serif;">Create stunning video backgrounds for your hero sections.</p> <p style="color:rgba(255,255,255,0.9);font-size:20px;line-height:1.6;margin-bottom:30px;font-family:Inter,sans-serif;">Create stunning video backgrounds for your hero sections.</p>
<a href="#" style="display:inline-block;padding:16px 40px;background:#10b981;color:#fff;font-size:18px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Learn More</a> <a href="#" style="display:inline-block;padding:16px 40px;background:#10b981;color:#fff;font-size:18px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Learn More</a>
@@ -973,11 +985,19 @@
category: 'Media', category: 'Media',
content: `<div class="file-embed-wrapper" data-file-embed="true" style="width:100%;max-width:800px;margin:0 auto;"> content: `<div class="file-embed-wrapper" data-file-embed="true" style="width:100%;max-width:800px;margin:0 auto;">
<iframe class="file-embed-frame" src="" style="width:100%;height:600px;border:1px solid #e5e7eb;border-radius:8px;display:none;" title="Embedded file"></iframe> <iframe class="file-embed-frame" src="" style="width:100%;height:600px;border:1px solid #e5e7eb;border-radius:8px;display:none;" title="Embedded file"></iframe>
<a class="file-download-card" href="#" download style="display:none;text-decoration:none;color:inherit;border:1px solid #e5e7eb;border-radius:8px;padding:20px 24px;background:#f9fafb;font-family:Inter,sans-serif;align-items:center;gap:16px;">
<svg style="width:40px;height:40px;flex-shrink:0;" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="12" y1="18" x2="12" y2="12"></line><polyline points="9 15 12 18 15 15"></polyline></svg>
<div style="flex:1;min-width:0;">
<div class="file-download-name" style="font-size:15px;font-weight:500;color:#1f2937;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">File</div>
<div class="file-download-hint" style="font-size:12px;color:#6b7280;margin-top:2px;">Click to download</div>
</div>
<svg style="width:24px;height:24px;flex-shrink:0;" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</a>
<div class="file-embed-placeholder" style="width:100%;height:600px;border:2px dashed #d1d5db;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f9fafb;font-family:Inter,sans-serif;color:#6b7280;"> <div class="file-embed-placeholder" style="width:100%;height:600px;border:2px dashed #d1d5db;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f9fafb;font-family:Inter,sans-serif;color:#6b7280;">
<div style="text-align:center;"> <div style="text-align:center;">
<div style="font-size:48px;margin-bottom:10px;">📄</div> <div style="font-size:48px;margin-bottom:10px;">📄</div>
<div style="font-size:14px;">Select this element, then enter File URL in Settings</div> <div style="font-size:14px;">Select this element, then enter File URL in Settings</div>
<div style="font-size:12px;margin-top:4px;opacity:0.7;">Supports PDF, DOC, and other embeddable files</div> <div style="font-size:12px;margin-top:4px;opacity:0.7;">Supports PDF, DOC, and other files</div>
</div> </div>
</div> </div>
</div>`, </div>`,
@@ -1294,11 +1314,23 @@
} }
} }
// Hide placeholder // Hide placeholder in the model (so preview/export shows the video, not the placeholder)
if (placeholder) { if (placeholder) {
placeholder.addStyle({ display: 'none' }); placeholder.addStyle({ display: 'none' });
// In the editor canvas, GrapesJS renders <video>/<iframe> as <div>,
// so show visual feedback via the DOM only (doesn't affect export)
const placeholderEl = placeholder.getEl(); const placeholderEl = placeholder.getEl();
if (placeholderEl) placeholderEl.style.display = 'none'; if (placeholderEl) {
const filename = url.split('/').pop().split('?')[0];
const icon = result.type === 'file' ? '🎬' : result.type === 'youtube' ? '▶️' : '🎥';
const label = result.type === 'file' ? filename : result.type === 'youtube' ? 'YouTube Video' : result.type === 'vimeo' ? 'Vimeo Video' : 'Embedded Video';
placeholderEl.innerHTML = `<div style="text-align:center;">` +
`<div style="font-size:32px;margin-bottom:8px;">${icon}</div>` +
`<div style="font-size:13px;font-weight:600;">${label}</div>` +
`<div style="font-size:11px;opacity:0.6;margin-top:4px;">Video will play in Preview</div>` +
`</div>`;
placeholderEl.style.display = 'flex';
}
} }
} }
@@ -1314,6 +1346,23 @@
name: 'videoUrl', name: 'videoUrl',
placeholder: 'YouTube, Vimeo, or .mp4 URL' placeholder: 'YouTube, Vimeo, or .mp4 URL'
}, },
{
type: 'button',
label: '',
text: '📁 Browse Video Assets',
full: true,
command: (editor) => {
if (!window.assetManager) return;
window.assetManager.openBrowser('video').then(asset => {
if (!asset) return;
const selected = editor.getSelected();
if (selected) {
selected.addAttributes({ videoUrl: asset.url });
selected.trigger('change:attributes:videoUrl');
}
});
}
},
{ {
type: 'button', type: 'button',
label: '', label: '',
@@ -1322,13 +1371,13 @@
command: (editor) => { command: (editor) => {
const selected = editor.getSelected(); const selected = editor.getSelected();
if (!selected) return; if (!selected) return;
const url = selected.getAttributes().videoUrl; const url = selected.getAttributes().videoUrl;
if (!url) { if (!url) {
alert('Please enter a Video URL first'); alert('Please enter a Video URL first');
return; return;
} }
console.log('Apply Video button clicked (regular video), URL:', url); console.log('Apply Video button clicked (regular video), URL:', url);
applyVideoUrl(selected, url); applyVideoUrl(selected, url);
alert('Video applied! If you see an error, the video owner may have disabled embedding.'); alert('Video applied! If you see an error, the video owner may have disabled embedding.');
@@ -1393,6 +1442,23 @@
name: 'videoUrl', name: 'videoUrl',
placeholder: 'YouTube, Vimeo, or .mp4 URL' placeholder: 'YouTube, Vimeo, or .mp4 URL'
}, },
{
type: 'button',
label: '',
text: '📁 Browse Video Assets',
full: true,
command: (editor) => {
if (!window.assetManager) return;
window.assetManager.openBrowser('video').then(asset => {
if (!asset) return;
const selected = editor.getSelected();
if (selected) {
selected.addAttributes({ videoUrl: asset.url });
selected.trigger('change:attributes:videoUrl');
}
});
}
},
{ {
type: 'button', type: 'button',
label: '', label: '',
@@ -1401,20 +1467,20 @@
command: (editor) => { command: (editor) => {
const selected = editor.getSelected(); const selected = editor.getSelected();
if (!selected) return; if (!selected) return;
const url = selected.getAttributes().videoUrl; const url = selected.getAttributes().videoUrl;
if (!url) { if (!url) {
alert('Please enter a Video URL first'); alert('Please enter a Video URL first');
return; return;
} }
console.log('Apply Video button clicked, URL:', url); console.log('Apply Video button clicked, URL:', url);
// Find the bg-video-wrapper child // Find the bg-video-wrapper child
const videoWrapper = selected.components().find(c => const videoWrapper = selected.components().find(c =>
c.getAttributes()['data-bg-video'] === 'true' c.getAttributes()['data-bg-video'] === 'true'
); );
if (videoWrapper) { if (videoWrapper) {
videoWrapper.addAttributes({ videoUrl: url }); videoWrapper.addAttributes({ videoUrl: url });
applyVideoUrl(videoWrapper, url); applyVideoUrl(videoWrapper, url);
@@ -1525,27 +1591,63 @@
command: (editor) => { command: (editor) => {
const selected = editor.getSelected(); const selected = editor.getSelected();
if (!selected) return; if (!selected) return;
const url = selected.getAttributes().fileUrl; const url = selected.getAttributes().fileUrl;
const height = selected.getAttributes().frameHeight || 600; const height = selected.getAttributes().frameHeight || 600;
if (!url) { if (!url) {
alert('Please enter a File URL first'); alert('Please enter a File URL first');
return; return;
} }
const iframe = selected.components().find(c => c.getClasses().includes('file-embed-frame')); const iframe = selected.components().find(c => c.getClasses().includes('file-embed-frame'));
const placeholder = selected.components().find(c => c.getClasses().includes('file-embed-placeholder')); const placeholder = selected.components().find(c => c.getClasses().includes('file-embed-placeholder'));
const downloadCard = selected.components().find(c => c.getClasses().includes('file-download-card'));
if (iframe) { const isPdf = url.match(/\.pdf(\?.*)?$/i);
// For Google Docs viewer for non-PDF files
let embedUrl = url; if (isPdf) {
if (!url.match(/\.pdf(\?.*)?$/i) && !url.includes('docs.google.com')) { // PDF: embed in iframe
embedUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`; if (iframe) {
iframe.addAttributes({ src: url });
iframe.addStyle({ display: 'block', height: height + 'px' });
const el = iframe.getEl();
if (el) { el.src = url; el.style.display = 'block'; el.style.height = height + 'px'; }
}
if (downloadCard) {
downloadCard.addStyle({ display: 'none' });
const el = downloadCard.getEl();
if (el) el.style.display = 'none';
}
} else {
// Non-PDF: show download card with file name
if (iframe) {
iframe.addStyle({ display: 'none' });
const el = iframe.getEl();
if (el) el.style.display = 'none';
}
if (downloadCard) {
const fileName = decodeURIComponent(url.split('/').pop().split('?')[0]) || 'File';
downloadCard.addAttributes({ href: url });
downloadCard.addStyle({ display: 'flex' });
const nameEl = downloadCard.components().find(c => {
const inner = c.components();
return inner && inner.find && inner.find(ic => ic.getClasses().includes('file-download-name'));
});
if (nameEl) {
const nameSpan = nameEl.components().find(ic => ic.getClasses().includes('file-download-name'));
if (nameSpan) {
nameSpan.components(fileName);
const el = nameSpan.getEl();
if (el) el.textContent = fileName;
}
}
const cardEl = downloadCard.getEl();
if (cardEl) {
cardEl.href = url;
cardEl.style.display = 'flex';
const nameNode = cardEl.querySelector('.file-download-name');
if (nameNode) nameNode.textContent = fileName;
}
} }
iframe.addAttributes({ src: embedUrl });
iframe.addStyle({ display: 'block', height: height + 'px' });
const el = iframe.getEl();
if (el) { el.src = embedUrl; el.style.display = 'block'; el.style.height = height + 'px'; }
} }
if (placeholder) { if (placeholder) {
placeholder.addStyle({ display: 'none' }); placeholder.addStyle({ display: 'none' });
@@ -4226,8 +4328,13 @@ ${page.html || ''}
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'template-card'; card.className = 'template-card';
card.dataset.category = t.category; card.dataset.category = t.category;
const pageCount = t.pages ? t.pages.length : 0;
const pageBadge = pageCount > 0 ? `<span style="position:absolute;top:8px;right:8px;background:rgba(59,130,246,0.9);color:#fff;font-size:11px;font-weight:600;padding:3px 8px;border-radius:4px;font-family:Inter,sans-serif;">${pageCount} pages</span>` : '';
card.innerHTML = ` card.innerHTML = `
<img class="template-card-thumb" src="templates/thumbnails/${t.id}.svg" alt="${t.name}" onerror="this.style.background='#2d2d3a'"> <div style="position:relative;">
<img class="template-card-thumb" src="templates/thumbnails/${t.id}.svg" alt="${t.name}" onerror="this.style.background='#2d2d3a'">
${pageBadge}
</div>
<div class="template-card-info"> <div class="template-card-info">
<div class="template-card-name">${t.name}</div> <div class="template-card-name">${t.name}</div>
<div class="template-card-desc">${t.description}</div> <div class="template-card-desc">${t.description}</div>
@@ -4249,6 +4356,14 @@ ${page.html || ''}
const modal = document.getElementById('template-modal'); const modal = document.getElementById('template-modal');
document.getElementById('template-modal-title').textContent = template.name; document.getElementById('template-modal-title').textContent = template.name;
document.getElementById('template-modal-desc').textContent = template.description + '\n\nUse case: ' + template.useCase; document.getElementById('template-modal-desc').textContent = template.description + '\n\nUse case: ' + template.useCase;
const warning = document.getElementById('template-modal-warning');
if (warning) {
if (template.pages && template.pages.length > 0) {
warning.textContent = '⚠️ This will replace ALL pages in your project with ' + template.pages.length + ' pages from this template.';
} else {
warning.textContent = '⚠️ This will replace all content on your current page.';
}
}
modal.style.display = 'flex'; modal.style.display = 'flex';
} }
@@ -4324,15 +4439,48 @@ ${page.html || ''}
templateModalConfirm.textContent = 'Loading...'; templateModalConfirm.textContent = 'Loading...';
templateModalConfirm.disabled = true; templateModalConfirm.disabled = true;
// Load template HTML via fetch if (template.pages && template.pages.length > 0) {
const resp = await fetch('templates/' + template.file); // Multi-page template: fetch all page HTML files in parallel
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); const fetches = template.pages.map(async (p) => {
const html = await resp.text(); const resp = await fetch('templates/' + p.file);
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText} (${p.file})`);
return resp.text();
});
const htmls = await Promise.all(fetches);
// Clear canvas and load template HTML // Save current page content before clearing
editor.DomComponents.clear(); saveCurrentPageContent();
editor.CssComposer.clear();
editor.setComponents(html); // Build new pages array from template
pages = template.pages.map((p, i) => ({
id: generateId(),
name: p.name,
slug: p.slug,
html: htmls[i],
css: ''
}));
// Set current page to first page and save
currentPageId = pages[0].id;
savePages();
// Load first page into editor
editor.DomComponents.clear();
editor.CssComposer.clear();
editor.setComponents(pages[0].html);
renderPagesList();
} else {
// Single-page template: existing behavior
const resp = await fetch('templates/' + template.file);
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
const html = await resp.text();
// Clear canvas and load template HTML
editor.DomComponents.clear();
editor.CssComposer.clear();
editor.setComponents(html);
}
closeTemplateModal(); closeTemplateModal();
closeTemplatesBrowser(); // Also close the templates browser closeTemplatesBrowser(); // Also close the templates browser