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');
modal.querySelector('#asset-browser-upload-btn').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async (e) => {
this._handleFiles(e.target.files);
const files = Array.from(e.target.files);
fileInput.value = '';
// Wait a moment for uploads to complete, then re-render
setTimeout(() => {
const activeTab = modal.querySelector('.asset-tab.active');
this.renderBrowserGrid(activeTab?.dataset.type || 'image');
}, 500);
// Upload files and re-render after all complete
const uploads = files.map(file => {
let type = 'file';
if (file.type.startsWith('image/')) type = 'image';
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');
// 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
const tabs = this.modal.querySelectorAll('.asset-tab');
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() {
const editor = this.editor;
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
]
}
}
});
}
// No-op: browse buttons are now built into video-wrapper and video-section types
}
// -- Patch File/PDF Block Traits --
@@ -683,22 +655,46 @@
selected.addAttributes({ fileUrl: asset.url });
// Apply the file: show iframe, hide placeholder
const url = asset.url;
const height = selected.getAttributes().frameHeight || 600;
let embedUrl = url;
if (!url.match(/\.pdf(\?.*)?$/i) && !url.includes('docs.google.com')) {
embedUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
}
const isPdf = url.match(/\.pdf(\?.*)?$/i);
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 downloadCard = selected.components().find(c => c.getClasses().includes('file-download-card'));
if (iframe) {
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 (isPdf) {
// PDF: embed in iframe
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
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) {
placeholder.addStyle({ display: 'none' });