ngx-wysiwyg-editor
Version:
A powerful and customizable WYSIWYG HTML editor for Angular applications
1 lines • 123 kB
Source Map (JSON)
{"version":3,"file":"ngx-wysiwyg-editor.mjs","sources":["../../../projects/ngx-wysiwyg-editor/src/lib/wysiwyg-editor.component.ts","../../../projects/ngx-wysiwyg-editor/src/lib/wysiwyg-editor.component.html","../../../projects/ngx-wysiwyg-editor/src/lib/ngx-wysiwyg-editor.module.ts","../../../projects/ngx-wysiwyg-editor/src/public-api.ts","../../../projects/ngx-wysiwyg-editor/src/ngx-wysiwyg-editor.ts"],"sourcesContent":["import { Component, ElementRef, EventEmitter, Input, Output, ViewChild, forwardRef, OnInit, AfterViewInit, OnDestroy, ViewEncapsulation, ChangeDetectorRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { CdkDragDrop, moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';\n\nexport interface EmailBlock {\n id: string;\n type: 'header' | 'text' | 'image' | 'button' | 'divider' | 'columns' | 'social' | 'spacer' | 'video' | 'html';\n content?: any;\n settings?: any;\n}\n\nexport interface EmailContent {\n html: string;\n blocks: EmailBlock[];\n settings?: any;\n}\n\nexport interface ToolbarConfig {\n showBlocksButton?: boolean;\n showSettingsButton?: boolean;\n showSaveButton?: boolean;\n showLoadButton?: boolean;\n showExportButton?: boolean;\n showDeviceSelector?: boolean;\n showViewModeToggle?: boolean;\n showClearAllButton?: boolean;\n}\n\nexport interface EditorConfig {\n theme?: 'light' | 'dark';\n showBlockPanel?: boolean;\n showPropertiesPanel?: boolean;\n emailWidth?: string;\n backgroundColor?: string;\n fontFamily?: string;\n height?: string;\n minHeight?: string;\n maxHeight?: string;\n toolbar?: ToolbarConfig;\n}\n\n@Component({\n selector: 'wysiwyg-editor',\n standalone: true,\n imports: [CommonModule, FormsModule, DragDropModule],\n templateUrl: './wysiwyg-editor.component.html',\n styleUrls: ['./wysiwyg-editor.component.scss'],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => WysiwygEditorComponent),\n multi: true\n }\n ],\n encapsulation: ViewEncapsulation.None\n})\nexport class WysiwygEditorComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {\n @ViewChild('emailCanvas', { static: false }) emailCanvas!: ElementRef<HTMLDivElement>;\n\n @Input() config: EditorConfig = {};\n @Input() disabled = false;\n @Input() set blocks(value: EmailBlock[]) {\n if (value && Array.isArray(value)) {\n this.emailBlocks = [...value];\n this.renderEmail();\n this.emitChange();\n }\n }\n @Input() set emailSettings(value: any) {\n if (value && typeof value === 'object') {\n this._emailSettings = { ...this._emailSettings, ...value };\n this.renderEmail();\n this.emitChange();\n }\n }\n @Input() set initialContent(value: EmailContent) {\n if (value && typeof value === 'object') {\n if (value.blocks && Array.isArray(value.blocks)) {\n this.emailBlocks = [...value.blocks];\n }\n if (value.settings && typeof value.settings === 'object') {\n this._emailSettings = { ...this._emailSettings, ...value.settings };\n }\n this.renderEmail();\n this.emitChange();\n }\n }\n\n @Output() contentChange = new EventEmitter<EmailContent>();\n @Output() blockSelected = new EventEmitter<EmailBlock>();\n @Output() blocksChange = new EventEmitter<EmailBlock[]>();\n\n // Editor state\n emailBlocks: EmailBlock[] = [];\n selectedBlock: EmailBlock | null = null;\n selectedBlockIndex: number = -1;\n content = '';\n\n // UI state\n showBlockPanel = true;\n showPropertiesPanel = true;\n activeTab: 'blocks' | 'settings' = 'blocks';\n activePropertiesTab: 'content' | 'style' | 'advanced' = 'content';\n isDragging = false;\n devicePreview: 'mobile' | 'tablet' | 'desktop' = 'desktop';\n viewMode: 'edit' | 'preview' = 'edit';\n isMobile = false;\n showExportDropdown = false;\n private resizeListener: any;\n\n // Email settings\n private _emailSettings = {\n width: '600px',\n backgroundColor: '#f4f4f4',\n contentBackgroundColor: '#ffffff',\n fontFamily: 'Arial, sans-serif',\n fontSize: '14px',\n textColor: '#333333',\n linkColor: '#2196F3',\n padding: '20px'\n };\n\n get emailSettings() {\n return this._emailSettings;\n }\n\n // Available blocks\n availableBlocks = [\n { type: 'header', icon: '📰', label: 'Header', description: 'Logo and navigation' },\n { type: 'text', icon: '📝', label: 'Text', description: 'Paragraph text block' },\n { type: 'image', icon: '🖼️', label: 'Image', description: 'Single image' },\n { type: 'button', icon: '🔲', label: 'Button', description: 'Call-to-action button' },\n { type: 'divider', icon: '➖', label: 'Divider', description: 'Horizontal line' },\n { type: 'columns', icon: '⬜', label: 'Columns', description: 'Multi-column layout' },\n { type: 'social', icon: '👥', label: 'Social', description: 'Social media links' },\n { type: 'spacer', icon: '⬜', label: 'Spacer', description: 'Empty space' },\n { type: 'video', icon: '▶️', label: 'Video', description: 'Video thumbnail with link' },\n { type: 'html', icon: '</>', label: 'HTML', description: 'Custom HTML code' }\n ];\n\n // Block templates\n blockTemplates: { [key: string]: any } = {\n header: {\n logo: '',\n companyName: 'Your Company',\n tagline: 'Your tagline here',\n backgroundColor: '#2196F3',\n textColor: '#ffffff',\n height: '120px',\n alignment: 'center'\n },\n text: {\n content: '<p>Enter your text content here. You can format it with <strong>bold</strong>, <em>italic</em>, and more.</p>',\n padding: '20px',\n fontSize: '14px',\n lineHeight: '1.6',\n textAlign: 'left'\n },\n image: {\n src: 'https://via.placeholder.com/600x300',\n alt: 'Image description',\n width: '100%',\n alignment: 'center',\n padding: '10px',\n link: ''\n },\n button: {\n text: 'Click Here',\n url: '#',\n backgroundColor: '#2196F3',\n textColor: '#ffffff',\n borderRadius: '4px',\n padding: '12px 24px',\n fontSize: '16px',\n alignment: 'center',\n width: 'auto'\n },\n divider: {\n style: 'solid',\n width: '100%',\n color: '#e0e0e0',\n thickness: '1px',\n margin: '20px 0'\n },\n columns: {\n count: 2,\n gap: '20px',\n columns: [\n { content: '<h3>Column 1</h3><p>This is the content for the first column. You can add any HTML content here.</p>' },\n { content: '<h3>Column 2</h3><p>This is the content for the second column. You can add any HTML content here.</p>' }\n ]\n },\n social: {\n platforms: [\n { name: 'facebook', url: '#', icon: '📘' },\n { name: 'twitter', url: '#', icon: '🐦' },\n { name: 'instagram', url: '#', icon: '📷' },\n { name: 'linkedin', url: '#', icon: '💼' }\n ],\n alignment: 'center',\n iconSize: '32px',\n spacing: '10px'\n },\n spacer: {\n height: '30px'\n },\n video: {\n thumbnail: 'https://via.placeholder.com/600x340',\n videoUrl: '#',\n playButtonStyle: 'circle',\n playButtonColor: '#ffffff',\n playButtonBackground: 'rgba(0,0,0,0.7)'\n },\n html: {\n code: '<!-- Enter your custom HTML here -->'\n }\n };\n\n // Property editors for each block type\n currentBlockProperties: any = {};\n\n onChange: (value: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor(\n private sanitizer: DomSanitizer,\n private cdr: ChangeDetectorRef\n ) {}\n\n ngOnInit(): void {\n this.initializeConfig();\n this.loadDefaultTemplate();\n this.checkMobileView();\n\n // Listen for window resize\n this.resizeListener = () => {\n this.checkMobileView();\n };\n window.addEventListener('resize', this.resizeListener);\n\n // Listen for clicks to close dropdown\n document.addEventListener('click', (event) => {\n if (this.showExportDropdown) {\n const target = event.target as HTMLElement;\n const dropdown = target.closest('.export-dropdown');\n if (!dropdown) {\n this.showExportDropdown = false;\n this.cdr.detectChanges();\n }\n }\n });\n }\n\n ngOnDestroy(): void {\n // Clean up resize listener\n if (this.resizeListener) {\n window.removeEventListener('resize', this.resizeListener);\n }\n }\n\n ngAfterViewInit(): void {\n this.renderEmail();\n // Set initial device preview\n setTimeout(() => {\n this.updatePreviewSize();\n }, 100);\n }\n\n private checkMobileView(): void {\n const wasMobile = this.isMobile;\n this.isMobile = window.innerWidth <= 768;\n\n // On mobile, close panel by default when switching from desktop\n if (this.isMobile && !wasMobile) {\n this.showBlockPanel = false;\n }\n }\n\n // Getter for toolbar configuration with defaults\n get toolbarConfig(): ToolbarConfig {\n return {\n showBlocksButton: true,\n showSettingsButton: true,\n showSaveButton: false,\n showLoadButton: false,\n showExportButton: true,\n showDeviceSelector: true,\n showViewModeToggle: true,\n showClearAllButton: true,\n ...(this.config.toolbar || {})\n };\n }\n\n private initializeConfig(): void {\n this.config = {\n theme: 'light',\n showBlockPanel: true,\n showPropertiesPanel: true,\n emailWidth: '600px',\n backgroundColor: '#f4f4f4',\n fontFamily: 'Arial, sans-serif',\n ...this.config\n };\n\n this.showBlockPanel = this.config.showBlockPanel !== false;\n this.showPropertiesPanel = this.config.showPropertiesPanel !== false;\n\n if (this.config.emailWidth) {\n this.emailSettings.width = this.config.emailWidth;\n }\n if (this.config.backgroundColor) {\n this.emailSettings.backgroundColor = this.config.backgroundColor;\n }\n if (this.config.fontFamily) {\n this.emailSettings.fontFamily = this.config.fontFamily;\n }\n }\n\n private loadDefaultTemplate(): void {\n // Load a basic email template\n this.emailBlocks = [\n {\n id: this.generateId(),\n type: 'header',\n content: { ...this.blockTemplates['header'] }\n },\n {\n id: this.generateId(),\n type: 'text',\n content: {\n ...this.blockTemplates['text'],\n content: '<h2>Welcome to Our Newsletter!</h2><p>Thank you for subscribing. We\\'re excited to share our latest updates with you.</p>'\n }\n },\n {\n id: this.generateId(),\n type: 'button',\n content: { ...this.blockTemplates['button'] }\n }\n ];\n }\n\n private generateId(): string {\n return `block_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n // Drag and drop handlers\n onDragStarted(): void {\n this.isDragging = true;\n }\n\n onDragEnded(): void {\n this.isDragging = false;\n }\n\n drop(event: CdkDragDrop<EmailBlock[]>): void {\n moveItemInArray(this.emailBlocks, event.previousIndex, event.currentIndex);\n this.renderEmail();\n this.emitChange();\n }\n\n // Block management\n addBlock(blockType: string): void {\n // Deep clone the template for proper initialization\n const template = JSON.parse(JSON.stringify(this.blockTemplates[blockType]));\n\n const newBlock: EmailBlock = {\n id: this.generateId(),\n type: blockType as any,\n content: template\n };\n\n if (this.selectedBlockIndex >= 0) {\n // Insert after selected block\n this.emailBlocks.splice(this.selectedBlockIndex + 1, 0, newBlock);\n } else {\n // Add at the end\n this.emailBlocks.push(newBlock);\n }\n\n this.selectBlock(newBlock, this.emailBlocks.length - 1);\n this.renderEmail();\n this.emitChange();\n }\n\n duplicateBlock(block: EmailBlock, index: number): void {\n const duplicatedBlock: EmailBlock = {\n id: this.generateId(),\n type: block.type,\n content: { ...block.content }\n };\n\n this.emailBlocks.splice(index + 1, 0, duplicatedBlock);\n this.renderEmail();\n this.emitChange();\n }\n\n deleteBlock(index: number): void {\n this.emailBlocks.splice(index, 1);\n this.selectedBlock = null;\n this.selectedBlockIndex = -1;\n this.renderEmail();\n this.emitChange();\n }\n\n moveBlockUp(index: number): void {\n if (index > 0) {\n const temp = this.emailBlocks[index];\n this.emailBlocks[index] = this.emailBlocks[index - 1];\n this.emailBlocks[index - 1] = temp;\n this.selectedBlockIndex = index - 1;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n moveBlockDown(index: number): void {\n if (index < this.emailBlocks.length - 1) {\n const temp = this.emailBlocks[index];\n this.emailBlocks[index] = this.emailBlocks[index + 1];\n this.emailBlocks[index + 1] = temp;\n this.selectedBlockIndex = index + 1;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n selectBlock(block: EmailBlock | null, index: number): void {\n this.selectedBlock = block;\n this.selectedBlockIndex = index;\n this.currentBlockProperties = block ? { ...block.content } : {};\n\n if (block) {\n this.blockSelected.emit(block);\n this.showPropertiesPanel = true;\n this.activePropertiesTab = 'content';\n }\n }\n\n // Property updates (handled by enhanced version below)\n\n updateEmailSetting(property: string, value: any): void {\n (this.emailSettings as any)[property] = value;\n this.renderEmail();\n this.emitChange();\n }\n\n // Rendering\n renderEmail(): void {\n if (!this.emailCanvas) return;\n\n const html = this.generateEmailHtml();\n this.content = html;\n\n // Update preview iframe if in preview mode\n if (this.viewMode === 'preview') {\n const iframe = this.emailCanvas.nativeElement.querySelector('.email-preview');\n\n //@ts-ignore\n if (iframe && iframe.contentDocument) {\n\n //@ts-ignore\n iframe.contentDocument.open();\n\n //@ts-ignore\n iframe.contentDocument.write(html);\n\n //@ts-ignore\n iframe.contentDocument.close();\n\n // Apply device preview size\n this.updatePreviewSize();\n }\n }\n }\n\n private generateEmailHtml(): string {\n const blocks = this.emailBlocks.map(block => this.renderBlock(block)).join('');\n const isPreview = this.viewMode === 'preview';\n const emailWidth = parseInt(this.emailSettings.width) || 600;\n\n return `\n <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n <html xmlns=\"http://www.w3.org/1999/xhtml\">\n <head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Email Template</title>\n <!--[if gte mso 9]>\n <xml>\n <o:OfficeDocumentSettings>\n <o:AllowPNG/>\n <o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml>\n <![endif]-->\n <style type=\"text/css\">\n /* Reset styles for better email client compatibility */\n body, table, td, p, a, li, blockquote {\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n }\n table, td {\n mso-table-lspace: 0pt;\n mso-table-rspace: 0pt;\n }\n img {\n -ms-interpolation-mode: bicubic;\n border: 0;\n height: auto;\n line-height: 100%;\n outline: none;\n text-decoration: none;\n }\n\n /* Main body styles */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background-color: ${this.emailSettings.backgroundColor} !important;\n font-family: ${this.emailSettings.fontFamily} !important;\n font-size: ${this.emailSettings.fontSize} !important;\n color: ${this.emailSettings.textColor} !important;\n }\n\n /* Link styles */\n a {\n color: ${this.emailSettings.linkColor};\n text-decoration: underline;\n }\n\n /* Responsive styles */\n @media only screen and (max-width: 600px) {\n .email-container {\n width: 100% !important;\n max-width: 100% !important;\n }\n .mobile-full-width {\n width: 100% !important;\n max-width: 100% !important;\n }\n .mobile-padding {\n padding: 10px !important;\n }\n }\n\n /* Hide elements for preview mode */\n ${!isPreview ? `.block-wrapper { border: 1px dashed #ddd; margin: 2px 0; }` : ''}\n </style>\n </head>\n <body style=\"margin: 0; padding: 0; background-color: ${this.emailSettings.backgroundColor}; font-family: ${this.emailSettings.fontFamily}; font-size: ${this.emailSettings.fontSize}; color: ${this.emailSettings.textColor};\">\n <!-- Email Container Table -->\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: ${this.emailSettings.backgroundColor};\">\n <tr>\n <td align=\"center\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"${emailWidth}\" class=\"email-container\" style=\"max-width: ${emailWidth}px; background-color: ${this.emailSettings.contentBackgroundColor};\">\n <tr>\n <td>\n ${blocks}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n </html>\n `;\n }\n\n // Helper function to properly encode and format URLs for SendGrid\n private formatUrlForEmail(url: string | undefined): string {\n if (!url || url === '#') return '#';\n\n // If URL doesn't have protocol, add https://\n if (!url.match(/^https?:\\/\\//i)) {\n url = 'https://' + url;\n }\n\n // Encode special characters except those needed for URL structure\n // SendGrid handles its own tracking parameters, so we keep URLs clean\n return url.replace(/[<>\"]/g, (char) => {\n const entities: {[key: string]: string} = {\n '<': '%3C',\n '>': '%3E',\n '\"': '%22'\n };\n return entities[char] || char;\n });\n }\n\n // Helper function to create bulletproof button HTML for better SendGrid compatibility\n private createBulletproofButton(content: any): string {\n const url = this.formatUrlForEmail(content.url);\n const buttonBgColor = content.backgroundColor || '#2196F3';\n const buttonTextColor = content.textColor || '#ffffff';\n const buttonText = content.text || 'Click Here';\n const borderRadius = content.borderRadius || '4px';\n const fontSize = content.fontSize || '16px';\n const padding = content.padding || '12px 24px';\n\n // Parse padding for VML\n const paddingParts = padding.split(' ');\n const verticalPadding = parseInt(paddingParts[0]) || 12;\n const horizontalPadding = parseInt(paddingParts[1] || paddingParts[0]) || 24;\n\n // Calculate line height based on font size for better proportions\n const lineHeight = parseInt(fontSize) * 1.2;\n const buttonHeight = verticalPadding * 2 + lineHeight;\n\n return `\n <!--[if mso]>\n <v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" href=\"${url}\" style=\"height:${buttonHeight}px;v-text-anchor:middle;width:${buttonText.length * 8 + horizontalPadding * 2}px;\" arcsize=\"${parseInt(borderRadius) * 10}%\" stroke=\"f\" fillcolor=\"${buttonBgColor}\">\n <w:anchorlock/>\n <center>\n <![endif]-->\n <a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"background-color:${buttonBgColor};border-radius:${borderRadius};color:${buttonTextColor};display:inline-block;font-family:Arial,sans-serif;font-size:${fontSize};font-weight:bold;line-height:${lineHeight}px;text-align:center;text-decoration:none;width:auto;-webkit-text-size-adjust:none;padding:${padding};mso-padding-alt:0px;\">${buttonText}</a>\n <!--[if mso]>\n </center>\n </v:roundrect>\n <![endif]-->\n `;\n }\n\n renderBlock(block: EmailBlock): string {\n const content = block.content || {};\n const shouldRender = this.shouldRenderBlock(content);\n const isPreview = this.viewMode === 'preview';\n\n if (!shouldRender) {\n return '';\n }\n\n let blockContent = '';\n\n switch (block.type) {\n case 'header':\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color: ${content.backgroundColor || '#2196F3'};\">\n <tr>\n <td align=\"${content.alignment || 'center'}\" style=\"padding: 20px; color: ${content.textColor || '#ffffff'};\">\n <h1 style=\"margin: 0; font-size: 28px; font-family: Arial, sans-serif; font-weight: bold; color: ${content.textColor || '#ffffff'};\">\n ${content.companyName || 'Your Company'}\n </h1>\n <p style=\"margin: 10px 0 0 0; font-size: 16px; color: ${content.textColor || '#ffffff'}; opacity: 0.9;\">\n ${content.tagline || 'Your tagline here'}\n </p>\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'text':\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td style=\"padding: ${content.padding || '20px'}; text-align: ${content.textAlign || 'left'}; font-size: ${content.fontSize || '14px'}; line-height: ${content.lineHeight || '1.6'}; font-family: Arial, sans-serif;\">\n ${content.content || '<p>Enter your text content here.</p>'}\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'image':\n const imageWidth = content.width === '100%' ? '100%' : (parseInt(content.width) || 600);\n const imageUrl = this.formatUrlForEmail(content.link);\n const hasLink = content.link && content.link !== '#';\n\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td align=\"${content.alignment || 'center'}\" style=\"padding: ${content.padding || '10px'};\">\n ${hasLink ? `<a href=\"${imageUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"text-decoration: none; border: none; outline: none;\">` : ''}\n <img src=\"${content.src || 'https://via.placeholder.com/600x300'}\" alt=\"${content.alt || 'Image'}\"\n style=\"display: block; max-width: 100%; height: auto; border: 0; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic;\"\n width=\"${imageWidth}\">\n ${hasLink ? '</a>' : ''}\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'button':\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td align=\"${content.alignment || 'center'}\" style=\"padding: 20px;\">\n ${this.createBulletproofButton(content)}\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'divider':\n const dividerColor = content.color || '#e0e0e0';\n const dividerThickness = parseInt(content.thickness) || 1;\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td style=\"padding: ${content.margin || '20px 0'};\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"${content.width || '100%'}\" align=\"center\">\n <tr>\n <td style=\"border-top: ${dividerThickness}px ${content.style || 'solid'} ${dividerColor}; font-size: 0; line-height: 0;\">\n \n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'columns':\n const columnCount = content.count || 2;\n const columnWidth = Math.floor(100 / columnCount);\n const gapValue = parseInt(content.gap) || 20;\n const bgColor = content.columnBackground || '#f9f9f9';\n const columnCells = content.columns?.map((column: any, index: number) =>\n `<td width=\"${columnWidth}%\" valign=\"top\" style=\"padding: 0 ${gapValue / 2}px;\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td style=\"padding: 15px; background-color: ${bgColor}; border: 1px dashed #ddd; font-family: Arial, sans-serif; font-size: 14px;\">\n ${column.content || `<p style=\"color: #999; margin: 0;\">Column ${index + 1} content</p>`}\n </td>\n </tr>\n </table>\n </td>`\n ).join('') || '';\n\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td style=\"padding: 20px;\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n ${columnCells}\n </tr>\n </table>\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'spacer':\n const spacerHeight = parseInt(content.height) || 30;\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td style=\"font-size: 0; line-height: 0; height: ${spacerHeight}px;\">\n \n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'video':\n // For email clients, we'll use a static thumbnail with a play button overlay\n const videoUrl = this.formatUrlForEmail(content.videoUrl);\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td align=\"center\" style=\"padding: 20px;\">\n <a href=\"${videoUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"display: inline-block; position: relative; text-decoration: none; border: none; outline: none;\">\n <img src=\"${content.thumbnail || 'https://via.placeholder.com/600x340'}\" alt=\"Video thumbnail\"\n style=\"display: block; max-width: 500px; width: 100%; height: auto; border: 0; -ms-interpolation-mode: bicubic;\">\n <!--[if !mso]><!-->\n <div style=\"position: absolute; top: 50%; left: 50%; width: 60px; height: 60px; margin-left: -30px; margin-top: -30px; background-color: ${content.playButtonBackground || 'rgba(0,0,0,0.7)'}; border-radius: ${content.playButtonStyle === 'circle' ? '30px' : '8px'};\">\n <!-- Play button triangle -->\n </div>\n <!--<![endif]-->\n </a>\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'social':\n const socialIcons = content.platforms?.map((platform: any) => {\n const socialUrl = this.formatUrlForEmail(platform.url);\n return `<td style=\"padding: 0 ${parseInt(content.spacing) / 2 || 5}px;\">\n <a href=\"${socialUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"text-decoration: none; font-size: ${content.iconSize || '32px'}; color: inherit; border: none; outline: none;\">\n ${platform.icon || '📱'}\n </a>\n </td>`;\n }).join('') || '';\n\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td align=\"${content.alignment || 'center'}\" style=\"padding: 20px;\">\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tr>\n ${socialIcons}\n </tr>\n </table>\n </td>\n </tr>\n </table>\n `;\n break;\n\n case 'html':\n blockContent = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n <tr>\n <td>\n ${content.code || content.html || '<!-- Custom HTML content -->'}\n </td>\n </tr>\n </table>\n `;\n break;\n\n default:\n return '';\n }\n\n // Wrap each block in a table with optional border for edit mode\n const borderStyle = !isPreview ? 'border: 1px dashed #ddd; margin: 2px 0;' : '';\n const wrapperTable = `\n <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" class=\"block-wrapper\" style=\"${borderStyle}\">\n <tr>\n <td>\n ${blockContent}\n </td>\n </tr>\n </table>\n `;\n\n // Wrap with block markers for parsing\n return `<!-- block:${block.type}:start -->${wrapperTable}<!-- block:${block.type}:end -->`;\n }\n\n // UI Actions\n toggleBlockPanel(): void {\n this.showBlockPanel = !this.showBlockPanel;\n\n // On mobile, open with blocks tab by default\n if (this.isMobile && this.showBlockPanel) {\n this.activeTab = 'blocks';\n }\n }\n\n togglePropertiesPanel(): void {\n this.showPropertiesPanel = !this.showPropertiesPanel;\n }\n\n showSettingsTab(): void {\n this.activeTab = 'settings';\n this.showBlockPanel = true;\n }\n\n closeMobilePanel(): void {\n if (this.isMobile) {\n this.showBlockPanel = false;\n }\n }\n\n closePropertiesPanel(): void {\n if (this.isMobile) {\n this.showPropertiesPanel = false;\n }\n }\n\n // Device preview methods\n setDevicePreview(device: 'mobile' | 'tablet' | 'desktop'): void {\n this.devicePreview = device;\n this.updatePreviewSize();\n // Re-render to apply visibility changes based on new device mode\n this.renderEmail();\n }\n\n // View mode methods\n setViewMode(mode: 'edit' | 'preview'): void {\n this.viewMode = mode;\n if (mode === 'preview') {\n // Hide panels when in preview mode\n this.showBlockPanel = false;\n this.showPropertiesPanel = false;\n setTimeout(() => {\n this.renderEmail();\n }, 100);\n } else {\n // Restore panels when in edit mode\n this.showBlockPanel = true;\n }\n }\n\n updatePreviewSize(): void {\n if (!this.emailCanvas) return;\n\n const container = this.emailCanvas.nativeElement;\n const iframe = container.querySelector('.email-preview');\n\n if (iframe && this.viewMode === 'preview') {\n // The container already handles the width via getDeviceWidth()\n // Just ensure the iframe fills its container\n //@ts-ignore\n iframe.style.width = '100%';\n //@ts-ignore\n iframe.style.height = '600px';\n //@ts-ignore\n iframe.style.display = 'block';\n }\n }\n\n getDeviceWidth(): string {\n switch (this.devicePreview) {\n case 'mobile':\n return '375px';\n case 'tablet':\n return '768px';\n case 'desktop':\n return '100%';\n default:\n return '100%';\n }\n }\n\n // Export dropdown methods\n toggleExportDropdown(): void {\n this.showExportDropdown = !this.showExportDropdown;\n }\n\n exportToClipboard(): void {\n const html = this.generateEmailHtml();\n navigator.clipboard.writeText(html).then(() => {\n // Show success feedback\n this.showNotification('HTML copied to clipboard successfully!');\n }).catch(err => {\n console.error('Failed to copy to clipboard:', err);\n this.showNotification('Failed to copy to clipboard. Please try again.');\n });\n this.showExportDropdown = false;\n }\n\n exportToFile(): void {\n const html = this.generateEmailHtml();\n const blob = new Blob([html], { type: 'text/html' });\n const url = window.URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'email-template.html';\n a.click();\n window.URL.revokeObjectURL(url);\n this.showExportDropdown = false;\n }\n\n // Legacy method for backward compatibility\n exportHtml(): void {\n this.exportToFile();\n }\n\n private showNotification(message: string): void {\n // Simple notification implementation\n // You could replace this with a more sophisticated notification system\n const notification = document.createElement('div');\n notification.textContent = message;\n notification.style.cssText = `\n position: fixed;\n top: 20px;\n right: 20px;\n background: #4CAF50;\n color: white;\n padding: 12px 24px;\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n z-index: 10000;\n font-family: Arial, sans-serif;\n font-size: 14px;\n transition: all 0.3s ease;\n `;\n\n document.body.appendChild(notification);\n\n // Auto-remove after 3 seconds\n setTimeout(() => {\n notification.style.opacity = '0';\n notification.style.transform = 'translateY(-20px)';\n setTimeout(() => {\n if (notification.parentNode) {\n notification.parentNode.removeChild(notification);\n }\n }, 300);\n }, 3000);\n }\n\n clearAll(): void {\n if (confirm('Are you sure you want to clear all blocks?')) {\n this.emailBlocks = [];\n this.selectedBlock = null;\n this.selectedBlockIndex = -1;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n // Public API methods for external block management\n getBlocks(): EmailBlock[] {\n return [...this.emailBlocks];\n }\n\n setBlocks(blocks: EmailBlock[]): void {\n if (blocks && Array.isArray(blocks)) {\n this.emailBlocks = [...blocks];\n this.selectedBlock = null;\n this.selectedBlockIndex = -1;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n addBlockAtIndex(block: EmailBlock, index: number): void {\n if (block && index >= 0 && index <= this.emailBlocks.length) {\n this.emailBlocks.splice(index, 0, block);\n this.renderEmail();\n this.emitChange();\n }\n }\n\n removeBlockAtIndex(index: number): void {\n if (index >= 0 && index < this.emailBlocks.length) {\n this.emailBlocks.splice(index, 1);\n if (this.selectedBlockIndex === index) {\n this.selectedBlock = null;\n this.selectedBlockIndex = -1;\n }\n this.renderEmail();\n this.emitChange();\n }\n }\n\n updateBlockAtIndex(index: number, block: EmailBlock): void {\n if (block && index >= 0 && index < this.emailBlocks.length) {\n this.emailBlocks[index] = block;\n if (this.selectedBlockIndex === index) {\n this.selectedBlock = block;\n }\n this.renderEmail();\n this.emitChange();\n }\n }\n\n // Template management\n saveTemplate(): void {\n const template = {\n blocks: this.emailBlocks,\n settings: this.emailSettings,\n timestamp: new Date().toISOString()\n };\n localStorage.setItem('email-template', JSON.stringify(template));\n alert('Template saved successfully!');\n }\n\n loadTemplate(): void {\n const saved = localStorage.getItem('email-template');\n if (saved) {\n const template = JSON.parse(saved);\n this.emailBlocks = template.blocks;\n this.emailSettings = template.settings;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n // ControlValueAccessor methods\n writeValue(value: string): void {\n if (value === this.content && this.emailBlocks.length > 0) {\n // No change needed if same value and blocks exist\n return;\n }\n\n this.content = value || '';\n\n // If we receive HTML content, try to parse it into blocks\n if (value && value.trim()) {\n this.parseHtmlToBlocks(value);\n } else {\n // Clear blocks if no value\n this.emailBlocks = [];\n this.selectedBlock = null;\n this.selectedBlockIndex = -1;\n }\n\n // Trigger change detection and update the preview\n this.cdr.detectChanges();\n\n // Update the preview after change detection\n setTimeout(() => {\n this.renderEmail();\n this.cdr.detectChanges();\n }, 0);\n }\n\n private parseHtmlToBlocks(html: string): void {\n // Try to extract blocks from the HTML content\n // This is a simplified parser - in production, you'd want a more robust solution\n this.emailBlocks = [];\n\n // Check if it's our generated HTML with block markers\n const blockMatches = html.match(/<!-- block:(\\w+):start -->([\\s\\S]*?)<!-- block:\\w+:end -->/g);\n\n if (blockMatches && blockMatches.length > 0) {\n // Parse our marked blocks\n blockMatches.forEach(match => {\n const typeMatch = match.match(/<!-- block:(\\w+):start -->/);\n const contentMatch = match.match(/<!-- block:\\w+:start -->([\\s\\S]*?)<!-- block:\\w+:end -->/);\n\n if (typeMatch && contentMatch) {\n const type = typeMatch[1];\n const blockHtml = contentMatch[1];\n const block = this.parseBlockFromHtml(type, blockHtml);\n if (block) {\n this.emailBlocks.push(block);\n }\n }\n });\n } else {\n // Fallback: Create a single HTML block with the entire content\n // Use 'code' property as that's what the template expects\n const htmlBlock: EmailBlock = {\n id: this.generateId(),\n type: 'html',\n content: {\n ...this.blockTemplates['html'],\n code: html // Use 'code' property, not 'html'\n },\n settings: {}\n };\n this.emailBlocks.push(htmlBlock);\n }\n }\n\n private parseBlockFromHtml(type: string, html: string): EmailBlock | null {\n const block: EmailBlock = {\n id: this.generateId(),\n type: type as any,\n content: { ...this.blockTemplates[type] },\n settings: {}\n };\n\n // Parse content based on block type\n switch (type) {\n case 'text':\n // For text blocks, use 'content' property\n block.content.content = html.trim();\n break;\n case 'html':\n // For HTML blocks, use 'code' property\n block.content.code = html;\n break;\n case 'header':\n // Extract company name and tagline from header HTML\n const h1Match = html.match(/<h1[^>]*>(.*?)<\\/h1>/);\n const pMatch = html.match(/<p[^>]*>(.*?)<\\/p>/);\n if (h1Match) block.content.companyName = h1Match[1];\n if (pMatch) block.content.tagline = pMatch[1];\n break;\n // Add more block type parsers as needed\n default:\n // For unknown types, try to use as HTML block\n block.type = 'html';\n block.content = { ...this.blockTemplates['html'], code: html };\n break;\n }\n\n return block;\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n }\n\n private emitChange(): void {\n const html = this.generateEmailHtml();\n this.onChange(html);\n\n // Emit comprehensive content model\n const emailContent: EmailContent = {\n html: html,\n blocks: [...this.emailBlocks],\n settings: this.emailSettings\n };\n this.contentChange.emit(emailContent);\n\n // Also emit blocks separately for backward compatibility\n this.blocksChange.emit([...this.emailBlocks]);\n }\n\n // Public method to get current email content on demand\n getEmailContent(): EmailContent {\n return {\n html: this.generateEmailHtml(),\n blocks: [...this.emailBlocks],\n settings: { ...this.emailSettings }\n };\n }\n\n // Utility methods\n getSafeHtml(html: string): SafeHtml {\n return this.sanitizer.bypassSecurityTrustHtml(html);\n }\n\n // Advanced properties helper methods\n getAdvancedStyles(content: any): string {\n let styles = '';\n\n if (content.customMargin) {\n styles += `margin: ${content.customMargin}; `;\n }\n\n // Use device preview mode instead of window width for better preview accuracy\n const isMobileView = this.devicePreview === 'mobile';\n const isTabletView = this.devicePreview === 'tablet';\n const isDesktopView = this.devicePreview === 'desktop';\n\n if (content.mobileWidth && isMobileView) {\n styles += `width: ${content.mobileWidth}; `;\n }\n\n if (content.visibility === 'hidden') {\n styles += 'display: none; ';\n } else if (content.visibility === 'mobile-only' && !isMobileView) {\n styles += 'display: none; ';\n } else if (content.visibility === 'tablet-only' && !isTabletView) {\n styles += 'display: none; ';\n } else if (content.visibility === 'desktop-only' && !isDesktopView) {\n styles += 'display: none; ';\n }\n\n if (content.animation && content.animation !== 'none') {\n const delay = content.animationDelay || 0;\n styles += `animation: ${content.animation} 0.5s ease-in-out ${delay}ms; `;\n }\n\n return styles;\n }\n\n getAdvancedAttributes(content: any): string {\n let attributes = '';\n\n if (content.blockId) {\n attributes += `id=\"${content.blockId}\" `;\n }\n\n if (content.customAttributes) {\n const lines = content.customAttributes.split('\\n');\n lines.forEach((line: string) => {\n if (line.trim()) {\n attributes += `${line.trim()} `;\n }\n });\n }\n\n if (content.debugMode) {\n attributes += `data-debug=\"true\" `;\n }\n\n return attributes;\n }\n\n getWrapperClasses(content: any): string {\n let classes = 'block-wrapper';\n\n if (content.cssClass) {\n classes += ` ${content.cssClass}`;\n }\n\n if (content.visibility) {\n classes += ` visibility-${content.visibility}`;\n }\n\n if (content.animation && content.animation !== 'none') {\n classes += ` animated ${content.animation}`;\n }\n\n return classes;\n }\n\n shouldRenderBlock(content: any): boolean {\n // Check if block should be excluded from export\n if (content.excludeFromExport) {\n return false;\n }\n\n // Check display condition (simplified - in real implementation, you'd evaluate the condition)\n if (content.displayCondition && content.displayCondition.trim()) {\n // For demo purposes, we'll just show the block\n // In a real implementation, you'd evaluate the condition\n console.log('Display condition:', content.displayCondition);\n }\n\n return true;\n }\n\n getBlockIcon(type: string): string {\n const block = this.availableBlocks.find(b => b.type === type);\n return block ? block.icon : '📄';\n }\n\n getBlockLabel(type: string): string {\n const block = this.availableBlocks.find(b => b.type === type);\n return block ? block.label : type;\n }\n\n // Columns management methods\n updateColumnsCount(newCount: number): void {\n if (!this.selectedBlock || this.selectedBlock.type !== 'columns') return;\n\n const currentColumns = this.selectedBlock.content.columns || [];\n const newColumns = [];\n\n // Keep existing columns and add new empty ones if needed\n for (let i = 0; i < newCount; i++) {\n if (i < currentColumns.length) {\n newColumns.push(currentColumns[i]);\n } else {\n newColumns.push({\n content: `<h3>Column ${i + 1}</h3><p>This is the content for column ${i + 1}. You can add any HTML content here.</p>`\n });\n }\n }\n\n this.selectedBlock.content.count = parseInt(newCount as any);\n this.selectedBlock.content.columns = newColumns;\n this.currentBlockProperties.count = newCount;\n this.currentBlockProperties.columns = [...newColumns];\n\n this.renderEmail();\n this.emitChange();\n }\n\n updateColumnContent(columnIndex: number, content: string): void {\n if (!this.selectedBlock || this.selectedBlock.type !== 'columns') return;\n\n if (this.selectedBlock.content.columns && this.selectedBlock.content.columns[columnIndex]) {\n this.selectedBlock.content.columns[columnIndex].content = content;\n this.currentBlockProperties.columns[columnIndex].content = content;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n // Social media management\n updateSocialPlatform(platformIndex: number, property: string, value: any): void {\n if (!this.selectedBlock || this.selectedBlock.type !== 'social') return;\n\n if (this.selectedBlock.content.platforms && this.selectedBlock.content.platforms[platformIndex]) {\n this.selectedBlock.content.platforms[platformIndex][property] = value;\n this.currentBlockProperties.platforms[platformIndex][property] = value;\n this.renderEmail();\n this.emitChange();\n }\n }\n\n // Enhanced block property update with special handling\n updateBlockProperty(property: string, value: any): void {\n if (this.selectedBlock) {\n if (!this.selectedBlock.content) {\n this.selectedBlock.content = {};\n }\n this.selectedBlock.content[property] = value;\n this.currentBlockProperties[property] = value;\n\n // Special handling for certain properties\n if (this.selectedBlock.type === 'columns' && property === 'gap') {\n // Ensure gap is applied to all columns\n this.renderEmail();\n }\n\n this.renderEmail();\n this.emitChange();\n }\n }\n}\n","<div class=\"email-editor\"\n [style.height]=\"config.height || '600px'\"\n [style.min-height]=\"config.minHeight\"\n [style.max-height]=\"config.maxHeight\">\n <!-- Top Toolbar -->\n <div class=\"editor-toolbar\">\n <div class=\"toolbar-left\">\n <button type=\"button\" class=\"toolbar-btn\"\n *ngIf=\"toolbarConfig.showBlocksButton\"\n (click)=\"toggleBlockPanel()\"\n [class.active]=\"showBlockPanel\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"></rect>\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"></rect>\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"></rect>\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"></rect>\n </svg>\n Blocks\n </button>\n <button type=\"button\" class=\"toolbar-btn\"\n *ngIf=\"toolbarConfig.showSettingsButton\"\n (click)=\"showSettingsTab()\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n <path d=\"M12 1v6m0 6v6m4.22-13.22l4.24 4.24M1.54 19.54l4.24-4.24M22.46 19.54l-4.24-4.24M1.54 4.46l4.24 4.24\"></path>\n </svg>\n Settings\n </button>\n </div>\n\n <div class=\"toolbar-center\">\n <span class=\"editor-title\">Email Template Editor</span>\n </div>\n\n <div class=\"toolbar-right\">\n <button type=\"button\" class=\"toolbar-btn\"\n *ngIf=\"toolbarConfig.showSaveButton\"\n (click)=\"saveTemplate()\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"></path>\n <polyline points=\"17 21 17 13 7 13 7 21\"></polyline>\n <polyline points=\"7 3 7 8 15 8\"></polyline>\n </svg>\n Save\n </button>\n <button type=\"button\" class=\"toolbar-btn\"\n *ngIf=\"toolbarConfig.showLoadButton\"\n (click)=\"loadTemplate()\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"></path>\n <polyline points=\"7 10 12 15 17 10\"></polyline>\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line>\n </svg>\n Load\n </button>\n <div class=\"export-dropdown\"\n *ngIf=\"toolbarConfig.showExportButton\"\n [class.open]=\"showExportDropdown\">\n <button type=\"button\" class=\"toolbar-btn primary dropdown-toggle\" (click)=\"toggleExportDropdown()\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"></path>\n <polyline points=\"17 8 12 3 7 8\"></polyline>\n <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"></line>\n </svg>\n Export\n <svg class=\"dropdown-arrow\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\