fix(image-radius): split out 3x-scale IMAGE_RADIUS_PRESETS for the image picker
Image radii need to be visibly larger than the radius scale that works for buttons/containers — at typical photo dimensions, 16px reads as nearly square. Add an image-specific scale at 3x the shared values (S=24px, M=48px, L=96px) and route ImageStylePanel through it. Other components (buttons, sections, containers) keep RADIUS_PRESETS unchanged. Note: this commit also bundles unrelated pre-existing working-tree changes in the legacy GrapesJS site-builder root (CLAUDE.md, index.html, css/editor.css, js/assets.js, js/editor.js, js/whp-integration.js) that were inadvertently picked up by an earlier `git add -u`. The image-radius change is the only intentional content of this commit; the rest is in-progress legacy work that happened to be sitting uncommitted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
683
js/editor.js
683
js/editor.js
@@ -185,6 +185,46 @@
|
||||
|
||||
// Plugins - using global function references from CDN scripts
|
||||
plugins: [
|
||||
// Custom plugin to fix image rendering (must be first)
|
||||
function whpImagePlugin(editor) {
|
||||
// Override the image component type so real images render
|
||||
// instead of GrapesJS's SVG placeholder
|
||||
editor.DomComponents.addType('image', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'img',
|
||||
type: 'image',
|
||||
resizable: { ratioDefault: 1 },
|
||||
traits: ['alt'],
|
||||
src: '',
|
||||
void: true,
|
||||
},
|
||||
},
|
||||
view: {
|
||||
tagName: 'img',
|
||||
events: {},
|
||||
onRender({ el, model }) {
|
||||
const src = model.getAttributes().src || model.get('src') || '';
|
||||
if (src) {
|
||||
el.src = src;
|
||||
}
|
||||
// On error: show a subtle visual indicator, NOT the GrapesJS SVG
|
||||
el.onerror = function() {
|
||||
this.style.outline = '2px dashed #f59e0b';
|
||||
this.style.minHeight = '60px';
|
||||
this.style.minWidth = '100px';
|
||||
this.style.background = 'rgba(245,158,11,0.1)';
|
||||
};
|
||||
el.onload = function() {
|
||||
this.style.outline = '';
|
||||
this.style.minHeight = '';
|
||||
this.style.minWidth = '';
|
||||
this.style.background = '';
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
window['gjs-blocks-basic'],
|
||||
window['grapesjs-preset-webpage'],
|
||||
window['grapesjs-plugin-forms'],
|
||||
@@ -342,8 +382,8 @@
|
||||
|
||||
// Navigation Menu (will be updated dynamically based on pages)
|
||||
blockManager.add('navbar', {
|
||||
label: 'Navigation',
|
||||
category: 'Layout',
|
||||
label: 'Menu',
|
||||
category: 'Basic',
|
||||
content: {
|
||||
type: 'navbar',
|
||||
tagName: 'nav',
|
||||
@@ -380,7 +420,7 @@
|
||||
components: [
|
||||
{
|
||||
tagName: 'a',
|
||||
attributes: { href: '#' },
|
||||
attributes: { href: '/' },
|
||||
style: {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
@@ -391,7 +431,7 @@
|
||||
},
|
||||
{
|
||||
tagName: 'a',
|
||||
attributes: { href: '#' },
|
||||
attributes: { href: 'about' },
|
||||
style: {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
@@ -402,7 +442,7 @@
|
||||
},
|
||||
{
|
||||
tagName: 'a',
|
||||
attributes: { href: '#' },
|
||||
attributes: { href: 'contact' },
|
||||
style: {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
@@ -536,7 +576,7 @@
|
||||
// Footer
|
||||
blockManager.add('footer', {
|
||||
label: 'Footer',
|
||||
category: 'Layout',
|
||||
category: 'Basic',
|
||||
content: `<footer style="padding:40px 20px;background:#1f2937;color:#9ca3af;text-align:center;">
|
||||
<div style="max-width:1200px;margin:0 auto;">
|
||||
<div style="display:flex;justify-content:center;gap:24px;margin-bottom:20px;">
|
||||
@@ -607,7 +647,7 @@
|
||||
blockManager.add('image-block', {
|
||||
label: 'Image',
|
||||
category: 'Media',
|
||||
content: '<img src="https://via.placeholder.com/800x400/3b82f6/ffffff?text=Click+to+change+image" style="max-width:100%;height:auto;display:block;border-radius:8px;" alt="Image">',
|
||||
content: `<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='400' fill='%23e2e8f0'%3E%3Crect width='800' height='400' fill='%23f1f5f9' rx='8'/%3E%3Ctext x='400' y='190' text-anchor='middle' font-family='sans-serif' font-size='24' fill='%2394a3b8'%3EClick to change image%3C/text%3E%3Ctext x='400' y='225' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%23cbd5e1'%3EDrag an image here or select from assets%3C/text%3E%3C/svg%3E" style="max-width:100%;height:auto;display:block;border-radius:8px;" alt="Image">`,
|
||||
attributes: { class: 'fa fa-image' }
|
||||
});
|
||||
|
||||
@@ -1828,8 +1868,11 @@
|
||||
// Store for preview page
|
||||
localStorage.setItem(STORAGE_KEY + '-preview', JSON.stringify({ html, css }));
|
||||
|
||||
// Open preview
|
||||
window.open('preview.html', '_blank');
|
||||
// Open preview (pass site_id in WHP mode so Back button works)
|
||||
const previewUrl = (window.WHP_CONFIG)
|
||||
? 'preview.html?site_id=' + WHP_CONFIG.siteId
|
||||
: 'preview.html';
|
||||
window.open(previewUrl, '_blank');
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
@@ -1948,6 +1991,9 @@
|
||||
font: document.getElementById('section-font'),
|
||||
textSize: document.getElementById('section-text-size'),
|
||||
fontWeight: document.getElementById('section-font-weight'),
|
||||
imageSrc: document.getElementById('section-image-src'),
|
||||
videoSrc: document.getElementById('section-video-src'),
|
||||
alignment: document.getElementById('section-alignment'),
|
||||
spacing: document.getElementById('section-spacing'),
|
||||
radius: document.getElementById('section-radius'),
|
||||
thickness: document.getElementById('section-thickness'),
|
||||
@@ -2041,10 +2087,25 @@
|
||||
// Section with background - show background image controls
|
||||
sections.bgImage.style.display = 'block';
|
||||
sections.spacing.style.display = 'block';
|
||||
// If it's a video section, also show video controls
|
||||
const attrs = component.getAttributes();
|
||||
if (attrs['data-video-section'] === 'true') {
|
||||
sections.videoSrc.style.display = 'block';
|
||||
loadVideoSrcValues(component);
|
||||
}
|
||||
loadBgImageValues(component);
|
||||
return;
|
||||
}
|
||||
|
||||
// Video wrapper (regular video block)
|
||||
if (component.getAttributes()['data-video-wrapper'] === 'true') {
|
||||
sections.videoSrc.style.display = 'block';
|
||||
sections.spacing.style.display = 'block';
|
||||
sections.radius.style.display = 'block';
|
||||
loadVideoSrcValues(component);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show relevant sections based on element type
|
||||
switch (elementType) {
|
||||
case 'text':
|
||||
@@ -2052,12 +2113,14 @@
|
||||
sections.font.style.display = 'block';
|
||||
sections.textSize.style.display = 'block';
|
||||
sections.fontWeight.style.display = 'block';
|
||||
sections.alignment.style.display = 'block';
|
||||
// Show heading level selector for headings
|
||||
const currentTag = component.get('tagName')?.toLowerCase();
|
||||
if (currentTag && currentTag.match(/^h[1-6]$/)) {
|
||||
sections.headingLevel.style.display = 'block';
|
||||
updateHeadingLevelButtons(currentTag);
|
||||
}
|
||||
updateAlignmentButtons(component);
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
@@ -2071,6 +2134,8 @@
|
||||
sections.font.style.display = 'block';
|
||||
sections.textSize.style.display = 'block';
|
||||
sections.fontWeight.style.display = 'block';
|
||||
sections.alignment.style.display = 'block';
|
||||
updateAlignmentButtons(component);
|
||||
// Load current link values
|
||||
loadLinkValues(component);
|
||||
break;
|
||||
@@ -2084,14 +2149,23 @@
|
||||
sections.bgColor.style.display = 'block';
|
||||
sections.bgGradient.style.display = 'block';
|
||||
sections.bgImage.style.display = 'block';
|
||||
sections.alignment.style.display = 'block';
|
||||
sections.spacing.style.display = 'block';
|
||||
sections.radius.style.display = 'block';
|
||||
loadBgImageValues(component);
|
||||
updateAlignmentButtons(component);
|
||||
break;
|
||||
|
||||
case 'media':
|
||||
const mediaTag = component.get('tagName')?.toLowerCase();
|
||||
if (mediaTag === 'img') {
|
||||
sections.imageSrc.style.display = 'block';
|
||||
loadImageSrcValues(component);
|
||||
}
|
||||
sections.alignment.style.display = 'block';
|
||||
sections.spacing.style.display = 'block';
|
||||
sections.radius.style.display = 'block';
|
||||
updateAlignmentButtons(component);
|
||||
break;
|
||||
|
||||
case 'form':
|
||||
@@ -2296,48 +2370,185 @@
|
||||
function loadNavLinks(component) {
|
||||
if (!component) return;
|
||||
|
||||
// Find the nav-links container within the nav
|
||||
const linksContainer = component.components().find(c => {
|
||||
const classes = c.getClasses();
|
||||
return classes.includes('nav-links');
|
||||
});
|
||||
|
||||
const linksContainer = component.components().find(c => c.getClasses().includes('nav-links'));
|
||||
if (!linksContainer) {
|
||||
navLinksList.innerHTML = '<p class="no-links-msg">No links container found</p>';
|
||||
navLinksList.textContent = 'No links container found';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all link components
|
||||
const links = linksContainer.components().filter(c => c.get('tagName')?.toLowerCase() === 'a');
|
||||
navLinksList.textContent = '';
|
||||
|
||||
// Clear and rebuild list
|
||||
navLinksList.innerHTML = '';
|
||||
if (links.length === 0) {
|
||||
const msg = document.createElement('p');
|
||||
msg.className = 'no-links-msg';
|
||||
msg.textContent = 'No links yet. Use the buttons above to add links.';
|
||||
navLinksList.appendChild(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
let dragSrcIndex = null;
|
||||
|
||||
links.forEach((link, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'nav-link-item';
|
||||
item.draggable = true;
|
||||
item.dataset.index = index;
|
||||
item.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding:8px;background:#1e1e2a;border-radius:6px;margin-bottom:4px;border:1px solid transparent;transition:border-color 0.15s;';
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.className = 'nav-link-text';
|
||||
textSpan.textContent = link.getEl()?.textContent || 'Link';
|
||||
const isCta = link.getClasses().includes('nav-cta');
|
||||
const href = link.getAttributes().href || '#';
|
||||
const text = link.getEl()?.textContent || link.components()?.at(0)?.get('content') || 'Link';
|
||||
|
||||
// Drag events for reordering
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
dragSrcIndex = index;
|
||||
item.style.opacity = '0.4';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
item.addEventListener('dragend', () => { item.style.opacity = '1'; });
|
||||
item.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
item.style.borderColor = '#3b82f6';
|
||||
});
|
||||
item.addEventListener('dragleave', () => { item.style.borderColor = 'transparent'; });
|
||||
item.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
item.style.borderColor = 'transparent';
|
||||
if (dragSrcIndex === null || dragSrcIndex === index) return;
|
||||
// Reorder in GrapesJS model
|
||||
const movedLink = links[dragSrcIndex];
|
||||
const targetPos = index;
|
||||
movedLink.move(linksContainer, { at: targetPos });
|
||||
dragSrcIndex = null;
|
||||
loadNavLinks(component);
|
||||
});
|
||||
|
||||
// Row 1: drag handle + text input + delete
|
||||
const row1 = document.createElement('div');
|
||||
row1.style.cssText = 'display:flex;align-items:center;gap:4px;';
|
||||
|
||||
const dragHandle = document.createElement('span');
|
||||
dragHandle.style.cssText = 'cursor:grab;color:#71717a;font-size:14px;padding:0 2px;user-select:none;';
|
||||
dragHandle.textContent = '\u2630'; // hamburger icon ☰
|
||||
dragHandle.title = 'Drag to reorder';
|
||||
|
||||
const textInput = document.createElement('input');
|
||||
textInput.type = 'text';
|
||||
textInput.value = text;
|
||||
textInput.className = 'guided-input';
|
||||
textInput.style.cssText = 'flex:1;padding:4px 8px;font-size:12px;';
|
||||
textInput.placeholder = 'Link text';
|
||||
textInput.addEventListener('change', () => {
|
||||
const el = link.getEl();
|
||||
if (el) el.textContent = textInput.value;
|
||||
link.components(textInput.value);
|
||||
});
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'nav-link-delete';
|
||||
deleteBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
||||
deleteBtn.style.cssText = 'background:none;border:none;color:#ef4444;cursor:pointer;padding:2px 4px;font-size:14px;line-height:1;';
|
||||
deleteBtn.textContent = '\u00d7';
|
||||
deleteBtn.title = 'Remove link';
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
link.remove();
|
||||
loadNavLinks(component);
|
||||
});
|
||||
|
||||
item.appendChild(textSpan);
|
||||
item.appendChild(deleteBtn);
|
||||
if (isCta) {
|
||||
const ctaBadge = document.createElement('span');
|
||||
ctaBadge.style.cssText = 'font-size:9px;background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;white-space:nowrap;';
|
||||
ctaBadge.textContent = 'CTA';
|
||||
row1.appendChild(ctaBadge);
|
||||
}
|
||||
|
||||
row1.appendChild(dragHandle);
|
||||
row1.appendChild(textInput);
|
||||
row1.appendChild(deleteBtn);
|
||||
|
||||
// Row 2: URL selector
|
||||
const row2 = document.createElement('div');
|
||||
row2.style.cssText = 'display:flex;align-items:center;gap:4px;padding-left:22px;';
|
||||
|
||||
const urlSelect = document.createElement('select');
|
||||
urlSelect.className = 'guided-input';
|
||||
urlSelect.style.cssText = 'flex:1;padding:3px 6px;font-size:11px;';
|
||||
|
||||
const customOpt = document.createElement('option');
|
||||
customOpt.value = '__custom__';
|
||||
customOpt.textContent = 'Custom URL...';
|
||||
urlSelect.appendChild(customOpt);
|
||||
|
||||
const anchorOpt = document.createElement('option');
|
||||
anchorOpt.value = '__anchor__';
|
||||
anchorOpt.textContent = 'Anchor on page (#)...';
|
||||
urlSelect.appendChild(anchorOpt);
|
||||
|
||||
pages.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.slug === 'index' ? '/' : p.slug;
|
||||
opt.textContent = p.name + ' (page)';
|
||||
const matchSlugs = [opt.value, p.slug + '.html', p.slug === 'index' ? 'index.html' : null, p.slug === 'index' ? '#' : null].filter(Boolean);
|
||||
if (matchSlugs.includes(href)) opt.selected = true;
|
||||
urlSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
const allPageSlugs = pages.flatMap(p => {
|
||||
const s = p.slug === 'index' ? '/' : p.slug;
|
||||
return [s, p.slug + '.html', p.slug === 'index' ? 'index.html' : null, p.slug === 'index' ? '#' : null].filter(Boolean);
|
||||
});
|
||||
const isAnchor = href.startsWith('#') && href !== '#';
|
||||
const isCustom = !isAnchor && !allPageSlugs.includes(href);
|
||||
|
||||
const urlInput = document.createElement('input');
|
||||
urlInput.type = 'text';
|
||||
urlInput.className = 'guided-input';
|
||||
urlInput.style.cssText = 'flex:1;padding:3px 8px;font-size:11px;display:' + ((isCustom || isAnchor) ? 'block' : 'none') + ';';
|
||||
urlInput.placeholder = isAnchor ? '#section-id' : 'https://...';
|
||||
urlInput.value = (isCustom || isAnchor) ? href : '';
|
||||
if (isCustom) urlSelect.value = '__custom__';
|
||||
if (isAnchor) urlSelect.value = '__anchor__';
|
||||
|
||||
urlSelect.addEventListener('change', () => {
|
||||
if (urlSelect.value === '__custom__') {
|
||||
urlInput.style.display = 'block';
|
||||
urlInput.placeholder = 'https://...';
|
||||
urlInput.value = '';
|
||||
urlInput.focus();
|
||||
} else if (urlSelect.value === '__anchor__') {
|
||||
urlInput.style.display = 'block';
|
||||
urlInput.placeholder = '#section-id';
|
||||
urlInput.value = '#';
|
||||
urlInput.focus();
|
||||
} else {
|
||||
urlInput.style.display = 'none';
|
||||
link.addAttributes({ href: urlSelect.value });
|
||||
// Remove target blank for internal links
|
||||
link.removeAttributes('target');
|
||||
link.removeAttributes('rel');
|
||||
}
|
||||
});
|
||||
|
||||
urlInput.addEventListener('change', () => {
|
||||
const val = urlInput.value.trim();
|
||||
if (val) {
|
||||
link.addAttributes({ href: val });
|
||||
if (val.startsWith('http://') || val.startsWith('https://')) {
|
||||
link.addAttributes({ target: '_blank', rel: 'noopener' });
|
||||
} else {
|
||||
link.removeAttributes('target');
|
||||
link.removeAttributes('rel');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
row2.appendChild(urlSelect);
|
||||
row2.appendChild(urlInput);
|
||||
|
||||
item.appendChild(row1);
|
||||
item.appendChild(row2);
|
||||
navLinksList.appendChild(item);
|
||||
});
|
||||
|
||||
if (links.length === 0) {
|
||||
navLinksList.innerHTML = '<p class="no-links-msg">No links in navigation</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to apply style to selected component
|
||||
@@ -2462,6 +2673,246 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Alignment presets
|
||||
document.querySelectorAll('.alignment-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected) return;
|
||||
const align = btn.dataset.align;
|
||||
const elementType = getElementType(selected.get('tagName'));
|
||||
|
||||
if (elementType === 'container') {
|
||||
// For containers, set text-align for child content AND flex alignment
|
||||
applyStyle('text-align', align);
|
||||
// Also set align-items for flex containers
|
||||
const currentDisplay = selected.getStyle()['display'];
|
||||
if (currentDisplay === 'flex' || currentDisplay === 'inline-flex') {
|
||||
const justifyMap = { left: 'flex-start', center: 'center', right: 'flex-end', justify: 'space-between' };
|
||||
applyStyle('justify-content', justifyMap[align] || align);
|
||||
applyStyle('align-items', 'center');
|
||||
}
|
||||
} else if (elementType === 'media') {
|
||||
// For images/media, center via parent's text-align or margin auto
|
||||
const parent = selected.parent();
|
||||
if (parent) {
|
||||
parent.addStyle({ 'text-align': align });
|
||||
}
|
||||
if (align === 'center') {
|
||||
applyStyle('margin-left', 'auto');
|
||||
applyStyle('margin-right', 'auto');
|
||||
applyStyle('display', 'block');
|
||||
} else if (align === 'left') {
|
||||
applyStyle('margin-left', '0');
|
||||
applyStyle('margin-right', 'auto');
|
||||
} else if (align === 'right') {
|
||||
applyStyle('margin-left', 'auto');
|
||||
applyStyle('margin-right', '0');
|
||||
}
|
||||
} else {
|
||||
// For text elements, set text-align
|
||||
applyStyle('text-align', align);
|
||||
}
|
||||
document.querySelectorAll('.alignment-preset').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to highlight the active alignment button based on current component styles
|
||||
function updateAlignmentButtons(component) {
|
||||
if (!component) return;
|
||||
const styles = component.getStyle();
|
||||
const currentAlign = styles['text-align'] || '';
|
||||
document.querySelectorAll('.alignment-preset').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.align === currentAlign);
|
||||
});
|
||||
}
|
||||
|
||||
// Image source controls
|
||||
const imageSrcInput = document.getElementById('image-src-input');
|
||||
const imageAltInput = document.getElementById('image-alt-input');
|
||||
const imageSrcUpload = document.getElementById('image-src-upload');
|
||||
const imageSrcBrowse = document.getElementById('image-src-browse');
|
||||
const imageSrcUrl = document.getElementById('image-src-url');
|
||||
|
||||
function loadImageSrcValues(component) {
|
||||
if (!component) return;
|
||||
const attrs = component.getAttributes();
|
||||
const src = attrs.src || '';
|
||||
// Show a friendly name instead of the full proxy URL
|
||||
const filename = src.includes('filename=') ? decodeURIComponent(src.split('filename=').pop().split('&')[0]) : src.split('/').pop();
|
||||
imageSrcInput.value = filename || 'No image set';
|
||||
imageAltInput.value = attrs.alt || '';
|
||||
}
|
||||
|
||||
if (imageAltInput) {
|
||||
imageAltInput.addEventListener('change', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (selected && selected.get('tagName')?.toLowerCase() === 'img') {
|
||||
selected.addAttributes({ alt: imageAltInput.value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (imageSrcUpload) {
|
||||
imageSrcUpload.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.addEventListener('change', async () => {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
const selected = editor.getSelected();
|
||||
if (!selected) return;
|
||||
|
||||
if (window.assetManager && window.assetManager.serverAvailable) {
|
||||
const asset = await window.assetManager._uploadFileToServer(file, 'image');
|
||||
if (asset && asset.url) {
|
||||
selected.addAttributes({ src: asset.url });
|
||||
selected.set('src', asset.url);
|
||||
loadImageSrcValues(selected);
|
||||
}
|
||||
}
|
||||
});
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (imageSrcBrowse) {
|
||||
imageSrcBrowse.addEventListener('click', async () => {
|
||||
if (window.assetManager) {
|
||||
const asset = await window.assetManager.openBrowser('image');
|
||||
if (asset && asset.url) {
|
||||
const selected = editor.getSelected();
|
||||
if (selected) {
|
||||
selected.addAttributes({ src: asset.url });
|
||||
selected.set('src', asset.url);
|
||||
loadImageSrcValues(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (imageSrcUrl) {
|
||||
imageSrcUrl.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected) return;
|
||||
const current = selected.getAttributes().src || '';
|
||||
const url = prompt('Enter image URL:', current);
|
||||
if (url !== null && url.trim()) {
|
||||
selected.addAttributes({ src: url.trim() });
|
||||
selected.set('src', url.trim());
|
||||
loadImageSrcValues(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Video source controls
|
||||
const videoSrcInput = document.getElementById('video-src-input');
|
||||
const videoSrcApply = document.getElementById('video-src-apply');
|
||||
const videoSrcBrowse = document.getElementById('video-src-browse');
|
||||
const videoSrcStatus = document.getElementById('video-src-status');
|
||||
|
||||
function loadVideoSrcValues(component) {
|
||||
if (!component) return;
|
||||
const attrs = component.getAttributes();
|
||||
const url = attrs.videoUrl || attrs.videourl || '';
|
||||
if (videoSrcInput) videoSrcInput.value = url;
|
||||
if (videoSrcStatus) {
|
||||
if (url) {
|
||||
const result = convertToEmbedUrl(url);
|
||||
const typeLabel = result ? (result.type === 'youtube' ? 'YouTube' : result.type === 'vimeo' ? 'Vimeo' : 'Direct video') : 'Unknown';
|
||||
videoSrcStatus.textContent = typeLabel + ' detected';
|
||||
} else {
|
||||
videoSrcStatus.textContent = 'Paste a YouTube, Vimeo, or .mp4 URL';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyVideoAndRefreshCanvas(component, url) {
|
||||
applyVideoUrl(component, url);
|
||||
|
||||
// Try to render the video/iframe in the editor canvas
|
||||
const frame = document.querySelector('.gjs-frame');
|
||||
if (!frame || !frame.contentDocument) return;
|
||||
const el = component.getEl();
|
||||
if (!el) return;
|
||||
|
||||
const result = convertToEmbedUrl(url);
|
||||
if (!result) return;
|
||||
|
||||
if (result.type === 'file') {
|
||||
// Direct video -- show HTML5 video element
|
||||
let videoEl = el.querySelector('video');
|
||||
if (!videoEl) {
|
||||
videoEl = frame.contentDocument.createElement('video');
|
||||
videoEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;';
|
||||
videoEl.controls = true;
|
||||
el.appendChild(videoEl);
|
||||
}
|
||||
videoEl.src = result.url;
|
||||
videoEl.style.display = 'block';
|
||||
// Hide iframe and placeholder
|
||||
const iframeEl = el.querySelector('iframe');
|
||||
if (iframeEl) iframeEl.style.display = 'none';
|
||||
const ph = el.querySelector('.video-placeholder, .bg-video-placeholder');
|
||||
if (ph) ph.style.display = 'none';
|
||||
} else {
|
||||
// YouTube/Vimeo -- show iframe
|
||||
let iframeEl = el.querySelector('iframe');
|
||||
if (!iframeEl) {
|
||||
iframeEl = frame.contentDocument.createElement('iframe');
|
||||
iframeEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;';
|
||||
iframeEl.setAttribute('allow', 'accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
|
||||
iframeEl.setAttribute('allowfullscreen', '');
|
||||
el.appendChild(iframeEl);
|
||||
}
|
||||
iframeEl.src = result.url;
|
||||
iframeEl.style.display = 'block';
|
||||
// Hide video and placeholder
|
||||
const videoEl = el.querySelector('video');
|
||||
if (videoEl) videoEl.style.display = 'none';
|
||||
const ph = el.querySelector('.video-placeholder, .bg-video-placeholder');
|
||||
if (ph) ph.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (videoSrcApply) {
|
||||
videoSrcApply.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected) return;
|
||||
const url = videoSrcInput.value.trim();
|
||||
if (!url) return;
|
||||
selected.addAttributes({ videoUrl: url });
|
||||
applyVideoAndRefreshCanvas(selected, url);
|
||||
loadVideoSrcValues(selected);
|
||||
});
|
||||
}
|
||||
|
||||
if (videoSrcInput) {
|
||||
videoSrcInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
videoSrcApply?.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (videoSrcBrowse) {
|
||||
videoSrcBrowse.addEventListener('click', async () => {
|
||||
if (!window.assetManager) return;
|
||||
const asset = await window.assetManager.openBrowser('video');
|
||||
if (asset && asset.url) {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected) return;
|
||||
videoSrcInput.value = asset.url;
|
||||
selected.addAttributes({ videoUrl: asset.url });
|
||||
applyVideoAndRefreshCanvas(selected, asset.url);
|
||||
loadVideoSrcValues(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Spacing presets
|
||||
document.querySelectorAll('.spacing-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -2577,102 +3028,92 @@
|
||||
// ==========================================
|
||||
|
||||
// Sync navigation with pages
|
||||
// Helper: find nav-links container and CTA insert position
|
||||
function getNavLinksContainer(navComponent) {
|
||||
const linksContainer = navComponent.components().find(c => c.getClasses().includes('nav-links'));
|
||||
if (!linksContainer) return null;
|
||||
const links = linksContainer.components();
|
||||
let insertIndex = links.length;
|
||||
links.forEach((link, index) => {
|
||||
if (link.getClasses().includes('nav-cta')) insertIndex = index;
|
||||
});
|
||||
return { container: linksContainer, insertIndex };
|
||||
}
|
||||
|
||||
const defaultLinkStyle = {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
'font-size': '15px',
|
||||
'font-family': 'Inter, sans-serif'
|
||||
};
|
||||
|
||||
// Sync navigation with pages -- creates links to actual page files
|
||||
syncNavPagesBtn.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected || !isNavigation(selected)) return;
|
||||
const nav = getNavLinksContainer(selected);
|
||||
if (!nav) { alert('Navigation structure not recognized.'); return; }
|
||||
|
||||
// Find the nav-links container
|
||||
const linksContainer = selected.components().find(c => {
|
||||
const classes = c.getClasses();
|
||||
return classes.includes('nav-links');
|
||||
});
|
||||
|
||||
if (!linksContainer) {
|
||||
alert('Navigation structure not recognized. Please use the Navigation block.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get CTA button if exists (keep it)
|
||||
const existingLinks = linksContainer.components();
|
||||
// Preserve CTA
|
||||
let ctaLink = null;
|
||||
existingLinks.forEach(link => {
|
||||
const classes = link.getClasses();
|
||||
if (classes.includes('nav-cta')) {
|
||||
ctaLink = link.clone();
|
||||
}
|
||||
nav.container.components().forEach(link => {
|
||||
if (link.getClasses().includes('nav-cta')) ctaLink = link.clone();
|
||||
});
|
||||
|
||||
// Clear existing links
|
||||
linksContainer.components().reset();
|
||||
nav.container.components().reset();
|
||||
|
||||
// Add links for each page
|
||||
pages.forEach((page, index) => {
|
||||
linksContainer.components().add({
|
||||
// Add a link for each page using clean slugs (no .html)
|
||||
pages.forEach(page => {
|
||||
const href = page.slug === 'index' ? '/' : page.slug;
|
||||
nav.container.components().add({
|
||||
tagName: 'a',
|
||||
attributes: { href: page.slug === 'index' ? '#' : `#${page.slug}` },
|
||||
style: {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
'font-size': '15px',
|
||||
'font-family': 'Inter, sans-serif'
|
||||
},
|
||||
attributes: { href },
|
||||
style: { ...defaultLinkStyle },
|
||||
content: page.name
|
||||
});
|
||||
});
|
||||
|
||||
// Re-add CTA if existed
|
||||
if (ctaLink) {
|
||||
linksContainer.components().add(ctaLink);
|
||||
}
|
||||
|
||||
// Refresh the links list UI
|
||||
if (ctaLink) nav.container.components().add(ctaLink);
|
||||
loadNavLinks(selected);
|
||||
});
|
||||
|
||||
// Add new link to navigation
|
||||
// Add page link
|
||||
addNavLinkBtn.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected || !isNavigation(selected)) return;
|
||||
const nav = getNavLinksContainer(selected);
|
||||
if (!nav) return;
|
||||
|
||||
// Find the nav-links container
|
||||
const linksContainer = selected.components().find(c => {
|
||||
const classes = c.getClasses();
|
||||
return classes.includes('nav-links');
|
||||
});
|
||||
|
||||
if (!linksContainer) {
|
||||
alert('Navigation structure not recognized. Please use the Navigation block.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find position to insert (before CTA if exists)
|
||||
const links = linksContainer.components();
|
||||
let insertIndex = links.length;
|
||||
|
||||
links.forEach((link, index) => {
|
||||
const classes = link.getClasses();
|
||||
if (classes.includes('nav-cta')) {
|
||||
insertIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
// Add new link
|
||||
linksContainer.components().add({
|
||||
nav.container.components().add({
|
||||
tagName: 'a',
|
||||
attributes: { href: '#' },
|
||||
style: {
|
||||
'color': '#4b5563',
|
||||
'text-decoration': 'none',
|
||||
'font-size': '15px',
|
||||
'font-family': 'Inter, sans-serif'
|
||||
},
|
||||
attributes: { href: '/' },
|
||||
style: { ...defaultLinkStyle },
|
||||
content: 'New Link'
|
||||
}, { at: insertIndex });
|
||||
}, { at: nav.insertIndex });
|
||||
|
||||
// Refresh the links list UI
|
||||
loadNavLinks(selected);
|
||||
});
|
||||
|
||||
// Add external link
|
||||
const addNavExternalBtn = document.getElementById('add-nav-external');
|
||||
if (addNavExternalBtn) {
|
||||
addNavExternalBtn.addEventListener('click', () => {
|
||||
const selected = editor.getSelected();
|
||||
if (!selected || !isNavigation(selected)) return;
|
||||
const nav = getNavLinksContainer(selected);
|
||||
if (!nav) return;
|
||||
|
||||
nav.container.components().add({
|
||||
tagName: 'a',
|
||||
attributes: { href: 'https://', target: '_blank', rel: 'noopener' },
|
||||
style: { ...defaultLinkStyle },
|
||||
content: 'External Link'
|
||||
}, { at: nav.insertIndex });
|
||||
|
||||
loadNavLinks(selected);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Link Editing
|
||||
// ==========================================
|
||||
@@ -2808,7 +3249,7 @@
|
||||
const styles = component.getStyle();
|
||||
|
||||
// Reset all active states
|
||||
document.querySelectorAll('.color-preset, .size-preset, .spacing-preset, .radius-preset, .thickness-preset, .font-preset, .weight-preset, .gradient-preset')
|
||||
document.querySelectorAll('.color-preset, .size-preset, .spacing-preset, .radius-preset, .thickness-preset, .font-preset, .weight-preset, .gradient-preset, .alignment-preset')
|
||||
.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Set active text color
|
||||
@@ -3611,14 +4052,50 @@
|
||||
currentPageId: currentPageId
|
||||
}));
|
||||
|
||||
// Open preview
|
||||
window.open('preview.html', '_blank');
|
||||
// Open preview (pass site_id in WHP mode so Back button works)
|
||||
const previewUrl = (window.WHP_CONFIG)
|
||||
? 'preview.html?site_id=' + WHP_CONFIG.siteId
|
||||
: 'preview.html';
|
||||
window.open(previewUrl, '_blank');
|
||||
});
|
||||
|
||||
// Make editor accessible globally for debugging
|
||||
window.editor = editor;
|
||||
window.sitePages = pages;
|
||||
|
||||
// ==========================================
|
||||
// WHP Mode: Asset Manager integration
|
||||
// ==========================================
|
||||
const isWHP = !!window.WHP_CONFIG;
|
||||
|
||||
if (isWHP) {
|
||||
// Override asset manager upload URL to use WHP API
|
||||
editor.AssetManager.getConfig().upload = WHP_CONFIG.apiUrl + '?action=upload_asset&site_id=' + WHP_CONFIG.siteId;
|
||||
editor.AssetManager.getConfig().uploadName = 'file';
|
||||
|
||||
// Custom headers for CSRF protection
|
||||
editor.AssetManager.getConfig().headers = {
|
||||
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||
};
|
||||
|
||||
// Load existing assets from WHP API
|
||||
fetch(WHP_CONFIG.apiUrl + '?action=list_assets&site_id=' + WHP_CONFIG.siteId)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success && data.assets) {
|
||||
data.assets.forEach(asset => {
|
||||
const isImage = asset.type && (asset.type === 'image' || asset.type.startsWith('image/'));
|
||||
editor.AssetManager.add({
|
||||
src: asset.url,
|
||||
name: asset.name,
|
||||
type: isImage ? 'image' : 'file'
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(e => console.log('Failed to load assets:', e));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Feature: Delete Section (parent + children)
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user