Visual drag-and-drop website builder using GrapesJS with: - Multi-page editor with live preview - File-based asset storage via PHP API (no localStorage base64) - Template library, Docker support, and Playwright test suite Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4380 lines
190 KiB
JavaScript
4380 lines
190 KiB
JavaScript
/**
|
|
* 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;
|
|
|
|
// 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 style="min-height:500px;display:flex;align-items:center;justify-content:center;position:relative;padding:60px 20px;text-align:center;overflow:hidden;">
|
|
<video autoplay muted loop playsinline style="position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;transform:translate(-50%,-50%);z-index:0;">
|
|
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
|
|
</video>
|
|
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1;"></div>
|
|
<div style="position:relative;z-index:2;max-width:800px;">
|
|
<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>
|
|
<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 embeddable 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
|
|
if (placeholder) {
|
|
placeholder.addStyle({ display: 'none' });
|
|
const placeholderEl = placeholder.getEl();
|
|
if (placeholderEl) placeholderEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 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: '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: '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'));
|
|
|
|
if (iframe) {
|
|
// For Google Docs viewer for non-PDF files
|
|
let embedUrl = url;
|
|
if (!url.match(/\.pdf(\?.*)?$/i) && !url.includes('docs.google.com')) {
|
|
embedUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
|
|
}
|
|
iframe.addAttributes({ src: embedUrl });
|
|
iframe.addStyle({ display: 'block', height: height + 'px' });
|
|
const el = iframe.getEl();
|
|
if (el) { el.src = embedUrl; el.style.display = 'block'; el.style.height = height + 'px'; }
|
|
}
|
|
if (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;
|
|
card.innerHTML = `
|
|
<img class="template-card-thumb" src="templates/thumbnails/${t.id}.svg" alt="${t.name}" onerror="this.style.background='#2d2d3a'">
|
|
<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;
|
|
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;
|
|
|
|
// Load template HTML via fetch
|
|
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();
|
|
|
|
})();
|