/** * 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: '
Paste HTML/CSS here
', 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: ``, 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: `
Click this section, then add Video URL in Settings →

Video Background

This section has a looping video background with an overlay.

`, attributes: { class: 'fa fa-play-circle' } }); // Footer blockManager.add('footer', { label: 'Footer', category: 'Layout', content: ``, attributes: { class: 'fa fa-window-minimize' } }); // Column Layouts blockManager.add('column-1', { label: '1 Column', category: 'Layout', content: `
`, attributes: { class: 'gjs-fonts gjs-f-b1' } }); blockManager.add('column-2', { label: '2 Columns', category: 'Layout', content: `
`, attributes: { class: 'gjs-fonts gjs-f-b2' } }); blockManager.add('column-3', { label: '3 Columns', category: 'Layout', content: `
`, attributes: { class: 'gjs-fonts gjs-f-b3' } }); blockManager.add('column-4', { label: '4 Columns', category: 'Layout', content: `
`, attributes: { class: 'gjs-fonts gjs-f-b4' } }); blockManager.add('column-3-7', { label: '2 Columns 3/7', category: 'Layout', content: `
`, attributes: { class: 'gjs-fonts gjs-f-b37' } }); // Image Block blockManager.add('image-block', { label: 'Image', category: 'Media', content: 'Image', attributes: { class: 'fa fa-image' } }); // Unified Video Block (YouTube, Vimeo, or direct file) blockManager.add('video-block', { label: 'Video', category: 'Media', content: `
Select container & add Video URL in Settings
Supports YouTube, Vimeo, or direct video files
`, attributes: { class: 'fa fa-play-circle' } }); // Hero with Image Background blockManager.add('hero-image', { label: 'Hero (Image)', category: 'Sections', content: `

Your Headline Here

Add your compelling subheadline or description text here to engage your visitors.

Get Started
`, attributes: { class: 'fa fa-image' } }); // Hero with Video Background blockManager.add('hero-video', { label: 'Hero (Video)', category: 'Sections', content: `

Video Background Hero

Create stunning video backgrounds for your hero sections.

Learn More
`, attributes: { class: 'fa fa-play-circle' } }); // Simple Hero Section blockManager.add('hero-simple', { label: 'Hero (Simple)', category: 'Sections', content: `

Welcome

Your introductory text goes here.

Call to Action
`, attributes: { class: 'fa fa-star' } }); // Features Section blockManager.add('features-section', { label: 'Features Grid', category: 'Sections', content: `

Features

Feature One

Description of your first amazing feature goes here.

Feature Two

Description of your second amazing feature goes here.

Feature Three

Description of your third amazing feature goes here.

`, attributes: { class: 'fa fa-th-large' } }); // Testimonials Section blockManager.add('testimonials-section', { label: 'Testimonials', category: 'Sections', content: `

What People Say

Hear from our satisfied customers

"This product has completely transformed how we work. The results speak for themselves."

JD
John Doe
CEO, Company Inc

"Exceptional quality and outstanding customer service. I couldn't be happier with my experience."

JS
Jane Smith
Designer, Studio Co

"A game-changer for our business. The ROI has been incredible from day one."

MB
Mike Brown
Founder, StartupXYZ
`, attributes: { class: 'fa fa-comments' } }); // Pricing Section blockManager.add('pricing-section', { label: 'Pricing Table', category: 'Sections', content: `

Simple Pricing

Choose the plan that's right for you

Starter

Perfect for individuals

$9 /month
  • ✓ 5 Projects
  • ✓ Basic Support
  • ✓ 1GB Storage
  • ✓ Community Access
Get Started
MOST POPULAR

Professional

Best for growing teams

$29 /month
  • ✓ Unlimited Projects
  • ✓ Priority Support
  • ✓ 10GB Storage
  • ✓ Advanced Analytics
Get Started

Enterprise

For large organizations

$99 /month
  • ✓ Everything in Pro
  • ✓ Dedicated Support
  • ✓ Unlimited Storage
  • ✓ Custom Integrations
Contact Sales
`, attributes: { class: 'fa fa-credit-card' } }); // Contact Section blockManager.add('contact-section', { label: 'Contact Section', category: 'Sections', content: `

Get in Touch

Have questions? We'd love to hear from you. Send us a message and we'll respond as soon as possible.

📍
Address
123 Business Street, City, ST 12345
📧
Email
hello@example.com
📞
Phone
(555) 123-4567
`, attributes: { class: 'fa fa-envelope' } }); // Call to Action Section blockManager.add('cta-section', { label: 'Call to Action', category: 'Sections', content: `

Ready to Get Started?

Join thousands of satisfied customers and take your business to the next level.

Start Free Trial Learn More
`, attributes: { class: 'fa fa-bullhorn' } }); // Text Block blockManager.add('text-block', { label: 'Text', category: 'Basic', content: '

Insert your text here. You can edit this content directly.

', attributes: { class: 'gjs-fonts gjs-f-text' } }); // Heading Block blockManager.add('heading', { label: 'Heading', category: 'Basic', content: '

Heading

', attributes: { class: 'fa fa-header' } }); // Button Block blockManager.add('button-block', { label: 'Button', category: 'Basic', content: 'Button', 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: '
', attributes: { class: 'fa fa-arrows-alt-v' } }); // Anchor Point Block blockManager.add('anchor-point', { label: 'Anchor Point', category: 'Basic', content: `
`, attributes: { class: 'fa fa-anchor' } }); // PDF / File Embed Block blockManager.add('file-embed', { label: 'File / PDF', category: 'Media', content: `
📄
Select this element, then enter File URL in Settings
Supports PDF, DOC, and other embeddable files
`, 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: `

Gallery

Gallery image 1
Gallery image 2
Gallery image 3
Gallery image 4
Gallery image 5
Gallery image 6
`, attributes: { class: 'fa fa-th' } }); // FAQ Accordion blockManager.add('faq-section', { label: 'FAQ Accordion', category: 'Sections', content: `

Frequently Asked Questions

What is your return policy?

We offer a 30-day money-back guarantee on all purchases. If you're not satisfied, contact our support team for a full refund.

How long does shipping take?

Standard shipping takes 5-7 business days. Express shipping is available for 2-3 business day delivery.

Do you offer customer support?

Yes! Our support team is available 24/7 via email and live chat. Phone support is available Mon-Fri, 9am-5pm EST.

Can I cancel my subscription?

You can cancel your subscription at any time from your account settings. No cancellation fees apply.

`, attributes: { class: 'fa fa-question-circle' } }); // Stats/Counter Section blockManager.add('stats-section', { label: 'Stats Counter', category: 'Sections', content: `
10K+
Happy Customers
500+
Projects Completed
99%
Satisfaction Rate
24/7
Support Available
`, attributes: { class: 'fa fa-bar-chart' } }); // Team Grid blockManager.add('team-section', { label: 'Team Grid', category: 'Sections', content: `

Meet Our Team

The talented people behind our success

AJ

Alex Johnson

CEO & Founder

Visionary leader with 15+ years of experience.

SK

Sarah Kim

Lead Designer

Award-winning designer creating beautiful experiences.

MP

Mike Patel

CTO

Full-stack engineer building scalable systems.

`, attributes: { class: 'fa fa-users' } }); // Newsletter Signup blockManager.add('newsletter-section', { label: 'Newsletter', category: 'Sections', content: `

Stay in the Loop

Subscribe to our newsletter for the latest updates and exclusive offers.

No spam, unsubscribe anytime.

`, attributes: { class: 'fa fa-newspaper' } }); // Logo Cloud / Trusted By blockManager.add('logo-cloud', { label: 'Logo Cloud', category: 'Sections', content: `

Trusted by leading companies

Company 1
Company 2
Company 3
Company 4
Company 5
`, 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 = ''; 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 = ''; 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 = ''; } } // 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(`

Welcome to Site Builder

Drag and drop components from the left panel to build your website. Click on any element to edit its content and style.

Get Started
`); } }); // ========================================== // 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 = ''; // 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 = ''; 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 = ''; 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 = ''; 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 = `
${asset.type === 'video' ? '🎬' : '📄'}
${asset.name}
`; 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 = ''; 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(/]*data-anchor="true"[^>]*>[\s\S]*?<\/div>/g, ''); html = html.replace(/]*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 ? '\n \n \n ' : ''; return ` ${page.name} ${fontsLink}${headCode ? headCode + '\n ' : ''} Skip to main content
${page.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 = ` 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 = `

❌ No templates available

${e.message}

Make sure HTTP server is running

`; } } } 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 = `
Blank Canvas
Start from Scratch
Begin with a blank page and build your own design.
`; 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 = ` ${t.name}
${t.name}
${t.description}
${t.tags.slice(0, 3).map(tag => `${tag}`).join('')}
${t.colors.map(c => `
`).join('')}
`; 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(); })();