Files
site-builder/js/editor.js

4528 lines
200 KiB
JavaScript
Raw Normal View History

/**
* Site Builder - GrapesJS Editor Configuration
*/
(function() {
'use strict';
// Storage key for localStorage
const STORAGE_KEY = 'sitebuilder-project';
// Initialize GrapesJS Editor
const editor = grapesjs.init({
container: '#gjs',
height: '100%',
width: 'auto',
fromElement: false,
// Storage configuration - localStorage
storageManager: {
type: 'local',
autosave: true,
autoload: true,
stepsBeforeSave: 1,
options: {
local: {
key: STORAGE_KEY
}
}
},
// Device manager for responsive preview
deviceManager: {
devices: [
{
name: 'Desktop',
width: ''
},
{
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
name: 'Mobile',
width: '375px',
widthMedia: '480px'
}
]
},
// Layer manager configuration
layerManager: {
appendTo: '#layers-container'
},
// Block manager configuration
blockManager: {
appendTo: '#blocks-container'
},
// Style manager configuration
styleManager: {
appendTo: '#advanced-styles',
sectors: [
{
name: 'Dimension',
open: false,
properties: [
'width', 'min-width', 'max-width',
'height', 'min-height', 'max-height',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left'
]
},
{
name: 'Typography',
open: false,
properties: [
{
property: 'font-family',
type: 'select',
options: [
{ id: 'Inter, sans-serif', label: 'Inter' },
{ id: 'Roboto, sans-serif', label: 'Roboto' },
{ id: 'Open Sans, sans-serif', label: 'Open Sans' },
{ id: 'Poppins, sans-serif', label: 'Poppins' },
{ id: 'Montserrat, sans-serif', label: 'Montserrat' },
{ id: 'Playfair Display, serif', label: 'Playfair Display' },
{ id: 'Merriweather, serif', label: 'Merriweather' },
{ id: 'Source Code Pro, monospace', label: 'Source Code Pro' },
{ id: 'Arial, Helvetica, sans-serif', label: 'Arial' },
{ id: 'Georgia, serif', label: 'Georgia' },
{ id: 'Times New Roman, serif', label: 'Times New Roman' }
]
},
'font-size',
{
property: 'font-weight',
type: 'select',
options: [
{ id: '100', label: 'Thin (100)' },
{ id: '200', label: 'Extra Light (200)' },
{ id: '300', label: 'Light (300)' },
{ id: '400', label: 'Normal (400)' },
{ id: '500', label: 'Medium (500)' },
{ id: '600', label: 'Semi Bold (600)' },
{ id: '700', label: 'Bold (700)' },
{ id: '800', label: 'Extra Bold (800)' },
{ id: '900', label: 'Black (900)' }
]
},
{
property: 'letter-spacing',
type: 'number',
units: ['px', 'em', 'rem'],
default: '0',
step: 0.1
},
{
property: 'line-height',
type: 'number',
units: ['px', 'em', '%', ''],
default: 'normal',
step: 0.1
},
{
property: 'text-align',
type: 'radio',
options: [
{ id: 'left', label: 'Left', className: 'fa fa-align-left' },
{ id: 'center', label: 'Center', className: 'fa fa-align-center' },
{ id: 'right', label: 'Right', className: 'fa fa-align-right' },
{ id: 'justify', label: 'Justify', className: 'fa fa-align-justify' }
]
},
'text-decoration', 'text-transform', 'color'
]
},
{
name: 'Background',
open: false,
properties: [
'background-color', 'background-image', 'background-repeat',
'background-position', 'background-size', 'background-attachment'
]
},
{
name: 'Border',
open: false,
properties: [
'border-radius', 'border-radius-top-left', 'border-radius-top-right',
'border-radius-bottom-left', 'border-radius-bottom-right',
'border', 'border-width', 'border-style', 'border-color'
]
},
{
name: 'Effects',
open: false,
properties: [
'opacity', 'box-shadow', 'transition'
]
},
{
name: 'Layout',
open: false,
properties: [
'display', 'position', 'top', 'right', 'bottom', 'left',
'flex-direction', 'flex-wrap', 'justify-content', 'align-items',
'align-content', 'gap', 'order', 'flex-basis', 'flex-grow', 'flex-shrink'
]
}
]
},
// Trait manager configuration
traitManager: {
appendTo: '#traits-container'
},
// Selector manager (CSS classes)
selectorManager: {
appendTo: '#advanced-styles',
componentFirst: true
},
// Plugins - using global function references from CDN scripts
plugins: [
window['gjs-blocks-basic'],
window['grapesjs-preset-webpage'],
window['grapesjs-plugin-forms'],
window['grapesjs-style-gradient']
].filter(Boolean), // Filter out any undefined plugins
pluginsOpts: {
[window['gjs-blocks-basic']]: {
flexGrid: true,
blocks: ['column1', 'column2', 'column3', 'column3-7', 'text', 'link', 'map']
},
[window['grapesjs-preset-webpage']]: {
modalImportTitle: 'Import Template',
modalImportLabel: '<div style="margin-bottom: 10px;">Paste HTML/CSS here</div>',
modalImportContent: '',
importViewerRecords: true,
textCleanCanvas: 'Are you sure you want to clear the canvas?',
showStylesOnChange: true,
useCustomTheme: false,
blocks: ['link-block', 'quote', 'text-basic']
},
[window['grapesjs-plugin-forms']]: {
blocks: ['form', 'input', 'textarea', 'select', 'button', 'label', 'checkbox', 'radio']
},
[window['grapesjs-style-gradient']]: {
colorPicker: 'default'
}
},
// Canvas configuration
canvas: {
styles: [
// Google Fonts - Popular choices
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap',
// Responsive images and base styles injected inline
'data:text/css,' + encodeURIComponent(`
/* Responsive images */
img {
max-width: 100%;
height: auto;
}
/* Responsive video containers */
video {
max-width: 100%;
height: auto;
}
/* Responsive columns on mobile */
@media (max-width: 480px) {
.row {
flex-direction: column !important;
}
.row .cell {
flex-basis: 100% !important;
width: 100% !important;
}
}
/* Editor-only anchor visualization */
.editor-anchor {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 28px;
border: 1px dashed #9ca3af;
padding: 4px 8px;
background: rgba(59,130,246,0.05);
border-radius: 4px;
}
.editor-anchor .anchor-icon {
font-size: 14px;
color: #6b7280;
line-height: 1;
}
.editor-anchor .anchor-name-input {
border: none;
background: transparent;
color: #374151;
font-size: 12px;
font-family: Inter, sans-serif;
font-weight: 500;
padding: 2px 4px;
outline: none;
min-width: 80px;
}
.editor-anchor .anchor-name-input:focus {
background: rgba(255,255,255,0.5);
border-radius: 2px;
}
`)
],
scripts: []
}
});
// ==========================================
// UI Elements
// ==========================================
const saveStatus = document.getElementById('save-status');
const statusText = saveStatus.querySelector('.status-text');
// ==========================================
// Add Custom Blocks (Columns, Hero, etc.)
// ==========================================
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',
category: 'Layout',
content: {
tagName: 'section',
style: {
'padding': '60px 20px',
'min-height': '200px',
'background-color': '#ffffff'
},
components: [
{
tagName: 'div',
style: {
'max-width': '1200px',
'margin': '0 auto'
},
components: []
}
]
},
attributes: { class: 'fa fa-columns' }
});
// Logo Block
blockManager.add('logo', {
label: 'Logo',
category: 'Layout',
content: `<a href="#" class="site-logo" style="display:inline-flex;align-items:center;gap:10px;text-decoration:none;">
<div style="width:40px;height:40px;background:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 100%);border-radius:8px;display:flex;align-items:center;justify-content:center;">
<span style="color:#fff;font-weight:700;font-size:18px;font-family:Inter,sans-serif;">S</span>
</div>
<span style="font-size:20px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">SiteName</span>
</a>`,
attributes: { class: 'fa fa-bookmark' }
});
// Navigation Menu (will be updated dynamically based on pages)
blockManager.add('navbar', {
label: 'Navigation',
category: 'Layout',
content: {
type: 'navbar',
tagName: 'nav',
attributes: { class: 'site-navbar', 'data-dynamic-nav': 'true' },
style: {
'display': 'flex',
'align-items': 'center',
'justify-content': 'space-between',
'padding': '16px 24px',
'background': '#ffffff',
'border-bottom': '1px solid #e5e7eb'
},
components: [
{
tagName: 'a',
attributes: { href: '#', class: 'nav-logo' },
style: {
'font-size': '20px',
'font-weight': '700',
'color': '#1f2937',
'text-decoration': 'none',
'font-family': 'Inter, sans-serif'
},
content: 'Logo'
},
{
tagName: 'div',
attributes: { class: 'nav-links' },
style: {
'display': 'flex',
'gap': '24px',
'align-items': 'center'
},
components: [
{
tagName: 'a',
attributes: { href: '#' },
style: {
'color': '#4b5563',
'text-decoration': 'none',
'font-size': '15px',
'font-family': 'Inter, sans-serif'
},
content: 'Home'
},
{
tagName: 'a',
attributes: { href: '#' },
style: {
'color': '#4b5563',
'text-decoration': 'none',
'font-size': '15px',
'font-family': 'Inter, sans-serif'
},
content: 'About'
},
{
tagName: 'a',
attributes: { href: '#' },
style: {
'color': '#4b5563',
'text-decoration': 'none',
'font-size': '15px',
'font-family': 'Inter, sans-serif'
},
content: 'Contact'
},
{
tagName: 'a',
attributes: { href: '#', class: 'nav-cta' },
style: {
'display': 'inline-block',
'padding': '10px 20px',
'background': '#3b82f6',
'color': '#fff',
'text-decoration': 'none',
'border-radius': '6px',
'font-size': '14px',
'font-weight': '500',
'font-family': 'Inter, sans-serif'
},
content: 'Get Started'
}
]
}
]
},
attributes: { class: 'fa fa-bars' }
});
// Section with Background (Image/Video + Overlay)
blockManager.add('section-bg', {
label: 'Section (Background)',
category: 'Layout',
content: {
tagName: 'section',
attributes: { class: 'section-with-bg', 'data-bg-section': 'true' },
style: {
'position': 'relative',
'min-height': '400px',
'display': 'flex',
'align-items': 'center',
'justify-content': 'center',
'background-image': 'url(https://images.unsplash.com/photo-1557683316-973673baf926?w=1920)',
'background-size': 'cover',
'background-position': 'center',
'padding': '60px 20px',
'overflow': 'hidden'
},
components: [
// Overlay
{
tagName: 'div',
attributes: { class: 'bg-overlay' },
style: {
'position': 'absolute',
'top': '0',
'left': '0',
'right': '0',
'bottom': '0',
'background': 'rgba(0,0,0,0.5)',
'z-index': '1'
},
selectable: true,
hoverable: true
},
// Content container
{
tagName: 'div',
attributes: { class: 'bg-content' },
style: {
'position': 'relative',
'z-index': '2',
'max-width': '800px',
'text-align': 'center'
},
components: [
{
tagName: 'h2',
style: {
'color': '#ffffff',
'font-size': '36px',
'font-weight': '700',
'margin-bottom': '16px',
'font-family': 'Inter, sans-serif'
},
content: 'Your Heading Here'
},
{
tagName: 'p',
style: {
'color': 'rgba(255,255,255,0.9)',
'font-size': '18px',
'line-height': '1.6',
'font-family': 'Inter, sans-serif'
},
content: 'Add your content here. This section supports background images and videos with customizable overlay.'
}
]
}
]
},
attributes: { class: 'fa fa-image' }
});
// Section with Video Background
blockManager.add('section-video-bg', {
label: 'Section (Video BG)',
category: 'Layout',
content: `<section class="section-with-video-bg" data-video-section="true" style="position:relative;min-height:400px;display:flex;align-items:center;justify-content:center;padding:60px 20px;overflow:hidden;">
<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;">
<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 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 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="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;">
<h2 style="color:#ffffff;font-size:36px;font-weight:700;margin-bottom:16px;font-family:Inter,sans-serif;">Video Background</h2>
<p style="color:rgba(255,255,255,0.9);font-size:18px;line-height:1.6;font-family:Inter,sans-serif;">This section has a looping video background with an overlay.</p>
</div>
</section>`,
attributes: { class: 'fa fa-play-circle' }
});
// Footer
blockManager.add('footer', {
label: 'Footer',
category: 'Layout',
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;">
<a href="#" style="color:#9ca3af;text-decoration:none;font-size:14px;font-family:Inter,sans-serif;">Privacy Policy</a>
<a href="#" style="color:#9ca3af;text-decoration:none;font-size:14px;font-family:Inter,sans-serif;">Terms of Service</a>
<a href="#" style="color:#9ca3af;text-decoration:none;font-size:14px;font-family:Inter,sans-serif;">Contact</a>
</div>
<p style="font-size:14px;font-family:Inter,sans-serif;">© 2024 Your Company. All rights reserved.</p>
</div>
</footer>`,
attributes: { class: 'fa fa-window-minimize' }
});
// Column Layouts
blockManager.add('column-1', {
label: '1 Column',
category: 'Layout',
content: `<div class="row" data-gjs-droppable=".cell" data-gjs-resizable='{"tl":0,"tc":0,"tr":0,"cl":0,"cr":0,"bl":0,"br":0}' style="display:flex;flex-wrap:wrap;padding:10px;">
<div class="cell" data-gjs-draggable=".row" data-gjs-resizable='{"tl":0,"tc":0,"tr":0,"cl":0,"cr":1,"bl":0,"br":0}' style="flex:1;min-height:75px;padding:10px;"></div>
</div>`,
attributes: { class: 'gjs-fonts gjs-f-b1' }
});
blockManager.add('column-2', {
label: '2 Columns',
category: 'Layout',
content: `<div class="row" data-gjs-droppable=".cell" style="display:flex;flex-wrap:wrap;padding:10px;">
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
</div>`,
attributes: { class: 'gjs-fonts gjs-f-b2' }
});
blockManager.add('column-3', {
label: '3 Columns',
category: 'Layout',
content: `<div class="row" data-gjs-droppable=".cell" style="display:flex;flex-wrap:wrap;padding:10px;">
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
</div>`,
attributes: { class: 'gjs-fonts gjs-f-b3' }
});
blockManager.add('column-4', {
label: '4 Columns',
category: 'Layout',
content: `<div class="row" data-gjs-droppable=".cell" style="display:flex;flex-wrap:wrap;padding:10px;">
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex:1;min-height:75px;padding:10px;"></div>
</div>`,
attributes: { class: 'gjs-fonts gjs-f-b4' }
});
blockManager.add('column-3-7', {
label: '2 Columns 3/7',
category: 'Layout',
content: `<div class="row" data-gjs-droppable=".cell" style="display:flex;flex-wrap:wrap;padding:10px;">
<div class="cell" data-gjs-draggable=".row" style="flex-basis:30%;min-height:75px;padding:10px;"></div>
<div class="cell" data-gjs-draggable=".row" style="flex-basis:70%;min-height:75px;padding:10px;"></div>
</div>`,
attributes: { class: 'gjs-fonts gjs-f-b37' }
});
// Image Block
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">',
attributes: { class: 'fa fa-image' }
});
// Unified Video Block (YouTube, Vimeo, or direct file)
blockManager.add('video-block', {
label: 'Video',
category: 'Media',
content: `<div class="video-wrapper" data-video-wrapper="true" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;border-radius:8px;background:#1a1a2e;">
<iframe class="video-frame" style="position:absolute;top:0;left:0;width:100%;height:100%;border:none;display:none;" src="" title="Video" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<video class="video-player" style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;display:none;" controls></video>
<div class="video-placeholder" style="position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;color:#fff;text-align:center;font-family:Inter,sans-serif;padding:20px;">
<div>
<div style="font-size:48px;margin-bottom:10px;"></div>
<div style="font-size:14px;opacity:0.8;">Select container & add Video URL in Settings</div>
<div style="font-size:12px;opacity:0.6;margin-top:8px;">Supports YouTube, Vimeo, or direct video files</div>
</div>
</div>
</div>`,
attributes: { class: 'fa fa-play-circle' }
});
// Hero with Image Background
blockManager.add('hero-image', {
label: 'Hero (Image)',
category: 'Sections',
content: `<section style="min-height:500px;display:flex;align-items:center;justify-content:center;background-image:url('https://images.unsplash.com/photo-1557683316-973673baf926?w=1920');background-size:cover;background-position:center;position:relative;padding:60px 20px;text-align:center;">
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);"></div>
<div style="position:relative;z-index:1;max-width:800px;">
<h1 style="color:#fff;font-size:48px;font-weight:700;margin-bottom:20px;font-family:Inter,sans-serif;">Your Headline Here</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;">Add your compelling subheadline or description text here to engage your visitors.</p>
<a href="#" style="display:inline-block;padding:16px 40px;background:#3b82f6;color:#fff;font-size:18px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Get Started</a>
</div>
</section>`,
attributes: { class: 'fa fa-image' }
});
// Hero with Video Background
blockManager.add('hero-video', {
label: 'Hero (Video)',
category: 'Sections',
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;">
<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;">
<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 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 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="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>
<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>
</div>
</section>`,
attributes: { class: 'fa fa-play-circle' }
});
// Simple Hero Section
blockManager.add('hero-simple', {
label: 'Hero (Simple)',
category: 'Sections',
content: `<section style="min-height:400px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);padding:60px 20px;text-align:center;">
<div style="max-width:600px;">
<h1 style="color:#fff;font-size:48px;font-weight:700;margin-bottom:20px;font-family:Inter,sans-serif;">Welcome</h1>
<p style="color:rgba(255,255,255,0.9);font-size:18px;line-height:1.6;margin-bottom:30px;font-family:Inter,sans-serif;">Your introductory text goes here.</p>
<a href="#" style="display:inline-block;padding:14px 32px;background:#fff;color:#667eea;font-size:16px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Call to Action</a>
</div>
</section>`,
attributes: { class: 'fa fa-star' }
});
// Features Section
blockManager.add('features-section', {
label: 'Features Grid',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#f9fafb;">
<div style="max-width:1200px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:50px;color:#1f2937;font-family:Inter,sans-serif;">Features</h2>
<div style="display:flex;flex-wrap:wrap;gap:30px;justify-content:center;">
<div style="flex:1;min-width:280px;max-width:350px;padding:30px;background:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,0.1);">
<div style="width:50px;height:50px;background:#3b82f6;border-radius:10px;margin-bottom:20px;"></div>
<h3 style="font-size:20px;font-weight:600;margin-bottom:12px;color:#1f2937;font-family:Inter,sans-serif;">Feature One</h3>
<p style="color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Description of your first amazing feature goes here.</p>
</div>
<div style="flex:1;min-width:280px;max-width:350px;padding:30px;background:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,0.1);">
<div style="width:50px;height:50px;background:#10b981;border-radius:10px;margin-bottom:20px;"></div>
<h3 style="font-size:20px;font-weight:600;margin-bottom:12px;color:#1f2937;font-family:Inter,sans-serif;">Feature Two</h3>
<p style="color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Description of your second amazing feature goes here.</p>
</div>
<div style="flex:1;min-width:280px;max-width:350px;padding:30px;background:#fff;border-radius:12px;box-shadow:0 4px 6px rgba(0,0,0,0.1);">
<div style="width:50px;height:50px;background:#f59e0b;border-radius:10px;margin-bottom:20px;"></div>
<h3 style="font-size:20px;font-weight:600;margin-bottom:12px;color:#1f2937;font-family:Inter,sans-serif;">Feature Three</h3>
<p style="color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Description of your third amazing feature goes here.</p>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-th-large' }
});
// Testimonials Section
blockManager.add('testimonials-section', {
label: 'Testimonials',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#ffffff;">
<div style="max-width:1200px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:16px;color:#1f2937;font-family:Inter,sans-serif;">What People Say</h2>
<p style="text-align:center;font-size:18px;color:#6b7280;margin-bottom:50px;max-width:600px;margin-left:auto;margin-right:auto;font-family:Inter,sans-serif;">Hear from our satisfied customers</p>
<div style="display:flex;flex-wrap:wrap;gap:30px;justify-content:center;">
<div style="flex:1;min-width:300px;max-width:380px;padding:30px;background:#f9fafb;border-radius:12px;">
<div style="display:flex;gap:4px;margin-bottom:16px;">
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
</div>
<p style="color:#374151;line-height:1.7;font-size:16px;margin-bottom:20px;font-family:Inter,sans-serif;font-style:italic;">"This product has completely transformed how we work. The results speak for themselves."</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;background:#3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-family:Inter,sans-serif;">JD</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">John Doe</div>
<div style="font-size:14px;color:#6b7280;font-family:Inter,sans-serif;">CEO, Company Inc</div>
</div>
</div>
</div>
<div style="flex:1;min-width:300px;max-width:380px;padding:30px;background:#f9fafb;border-radius:12px;">
<div style="display:flex;gap:4px;margin-bottom:16px;">
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
</div>
<p style="color:#374151;line-height:1.7;font-size:16px;margin-bottom:20px;font-family:Inter,sans-serif;font-style:italic;">"Exceptional quality and outstanding customer service. I couldn't be happier with my experience."</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;background:#10b981;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-family:Inter,sans-serif;">JS</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">Jane Smith</div>
<div style="font-size:14px;color:#6b7280;font-family:Inter,sans-serif;">Designer, Studio Co</div>
</div>
</div>
</div>
<div style="flex:1;min-width:300px;max-width:380px;padding:30px;background:#f9fafb;border-radius:12px;">
<div style="display:flex;gap:4px;margin-bottom:16px;">
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
<span style="color:#f59e0b;font-size:20px;"></span>
</div>
<p style="color:#374151;line-height:1.7;font-size:16px;margin-bottom:20px;font-family:Inter,sans-serif;font-style:italic;">"A game-changer for our business. The ROI has been incredible from day one."</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;background:#8b5cf6;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-family:Inter,sans-serif;">MB</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">Mike Brown</div>
<div style="font-size:14px;color:#6b7280;font-family:Inter,sans-serif;">Founder, StartupXYZ</div>
</div>
</div>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-comments' }
});
// Pricing Section
blockManager.add('pricing-section', {
label: 'Pricing Table',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#f9fafb;">
<div style="max-width:1200px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:16px;color:#1f2937;font-family:Inter,sans-serif;">Simple Pricing</h2>
<p style="text-align:center;font-size:18px;color:#6b7280;margin-bottom:50px;font-family:Inter,sans-serif;">Choose the plan that's right for you</p>
<div style="display:flex;flex-wrap:wrap;gap:30px;justify-content:center;align-items:stretch;">
<div style="flex:1;min-width:280px;max-width:350px;padding:40px 30px;background:#fff;border-radius:16px;border:2px solid #e5e7eb;text-align:center;">
<h3 style="font-size:24px;font-weight:600;margin-bottom:8px;color:#1f2937;font-family:Inter,sans-serif;">Starter</h3>
<p style="font-size:14px;color:#6b7280;margin-bottom:24px;font-family:Inter,sans-serif;">Perfect for individuals</p>
<div style="margin-bottom:24px;">
<span style="font-size:48px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">$9</span>
<span style="font-size:16px;color:#6b7280;font-family:Inter,sans-serif;">/month</span>
</div>
<ul style="list-style:none;padding:0;margin:0 0 30px 0;text-align:left;">
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> 5 Projects</li>
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> Basic Support</li>
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> 1GB Storage</li>
<li style="padding:12px 0;color:#374151;font-family:Inter,sans-serif;"> Community Access</li>
</ul>
<a href="#" style="display:block;padding:14px 24px;background:#fff;color:#3b82f6;font-size:16px;font-weight:600;text-decoration:none;border-radius:8px;border:2px solid #3b82f6;font-family:Inter,sans-serif;">Get Started</a>
</div>
<div style="flex:1;min-width:280px;max-width:350px;padding:40px 30px;background:#3b82f6;border-radius:16px;text-align:center;transform:scale(1.05);">
<div style="background:#2563eb;color:#fff;font-size:12px;font-weight:600;padding:6px 16px;border-radius:20px;display:inline-block;margin-bottom:16px;font-family:Inter,sans-serif;">MOST POPULAR</div>
<h3 style="font-size:24px;font-weight:600;margin-bottom:8px;color:#fff;font-family:Inter,sans-serif;">Professional</h3>
<p style="font-size:14px;color:rgba(255,255,255,0.8);margin-bottom:24px;font-family:Inter,sans-serif;">Best for growing teams</p>
<div style="margin-bottom:24px;">
<span style="font-size:48px;font-weight:700;color:#fff;font-family:Inter,sans-serif;">$29</span>
<span style="font-size:16px;color:rgba(255,255,255,0.8);font-family:Inter,sans-serif;">/month</span>
</div>
<ul style="list-style:none;padding:0;margin:0 0 30px 0;text-align:left;">
<li style="padding:12px 0;border-bottom:1px solid rgba(255,255,255,0.2);color:#fff;font-family:Inter,sans-serif;"> Unlimited Projects</li>
<li style="padding:12px 0;border-bottom:1px solid rgba(255,255,255,0.2);color:#fff;font-family:Inter,sans-serif;"> Priority Support</li>
<li style="padding:12px 0;border-bottom:1px solid rgba(255,255,255,0.2);color:#fff;font-family:Inter,sans-serif;"> 10GB Storage</li>
<li style="padding:12px 0;color:#fff;font-family:Inter,sans-serif;"> Advanced Analytics</li>
</ul>
<a href="#" style="display:block;padding:14px 24px;background:#fff;color:#3b82f6;font-size:16px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Get Started</a>
</div>
<div style="flex:1;min-width:280px;max-width:350px;padding:40px 30px;background:#fff;border-radius:16px;border:2px solid #e5e7eb;text-align:center;">
<h3 style="font-size:24px;font-weight:600;margin-bottom:8px;color:#1f2937;font-family:Inter,sans-serif;">Enterprise</h3>
<p style="font-size:14px;color:#6b7280;margin-bottom:24px;font-family:Inter,sans-serif;">For large organizations</p>
<div style="margin-bottom:24px;">
<span style="font-size:48px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">$99</span>
<span style="font-size:16px;color:#6b7280;font-family:Inter,sans-serif;">/month</span>
</div>
<ul style="list-style:none;padding:0;margin:0 0 30px 0;text-align:left;">
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> Everything in Pro</li>
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> Dedicated Support</li>
<li style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#374151;font-family:Inter,sans-serif;"> Unlimited Storage</li>
<li style="padding:12px 0;color:#374151;font-family:Inter,sans-serif;"> Custom Integrations</li>
</ul>
<a href="#" style="display:block;padding:14px 24px;background:#fff;color:#3b82f6;font-size:16px;font-weight:600;text-decoration:none;border-radius:8px;border:2px solid #3b82f6;font-family:Inter,sans-serif;">Contact Sales</a>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-credit-card' }
});
// Contact Section
blockManager.add('contact-section', {
label: 'Contact Section',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#ffffff;">
<div style="max-width:1200px;margin:0 auto;">
<div style="display:flex;flex-wrap:wrap;gap:60px;">
<div style="flex:1;min-width:300px;">
<h2 style="font-size:36px;font-weight:700;margin-bottom:16px;color:#1f2937;font-family:Inter,sans-serif;">Get in Touch</h2>
<p style="font-size:18px;color:#6b7280;line-height:1.7;margin-bottom:30px;font-family:Inter,sans-serif;">Have questions? We'd love to hear from you. Send us a message and we'll respond as soon as possible.</p>
<div style="margin-bottom:24px;">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;">
<div style="width:48px;height:48px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#3b82f6;font-size:20px;">📍</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">Address</div>
<div style="color:#6b7280;font-family:Inter,sans-serif;">123 Business Street, City, ST 12345</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;">
<div style="width:48px;height:48px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#3b82f6;font-size:20px;">📧</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">Email</div>
<div style="color:#6b7280;font-family:Inter,sans-serif;">hello@example.com</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:48px;height:48px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#3b82f6;font-size:20px;">📞</div>
<div>
<div style="font-weight:600;color:#1f2937;font-family:Inter,sans-serif;">Phone</div>
<div style="color:#6b7280;font-family:Inter,sans-serif;">(555) 123-4567</div>
</div>
</div>
</div>
</div>
<div style="flex:1;min-width:300px;background:#f9fafb;padding:40px;border-radius:16px;">
<form>
<div style="margin-bottom:20px;">
<label style="display:block;font-weight:500;margin-bottom:8px;color:#374151;font-family:Inter,sans-serif;">Name</label>
<input type="text" placeholder="Your name" style="width:100%;padding:14px 16px;border:1px solid #d1d5db;border-radius:8px;font-size:16px;font-family:Inter,sans-serif;box-sizing:border-box;">
</div>
<div style="margin-bottom:20px;">
<label style="display:block;font-weight:500;margin-bottom:8px;color:#374151;font-family:Inter,sans-serif;">Email</label>
<input type="email" placeholder="your@email.com" style="width:100%;padding:14px 16px;border:1px solid #d1d5db;border-radius:8px;font-size:16px;font-family:Inter,sans-serif;box-sizing:border-box;">
</div>
<div style="margin-bottom:20px;">
<label style="display:block;font-weight:500;margin-bottom:8px;color:#374151;font-family:Inter,sans-serif;">Message</label>
<textarea placeholder="How can we help?" rows="4" style="width:100%;padding:14px 16px;border:1px solid #d1d5db;border-radius:8px;font-size:16px;font-family:Inter,sans-serif;resize:vertical;box-sizing:border-box;"></textarea>
</div>
<button type="submit" style="width:100%;padding:16px 24px;background:#3b82f6;color:#fff;font-size:16px;font-weight:600;border:none;border-radius:8px;cursor:pointer;font-family:Inter,sans-serif;">Send Message</button>
</form>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-envelope' }
});
// Call to Action Section
blockManager.add('cta-section', {
label: 'Call to Action',
category: 'Sections',
content: `<section style="padding:80px 20px;background:linear-gradient(135deg,#3b82f6 0%,#8b5cf6 100%);">
<div style="max-width:800px;margin:0 auto;text-align:center;">
<h2 style="font-size:40px;font-weight:700;margin-bottom:16px;color:#fff;font-family:Inter,sans-serif;">Ready to Get Started?</h2>
<p style="font-size:20px;color:rgba(255,255,255,0.9);line-height:1.6;margin-bottom:40px;font-family:Inter,sans-serif;">Join thousands of satisfied customers and take your business to the next level.</p>
<div style="display:flex;gap:16px;justify-content:center;flex-wrap:wrap;">
<a href="#" style="display:inline-block;padding:16px 40px;background:#fff;color:#3b82f6;font-size:18px;font-weight:600;text-decoration:none;border-radius:8px;font-family:Inter,sans-serif;">Start Free Trial</a>
<a href="#" style="display:inline-block;padding:16px 40px;background:transparent;color:#fff;font-size:18px;font-weight:600;text-decoration:none;border-radius:8px;border:2px solid rgba(255,255,255,0.5);font-family:Inter,sans-serif;">Learn More</a>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-bullhorn' }
});
// Text Block
blockManager.add('text-block', {
label: 'Text',
category: 'Basic',
content: '<p style="font-family:Inter,sans-serif;font-size:16px;line-height:1.6;color:#374151;">Insert your text here. You can edit this content directly.</p>',
attributes: { class: 'gjs-fonts gjs-f-text' }
});
// Heading Block
blockManager.add('heading', {
label: 'Heading',
category: 'Basic',
content: '<h2 style="font-family:Inter,sans-serif;font-size:32px;font-weight:700;color:#1f2937;">Heading</h2>',
attributes: { class: 'fa fa-header' }
});
// Button Block
blockManager.add('button-block', {
label: 'Button',
category: 'Basic',
content: '<a href="#" style="display:inline-block;padding:12px 24px;background:#3b82f6;color:#fff;font-size:16px;font-weight:500;text-decoration:none;border-radius:6px;font-family:Inter,sans-serif;">Button</a>',
attributes: { class: 'fa fa-link' }
});
// Divider Block - Resizable with height control
blockManager.add('divider', {
label: 'Divider',
category: 'Basic',
content: {
tagName: 'hr',
style: {
'border': 'none',
'border-top': '2px solid #e5e7eb',
'margin': '20px 0',
'width': '100%',
'height': '0'
},
resizable: {
tl: 0, tc: 0, tr: 0,
cl: 1, cr: 1,
bl: 0, bc: 0, br: 0,
keyWidth: 'width'
}
},
attributes: { class: 'fa fa-minus' }
});
// Spacer Block
blockManager.add('spacer', {
label: 'Spacer',
category: 'Basic',
content: '<div style="height:50px;"></div>',
attributes: { class: 'fa fa-arrows-alt-v' }
});
// Anchor Point Block
blockManager.add('anchor-point', {
label: 'Anchor Point',
category: 'Basic',
content: `<div data-anchor="true" id="anchor-1" class="editor-anchor">
<span class="anchor-icon"></span>
<input type="text" class="anchor-name-input" value="anchor-1" placeholder="anchor-name" />
</div>`,
attributes: { class: 'fa fa-anchor' }
});
// PDF / File Embed Block
blockManager.add('file-embed', {
label: 'File / PDF',
category: 'Media',
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>
<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 style="text-align:center;">
<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:12px;margin-top:4px;opacity:0.7;">Supports PDF, DOC, and other files</div>
</div>
</div>
</div>`,
attributes: { class: 'fa fa-file-pdf' }
});
// Text Box Block (for overlaying on backgrounds)
blockManager.add('text-box', {
label: 'Text Box',
category: 'Basic',
content: {
tagName: 'div',
attributes: { class: 'text-box' },
style: {
'padding': '24px',
'background': 'rgba(255,255,255,0.95)',
'border-radius': '8px',
'max-width': '600px',
'box-shadow': '0 4px 6px rgba(0,0,0,0.1)'
},
components: [
{
tagName: 'h3',
style: {
'color': '#1f2937',
'font-size': '24px',
'font-weight': '600',
'margin-bottom': '12px',
'font-family': 'Inter, sans-serif'
},
content: 'Text Box Title'
},
{
tagName: 'p',
style: {
'color': '#4b5563',
'font-size': '16px',
'line-height': '1.6',
'font-family': 'Inter, sans-serif'
},
content: 'Add your content here. This text box can be placed over images or video backgrounds.'
}
]
},
attributes: { class: 'fa fa-file-text' }
});
// ==========================================
// Enhanced Block Library
// ==========================================
// Image Gallery
blockManager.add('image-gallery', {
label: 'Image Gallery',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#ffffff;" role="region" aria-label="Image Gallery">
<div style="max-width:1200px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:50px;color:#1f2937;font-family:Inter,sans-serif;">Gallery</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px;">
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=600&q=80" alt="Gallery image 1" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=600&q=80" alt="Gallery image 2" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=600&q=80" alt="Gallery image 3" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=600&q=80" alt="Gallery image 4" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=600&q=80" alt="Gallery image 5" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
<div style="border-radius:12px;overflow:hidden;aspect-ratio:4/3;background:#e5e7eb;position:relative;">
<img src="https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=600&q=80" alt="Gallery image 6" loading="lazy" style="width:100%;height:100%;object-fit:cover;">
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-th' }
});
// FAQ Accordion
blockManager.add('faq-section', {
label: 'FAQ Accordion',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#f9fafb;" role="region" aria-label="Frequently Asked Questions">
<div style="max-width:800px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:50px;color:#1f2937;font-family:Inter,sans-serif;">Frequently Asked Questions</h2>
<div style="display:flex;flex-direction:column;gap:16px;">
<details style="background:#ffffff;border-radius:12px;padding:24px;border:1px solid #e5e7eb;">
<summary style="font-size:18px;font-weight:600;color:#1f2937;cursor:pointer;font-family:Inter,sans-serif;">What is your return policy?</summary>
<p style="margin-top:16px;color:#6b7280;line-height:1.7;font-family:Inter,sans-serif;">We offer a 30-day money-back guarantee on all purchases. If you're not satisfied, contact our support team for a full refund.</p>
</details>
<details style="background:#ffffff;border-radius:12px;padding:24px;border:1px solid #e5e7eb;">
<summary style="font-size:18px;font-weight:600;color:#1f2937;cursor:pointer;font-family:Inter,sans-serif;">How long does shipping take?</summary>
<p style="margin-top:16px;color:#6b7280;line-height:1.7;font-family:Inter,sans-serif;">Standard shipping takes 5-7 business days. Express shipping is available for 2-3 business day delivery.</p>
</details>
<details style="background:#ffffff;border-radius:12px;padding:24px;border:1px solid #e5e7eb;">
<summary style="font-size:18px;font-weight:600;color:#1f2937;cursor:pointer;font-family:Inter,sans-serif;">Do you offer customer support?</summary>
<p style="margin-top:16px;color:#6b7280;line-height:1.7;font-family:Inter,sans-serif;">Yes! Our support team is available 24/7 via email and live chat. Phone support is available Mon-Fri, 9am-5pm EST.</p>
</details>
<details style="background:#ffffff;border-radius:12px;padding:24px;border:1px solid #e5e7eb;">
<summary style="font-size:18px;font-weight:600;color:#1f2937;cursor:pointer;font-family:Inter,sans-serif;">Can I cancel my subscription?</summary>
<p style="margin-top:16px;color:#6b7280;line-height:1.7;font-family:Inter,sans-serif;">You can cancel your subscription at any time from your account settings. No cancellation fees apply.</p>
</details>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-question-circle' }
});
// Stats/Counter Section
blockManager.add('stats-section', {
label: 'Stats Counter',
category: 'Sections',
content: `<section style="padding:80px 20px;background:linear-gradient(135deg,#1f2937 0%,#111827 100%);" role="region" aria-label="Statistics">
<div style="max-width:1200px;margin:0 auto;">
<div style="display:flex;flex-wrap:wrap;gap:40px;justify-content:center;text-align:center;">
<div style="flex:1;min-width:200px;">
<div style="font-size:48px;font-weight:700;color:#3b82f6;margin-bottom:8px;font-family:Inter,sans-serif;">10K+</div>
<div style="font-size:16px;color:#9ca3af;font-family:Inter,sans-serif;">Happy Customers</div>
</div>
<div style="flex:1;min-width:200px;">
<div style="font-size:48px;font-weight:700;color:#10b981;margin-bottom:8px;font-family:Inter,sans-serif;">500+</div>
<div style="font-size:16px;color:#9ca3af;font-family:Inter,sans-serif;">Projects Completed</div>
</div>
<div style="flex:1;min-width:200px;">
<div style="font-size:48px;font-weight:700;color:#f59e0b;margin-bottom:8px;font-family:Inter,sans-serif;">99%</div>
<div style="font-size:16px;color:#9ca3af;font-family:Inter,sans-serif;">Satisfaction Rate</div>
</div>
<div style="flex:1;min-width:200px;">
<div style="font-size:48px;font-weight:700;color:#ec4899;margin-bottom:8px;font-family:Inter,sans-serif;">24/7</div>
<div style="font-size:16px;color:#9ca3af;font-family:Inter,sans-serif;">Support Available</div>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-bar-chart' }
});
// Team Grid
blockManager.add('team-section', {
label: 'Team Grid',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#ffffff;" role="region" aria-label="Our Team">
<div style="max-width:1200px;margin:0 auto;">
<h2 style="text-align:center;font-size:36px;font-weight:700;margin-bottom:16px;color:#1f2937;font-family:Inter,sans-serif;">Meet Our Team</h2>
<p style="text-align:center;font-size:18px;color:#6b7280;margin-bottom:50px;max-width:600px;margin-left:auto;margin-right:auto;font-family:Inter,sans-serif;">The talented people behind our success</p>
<div style="display:flex;flex-wrap:wrap;gap:30px;justify-content:center;">
<div style="flex:1;min-width:250px;max-width:300px;text-align:center;">
<div style="width:120px;height:120px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#8b5cf6);margin:0 auto 20px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:36px;font-weight:700;font-family:Inter,sans-serif;">AJ</div>
<h3 style="font-size:20px;font-weight:600;color:#1f2937;margin-bottom:4px;font-family:Inter,sans-serif;">Alex Johnson</h3>
<p style="font-size:14px;color:#3b82f6;margin-bottom:12px;font-family:Inter,sans-serif;">CEO & Founder</p>
<p style="font-size:14px;color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Visionary leader with 15+ years of experience.</p>
</div>
<div style="flex:1;min-width:250px;max-width:300px;text-align:center;">
<div style="width:120px;height:120px;border-radius:50%;background:linear-gradient(135deg,#10b981,#059669);margin:0 auto 20px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:36px;font-weight:700;font-family:Inter,sans-serif;">SK</div>
<h3 style="font-size:20px;font-weight:600;color:#1f2937;margin-bottom:4px;font-family:Inter,sans-serif;">Sarah Kim</h3>
<p style="font-size:14px;color:#3b82f6;margin-bottom:12px;font-family:Inter,sans-serif;">Lead Designer</p>
<p style="font-size:14px;color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Award-winning designer creating beautiful experiences.</p>
</div>
<div style="flex:1;min-width:250px;max-width:300px;text-align:center;">
<div style="width:120px;height:120px;border-radius:50%;background:linear-gradient(135deg,#f59e0b,#d97706);margin:0 auto 20px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:36px;font-weight:700;font-family:Inter,sans-serif;">MP</div>
<h3 style="font-size:20px;font-weight:600;color:#1f2937;margin-bottom:4px;font-family:Inter,sans-serif;">Mike Patel</h3>
<p style="font-size:14px;color:#3b82f6;margin-bottom:12px;font-family:Inter,sans-serif;">CTO</p>
<p style="font-size:14px;color:#6b7280;line-height:1.6;font-family:Inter,sans-serif;">Full-stack engineer building scalable systems.</p>
</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-users' }
});
// Newsletter Signup
blockManager.add('newsletter-section', {
label: 'Newsletter',
category: 'Sections',
content: `<section style="padding:80px 20px;background:#f9fafb;" role="region" aria-label="Newsletter Signup">
<div style="max-width:600px;margin:0 auto;text-align:center;">
<h2 style="font-size:32px;font-weight:700;margin-bottom:16px;color:#1f2937;font-family:Inter,sans-serif;">Stay in the Loop</h2>
<p style="font-size:18px;color:#6b7280;margin-bottom:32px;line-height:1.6;font-family:Inter,sans-serif;">Subscribe to our newsletter for the latest updates and exclusive offers.</p>
<form style="display:flex;gap:12px;max-width:480px;margin:0 auto;flex-wrap:wrap;justify-content:center;">
<input type="email" placeholder="Enter your email" required aria-label="Email address" style="flex:1;min-width:240px;padding:14px 20px;border:2px solid #e5e7eb;border-radius:8px;font-size:16px;font-family:Inter,sans-serif;outline:none;box-sizing:border-box;">
<button type="submit" style="padding:14px 32px;background:#3b82f6;color:#fff;font-size:16px;font-weight:600;border:none;border-radius:8px;cursor:pointer;font-family:Inter,sans-serif;white-space:nowrap;">Subscribe</button>
</form>
<p style="font-size:13px;color:#9ca3af;margin-top:16px;font-family:Inter,sans-serif;">No spam, unsubscribe anytime.</p>
</div>
</section>`,
attributes: { class: 'fa fa-newspaper' }
});
// Logo Cloud / Trusted By
blockManager.add('logo-cloud', {
label: 'Logo Cloud',
category: 'Sections',
content: `<section style="padding:60px 20px;background:#ffffff;" role="region" aria-label="Trusted by leading companies">
<div style="max-width:1200px;margin:0 auto;text-align:center;">
<p style="font-size:14px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:2px;margin-bottom:32px;font-family:Inter,sans-serif;">Trusted by leading companies</p>
<div style="display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:40px;opacity:0.5;">
<div style="font-size:24px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">Company 1</div>
<div style="font-size:24px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">Company 2</div>
<div style="font-size:24px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">Company 3</div>
<div style="font-size:24px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">Company 4</div>
<div style="font-size:24px;font-weight:700;color:#1f2937;font-family:Inter,sans-serif;">Company 5</div>
</div>
</div>
</section>`,
attributes: { class: 'fa fa-building' }
});
// ==========================================
// Custom Component Types for Better Editing
// ==========================================
// Helper to convert YouTube/Vimeo URLs to embed format
function convertToEmbedUrl(url, isBackground = false) {
if (!url) return null;
// YouTube: youtube.com/watch?v=ID or youtu.be/ID
const youtubeMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
if (youtubeMatch) {
const videoId = youtubeMatch[1];
// Use youtube-nocookie.com to avoid Error 153 (referrer requirements)
// The referrerpolicy attribute in the iframe handles the referrer header
if (isBackground) {
// Background video: autoplay, muted, looped, no controls
return {
type: 'youtube',
url: `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=1&loop=1&playlist=${videoId}&controls=0&modestbranding=1&rel=0&showinfo=0`
};
} else {
// Regular video: user controls, no autoplay
return {
type: 'youtube',
url: `https://www.youtube-nocookie.com/embed/${videoId}?rel=0`
};
}
}
// Vimeo: vimeo.com/ID or player.vimeo.com/video/ID
const vimeoMatch = url.match(/(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/);
if (vimeoMatch) {
if (isBackground) {
// Background video parameters
return { type: 'vimeo', url: `https://player.vimeo.com/video/${vimeoMatch[1]}?muted=1&loop=1&background=1&autoplay=1` };
} else {
// Regular video with controls
return { type: 'vimeo', url: `https://player.vimeo.com/video/${vimeoMatch[1]}` };
}
}
// Direct video file
if (url.match(/\.(mp4|webm|ogg|mov)(\?.*)?$/i)) {
return { type: 'file', url: url };
}
// Assume it's an embed URL if nothing else matches
return { type: 'embed', url: url };
}
// Helper to apply video URL to a video wrapper component
function applyVideoUrl(component, url) {
if (!url) return;
// Detect if this is a background video by checking for bg-video classes
const iframe = component.components().find(c => c.getClasses().includes('video-frame') || c.getClasses().includes('bg-video-frame'));
const isBackground = iframe && iframe.getClasses().includes('bg-video-frame');
const result = convertToEmbedUrl(url, isBackground);
if (!result) return;
console.log('Applying video URL:', url);
console.log('Converted to:', result.url);
console.log('Video type:', result.type);
console.log('Is background:', isBackground);
// Find child elements (iframe already found above)
const video = component.components().find(c => c.getClasses().includes('video-player') || c.getClasses().includes('bg-video-player'));
const placeholder = component.components().find(c => c.getClasses().includes('video-placeholder') || c.getClasses().includes('bg-video-placeholder'));
if (result.type === 'file') {
// Use HTML5 video
if (video) {
video.addAttributes({ src: result.url });
video.addStyle({ display: 'block' });
const videoEl = video.getEl();
if (videoEl) {
videoEl.src = result.url;
videoEl.style.display = 'block';
}
}
if (iframe) {
iframe.addStyle({ display: 'none' });
const iframeEl = iframe.getEl();
if (iframeEl) iframeEl.style.display = 'none';
}
} else {
// Use iframe for YouTube/Vimeo/embeds
if (iframe) {
iframe.addAttributes({ src: result.url });
iframe.addStyle({ display: 'block' });
const iframeEl = iframe.getEl();
if (iframeEl) {
iframeEl.src = result.url;
iframeEl.style.display = 'block';
}
}
if (video) {
video.addStyle({ display: 'none' });
const videoEl = video.getEl();
if (videoEl) videoEl.style.display = 'none';
}
}
// 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 <video>/<iframe> as <div>,
// so show visual feedback via the DOM only (doesn't affect export)
const placeholderEl = placeholder.getEl();
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';
}
}
}
// Video Wrapper Component (for Video block)
editor.DomComponents.addType('video-wrapper', {
isComponent: el => el.getAttribute && el.getAttribute('data-video-wrapper') === 'true',
model: {
defaults: {
traits: [
{
type: 'text',
label: 'Video URL',
name: 'videoUrl',
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',
label: '',
text: 'Apply Video',
full: true,
command: (editor) => {
const selected = editor.getSelected();
if (!selected) return;
const url = selected.getAttributes().videoUrl;
if (!url) {
alert('Please enter a Video URL first');
return;
}
console.log('Apply Video button clicked (regular video), URL:', url);
applyVideoUrl(selected, url);
alert('Video applied! If you see an error, the video owner may have disabled embedding.');
}
}
]
},
init() {
this.on('change:attributes:videoUrl', this.onVideoUrlChange);
// Make child elements non-selectable so clicks bubble to wrapper
this.components().forEach(child => {
child.set({
selectable: false,
hoverable: false,
editable: false,
draggable: false,
droppable: false,
badgable: false,
layerable: true // Still show in layers panel
});
});
},
onVideoUrlChange() {
const url = this.getAttributes().videoUrl;
applyVideoUrl(this, url);
}
}
});
// Background Video Wrapper Component (for Section Video BG)
// NOTE: No traits here! Users should set Video URL on the parent section element.
editor.DomComponents.addType('bg-video-wrapper', {
isComponent: el => el.getAttribute && el.getAttribute('data-bg-video') === 'true',
model: {
defaults: {
draggable: false, // Prevent moving the video wrapper independently
selectable: false, // Don't let users select it directly
hoverable: false, // Don't highlight on hover
traits: [] // No traits - configured via parent section
},
init() {
// Listen for videoUrl attribute changes (set by parent section)
this.on('change:attributes:videoUrl', this.onVideoUrlChange);
},
onVideoUrlChange() {
const url = this.getAttributes().videoUrl;
applyVideoUrl(this, url);
}
}
});
// Video Section Component (outer section with video background)
editor.DomComponents.addType('video-section', {
isComponent: el => el.getAttribute && el.getAttribute('data-video-section') === 'true',
model: {
defaults: {
traits: [
{
type: 'text',
label: 'Video URL',
name: 'videoUrl',
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',
label: '',
text: 'Apply Video',
full: true,
command: (editor) => {
const selected = editor.getSelected();
if (!selected) return;
const url = selected.getAttributes().videoUrl;
if (!url) {
alert('Please enter a Video URL first');
return;
}
console.log('Apply Video button clicked, URL:', url);
// Find the bg-video-wrapper child
const videoWrapper = selected.components().find(c =>
c.getAttributes()['data-bg-video'] === 'true'
);
if (videoWrapper) {
videoWrapper.addAttributes({ videoUrl: url });
applyVideoUrl(videoWrapper, url);
alert('Video applied! If you see an error, the video owner may have disabled embedding.');
} else {
alert('Error: Video wrapper not found');
}
}
}
]
},
init() {
// Listen for attribute changes
this.on('change:attributes:videoUrl', () => {
const url = this.getAttributes().videoUrl;
if (!url) return;
console.log('Video URL changed:', url);
// Find the bg-video-wrapper child and apply the video URL to it
const videoWrapper = this.components().find(c =>
c.getAttributes()['data-bg-video'] === 'true'
);
console.log('Video wrapper found:', !!videoWrapper);
if (videoWrapper) {
videoWrapper.addAttributes({ videoUrl: url });
applyVideoUrl(videoWrapper, url);
}
});
// Make child elements non-selectable so clicking them selects the parent section
setTimeout(() => {
this.components().forEach(child => {
// Skip the content layer (users should be able to edit text)
const classes = child.getClasses();
if (!classes.includes('bg-content')) {
child.set({
selectable: false,
hoverable: false,
editable: false
});
}
});
}, 100);
}
}
});
// Anchor Point Component
editor.DomComponents.addType('anchor-point', {
isComponent: el => el.getAttribute && el.getAttribute('data-anchor') === 'true',
model: {
defaults: {
traits: [
{
type: 'text',
label: 'Anchor Name',
name: 'id',
placeholder: 'e.g. about-us'
}
]
},
init() {
// Make child elements (icon and input) non-selectable
// This prevents users from accidentally selecting/deleting them
this.components().forEach(child => {
child.set({
selectable: false,
hoverable: false,
editable: false,
draggable: false,
droppable: false,
badgable: false,
layerable: false,
removable: false
});
});
}
}
});
// File Embed Component
editor.DomComponents.addType('file-embed', {
isComponent: el => el.getAttribute && el.getAttribute('data-file-embed') === 'true',
model: {
defaults: {
traits: [
{
type: 'text',
label: 'File URL',
name: 'fileUrl',
placeholder: 'https://example.com/file.pdf'
},
{
type: 'number',
label: 'Height (px)',
name: 'frameHeight',
placeholder: '600',
default: 600
},
{
type: 'button',
label: '',
text: 'Apply File',
full: true,
command: (editor) => {
const selected = editor.getSelected();
if (!selected) return;
const url = selected.getAttributes().fileUrl;
const height = selected.getAttributes().frameHeight || 600;
if (!url) {
alert('Please enter a File URL first');
return;
}
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'));
const isPdf = url.match(/\.pdf(\?.*)?$/i);
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 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;
}
}
}
if (placeholder) {
placeholder.addStyle({ display: 'none' });
const el = placeholder.getEl();
if (el) el.style.display = 'none';
}
}
}
]
},
init() {
// Make child elements non-selectable so clicks bubble to wrapper
this.components().forEach(child => {
child.set({
selectable: false,
hoverable: false,
editable: false,
draggable: false,
droppable: false,
badgable: false,
layerable: false,
removable: false
});
});
}
}
});
// Logo Component with image support
editor.DomComponents.addType('site-logo', {
isComponent: el => el.classList && el.classList.contains('site-logo'),
model: {
defaults: {
traits: [
{
type: 'text',
label: 'Logo Text',
name: 'logoText',
placeholder: 'SiteName'
},
{
type: 'text',
label: 'Logo Image URL',
name: 'logoImage',
placeholder: 'https://example.com/logo.png'
},
{
type: 'select',
label: 'Logo Mode',
name: 'logoMode',
options: [
{ id: 'text', name: 'Text Only' },
{ id: 'image', name: 'Image Only' },
{ id: 'both', name: 'Image + Text' }
]
},
{
type: 'button',
label: '',
text: 'Apply Logo',
full: true,
command: (editor) => {
const selected = editor.getSelected();
if (!selected) return;
const attrs = selected.getAttributes();
const mode = attrs.logoMode || 'text';
const text = attrs.logoText || 'SiteName';
const imageUrl = attrs.logoImage || '';
// Clear existing children
selected.components().reset();
if (mode === 'image' && imageUrl) {
selected.components().add({
tagName: 'img',
attributes: { src: imageUrl, alt: text },
style: { height: '40px', width: 'auto' }
});
} else if (mode === 'both' && imageUrl) {
selected.components().add({
tagName: 'img',
attributes: { src: imageUrl, alt: text },
style: { height: '40px', width: 'auto' }
});
selected.components().add({
tagName: 'span',
style: { 'font-size': '20px', 'font-weight': '700', 'color': '#1f2937', 'font-family': 'Inter, sans-serif' },
content: text
});
} else {
// Text mode (default icon + text)
selected.components().add({
tagName: 'div',
style: { width: '40px', height: '40px', background: 'linear-gradient(135deg,#3b82f6 0%,#8b5cf6 100%)', 'border-radius': '8px', display: 'flex', 'align-items': 'center', 'justify-content': 'center' },
components: [{ tagName: 'span', style: { color: '#fff', 'font-weight': '700', 'font-size': '18px', 'font-family': 'Inter,sans-serif' }, content: text.charAt(0).toUpperCase() }]
});
selected.components().add({
tagName: 'span',
style: { 'font-size': '20px', 'font-weight': '700', 'color': '#1f2937', 'font-family': 'Inter, sans-serif' },
content: text
});
}
}
}
]
}
}
});
// ==========================================
// Device Switching
// ==========================================
const deviceButtons = {
desktop: document.getElementById('device-desktop'),
tablet: document.getElementById('device-tablet'),
mobile: document.getElementById('device-mobile')
};
function setDevice(device) {
// Update button states
Object.values(deviceButtons).forEach(btn => btn.classList.remove('active'));
deviceButtons[device].classList.add('active');
// Set device in editor
const deviceMap = {
desktop: 'Desktop',
tablet: 'Tablet',
mobile: 'Mobile'
};
editor.setDevice(deviceMap[device]);
// Force canvas refresh
editor.refresh();
}
deviceButtons.desktop.addEventListener('click', () => setDevice('desktop'));
deviceButtons.tablet.addEventListener('click', () => setDevice('tablet'));
deviceButtons.mobile.addEventListener('click', () => setDevice('mobile'));
// ==========================================
// Undo/Redo
// ==========================================
document.getElementById('btn-undo').addEventListener('click', () => {
editor.UndoManager.undo();
});
document.getElementById('btn-redo').addEventListener('click', () => {
editor.UndoManager.redo();
});
// ==========================================
// Clear Canvas
// ==========================================
document.getElementById('btn-clear').addEventListener('click', () => {
if (confirm('Are you sure you want to clear the canvas and reset the project? This will delete all saved data and cannot be undone.')) {
editor.DomComponents.clear();
editor.CssComposer.clear();
// Clear localStorage to prevent reloading old content
localStorage.removeItem(STORAGE_KEY);
localStorage.removeItem(STORAGE_KEY + '-preview');
alert('Canvas cleared! Refresh the page for a clean start.');
}
});
// ==========================================
// Preview
// ==========================================
document.getElementById('btn-preview').addEventListener('click', () => {
// Get the HTML and CSS
const html = editor.getHtml();
const css = editor.getCss();
// Store for preview page
localStorage.setItem(STORAGE_KEY + '-preview', JSON.stringify({ html, css }));
// Open preview
window.open('preview.html', '_blank');
});
// ==========================================
// Panel Tabs
// ==========================================
// Left panel tabs (Blocks / Pages / Layers)
document.querySelectorAll('.panel-left .panel-tab').forEach(tab => {
tab.addEventListener('click', () => {
const panel = tab.dataset.panel;
// Update tab states
document.querySelectorAll('.panel-left .panel-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Show/hide panels
document.getElementById('blocks-container').style.display = panel === 'blocks' ? 'block' : 'none';
document.getElementById('pages-container').style.display = panel === 'pages' ? 'block' : 'none';
document.getElementById('layers-container').style.display = panel === 'layers' ? 'block' : 'none';
document.getElementById('assets-container').style.display = panel === 'assets' ? 'block' : 'none';
});
});
// Right panel tabs (Styles / Settings)
document.querySelectorAll('.panel-right .panel-tab').forEach(tab => {
tab.addEventListener('click', () => {
const panel = tab.dataset.panel;
// Update tab states
document.querySelectorAll('.panel-right .panel-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Show/hide panels
document.getElementById('styles-container').style.display = panel === 'styles' ? 'block' : 'none';
document.getElementById('traits-container').style.display = panel === 'traits' ? 'block' : 'none';
const headContainer = document.getElementById('head-elements-container');
if (headContainer) headContainer.style.display = panel === 'head' ? 'block' : 'none';
});
});
// ==========================================
// Style Mode Toggle (Guided / Advanced)
// ==========================================
const modeGuided = document.getElementById('mode-guided');
const modeAdvanced = document.getElementById('mode-advanced');
const guidedStyles = document.getElementById('guided-styles');
const advancedStyles = document.getElementById('advanced-styles');
modeGuided.addEventListener('click', () => {
modeGuided.classList.add('active');
modeAdvanced.classList.remove('active');
guidedStyles.style.display = 'flex';
advancedStyles.style.display = 'none';
});
modeAdvanced.addEventListener('click', () => {
modeAdvanced.classList.add('active');
modeGuided.classList.remove('active');
advancedStyles.style.display = 'block';
guidedStyles.style.display = 'none';
});
// ==========================================
// Context-Aware Guided Style Controls
// ==========================================
// Element type definitions
const ELEMENT_TYPES = {
TEXT: ['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label'],
LINK: ['a'],
DIVIDER: ['hr'],
CONTAINER: ['div', 'section', 'article', 'header', 'footer', 'nav', 'main', 'aside'],
MEDIA: ['img', 'video', 'iframe'],
FORM: ['form', 'input', 'textarea', 'select', 'button']
};
// Get element type category
function getElementType(tagName) {
const tag = tagName?.toLowerCase();
if (ELEMENT_TYPES.TEXT.includes(tag)) return 'text';
if (ELEMENT_TYPES.LINK.includes(tag)) return 'link';
if (ELEMENT_TYPES.DIVIDER.includes(tag)) return 'divider';
if (ELEMENT_TYPES.MEDIA.includes(tag)) return 'media';
if (ELEMENT_TYPES.FORM.includes(tag)) return 'form';
if (ELEMENT_TYPES.CONTAINER.includes(tag)) return 'container';
return 'other';
}
// Check if element is button-like (styled link)
function isButtonLike(component) {
if (!component) return false;
const tagName = component.get('tagName')?.toLowerCase();
if (tagName !== 'a') return false;
const styles = component.getStyle();
// Check if it has button-like styling
return styles['display'] === 'inline-block' ||
styles['padding'] ||
styles['background'] ||
styles['background-color'];
}
// UI Section references
const sections = {
noSelection: document.getElementById('no-selection-msg'),
link: document.getElementById('section-link'),
textColor: document.getElementById('section-text-color'),
headingLevel: document.getElementById('section-heading-level'),
htmlEditorToggle: document.getElementById('section-html-editor-toggle'),
htmlEditor: document.getElementById('section-html-editor'),
bgColor: document.getElementById('section-bg-color'),
bgGradient: document.getElementById('section-bg-gradient'),
bgImage: document.getElementById('section-bg-image'),
overlay: document.getElementById('section-overlay'),
dividerColor: document.getElementById('section-divider-color'),
font: document.getElementById('section-font'),
textSize: document.getElementById('section-text-size'),
fontWeight: document.getElementById('section-font-weight'),
spacing: document.getElementById('section-spacing'),
radius: document.getElementById('section-radius'),
thickness: document.getElementById('section-thickness'),
buttonStyle: document.getElementById('section-button-style'),
navLinks: document.getElementById('section-nav-links')
};
// Link input elements
const linkUrlInput = document.getElementById('link-url-input');
const linkNewTabCheckbox = document.getElementById('link-new-tab');
// Background image elements
const bgImageUrlInput = document.getElementById('bg-image-url');
const bgSizeSelect = document.getElementById('bg-size');
const bgPositionSelect = document.getElementById('bg-position');
const removeBgImageBtn = document.getElementById('remove-bg-image');
// Overlay elements
const overlayOpacitySlider = document.getElementById('overlay-opacity');
const overlayOpacityValue = document.getElementById('overlay-opacity-value');
// Navigation elements
const syncNavPagesBtn = document.getElementById('sync-nav-pages');
const addNavLinkBtn = document.getElementById('add-nav-link');
const navLinksList = document.getElementById('nav-links-list');
// Current overlay state
let currentOverlayColor = '0,0,0';
let currentOverlayOpacity = 50;
// Check if element is an overlay
function isOverlay(component) {
if (!component) return false;
const classes = component.getClasses();
return classes.includes('bg-overlay');
}
// Check if element is a section with background
function isSectionWithBg(component) {
if (!component) return false;
const attrs = component.getAttributes();
return attrs['data-bg-section'] === 'true' || attrs['data-video-section'] === 'true';
}
// Check if element is a navigation
function isNavigation(component) {
if (!component) return false;
const tagName = component.get('tagName')?.toLowerCase();
const classes = component.getClasses();
return tagName === 'nav' || classes.includes('site-navbar');
}
// Hide all context sections
function hideAllSections() {
Object.values(sections).forEach(section => {
if (section) section.style.display = 'none';
});
}
// Show sections based on element type
function showSectionsForElement(component) {
hideAllSections();
if (!component) {
sections.noSelection.style.display = 'block';
return;
}
const tagName = component.get('tagName');
const elementType = getElementType(tagName);
const isButton = isButtonLike(component);
// Check for special element types first
if (isOverlay(component)) {
// Overlay element - show only overlay controls
sections.overlay.style.display = 'block';
loadOverlayValues(component);
return;
}
if (isNavigation(component)) {
// Navigation element - show navigation controls
sections.navLinks.style.display = 'block';
sections.bgColor.style.display = 'block';
sections.spacing.style.display = 'block';
loadNavLinks(component);
return;
}
if (isSectionWithBg(component)) {
// Section with background - show background image controls
sections.bgImage.style.display = 'block';
sections.spacing.style.display = 'block';
loadBgImageValues(component);
return;
}
// Show relevant sections based on element type
switch (elementType) {
case 'text':
sections.textColor.style.display = 'block';
sections.font.style.display = 'block';
sections.textSize.style.display = 'block';
sections.fontWeight.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);
}
break;
case 'link':
sections.link.style.display = 'block';
if (isButton) {
sections.buttonStyle.style.display = 'block';
sections.radius.style.display = 'block';
sections.spacing.style.display = 'block';
}
sections.textColor.style.display = 'block';
sections.font.style.display = 'block';
sections.textSize.style.display = 'block';
sections.fontWeight.style.display = 'block';
// Load current link values
loadLinkValues(component);
break;
case 'divider':
sections.dividerColor.style.display = 'block';
sections.thickness.style.display = 'block';
break;
case 'container':
sections.bgColor.style.display = 'block';
sections.bgGradient.style.display = 'block';
sections.bgImage.style.display = 'block';
sections.spacing.style.display = 'block';
sections.radius.style.display = 'block';
loadBgImageValues(component);
break;
case 'media':
sections.spacing.style.display = 'block';
sections.radius.style.display = 'block';
break;
case 'form':
if (tagName?.toLowerCase() === 'button') {
sections.buttonStyle.style.display = 'block';
sections.link.style.display = 'block';
}
sections.bgColor.style.display = 'block';
sections.textColor.style.display = 'block';
sections.font.style.display = 'block';
sections.spacing.style.display = 'block';
sections.radius.style.display = 'block';
break;
default:
// Show common controls for unknown elements
sections.bgColor.style.display = 'block';
sections.spacing.style.display = 'block';
sections.radius.style.display = 'block';
}
// Always show HTML editor toggle button for any selected element
sections.htmlEditorToggle.style.display = 'block';
}
// Load link values into the input
function loadLinkValues(component) {
if (!component) return;
const attrs = component.getAttributes();
linkUrlInput.value = attrs.href || '';
linkNewTabCheckbox.checked = attrs.target === '_blank';
}
// Update heading level buttons to show active state
function updateHeadingLevelButtons(currentTag) {
const buttons = sections.headingLevel.querySelectorAll('.heading-level-btn');
buttons.forEach(btn => {
const level = btn.getAttribute('data-level');
if (level === currentTag) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
// HTML Editor elements
const htmlEditorTextarea = document.getElementById('html-editor-textarea');
const htmlEditorApply = document.getElementById('html-editor-apply');
const htmlEditorCancel = document.getElementById('html-editor-cancel');
const htmlEditorToggleBtn = document.getElementById('html-editor-toggle-btn');
const htmlEditorClose = document.getElementById('html-editor-close');
let originalHtml = '';
let currentEditingComponent = null;
// Load HTML into editor
function loadHtmlEditor(component) {
if (!component) return;
currentEditingComponent = component;
// Get the HTML of the selected component
const html = component.toHTML();
htmlEditorTextarea.value = html;
originalHtml = html;
}
// Show HTML editor
function showHtmlEditor() {
const selected = editor.getSelected();
if (!selected) return;
loadHtmlEditor(selected);
sections.htmlEditor.style.display = 'block';
sections.htmlEditorToggle.style.display = 'none';
}
// Hide HTML editor
function hideHtmlEditor() {
sections.htmlEditor.style.display = 'none';
sections.htmlEditorToggle.style.display = 'block';
currentEditingComponent = null;
}
// Toggle button click
htmlEditorToggleBtn.addEventListener('click', showHtmlEditor);
// Close button click
htmlEditorClose.addEventListener('click', hideHtmlEditor);
// Apply HTML changes
htmlEditorApply.addEventListener('click', () => {
const selected = editor.getSelected();
if (!selected) return;
const newHtml = htmlEditorTextarea.value.trim();
try {
// Replace the component with new HTML
const parent = selected.parent();
const index = parent.components().indexOf(selected);
// Remove old component
selected.remove();
// Add new component from HTML
parent.append(newHtml, { at: index });
// Select the new component
const newComponent = parent.components().at(index);
if (newComponent) {
editor.select(newComponent);
}
// Hide editor after applying
hideHtmlEditor();
} catch (error) {
alert('Invalid HTML: ' + error.message);
htmlEditorTextarea.value = originalHtml;
}
});
// Cancel HTML changes
htmlEditorCancel.addEventListener('click', () => {
htmlEditorTextarea.value = originalHtml;
hideHtmlEditor();
});
// Default font sizes for each heading level
const headingSizes = {
h1: '48px',
h2: '36px',
h3: '28px',
h4: '24px',
h5: '20px',
h6: '18px'
};
// Handle heading level button clicks
function setupHeadingLevelButtons() {
const buttons = sections.headingLevel.querySelectorAll('.heading-level-btn');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const newLevel = btn.getAttribute('data-level');
const selected = editor.getSelected();
if (!selected) return;
// Change the tag name
selected.set('tagName', newLevel);
// Update font size to match heading level
const defaultSize = headingSizes[newLevel];
if (defaultSize) {
selected.addStyle({ 'font-size': defaultSize });
}
// Update button states
updateHeadingLevelButtons(newLevel);
});
});
}
// Load background image values
function loadBgImageValues(component) {
if (!component) return;
const styles = component.getStyle();
const bgImage = styles['background-image'] || '';
const bgSize = styles['background-size'] || 'cover';
const bgPosition = styles['background-position'] || 'center';
// Extract URL from background-image
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
bgImageUrlInput.value = urlMatch ? urlMatch[1] : '';
bgSizeSelect.value = bgSize;
bgPositionSelect.value = bgPosition;
}
// Load overlay values
function loadOverlayValues(component) {
if (!component) return;
const styles = component.getStyle();
const bg = styles['background'] || '';
// Parse rgba value
const rgbaMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)/);
if (rgbaMatch) {
currentOverlayColor = `${rgbaMatch[1]},${rgbaMatch[2]},${rgbaMatch[3]}`;
currentOverlayOpacity = rgbaMatch[4] ? Math.round(parseFloat(rgbaMatch[4]) * 100) : 100;
overlayOpacitySlider.value = currentOverlayOpacity;
overlayOpacityValue.textContent = currentOverlayOpacity + '%';
// Update active color button
document.querySelectorAll('.overlay-color').forEach(btn => {
btn.classList.toggle('active', btn.dataset.color === currentOverlayColor);
});
}
}
// Load navigation links
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');
});
if (!linksContainer) {
navLinksList.innerHTML = '<p class="no-links-msg">No links container found</p>';
return;
}
// Get all link components
const links = linksContainer.components().filter(c => c.get('tagName')?.toLowerCase() === 'a');
// Clear and rebuild list
navLinksList.innerHTML = '';
links.forEach((link, index) => {
const item = document.createElement('div');
item.className = 'nav-link-item';
const textSpan = document.createElement('span');
textSpan.className = 'nav-link-text';
textSpan.textContent = link.getEl()?.textContent || 'Link';
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.title = 'Remove link';
deleteBtn.addEventListener('click', () => {
link.remove();
loadNavLinks(component);
});
item.appendChild(textSpan);
item.appendChild(deleteBtn);
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
function applyStyle(property, value) {
const selected = editor.getSelected();
if (selected) {
selected.addStyle({ [property]: value });
}
}
// ==========================================
// Color Preset Handlers
// ==========================================
// Text color presets
document.querySelectorAll('.color-preset.text-color').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('color', btn.dataset.color);
document.querySelectorAll('.text-color').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Background color presets
document.querySelectorAll('.color-preset.bg-color').forEach(btn => {
btn.addEventListener('click', () => {
const selected = editor.getSelected();
if (selected) {
// Remove gradient when applying solid color
selected.addStyle({
'background-color': btn.dataset.color,
'background-image': 'none',
'background': btn.dataset.color
});
}
document.querySelectorAll('.bg-color').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.gradient-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Gradient presets
document.querySelectorAll('.gradient-preset').forEach(btn => {
btn.addEventListener('click', () => {
const gradient = btn.dataset.gradient;
const selected = editor.getSelected();
if (selected) {
if (gradient === 'none') {
// Remove gradient
selected.addStyle({
'background-image': 'none',
'background': ''
});
} else {
// Apply gradient
selected.addStyle({
'background': gradient,
'background-image': gradient
});
}
}
document.querySelectorAll('.gradient-preset').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.bg-color').forEach(b => b.classList.remove('active'));
if (gradient !== 'none') {
btn.classList.add('active');
}
});
});
// Divider color presets
document.querySelectorAll('.color-preset.divider-color').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('border-top-color', btn.dataset.color);
document.querySelectorAll('.divider-color').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Button background color presets
document.querySelectorAll('.color-preset.btn-bg-color').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('background-color', btn.dataset.color);
// Also update text color for contrast
const color = btn.dataset.color;
if (color === '#ffffff' || color === '#f9fafb') {
applyStyle('color', '#1f2937');
} else {
applyStyle('color', '#ffffff');
}
document.querySelectorAll('.btn-bg-color').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// ==========================================
// Font Preset Handlers
// ==========================================
document.querySelectorAll('.font-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('font-family', btn.dataset.font);
document.querySelectorAll('.font-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Font weight presets
document.querySelectorAll('.weight-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('font-weight', btn.dataset.weight);
document.querySelectorAll('.weight-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Text size presets
document.querySelectorAll('.size-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('font-size', btn.dataset.size);
document.querySelectorAll('.size-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Spacing presets
document.querySelectorAll('.spacing-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('padding', btn.dataset.padding);
document.querySelectorAll('.spacing-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Border radius presets
document.querySelectorAll('.radius-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('border-radius', btn.dataset.radius);
document.querySelectorAll('.radius-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Line thickness presets (for dividers/hr)
document.querySelectorAll('.thickness-preset').forEach(btn => {
btn.addEventListener('click', () => {
applyStyle('border-top-width', btn.dataset.thickness);
document.querySelectorAll('.thickness-preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// ==========================================
// Heading Level Controls
// ==========================================
setupHeadingLevelButtons();
// ==========================================
// Background Image Controls
// ==========================================
// Update background image URL
const updateBgImage = debounce(() => {
const selected = editor.getSelected();
if (selected) {
const url = bgImageUrlInput.value.trim();
if (url) {
selected.addStyle({ 'background-image': `url(${url})` });
} else {
selected.addStyle({ 'background-image': 'none' });
}
}
}, 300);
bgImageUrlInput.addEventListener('input', updateBgImage);
// Background size
bgSizeSelect.addEventListener('change', () => {
const selected = editor.getSelected();
if (selected) {
selected.addStyle({ 'background-size': bgSizeSelect.value });
}
});
// Background position
bgPositionSelect.addEventListener('change', () => {
const selected = editor.getSelected();
if (selected) {
selected.addStyle({ 'background-position': bgPositionSelect.value });
}
});
// Remove background image
removeBgImageBtn.addEventListener('click', () => {
const selected = editor.getSelected();
if (selected) {
selected.addStyle({
'background-image': 'none',
'background-color': '#ffffff'
});
bgImageUrlInput.value = '';
}
});
// ==========================================
// Overlay Controls
// ==========================================
// Overlay color presets
document.querySelectorAll('.overlay-color').forEach(btn => {
btn.addEventListener('click', () => {
currentOverlayColor = btn.dataset.color;
const selected = editor.getSelected();
if (selected) {
const opacity = currentOverlayOpacity / 100;
selected.addStyle({ 'background': `rgba(${currentOverlayColor},${opacity})` });
}
document.querySelectorAll('.overlay-color').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Overlay opacity slider
overlayOpacitySlider.addEventListener('input', () => {
currentOverlayOpacity = parseInt(overlayOpacitySlider.value);
overlayOpacityValue.textContent = currentOverlayOpacity + '%';
const selected = editor.getSelected();
if (selected && isOverlay(selected)) {
const opacity = currentOverlayOpacity / 100;
selected.addStyle({ 'background': `rgba(${currentOverlayColor},${opacity})` });
}
});
// ==========================================
// Navigation Controls
// ==========================================
// Sync navigation with pages
syncNavPagesBtn.addEventListener('click', () => {
const selected = editor.getSelected();
if (!selected || !isNavigation(selected)) 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();
let ctaLink = null;
existingLinks.forEach(link => {
const classes = link.getClasses();
if (classes.includes('nav-cta')) {
ctaLink = link.clone();
}
});
// Clear existing links
linksContainer.components().reset();
// Add links for each page
pages.forEach((page, index) => {
linksContainer.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'
},
content: page.name
});
});
// Re-add CTA if existed
if (ctaLink) {
linksContainer.components().add(ctaLink);
}
// Refresh the links list UI
loadNavLinks(selected);
});
// Add new link to navigation
addNavLinkBtn.addEventListener('click', () => {
const selected = editor.getSelected();
if (!selected || !isNavigation(selected)) 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({
tagName: 'a',
attributes: { href: '#' },
style: {
'color': '#4b5563',
'text-decoration': 'none',
'font-size': '15px',
'font-family': 'Inter, sans-serif'
},
content: 'New Link'
}, { at: insertIndex });
// Refresh the links list UI
loadNavLinks(selected);
});
// ==========================================
// Link Editing
// ==========================================
// Debounce helper
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Update link href
const updateLinkHref = debounce(() => {
const selected = editor.getSelected();
if (selected && (selected.get('tagName')?.toLowerCase() === 'a' || selected.get('tagName')?.toLowerCase() === 'button')) {
selected.addAttributes({ href: linkUrlInput.value });
}
}, 300);
linkUrlInput.addEventListener('input', updateLinkHref);
// Update link target
linkNewTabCheckbox.addEventListener('change', () => {
const selected = editor.getSelected();
if (selected && selected.get('tagName')?.toLowerCase() === 'a') {
if (linkNewTabCheckbox.checked) {
selected.addAttributes({ target: '_blank', rel: 'noopener noreferrer' });
} else {
selected.removeAttributes('target');
selected.removeAttributes('rel');
}
}
});
// ==========================================
// Save Status Indicator
// ==========================================
editor.on('storage:start', () => {
saveStatus.classList.add('saving');
saveStatus.classList.remove('saved');
statusText.textContent = 'Saving...';
});
editor.on('storage:end', () => {
saveStatus.classList.remove('saving');
saveStatus.classList.add('saved');
statusText.textContent = 'Saved';
});
editor.on('storage:error', (err) => {
saveStatus.classList.remove('saving');
statusText.textContent = 'Error saving';
console.error('Storage error:', err);
});
// ==========================================
// Keyboard Shortcuts
// ==========================================
document.addEventListener('keydown', (e) => {
// Only handle if not typing in an input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Ctrl/Cmd + Z = Undo
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
editor.UndoManager.undo();
}
// Ctrl/Cmd + Shift + Z = Redo (or Ctrl + Y)
if ((e.ctrlKey || e.metaKey) && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) {
e.preventDefault();
editor.UndoManager.redo();
}
// Delete/Backspace = Remove selected
if (e.key === 'Delete' || e.key === 'Backspace') {
const selected = editor.getSelected();
if (selected && !e.target.isContentEditable) {
e.preventDefault();
selected.remove();
}
}
// Escape = Deselect
if (e.key === 'Escape') {
editor.select(null);
}
});
// ==========================================
// Update guided controls when selection changes
// ==========================================
// Video Section Trait Change Listener
// ==========================================
// Global listener for video URL trait changes
editor.on('component:update', (component) => {
// Check if this is a video section
const attrs = component.getAttributes();
if (attrs && attrs['data-video-section'] === 'true') {
const videoUrl = attrs.videoUrl;
if (videoUrl) {
console.log('Video section updated with URL:', videoUrl);
// Find the bg-video-wrapper child
const videoWrapper = component.components().find(c =>
c.getAttributes()['data-bg-video'] === 'true'
);
if (videoWrapper) {
console.log('Applying video URL to wrapper');
videoWrapper.addAttributes({ videoUrl: videoUrl });
applyVideoUrl(videoWrapper, videoUrl);
} else {
console.error('Video wrapper not found!');
}
}
}
});
// ==========================================
editor.on('component:selected', (component) => {
// Show context-aware UI sections
showSectionsForElement(component);
if (!component) return;
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')
.forEach(btn => btn.classList.remove('active'));
// Set active text color
const textColor = styles['color'];
if (textColor) {
document.querySelectorAll('.text-color').forEach(btn => {
if (btn.dataset.color === textColor) btn.classList.add('active');
});
}
// Set active background color or gradient
const bgColor = styles['background-color'];
const bgImage = styles['background-image'] || styles['background'];
// Check for gradient first
if (bgImage && bgImage.includes('gradient')) {
document.querySelectorAll('.gradient-preset').forEach(btn => {
// Normalize gradient strings for comparison
const btnGradient = btn.dataset.gradient?.replace(/\s/g, '');
const elGradient = bgImage.replace(/\s/g, '');
if (btnGradient && elGradient.includes(btnGradient.replace(/\s/g, ''))) {
btn.classList.add('active');
}
});
} else if (bgColor) {
document.querySelectorAll('.bg-color, .btn-bg-color').forEach(btn => {
if (btn.dataset.color === bgColor) btn.classList.add('active');
});
}
// Set active divider color
const borderTopColor = styles['border-top-color'];
if (borderTopColor) {
document.querySelectorAll('.divider-color').forEach(btn => {
if (btn.dataset.color === borderTopColor) btn.classList.add('active');
});
}
// Set active font family
const fontFamily = styles['font-family'];
if (fontFamily) {
document.querySelectorAll('.font-preset').forEach(btn => {
if (fontFamily.includes(btn.dataset.font.split(',')[0].trim())) {
btn.classList.add('active');
}
});
}
// Set active font weight
const fontWeight = styles['font-weight'];
if (fontWeight) {
document.querySelectorAll('.weight-preset').forEach(btn => {
if (btn.dataset.weight === fontWeight) btn.classList.add('active');
});
}
// Set active font size
const fontSize = styles['font-size'];
if (fontSize) {
document.querySelectorAll('.size-preset').forEach(btn => {
if (btn.dataset.size === fontSize) btn.classList.add('active');
});
}
// Set active padding
const padding = styles['padding'];
if (padding) {
document.querySelectorAll('.spacing-preset').forEach(btn => {
if (btn.dataset.padding === padding) btn.classList.add('active');
});
}
// Set active border radius
const borderRadius = styles['border-radius'];
if (borderRadius) {
document.querySelectorAll('.radius-preset').forEach(btn => {
if (btn.dataset.radius === borderRadius) btn.classList.add('active');
});
}
// Set active thickness
const borderTopWidth = styles['border-top-width'];
if (borderTopWidth) {
document.querySelectorAll('.thickness-preset').forEach(btn => {
if (btn.dataset.thickness === borderTopWidth) btn.classList.add('active');
});
}
});
// Handle deselection
editor.on('component:deselected', () => {
showSectionsForElement(null);
});
// ==========================================
// Context Menu
// ==========================================
const contextMenu = document.getElementById('context-menu');
let clipboard = null; // Store copied component
// Hide context menu
function hideContextMenu() {
contextMenu.classList.remove('visible');
}
// Show context menu at position
function showContextMenu(x, y) {
const selected = editor.getSelected();
if (!selected) return;
// Update disabled states
const pasteItem = contextMenu.querySelector('[data-action="paste"]');
if (pasteItem) {
pasteItem.classList.toggle('disabled', !clipboard);
}
// Position the menu
contextMenu.style.left = x + 'px';
contextMenu.style.top = y + 'px';
// Make sure menu doesn't go off screen
contextMenu.classList.add('visible');
const rect = contextMenu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
contextMenu.style.left = (x - rect.width) + 'px';
}
if (rect.bottom > window.innerHeight) {
contextMenu.style.top = (y - rect.height) + 'px';
}
}
// Listen for right-click on canvas
editor.on('component:selected', (component) => {
if (!component) return;
// Get the component's element in the canvas
const el = component.getEl();
if (!el) return;
// Remove any existing listener
el.removeEventListener('contextmenu', handleContextMenu);
// Add context menu listener
el.addEventListener('contextmenu', handleContextMenu);
});
function handleContextMenu(e) {
e.preventDefault();
e.stopPropagation();
const selected = editor.getSelected();
if (!selected) return;
// Get canvas iframe offset
const canvas = editor.Canvas;
const canvasEl = canvas.getElement();
const iframe = canvasEl.querySelector('iframe');
const iframeRect = iframe.getBoundingClientRect();
// Calculate position relative to main window
const x = e.clientX + iframeRect.left;
const y = e.clientY + iframeRect.top;
showContextMenu(x, y);
}
// Close context menu when clicking elsewhere
document.addEventListener('click', hideContextMenu);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') hideContextMenu();
});
// Handle context menu actions
contextMenu.addEventListener('click', (e) => {
const item = e.target.closest('.context-menu-item');
if (!item || item.classList.contains('disabled')) return;
const action = item.dataset.action;
const selected = editor.getSelected();
if (!selected && action !== 'paste') {
hideContextMenu();
return;
}
switch (action) {
case 'edit':
// Trigger inline editing if available
if (selected.get('editable') !== false) {
const el = selected.getEl();
if (el) {
el.setAttribute('contenteditable', 'true');
el.focus();
}
}
break;
case 'duplicate':
const parent = selected.parent();
if (parent) {
const index = parent.components().indexOf(selected);
const clone = selected.clone();
parent.components().add(clone, { at: index + 1 });
editor.select(clone);
}
break;
case 'copy':
clipboard = selected.clone();
break;
case 'paste':
if (clipboard) {
const targetParent = selected.parent() || editor.getWrapper();
if (targetParent) {
const index = selected ? targetParent.components().indexOf(selected) + 1 : undefined;
const pasted = clipboard.clone();
targetParent.components().add(pasted, { at: index });
editor.select(pasted);
}
}
break;
case 'move-up':
const parentUp = selected.parent();
if (parentUp) {
const components = parentUp.components();
const indexUp = components.indexOf(selected);
if (indexUp > 0) {
components.remove(selected);
components.add(selected, { at: indexUp - 1 });
editor.select(selected);
}
}
break;
case 'move-down':
const parentDown = selected.parent();
if (parentDown) {
const components = parentDown.components();
const indexDown = components.indexOf(selected);
if (indexDown < components.length - 1) {
components.remove(selected);
components.add(selected, { at: indexDown + 1 });
editor.select(selected);
}
}
break;
case 'select-parent':
const parentEl = selected.parent();
if (parentEl && parentEl.get('type') !== 'wrapper') {
editor.select(parentEl);
}
break;
case 'wrap':
const wrapParent = selected.parent();
if (wrapParent) {
const wrapIndex = wrapParent.components().indexOf(selected);
// Create wrapper div
const wrapper = wrapParent.components().add({
tagName: 'div',
style: { padding: '20px' },
components: []
}, { at: wrapIndex });
// Move selected into wrapper
selected.move(wrapper, {});
editor.select(wrapper);
}
break;
case 'delete':
selected.remove();
break;
case 'delete-section':
// Find the topmost parent section/container (not wrapper)
{
let sectionTarget = selected;
let sectionParent = sectionTarget.parent();
while (sectionParent && sectionParent.get('type') !== 'wrapper') {
sectionTarget = sectionParent;
sectionParent = sectionTarget.parent();
}
if (confirm('Delete this entire section and all its children?')) {
sectionTarget.remove();
}
}
break;
}
hideContextMenu();
});
// Add keyboard shortcuts for copy/paste/duplicate
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
const selected = editor.getSelected();
if (!selected) return;
// Ctrl/Cmd + C = Copy
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
clipboard = selected.clone();
}
// Ctrl/Cmd + V = Paste
if ((e.ctrlKey || e.metaKey) && e.key === 'v' && clipboard) {
e.preventDefault();
const targetParent = selected.parent() || editor.getWrapper();
if (targetParent) {
const index = targetParent.components().indexOf(selected) + 1;
const pasted = clipboard.clone();
targetParent.components().add(pasted, { at: index });
editor.select(pasted);
}
}
// Ctrl/Cmd + D = Duplicate
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
e.preventDefault();
const parent = selected.parent();
if (parent) {
const index = parent.components().indexOf(selected);
const clone = selected.clone();
parent.components().add(clone, { at: index + 1 });
editor.select(clone);
}
}
});
// ==========================================
// Add default content if empty
// ==========================================
editor.on('load', () => {
const components = editor.getComponents();
if (components.length === 0) {
// Add a starter section
editor.addComponents(`
<section style="min-height: 400px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 60px 20px; text-align: center;">
<div style="max-width: 600px;">
<h1 style="color: #fff; font-size: 48px; font-weight: 700; margin-bottom: 20px; font-family: Inter, sans-serif;">Welcome to Site Builder</h1>
<p style="color: rgba(255,255,255,0.9); font-size: 18px; line-height: 1.6; margin-bottom: 30px; font-family: Inter, sans-serif;">Drag and drop components from the left panel to build your website. Click on any element to edit its content and style.</p>
<a href="#" style="display: inline-block; padding: 14px 32px; background: #fff; color: #667eea; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 8px; font-family: Inter, sans-serif;">Get Started</a>
</div>
</section>
`);
}
});
// ==========================================
// Anchor Name Input Sync
// ==========================================
// Sync anchor input field with ID attribute
editor.on('component:mount', (component) => {
if (component.get('attributes')?.['data-anchor']) {
setupAnchorInput(component);
}
});
function setupAnchorInput(component) {
const view = component.getEl();
if (!view) return;
const input = view.querySelector('.anchor-name-input');
if (!input) return;
// Sync input value with component ID
const updateInputFromId = () => {
const id = component.getId();
if (input.value !== id) {
input.value = id;
}
};
// Sync component ID with input value
const updateIdFromInput = () => {
const newId = input.value.trim();
if (newId && newId !== component.getId()) {
// Sanitize ID (replace spaces with hyphens, remove special chars)
const sanitizedId = newId
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-_]/g, '');
if (sanitizedId) {
component.setId(sanitizedId);
component.set('attributes', {
...component.get('attributes'),
id: sanitizedId
});
input.value = sanitizedId;
}
}
};
// Prevent GrapesJS from intercepting keyboard events
const stopPropagation = (e) => {
e.stopPropagation();
};
input.addEventListener('keydown', stopPropagation);
input.addEventListener('keyup', stopPropagation);
input.addEventListener('keypress', stopPropagation);
// Prevent component deletion on backspace
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.stopImmediatePropagation();
}
}, true); // Use capture phase
// Initial sync
updateInputFromId();
// Listen for changes
input.addEventListener('input', updateIdFromInput);
input.addEventListener('blur', updateIdFromInput);
// Listen for ID changes from trait manager
component.on('change:attributes:id', updateInputFromId);
}
// ==========================================
// Page Management
// ==========================================
const PAGES_STORAGE_KEY = 'sitebuilder-pages';
const pagesList = document.getElementById('pages-list');
const addPageBtn = document.getElementById('add-page-btn');
const pageModal = document.getElementById('page-modal');
const modalTitle = document.getElementById('modal-title');
const pageNameInput = document.getElementById('page-name');
const pageSlugInput = document.getElementById('page-slug');
const modalSave = document.getElementById('modal-save');
const modalCancel = document.getElementById('modal-cancel');
const modalClose = document.getElementById('modal-close');
const modalDelete = document.getElementById('modal-delete');
let pages = [];
let currentPageId = null;
let editingPageId = null;
// Generate unique ID
function generateId() {
return 'page_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// Generate slug from name
function slugify(text) {
return text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Load pages from storage
function loadPages() {
const stored = localStorage.getItem(PAGES_STORAGE_KEY);
if (stored) {
try {
pages = JSON.parse(stored);
} catch (e) {
pages = [];
}
}
// Create default home page if no pages exist
if (pages.length === 0) {
pages = [{
id: generateId(),
name: 'Home',
slug: 'index',
html: '',
css: ''
}];
savePages();
}
// Set current page to first page if not set
if (!currentPageId) {
currentPageId = pages[0].id;
}
renderPagesList();
loadPageContent(currentPageId);
}
// Save pages to storage
function savePages() {
localStorage.setItem(PAGES_STORAGE_KEY, JSON.stringify(pages));
}
// Save current page content
function saveCurrentPageContent() {
if (!currentPageId) return;
const page = pages.find(p => p.id === currentPageId);
if (page) {
page.html = editor.getHtml();
page.css = editor.getCss();
savePages();
}
}
// Load page content into editor
function loadPageContent(pageId) {
const page = pages.find(p => p.id === pageId);
if (!page) return;
currentPageId = pageId;
// Clear current content and load page content
editor.DomComponents.clear();
editor.CssComposer.clear();
if (page.html) {
editor.setComponents(page.html);
}
if (page.css) {
editor.setStyle(page.css);
}
renderPagesList();
}
// Switch to a different page
function switchToPage(pageId) {
if (pageId === currentPageId) return;
// Save current page first
saveCurrentPageContent();
// Load new page
loadPageContent(pageId);
}
// Create a single page item element
function createPageItem(page) {
const item = document.createElement('div');
item.className = 'page-item' + (page.id === currentPageId ? ' active' : '');
item.dataset.pageId = page.id;
// Create icon
const icon = document.createElement('div');
icon.className = 'page-item-icon';
icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></svg>';
// Create info section
const info = document.createElement('div');
info.className = 'page-item-info';
const nameEl = document.createElement('div');
nameEl.className = 'page-item-name';
nameEl.textContent = page.name; // Safe: uses textContent
const slugEl = document.createElement('div');
slugEl.className = 'page-item-slug';
slugEl.textContent = '/' + page.slug; // Safe: uses textContent
info.appendChild(nameEl);
info.appendChild(slugEl);
// Create actions
const actions = document.createElement('div');
actions.className = 'page-item-actions';
// Edit button
const editBtn = document.createElement('button');
editBtn.className = 'page-action-btn';
editBtn.dataset.action = 'edit';
editBtn.title = 'Edit';
editBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
actions.appendChild(editBtn);
// Delete button (only if more than one page)
if (pages.length > 1) {
const deleteBtn = document.createElement('button');
deleteBtn.className = 'page-action-btn danger';
deleteBtn.dataset.action = 'delete';
deleteBtn.title = 'Delete';
deleteBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
actions.appendChild(deleteBtn);
}
item.appendChild(icon);
item.appendChild(info);
item.appendChild(actions);
// Add click handler for switching pages
item.addEventListener('click', (e) => {
if (e.target.closest('.page-action-btn')) return;
switchToPage(page.id);
});
// Add action button handlers
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
openEditModal(page.id);
});
const deleteBtn = actions.querySelector('[data-action="delete"]');
if (deleteBtn) {
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deletePage(page.id);
});
}
return item;
}
// Render pages list using safe DOM methods
function renderPagesList() {
pagesList.innerHTML = '';
pages.forEach(page => {
pagesList.appendChild(createPageItem(page));
});
}
// Open modal for new page
function openNewPageModal() {
editingPageId = null;
modalTitle.textContent = 'Add New Page';
pageNameInput.value = '';
pageSlugInput.value = '';
modalDelete.style.display = 'none';
modalSave.textContent = 'Add Page';
pageModal.classList.add('visible');
pageNameInput.focus();
}
// Open modal for editing page
function openEditModal(pageId) {
const page = pages.find(p => p.id === pageId);
if (!page) return;
editingPageId = pageId;
modalTitle.textContent = 'Edit Page';
pageNameInput.value = page.name;
pageSlugInput.value = page.slug;
modalDelete.style.display = pages.length > 1 ? 'inline-block' : 'none';
modalSave.textContent = 'Save Changes';
pageModal.classList.add('visible');
pageNameInput.focus();
}
// Close modal
function closeModal() {
pageModal.classList.remove('visible');
editingPageId = null;
}
// Save page (new or edit)
function savePage() {
const name = pageNameInput.value.trim();
let slug = pageSlugInput.value.trim();
if (!name) {
alert('Please enter a page name');
return;
}
// Generate slug if empty
if (!slug) {
slug = slugify(name);
} else {
slug = slugify(slug);
}
// Ensure slug is unique
const existingPage = pages.find(p => p.slug === slug && p.id !== editingPageId);
if (existingPage) {
slug = slug + '-' + Date.now();
}
if (editingPageId) {
// Update existing page
const page = pages.find(p => p.id === editingPageId);
if (page) {
page.name = name;
page.slug = slug;
}
} else {
// Create new page
const newPage = {
id: generateId(),
name: name,
slug: slug,
html: '',
css: ''
};
pages.push(newPage);
// Save current page before switching
saveCurrentPageContent();
// Switch to new page
currentPageId = newPage.id;
editor.DomComponents.clear();
editor.CssComposer.clear();
}
savePages();
renderPagesList();
closeModal();
}
// Delete page
function deletePage(pageId) {
if (pages.length <= 1) {
alert('Cannot delete the last page');
return;
}
if (!confirm('Are you sure you want to delete this page? This cannot be undone.')) {
return;
}
const index = pages.findIndex(p => p.id === pageId);
if (index > -1) {
pages.splice(index, 1);
savePages();
// If deleting current page, switch to first page
if (pageId === currentPageId) {
currentPageId = pages[0].id;
loadPageContent(currentPageId);
} else {
renderPagesList();
}
}
closeModal();
}
// Auto-save current page on changes
editor.on('storage:end', () => {
saveCurrentPageContent();
});
// Auto-generate slug from name
pageNameInput.addEventListener('input', () => {
if (!editingPageId) {
pageSlugInput.value = slugify(pageNameInput.value);
}
});
// Modal event handlers
addPageBtn.addEventListener('click', openNewPageModal);
modalClose.addEventListener('click', closeModal);
modalCancel.addEventListener('click', closeModal);
modalSave.addEventListener('click', savePage);
modalDelete.addEventListener('click', () => {
if (editingPageId) {
deletePage(editingPageId);
}
});
// Close modal on overlay click
pageModal.addEventListener('click', (e) => {
if (e.target === pageModal) {
closeModal();
}
});
// Close modal on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && pageModal.classList.contains('visible')) {
closeModal();
}
});
// Initialize pages
loadPages();
// ==========================================
// Update Preview to include all pages
// ==========================================
// Override preview button to save all pages data
document.getElementById('btn-preview').addEventListener('click', () => {
// Save current page first
saveCurrentPageContent();
// Store all pages for preview
localStorage.setItem(STORAGE_KEY + '-preview', JSON.stringify({
pages: pages,
currentPageId: currentPageId
}));
// Open preview
window.open('preview.html', '_blank');
});
// Make editor accessible globally for debugging
window.editor = editor;
window.sitePages = pages;
// ==========================================
// Feature: Delete Section (parent + children)
// ==========================================
// Added in context menu handler below
// ==========================================
// Feature: Link Type Selector (URL / Page / Anchor)
// ==========================================
const linkTypeSelect = document.getElementById('link-type-select');
const linkUrlGroup = document.getElementById('link-url-group');
const linkPageGroup = document.getElementById('link-page-group');
const linkAnchorGroup = document.getElementById('link-anchor-group');
const linkPageSelect = document.getElementById('link-page-select');
const linkAnchorSelect = document.getElementById('link-anchor-select');
// Populate page dropdown
function populatePageDropdown() {
linkPageSelect.innerHTML = '';
pages.forEach(page => {
const opt = document.createElement('option');
opt.value = page.slug === 'index' ? '#' : page.slug + '.html';
opt.textContent = page.name;
linkPageSelect.appendChild(opt);
});
}
// Find all anchors on current page
function findAnchors() {
const anchors = [];
function walk(component) {
const id = component.getAttributes().id;
if (id) anchors.push(id);
component.components().forEach(walk);
}
editor.getWrapper().components().forEach(walk);
return anchors;
}
// Populate anchor dropdown
function populateAnchorDropdown() {
linkAnchorSelect.innerHTML = '<option value="">-- Select Anchor --</option>';
findAnchors().forEach(id => {
const opt = document.createElement('option');
opt.value = '#' + id;
opt.textContent = id;
linkAnchorSelect.appendChild(opt);
});
}
// Handle link type change
if (linkTypeSelect) {
linkTypeSelect.addEventListener('change', () => {
const type = linkTypeSelect.value;
linkUrlGroup.style.display = type === 'url' ? 'block' : 'none';
linkPageGroup.style.display = type === 'page' ? 'block' : 'none';
linkAnchorGroup.style.display = type === 'anchor' ? 'block' : 'none';
if (type === 'page') populatePageDropdown();
if (type === 'anchor') populateAnchorDropdown();
});
linkPageSelect.addEventListener('change', () => {
const selected = editor.getSelected();
if (selected) selected.addAttributes({ href: linkPageSelect.value });
});
linkAnchorSelect.addEventListener('change', () => {
const selected = editor.getSelected();
if (selected && linkAnchorSelect.value) {
selected.addAttributes({ href: linkAnchorSelect.value });
}
});
}
// ==========================================
// Feature: Asset Manager (inline - delegates to server when available)
// ==========================================
const ASSETS_STORAGE_KEY = 'sitebuilder-assets';
const assetUploadBtn = document.getElementById('asset-upload-btn');
const assetUploadInput = document.getElementById('asset-upload-input');
const assetUrlInput = document.getElementById('asset-url-input');
const assetAddUrlBtn = document.getElementById('asset-add-url-btn');
const assetsGrid = document.getElementById('assets-grid');
let assets = [];
let editorServerAvailable = false;
// Check if server API is available
async function checkEditorServer() {
try {
const resp = await fetch('/api/health');
if (resp.ok) {
const data = await resp.json();
editorServerAvailable = data.status === 'ok';
}
} catch (e) {
editorServerAvailable = false;
}
}
async function loadAssets() {
await checkEditorServer();
if (editorServerAvailable) {
try {
const resp = await fetch('/api/assets');
if (resp.ok) {
const data = await resp.json();
if (data.success && Array.isArray(data.assets)) {
assets = data.assets;
// Save lightweight metadata index to localStorage (no file contents)
saveAssetsMetadata();
renderAssets();
return;
}
}
} catch (e) {
console.warn('Failed to load assets from server:', e.message);
}
}
// Fallback: load from localStorage (metadata only, filter out base64)
try {
assets = JSON.parse(localStorage.getItem(ASSETS_STORAGE_KEY) || '[]');
assets = assets.filter(a => !a.url || !a.url.startsWith('data:'));
} catch(e) { assets = []; }
renderAssets();
}
function saveAssetsMetadata() {
// Save only metadata (no file contents) to localStorage
try {
const metadata = assets.map(a => ({
id: a.id, name: a.name, url: a.url,
type: a.type, size: a.size, added: a.added
}));
localStorage.setItem(ASSETS_STORAGE_KEY, JSON.stringify(metadata));
} catch (e) {
console.warn('Could not cache asset metadata to localStorage:', e.message);
}
}
function saveAssets() {
saveAssetsMetadata();
}
async function uploadFileToServer(file) {
const formData = new FormData();
formData.append('file', file);
const resp = await fetch('/api/assets/upload', { method: 'POST', body: formData });
if (!resp.ok) {
const errData = await resp.json().catch(() => ({ error: 'Upload failed' }));
throw new Error(errData.error || 'Upload failed');
}
const data = await resp.json();
if (data.success && data.assets && data.assets.length > 0) {
return data.assets[0];
}
throw new Error('No asset returned from server');
}
async function deleteAssetFromServer(asset) {
if (asset && asset.url && asset.url.startsWith('/storage/assets/')) {
const filename = asset.id || asset.filename || asset.url.split('/').pop();
try {
await fetch('/api/assets/' + encodeURIComponent(filename), { method: 'DELETE' });
} catch (e) {
console.warn('Server delete failed:', e.message);
}
}
}
function renderAssets() {
if (!assetsGrid) return;
assetsGrid.innerHTML = '';
assets.forEach((asset, i) => {
const item = document.createElement('div');
item.style.cssText = 'position:relative;border-radius:6px;overflow:hidden;border:1px solid #2d2d3a;cursor:pointer;aspect-ratio:1;background:#16161a;display:flex;align-items:center;justify-content:center;';
if (asset.type === 'image') {
const img = document.createElement('img');
img.src = asset.url;
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
img.alt = asset.name;
item.appendChild(img);
} else {
const icon = document.createElement('div');
icon.style.cssText = 'text-align:center;color:#71717a;font-size:11px;padding:8px;';
icon.innerHTML = `<div style="font-size:24px;margin-bottom:4px;">${asset.type === 'video' ? '🎬' : '📄'}</div><div style="word-break:break-all;">${asset.name}</div>`;
item.appendChild(icon);
}
// Delete button
const del = document.createElement('button');
del.style.cssText = 'position:absolute;top:4px;right:4px;background:rgba(0,0,0,0.7);border:none;color:#fff;width:20px;height:20px;border-radius:50%;cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center;';
del.textContent = '\u00d7';
del.addEventListener('click', async (e) => {
e.stopPropagation();
if (editorServerAvailable) {
await deleteAssetFromServer(assets[i]);
}
assets.splice(i, 1);
saveAssets();
renderAssets();
});
item.appendChild(del);
// Click to copy URL
item.addEventListener('click', () => {
navigator.clipboard.writeText(asset.url).then(() => {
item.style.outline = '2px solid #3b82f6';
setTimeout(() => item.style.outline = '', 1000);
});
});
assetsGrid.appendChild(item);
});
}
if (assetUploadBtn) {
assetUploadBtn.addEventListener('click', () => assetUploadInput.click());
assetUploadInput.addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
for (const file of files) {
const type = file.type.startsWith('image') ? 'image' : file.type.startsWith('video') ? 'video' : 'file';
if (editorServerAvailable) {
// Upload to server (no base64)
try {
const serverAsset = await uploadFileToServer(file);
assets.push(serverAsset);
saveAssets();
renderAssets();
if (serverAsset.type === 'image') {
editor.AssetManager.add({ src: serverAsset.url, name: serverAsset.name });
}
} catch (err) {
alert('Upload failed: ' + err.message);
}
} else {
// No server available - show error for file uploads
alert('File upload requires server.py to be running.\n\nStart it with: python3 server.py\n\nYou can still add assets by pasting external URLs.');
break;
}
}
assetUploadInput.value = '';
});
}
if (assetAddUrlBtn) {
assetAddUrlBtn.addEventListener('click', () => {
const url = assetUrlInput.value.trim();
if (!url) return;
const name = url.split('/').pop() || 'asset';
const type = url.match(/\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i) ? 'image' :
url.match(/\.(mp4|webm|ogg|mov)(\?.*)?$/i) ? 'video' : 'file';
assets.push({ name, url, type, id: 'asset_' + Date.now(), added: Date.now() });
saveAssets();
renderAssets();
if (type === 'image') editor.AssetManager.add({ src: url, name });
assetUrlInput.value = '';
});
}
loadAssets();
// ==========================================
// Feature: Head Elements & Site-wide CSS
// ==========================================
const HEAD_CODE_KEY = 'sitebuilder-head-code';
const SITEWIDE_CSS_KEY = 'sitebuilder-sitewide-css';
const headCodeTextarea = document.getElementById('head-code-textarea');
const headCodeApply = document.getElementById('head-code-apply');
const sitwideCssTextarea = document.getElementById('sitewide-css-textarea');
const sitewideCssApply = document.getElementById('sitewide-css-apply');
// Load saved head code
if (headCodeTextarea) {
headCodeTextarea.value = localStorage.getItem(HEAD_CODE_KEY) || '';
}
if (sitwideCssTextarea) {
sitwideCssTextarea.value = localStorage.getItem(SITEWIDE_CSS_KEY) || '';
}
if (headCodeApply) {
headCodeApply.addEventListener('click', () => {
localStorage.setItem(HEAD_CODE_KEY, headCodeTextarea.value);
alert('Head code saved! It will be included in exports.');
});
}
if (sitewideCssApply) {
sitewideCssApply.addEventListener('click', () => {
localStorage.setItem(SITEWIDE_CSS_KEY, sitwideCssTextarea.value);
// Apply to canvas immediately
const frame = editor.Canvas.getFrameEl();
if (frame && frame.contentDocument) {
let style = frame.contentDocument.getElementById('sitewide-css');
if (!style) {
style = frame.contentDocument.createElement('style');
style.id = 'sitewide-css';
frame.contentDocument.head.appendChild(style);
}
style.textContent = sitwideCssTextarea.value;
}
alert('Site-wide CSS saved and applied!');
});
}
// ==========================================
// Page HTML Editor Modal
// ==========================================
const pageCodeModal = document.getElementById('page-code-modal');
const pageCodeModalClose = document.getElementById('page-code-modal-close');
const pageCodeTextarea = document.getElementById('page-code-textarea');
const pageCodeApply = document.getElementById('page-code-apply');
const pageCodeCancel = document.getElementById('page-code-cancel');
// Open page HTML editor
document.getElementById('btn-view-code').addEventListener('click', () => {
const html = editor.getHtml();
const css = editor.getCss();
// Show HTML in textarea
pageCodeTextarea.value = html;
pageCodeModal.classList.add('visible');
});
// Close modal
function closePageCodeModal() {
pageCodeModal.classList.remove('visible');
}
pageCodeModalClose.addEventListener('click', closePageCodeModal);
pageCodeCancel.addEventListener('click', closePageCodeModal);
pageCodeModal.addEventListener('click', (e) => {
if (e.target === pageCodeModal) closePageCodeModal();
});
// Apply HTML changes
pageCodeApply.addEventListener('click', () => {
try {
const newHtml = pageCodeTextarea.value.trim();
// Clear current components
editor.DomComponents.clear();
// Set new HTML
editor.setComponents(newHtml);
// Close modal
closePageCodeModal();
// Deselect all
editor.select(null);
} catch (error) {
alert('Invalid HTML: ' + error.message);
}
});
// ==========================================
// Export Functionality
// ==========================================
const exportModal = document.getElementById('export-modal');
const exportModalClose = document.getElementById('export-modal-close');
const exportModalCancel = document.getElementById('export-modal-cancel');
const exportDownloadBtn = document.getElementById('export-download');
const exportPagesList = document.getElementById('export-pages-list');
const exportMinify = document.getElementById('export-minify');
const exportIncludeFonts = document.getElementById('export-include-fonts');
// Open export modal
document.getElementById('btn-export').addEventListener('click', () => {
// Save current page first
saveCurrentPageContent();
// Populate pages list
renderExportPagesList();
exportModal.classList.add('visible');
});
// Close export modal
function closeExportModal() {
exportModal.classList.remove('visible');
}
exportModalClose.addEventListener('click', closeExportModal);
exportModalCancel.addEventListener('click', closeExportModal);
exportModal.addEventListener('click', (e) => {
if (e.target === exportModal) closeExportModal();
});
// Render pages list in export modal
function renderExportPagesList() {
exportPagesList.innerHTML = '';
pages.forEach(page => {
const item = document.createElement('div');
item.className = 'export-page-item';
const info = document.createElement('div');
info.className = 'export-page-info';
const icon = document.createElement('div');
icon.className = 'export-page-icon';
icon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></svg>';
const textDiv = document.createElement('div');
const nameEl = document.createElement('div');
nameEl.className = 'export-page-name';
nameEl.textContent = page.name;
const fileEl = document.createElement('div');
fileEl.className = 'export-page-file';
fileEl.textContent = page.slug + '.html';
textDiv.appendChild(nameEl);
textDiv.appendChild(fileEl);
info.appendChild(icon);
info.appendChild(textDiv);
item.appendChild(info);
exportPagesList.appendChild(item);
});
}
// Generate HTML template for a page
function generatePageHtml(page, includeFonts, minifyCss) {
let css = page.css || '';
let html = page.html || '';
// Remove editor-only anchor elements completely (with nested content)
html = html.replace(/<div[^>]*data-anchor="true"[^>]*>[\s\S]*?<\/div>/g, '');
html = html.replace(/<div[^>]*class="editor-anchor"[^>]*>[\s\S]*?<\/div>/g, '');
// Minify CSS if requested
if (minifyCss && css) {
css = css
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
.replace(/\s+/g, ' ') // Collapse whitespace
.replace(/\s*([{};:,>+~])\s*/g, '$1') // Remove space around punctuation
.trim();
}
// Remove editor-anchor CSS rules from page CSS
css = css.replace(/\.editor-anchor[^}]*}/g, '');
const headCode = localStorage.getItem(HEAD_CODE_KEY) || '';
const sitewideCss = localStorage.getItem(SITEWIDE_CSS_KEY) || '';
const fontsLink = includeFonts
? '<link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap" rel="stylesheet">\n '
: '';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light">
<title>${page.name}</title>
${fontsLink}${headCode ? headCode + '\n ' : ''}<style>
/* Reset & Base */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; -webkit-font-smoothing: antialiased; }
img, video { max-width: 100%; height: auto; }
img { display: block; }
a { color: inherit; }
/* Accessibility: focus visible */
:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
:focus:not(:focus-visible) { outline: none; }
/* Touch-friendly: min tap targets */
a, button, input, select, textarea { min-height: 44px; }
button, [type="submit"] { cursor: pointer; touch-action: manipulation; }
/* Responsive columns */
@media (max-width: 768px) {
.row { flex-direction: column !important; }
.row .cell { flex-basis: 100% !important; width: 100% !important; }
section { padding-left: 16px !important; padding-right: 16px !important; }
}
@media (max-width: 480px) {
.row { flex-direction: column !important; }
.row .cell { flex-basis: 100% !important; width: 100% !important; }
h1 { font-size: 32px !important; }
h2 { font-size: 28px !important; }
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* Site-wide Styles */
${sitewideCss}
/* Page Styles */
${css}
/* Failsafe: Hide any editor-only elements that slipped through */
.editor-anchor,
[data-anchor="true"] {
display: none !important;
visibility: hidden !important;
position: absolute !important;
width: 0 !important;
height: 0 !important;
overflow: hidden !important;
}
</style>
</head>
<body>
<a href="#main-content" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;" onfocus="this.style.position='static';this.style.width='auto';this.style.height='auto';">Skip to main content</a>
<main id="main-content">
${page.html || ''}
</main>
</body>
</html>`;
}
// Copy HTML to clipboard (bypasses Windows security warnings)
const exportCopyBtn = document.getElementById('export-copy-html');
exportCopyBtn.addEventListener('click', async () => {
const includeFonts = exportIncludeFonts.checked;
const minifyCss = exportMinify.checked;
// Save current page first
saveCurrentPageContent();
// Get current page
const currentPage = pages.find(p => p.id === currentPageId);
if (!currentPage) {
alert('No page to export!');
return;
}
// Generate HTML
const html = generatePageHtml(currentPage, includeFonts, minifyCss);
// Copy to clipboard
try {
await navigator.clipboard.writeText(html);
// Show success feedback
const originalText = exportCopyBtn.innerHTML;
exportCopyBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Copied!
`;
exportCopyBtn.style.background = '#10b981';
setTimeout(() => {
exportCopyBtn.innerHTML = originalText;
exportCopyBtn.style.background = '';
}, 2000);
// Show instructions
alert(`✅ HTML copied to clipboard!\n\nNext steps:\n1. Open Notepad (or any text editor)\n2. Paste (Ctrl+V)\n3. Save as "${currentPage.slug}.html"\n4. Open the saved file in your browser\n\nThis bypasses Windows security warnings!`);
} catch (err) {
console.error('Copy failed:', err);
alert('Failed to copy to clipboard. Make sure you\'re using a modern browser with clipboard permissions.');
}
});
// Download as ZIP using JSZip (loaded dynamically)
exportDownloadBtn.addEventListener('click', async () => {
const includeFonts = exportIncludeFonts.checked;
const minifyCss = exportMinify.checked;
// Save current page first
saveCurrentPageContent();
// Check if JSZip is available, if not, load it
if (typeof JSZip === 'undefined') {
// Load JSZip dynamically
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
script.onload = () => createAndDownloadZip(includeFonts, minifyCss);
script.onerror = () => {
// Fallback: download single page if JSZip fails to load
alert('Could not load ZIP library. Downloading current page only.');
downloadSinglePage(pages.find(p => p.id === currentPageId), includeFonts, minifyCss);
};
document.head.appendChild(script);
} else {
createAndDownloadZip(includeFonts, minifyCss);
}
});
// Create ZIP and trigger download
async function createAndDownloadZip(includeFonts, minifyCss) {
const zip = new JSZip();
// Add each page as HTML file
pages.forEach(page => {
const html = generatePageHtml(page, includeFonts, minifyCss);
const filename = page.slug + '.html';
zip.file(filename, html);
});
// Generate and download ZIP
const content = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = 'site-export.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
closeExportModal();
}
// Fallback: download single page as HTML
function downloadSinglePage(page, includeFonts, minifyCss) {
if (!page) return;
const html = generatePageHtml(page, includeFonts, minifyCss);
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = page.slug + '.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
closeExportModal();
}
// ==========================================
// Template System
// ==========================================
let templateIndex = [];
let pendingTemplateId = null;
// Load template index via fetch (requires HTTP server)
async function loadTemplateIndex() {
try {
const resp = await fetch('templates/index.json');
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
templateIndex = await resp.json();
console.log('Loaded', templateIndex.length, 'templates from templates/index.json');
renderTemplateGrid(templateIndex);
} catch (e) {
console.error('Could not load templates:', e);
const grid = document.getElementById('templates-grid');
if (grid) {
grid.innerHTML = `
<div style="padding:24px;text-align:center;color:#71717a;">
<p style="font-size:14px;margin-bottom:8px;"> No templates available</p>
<p style="font-size:12px;">${e.message}</p>
<p style="font-size:11px;margin-top:8px;opacity:0.7;">Make sure HTTP server is running</p>
</div>
`;
}
}
}
function renderTemplateGrid(templates) {
const grid = document.getElementById('templates-grid');
if (!grid) return;
grid.innerHTML = '';
// Add "Start from Blank" card
const blankCard = document.createElement('div');
blankCard.className = 'template-card';
blankCard.innerHTML = `
<div style="width:100%;height:140px;background:#16161a;display:flex;align-items:center;justify-content:center;">
<div style="text-align:center;color:#71717a;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin:0 auto 8px;display:block;">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
<span style="font-size:12px;">Blank Canvas</span>
</div>
</div>
<div class="template-card-info">
<div class="template-card-name">Start from Scratch</div>
<div class="template-card-desc">Begin with a blank page and build your own design.</div>
</div>
`;
blankCard.addEventListener('click', () => {
if (confirm('Clear the canvas and start fresh?')) {
editor.DomComponents.clear();
editor.CssComposer.clear();
}
});
grid.appendChild(blankCard);
templates.forEach(t => {
const card = document.createElement('div');
card.className = 'template-card';
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 = `
<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-name">${t.name}</div>
<div class="template-card-desc">${t.description}</div>
<div class="template-card-tags">
${t.tags.slice(0, 3).map(tag => `<span class="template-tag">${tag}</span>`).join('')}
</div>
<div class="template-card-colors">
${t.colors.map(c => `<div class="template-color-dot" style="background:${c}"></div>`).join('')}
</div>
</div>
`;
card.addEventListener('click', () => showTemplateConfirm(t));
grid.appendChild(card);
});
}
function showTemplateConfirm(template) {
pendingTemplateId = template.id;
const modal = document.getElementById('template-modal');
document.getElementById('template-modal-title').textContent = template.name;
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';
}
// Templates browser modal handlers
const templatesBrowserModal = document.getElementById('templates-browser-modal');
const templatesBrowserClose = document.getElementById('templates-browser-close');
const btnTemplates = document.getElementById('btn-templates');
function openTemplatesBrowser() {
if (templatesBrowserModal) {
templatesBrowserModal.style.display = 'flex';
// Reload templates when opened
if (templateIndex.length > 0) {
renderTemplateGrid(templateIndex);
}
}
}
function closeTemplatesBrowser() {
if (templatesBrowserModal) {
templatesBrowserModal.style.display = 'none';
}
}
if (btnTemplates) {
btnTemplates.addEventListener('click', openTemplatesBrowser);
}
if (templatesBrowserClose) {
templatesBrowserClose.addEventListener('click', closeTemplatesBrowser);
}
if (templatesBrowserModal) {
// Close on background click
templatesBrowserModal.addEventListener('click', (e) => {
if (e.target === templatesBrowserModal) {
closeTemplatesBrowser();
}
});
// Close on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && templatesBrowserModal.style.display === 'flex') {
closeTemplatesBrowser();
}
});
}
// Template modal events
const templateModal = document.getElementById('template-modal');
const templateModalClose = document.getElementById('template-modal-close');
const templateModalCancel = document.getElementById('template-modal-cancel');
const templateModalConfirm = document.getElementById('template-modal-confirm');
function closeTemplateModal() {
templateModal.style.display = 'none';
pendingTemplateId = null;
}
if (templateModalClose) templateModalClose.addEventListener('click', closeTemplateModal);
if (templateModalCancel) templateModalCancel.addEventListener('click', closeTemplateModal);
if (templateModal) templateModal.addEventListener('click', (e) => {
if (e.target === templateModal) closeTemplateModal();
});
if (templateModalConfirm) {
templateModalConfirm.addEventListener('click', async () => {
if (!pendingTemplateId) return;
const template = templateIndex.find(t => t.id === pendingTemplateId);
if (!template) return;
try {
templateModalConfirm.textContent = 'Loading...';
templateModalConfirm.disabled = true;
if (template.pages && template.pages.length > 0) {
// Multi-page template: fetch all page HTML files in parallel
const fetches = template.pages.map(async (p) => {
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);
// Save current page content before clearing
saveCurrentPageContent();
// 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();
closeTemplatesBrowser(); // Also close the templates browser
// Show success notification
const status = document.getElementById('save-status');
if (status) {
const statusText = status.querySelector('.status-text');
const statusDot = status.querySelector('.status-dot');
if (statusText) statusText.textContent = 'Template loaded!';
if (statusDot) statusDot.style.background = '#10b981';
setTimeout(() => {
if (statusText) statusText.textContent = 'Saved';
if (statusDot) statusDot.style.background = '';
}, 2000);
}
} catch (e) {
console.error('Failed to load template:', e);
alert('Failed to load template: ' + e.message + '\n\nPlease check the console for details.');
} finally {
templateModalConfirm.textContent = 'Use Template';
templateModalConfirm.disabled = false;
}
});
}
// Template filter buttons
document.querySelectorAll('.template-filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.template-filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const cat = btn.dataset.category;
if (cat === 'all') {
renderTemplateGrid(templateIndex);
} else {
renderTemplateGrid(templateIndex.filter(t => t.category === cat));
}
});
});
// Load templates on init
loadTemplateIndex();
})();