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:
148
js/assets.js
148
js/assets.js
@@ -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' });
|
||||||
|
|||||||
202
js/editor.js
202
js/editor.js
@@ -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: '',
|
||||||
@@ -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: '',
|
||||||
@@ -1535,17 +1601,53 @@
|
|||||||
|
|
||||||
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'));
|
||||||
|
const isPdf = url.match(/\.pdf(\?.*)?$/i);
|
||||||
|
|
||||||
if (iframe) {
|
if (isPdf) {
|
||||||
// For Google Docs viewer for non-PDF files
|
// PDF: embed in iframe
|
||||||
let embedUrl = url;
|
if (iframe) {
|
||||||
if (!url.match(/\.pdf(\?.*)?$/i) && !url.includes('docs.google.com')) {
|
iframe.addAttributes({ src: url });
|
||||||
embedUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user