diff --git a/js/assets.js b/js/assets.js index a8c9269..2ca57b3 100644 --- a/js/assets.js +++ b/js/assets.js @@ -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' }); diff --git a/js/editor.js b/js/editor.js index ce58779..aabdd97 100644 --- a/js/editor.js +++ b/js/editor.js @@ -297,6 +297,11 @@ 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 blockManager.add('section', { label: 'Section', @@ -643,12 +648,19 @@ blockManager.add('hero-video', { label: 'Hero (Video)', category: 'Sections', - content: `
- -
-
+ content: `
+
+ + +
+
+
+
Click this section, then add Video URL in Settings →
+
+
+
+
+

Video Background Hero

Create stunning video backgrounds for your hero sections.

Learn More @@ -973,11 +985,19 @@ category: 'Media', content: `
+
📄
Select this element, then enter File URL in Settings
-
Supports PDF, DOC, and other embeddable files
+
Supports PDF, DOC, and other files
`, @@ -1294,11 +1314,23 @@ } } - // Hide placeholder + // Hide placeholder in the model (so preview/export shows the video, not the placeholder) if (placeholder) { placeholder.addStyle({ display: 'none' }); + // In the editor canvas, GrapesJS renders