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:
2026-04-26 21:31:58 -07:00
parent 8eeaecd857
commit 606c9b78c8
8 changed files with 1591 additions and 364 deletions

View File

@@ -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)
// ==========================================