UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

1,525 lines (1,366 loc) 51.9 kB
/** * Styling Tab Component * Handles the styling configuration tab in the bot management interface */ import { waitForElement, getSafeElement, waitForWidgetReady, } from '../../../utils/DOMReady.js'; export class StylingTab { constructor(widget, state, i18n = null) { this.widget = widget; this.state = state; this.i18n = i18n; this.isReady = false; this.readyPromise = null; } /** * Update language * @param {string} language - New language code */ updateLanguage(language) { if (this.i18n) { console.log(`[StylingTab] Language updated to: ${language}`); // Re-render styling content if needed } } /** * Get translated text * @param {string} key - Translation key * @param {Object} variables - Variables for substitution * @returns {string} Translated text */ t(key, variables = {}) { return this.i18n ? this.i18n.t(`styling.${key}`, variables) : key; } /** * Initialize the tab and ensure it's ready for operations * @returns {Promise<void>} Promise that resolves when tab is ready */ async initialize() { if (this.readyPromise) { return this.readyPromise; } this.readyPromise = this.createReadyPromise(); return this.readyPromise; } /** * Create the ready promise for this tab * @returns {Promise<void>} Promise that resolves when ready */ async createReadyPromise() { const requiredElements = [ '#bm-primaryColorPicker', '#bm-fontFamilySelect', '#bm-widgetSizeSelect', ]; if (this.widget) { await waitForWidgetReady(this.widget, requiredElements, 10000); } this.isReady = true; console.log('[SUCCESS] StylingTab is ready'); } /** * Generate the HTML for the styling tab * @returns {string} Styling tab HTML string */ getHTML() { return ` <div class="bm-tab-content" id="bm-tab-styling"> <h2 style="font-size: 24px; font-weight: 500; color: #022d54; margin: 0 0 8px 0;">${this.t('title')}</h2> <p style="font-size: 14px; color: #6b7684; margin: 0 0 32px 0;">${this.t('subtitle')}</p> <div class="bm-grid"> <!-- Widget Styling --> ${this.getWidgetStylingCard()} <!-- Advanced Custom Styling --> ${this.getAdvancedStylingCard()} </div> <div class="bm-actions"> <button class="bm-btn bm-btn-primary" id="bm-saveStylingBtn">${this.t('actions.save')}</button> <button class="bm-btn bm-btn-secondary" id="bm-resetStylingBtn">${this.t('actions.reset')}</button> </div> </div> `; } /** * Get Widget Styling card HTML */ getWidgetStylingCard() { return ` <div class="bm-card bm-card-full"> <h2 class="bm-card-title" style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> </svg> Widget Styling </h2> <div class="bm-styling-grid"> ${this.getColorControls()} ${this.getLogoUploadControls()} ${this.getSizeControls()} ${this.getStyleControls()} </div> </div> `; } /** * Get color controls HTML */ getColorControls() { return ` <!-- Primary Brand Colors --> <div class="bm-form-group"> <label class="bm-form-label">Primary Color</label> <input type="color" class="bm-form-input color-input" id="bm-primaryColorPicker" value="#5897de"> </div> <div class="bm-form-group"> <label class="bm-form-label">Secondary Color</label> <input type="color" class="bm-form-input color-input" id="bm-secondaryColorPicker" value="#022d54"> </div> <div class="bm-form-group"> <label class="bm-form-label">Accent Color</label> <input type="color" class="bm-form-input color-input" id="bm-accentColorPicker" value="#ffbc82"> </div> <!-- Advanced Colors Section --> <div class="bm-advanced-colors"> <button type="button" class="bm-expand-toggle" id="bm-expandAdvancedColors"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="6 9 12 15 18 9"></polyline> </svg> Show more color options </button> <div class="bm-advanced-colors-content" id="bm-advancedColorsContent" style="display: none;"> <!-- Chat Bubble Colors --> <div class="bm-color-section"> <h4 class="bm-color-section-title">Chat Bubble Colors</h4> <div class="bm-form-group"> <label class="bm-form-label">User Bubble Background</label> <input type="color" class="bm-form-input color-input" id="bm-userBubbleBgColorPicker" value="#5897de"> </div> <div class="bm-form-group"> <label class="bm-form-label">User Bubble Text</label> <input type="color" class="bm-form-input color-input" id="bm-userBubbleTextColorPicker" value="#ffffff"> </div> <div class="bm-form-group"> <label class="bm-form-label">Bot Bubble Background</label> <input type="color" class="bm-form-input color-input" id="bm-botBubbleBgColorPicker" value="#f0f4f8"> </div> <div class="bm-form-group"> <label class="bm-form-label">Bot Bubble Text</label> <input type="color" class="bm-form-input color-input" id="bm-botBubbleTextColorPicker" value="#333333"> </div> </div> <!-- Interface Colors --> <div class="bm-color-section"> <h4 class="bm-color-section-title">Interface Colors</h4> <div class="bm-form-group"> <label class="bm-form-label">Header Background</label> <input type="color" class="bm-form-input color-input" id="bm-headerBgColorPicker" value="#ffffff"> </div> <div class="bm-form-group"> <label class="bm-form-label">Header Text</label> <input type="color" class="bm-form-input color-input" id="bm-headerTextColorPicker" value="#333333"> </div> <div class="bm-form-group"> <label class="bm-form-label">Chat Background</label> <input type="color" class="bm-form-input color-input" id="bm-chatBgColorPicker" value="#ffffff"> </div> </div> <!-- Input Styling --> <div class="bm-color-section"> <h4 class="bm-color-section-title">Input Styling</h4> <div class="bm-form-group"> <label class="bm-form-label">Input Background</label> <input type="color" class="bm-form-input color-input" id="bm-inputBgColorPicker" value="#ffffff"> </div> <div class="bm-form-group"> <label class="bm-form-label">Input Border</label> <input type="color" class="bm-form-input color-input" id="bm-inputBorderColorPicker" value="#e2e5e9"> </div> <div class="bm-form-group"> <label class="bm-form-label">Input Text</label> <input type="color" class="bm-form-input color-input" id="bm-inputTextColorPicker" value="#333333"> </div> </div> <!-- Interaction Colors --> <div class="bm-color-section"> <h4 class="bm-color-section-title">Interaction Colors</h4> <div class="bm-form-group"> <label class="bm-form-label">Button Hover</label> <input type="color" class="bm-form-input color-input" id="bm-buttonHoverColorPicker" value="#4a7bc8"> </div> <div class="bm-form-group"> <label class="bm-form-label">Scrollbar</label> <input type="color" class="bm-form-input color-input" id="bm-scrollbarColorPicker" value="#d0d4da"> </div> <div class="bm-form-group"> <label class="bm-form-label">Shadow Color</label> <input type="color" class="bm-form-input color-input" id="bm-shadowColorPicker" value="#000000"> <p class="bm-form-hint">Shadow color (opacity will be applied automatically)</p> </div> </div> <!-- Table Overflow Warning Colors --> <div class="bm-color-section"> <h4 class="bm-color-section-title">Table Overflow Warning</h4> <div class="bm-form-group"> <label class="bm-form-label">Table Overflow Background</label> <input type="color" class="bm-form-input color-input" id="bm-tableOverflowBgColorPicker" value="#fff3cd"> <p class="bm-form-hint">Background color for table overflow warning</p> </div> <div class="bm-form-group"> <label class="bm-form-label">Table Overflow Border</label> <input type="color" class="bm-form-input color-input" id="bm-tableOverflowBorderColorPicker" value="#ffeaa7"> <p class="bm-form-hint">Border color for table overflow warning</p> </div> <div class="bm-form-group"> <label class="bm-form-label">Table Overflow Text</label> <input type="color" class="bm-form-input color-input" id="bm-tableOverflowTextColorPicker" value="#856404"> <p class="bm-form-hint">Text color for table overflow warning</p> </div> </div> </div> </div> `; } /** * Get logo upload controls HTML */ getLogoUploadControls() { return ` <!-- Logo Upload Section --> <div class="bm-form-group bm-logo-upload-section"> <label class="bm-form-label"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;"> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> <circle cx="8.5" cy="8.5" r="1.5"/> <polyline points="21,15 16,10 5,21"/> </svg> Bot Logo </label> <div class="bm-logo-upload-container"> <div class="bm-logo-preview-area" id="bm-logoPreviewArea"> <div class="bm-logo-placeholder" id="bm-logoPlaceholder"> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> <circle cx="8.5" cy="8.5" r="1.5"/> <polyline points="21,15 16,10 5,21"/> </svg> </div> <img id="bm-logoPreview" style="display: none; max-width: 100%; max-height: 100%; object-fit: contain;" /> </div> <div class="bm-upload-info"> <div class="bm-upload-title">Upload Logo</div> <div class="bm-upload-specs">PNG, JPG or SVG • Max 2MB • Recommended: 120x120px</div> </div> <div class="bm-upload-actions"> <input type="file" id="bm-logoFileInput" accept="image/*" style="display: none;"> <button type="button" class="bm-btn bm-btn-secondary" id="bm-uploadLogoBtn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> <polyline points="7 10 12 15 17 10"/> <line x1="12" y1="15" x2="12" y2="3"/> </svg> Choose File </button> <button type="button" class="bm-btn bm-btn-text" id="bm-removeLogoBtn" style="display: none;">Remove</button> </div> </div> </div> `; } /** * Get size controls HTML */ getSizeControls() { return ` <div class="bm-form-group"> <label class="bm-form-label">Widget Size</label> <select class="bm-form-select" id="bm-widgetSizeSelect"> <option value="small">Small (320px)</option> <option value="medium" selected>Medium (400px)</option> <option value="large">Large (500px)</option> </select> </div> <div class="bm-form-group"> <label class="bm-form-label">Logo Size</label> <select class="bm-form-select" id="bm-logoSizeSelect"> <option value="small">Small (24px)</option> <option value="medium" selected>Medium (32px)</option> <option value="large">Large (48px)</option> </select> </div> <div class="bm-form-group"> <label class="bm-form-label">Message Font Size</label> <select class="bm-form-select" id="bm-messageFontSizeSelect"> <option value="small">Small (13px)</option> <option value="medium" selected>Medium (14px)</option> <option value="large">Large (16px)</option> </select> </div> `; } /** * Get style controls HTML */ getStyleControls() { return ` <div class="bm-form-group"> <label class="bm-form-label">Font Family</label> <select class="bm-form-select" id="bm-fontFamilySelect"> <option value="system">System Default</option> <option value="Inter">Inter</option> <option value="Roboto">Roboto</option> <option value="Open Sans">Open Sans</option> <option value="Poppins">Poppins</option> <option value="Lato">Lato</option> <option value="Nunito">Nunito</option> </select> </div> <div class="bm-form-group"> <label class="bm-form-label">Chat Bubble Style</label> <select class="bm-form-select" id="bm-chatBubbleStyleSelect"> <option value="modern" selected>Modern</option> <option value="classic">Classic</option> <option value="minimal">Minimal</option> <option value="rounded">Rounded</option> </select> </div> <div class="bm-form-group"> <label class="bm-form-label">Typing Indicator</label> <select class="bm-form-select" id="bm-typingIndicatorStyleSelect"> <option value="dots" selected>Dots</option> <option value="pulse">Pulse</option> <option value="wave">Wave</option> <option value="minimal">Minimal</option> </select> </div> <div class="bm-form-group"> <label class="bm-form-label"> <input type="checkbox" id="bm-closeOnOutsideClickCheckbox" checked> Close on outside click </label> </div> `; } /** * Get Advanced Styling card HTML */ getAdvancedStylingCard() { return ` <div class="bm-card bm-card-full bm-card-disabled"> <h2 class="bm-card-title" style="display: flex; align-items: center; gap: 8px;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 20h9"></path> <path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"></path> </svg> Advanced Custom Styling <span class="bm-upcoming-badge">Coming Soon</span> </h2> <div class="bm-disabled-content"> <div class="bm-disabled-overlay"> <div class="besper-h3">Advanced Styling Coming Soon</div> <div class="besper-p">We're working on advanced styling features including custom CSS editor, visual component editor, and live preview capabilities.</div> <div class="besper-p">Stay tuned for these exciting new features!</div> </div> </div> </div> `; } /** * Setup event listeners for styling tab * @param {Object} callbacks - Callback functions for events */ setupEventListeners(callbacks = {}) { // Color picker changes const colorPickers = this.widget.querySelectorAll('.color-input'); colorPickers.forEach(picker => { picker.addEventListener('change', () => { if (callbacks.onColorChange) { callbacks.onColorChange(this.getStylingData()); } }); }); // Select changes const selects = this.widget.querySelectorAll('.bm-form-select'); selects.forEach(select => { select.addEventListener('change', () => { if (callbacks.onStyleChange) { callbacks.onStyleChange(this.getStylingData()); } }); }); // Checkbox changes const checkboxes = this.widget.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { checkbox.addEventListener('change', () => { if (callbacks.onStyleChange) { callbacks.onStyleChange(this.getStylingData()); } }); }); // Advanced colors expand/collapse toggle const expandToggle = this.widget.querySelector('#bm-expandAdvancedColors'); const advancedContent = this.widget.querySelector( '#bm-advancedColorsContent' ); if (expandToggle && advancedContent) { expandToggle.addEventListener('click', e => { e.preventDefault(); const isExpanded = advancedContent.style.display !== 'none'; if (isExpanded) { advancedContent.style.display = 'none'; expandToggle.innerHTML = ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="6 9 12 15 18 9"></polyline> </svg> Show more color options `; } else { advancedContent.style.display = 'block'; expandToggle.innerHTML = ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="18 15 12 9 6 15"></polyline> </svg> Hide more color options `; } }); } // Logo upload functionality this.setupLogoUpload(callbacks); // Save and reset buttons const saveBtn = this.widget.querySelector('#bm-saveStylingBtn'); const resetBtn = this.widget.querySelector('#bm-resetStylingBtn'); if (saveBtn && callbacks.onSave) { saveBtn.addEventListener('click', callbacks.onSave); } if (resetBtn && callbacks.onReset) { resetBtn.addEventListener('click', callbacks.onReset); } } /** * Setup logo upload functionality * @param {Object} callbacks - Callback functions for events */ setupLogoUpload(callbacks = {}) { const fileInput = this.widget.querySelector('#bm-logoFileInput'); const uploadBtn = this.widget.querySelector('#bm-uploadLogoBtn'); const removeBtn = this.widget.querySelector('#bm-removeLogoBtn'); if (uploadBtn && fileInput) { uploadBtn.addEventListener('click', () => { fileInput.click(); }); } if (fileInput) { fileInput.addEventListener('change', e => { const file = e.target.files[0]; if (file) { this.handleLogoUpload(file, callbacks); } }); } if (removeBtn) { removeBtn.addEventListener('click', () => { this.removeLogo(callbacks); }); } } /** * Handle logo file upload * @param {File} file - The uploaded file * @param {Object} callbacks - Callback functions */ handleLogoUpload(file, callbacks = {}) { // Validate file type if (!file.type.startsWith('image/')) { console.error('Please select an image file (PNG, JPG, SVG).'); return; } // Validate file size (2MB limit) if (file.size > 2 * 1024 * 1024) { console.error('File size must be less than 2MB.'); return; } // Read file as base64 const reader = new FileReader(); reader.onload = e => { const base64 = e.target.result; this.setLogoPreview(base64); // Store logo data this.logoData = { name: file.name, base64, size: file.size, }; // Trigger callback if (callbacks.onLogoChange) { callbacks.onLogoChange(this.logoData); } }; reader.readAsDataURL(file); } /** * Set logo preview * @param {string} base64 - Base64 encoded image */ setLogoPreview(base64) { const preview = this.widget.querySelector('#bm-logoPreview'); const placeholder = this.widget.querySelector('#bm-logoPlaceholder'); const removeBtn = this.widget.querySelector('#bm-removeLogoBtn'); if (preview && placeholder && removeBtn) { preview.src = base64; preview.style.display = 'block'; placeholder.style.display = 'none'; removeBtn.style.display = 'inline-flex'; } } /** * Remove logo * @param {Object} callbacks - Callback functions */ removeLogo(callbacks = {}) { const preview = this.widget.querySelector('#bm-logoPreview'); const placeholder = this.widget.querySelector('#bm-logoPlaceholder'); const removeBtn = this.widget.querySelector('#bm-removeLogoBtn'); const fileInput = this.widget.querySelector('#bm-logoFileInput'); if (preview && placeholder && removeBtn && fileInput) { preview.style.display = 'none'; placeholder.style.display = 'flex'; removeBtn.style.display = 'none'; fileInput.value = ''; // Clear logo data this.logoData = null; // Trigger callback if (callbacks.onLogoChange) { callbacks.onLogoChange(null); } } } /** * Get current styling data from the form * @returns {Object} Styling data */ getStylingData() { // Debug logging to track form data capture console.log('🔍 StylingTab.getStylingData() debug:'); console.log(' widget available:', !!this.widget); console.log( ' widget type:', this.widget ? typeof this.widget : 'undefined' ); // Test if key elements exist before processing const primaryColorPicker = this.widget ? this.widget.querySelector('#bm-primaryColorPicker') : document.getElementById('bm-primaryColorPicker'); const widgetSizeSelect = this.widget ? this.widget.querySelector('#bm-widgetSizeSelect') : document.getElementById('bm-widgetSizeSelect'); console.log( ' primaryColorPicker found via basic search:', !!primaryColorPicker ); console.log( ' primaryColorPicker.value via basic search:', primaryColorPicker ? primaryColorPicker.value : 'null' ); console.log( ' widgetSizeSelect found via basic search:', !!widgetSizeSelect ); console.log( ' widgetSizeSelect.value via basic search:', widgetSizeSelect ? widgetSizeSelect.value : 'null' ); console.log('🔧 Collecting styling data with enhanced element finding...'); const result = { // Primary brand colors primary_color: this.getInputValue('bm-primaryColorPicker'), secondary_color: this.getInputValue('bm-secondaryColorPicker'), accent_color: this.getInputValue('bm-accentColorPicker'), // Chat bubble colors user_bubble_bg_color: this.getInputValue('bm-userBubbleBgColorPicker'), user_bubble_text_color: this.getInputValue( 'bm-userBubbleTextColorPicker' ), bot_bubble_bg_color: this.getInputValue('bm-botBubbleBgColorPicker'), bot_bubble_text_color: this.getInputValue('bm-botBubbleTextColorPicker'), // Interface colors header_bg_color: this.getInputValue('bm-headerBgColorPicker'), header_text_color: this.getInputValue('bm-headerTextColorPicker'), chat_bg_color: this.getInputValue('bm-chatBgColorPicker'), // Input styling input_bg_color: this.getInputValue('bm-inputBgColorPicker'), input_border_color: this.getInputValue('bm-inputBorderColorPicker'), input_text_color: this.getInputValue('bm-inputTextColorPicker'), // Interaction colors button_hover_color: this.getInputValue('bm-buttonHoverColorPicker'), scrollbar_color: this.getInputValue('bm-scrollbarColorPicker'), shadow_color: this.getInputValue('bm-shadowColorPicker'), // Table overflow warning colors table_overflow_bg_color: this.getInputValue( 'bm-tableOverflowBgColorPicker' ), table_overflow_border_color: this.getInputValue( 'bm-tableOverflowBorderColorPicker' ), table_overflow_text_color: this.getInputValue( 'bm-tableOverflowTextColorPicker' ), // Typography and layout font_family: this.getInputValue('bm-fontFamilySelect'), widget_size: this.getInputValue('bm-widgetSizeSelect'), logo_size: this.getInputValue('bm-logoSizeSelect'), message_font_size: this.getInputValue('bm-messageFontSizeSelect'), chat_bubble_style: this.getInputValue('bm-chatBubbleStyleSelect'), typing_indicator_style: this.getInputValue( 'bm-typingIndicatorStyleSelect' ), close_on_outside_click: this.getCheckboxValue( 'bm-closeOnOutsideClickCheckbox' ), // Logo data logo_url: this.logoData ? this.logoData.base64 : null, // Custom CSS (if implemented) custom_css: this.getInputValue('bm-customCssEditor'), }; console.log('📋 Final styling data collected:'); console.log(' primary_color:', `"${result.primary_color}"`); console.log(' secondary_color:', `"${result.secondary_color}"`); console.log(' widget_size:', `"${result.widget_size}"`); console.log(' font_family:', `"${result.font_family}"`); console.log(' close_on_outside_click:', result.close_on_outside_click); console.log( ' total fields with values:', Object.entries(result).filter(([_key, value]) => value && value !== '') .length ); // Check for completely empty result const hasAnyData = Object.entries(result).some( ([_key, value]) => value && value !== '' && value !== null && value !== false ); if (!hasAnyData) { console.warn('[WARN] No styling data captured - all fields are empty!'); // Try emergency data capture using document search console.log('🚨 Attempting emergency data capture...'); const emergencyData = this.emergencyDataCapture(); if (emergencyData && Object.keys(emergencyData).length > 0) { console.log( '[SUCCESS] Emergency data capture successful:', emergencyData ); return { ...result, ...emergencyData }; } } return result; } /** * Emergency data capture method when normal methods fail * @returns {Object} Emergency styling data */ emergencyDataCapture() { console.log( '🚨 emergencyDataCapture() - trying to find ANY styling elements' ); const emergencyData = {}; // Try to find color inputs by class or type const colorInputs = document.querySelectorAll( 'input[type="color"], .color-input' ); console.log( ` Found ${colorInputs.length} color inputs via type/class search` ); colorInputs.forEach((input, index) => { const id = input.id || `color_${index}`; const value = input.value || ''; console.log(` color input ${index}: id="${id}", value="${value}"`); // Map common IDs to our field names if (id.includes('primary') || id.includes('Primary')) { emergencyData.primary_color = value; } else if (id.includes('secondary') || id.includes('Secondary')) { emergencyData.secondary_color = value; } else if (id.includes('accent') || id.includes('Accent')) { emergencyData.accent_color = value; } }); // Try to find selects const selects = document.querySelectorAll( 'select.bm-form-select, select[id*="widget"], select[id*="font"]' ); console.log(` Found ${selects.length} select elements`); selects.forEach((select, index) => { const id = select.id || `select_${index}`; const value = select.value || ''; console.log(` select ${index}: id="${id}", value="${value}"`); if (id.includes('widget') || id.includes('Widget')) { emergencyData.widget_size = value; } else if (id.includes('font') || id.includes('Font')) { emergencyData.font_family = value; } }); console.log(' Emergency data collected:', emergencyData); return emergencyData; } /** * Load styling data into the form */ async loadStylingData(stylingData) { if (!stylingData) { console.log( '[WARN] loadStylingData called with no data - using HTML defaults' ); return; } console.log('🔧 loadStylingData called with:', stylingData); console.log(' widget available:', !!this.widget); console.log(' stylingData keys:', Object.keys(stylingData)); // Ensure tab is ready before loading data await this.initialize(); // Load primary brand colors (with fallbacks to ensure values are never empty) await this.setInputValueSafely( 'bm-primaryColorPicker', stylingData.primary_color || '#5897de' ); await this.setInputValueSafely( 'bm-secondaryColorPicker', stylingData.secondary_color || '#022d54' ); await this.setInputValueSafely( 'bm-accentColorPicker', stylingData.accent_color || '#ffbc82' ); // Load chat bubble colors await this.setInputValueSafely( 'bm-userBubbleBgColorPicker', stylingData.user_bubble_bg_color || '#5897de' ); await this.setInputValueSafely( 'bm-userBubbleTextColorPicker', stylingData.user_bubble_text_color || '#ffffff' ); await this.setInputValueSafely( 'bm-botBubbleBgColorPicker', stylingData.bot_bubble_bg_color || '#f0f4f8' ); await this.setInputValueSafely( 'bm-botBubbleTextColorPicker', stylingData.bot_bubble_text_color || '#333333' ); // Load interface colors await this.setInputValueSafely( 'bm-headerBgColorPicker', stylingData.header_bg_color || '#ffffff' ); await this.setInputValueSafely( 'bm-headerTextColorPicker', stylingData.header_text_color || '#333333' ); await this.setInputValueSafely( 'bm-chatBgColorPicker', stylingData.chat_bg_color || '#ffffff' ); // Load input styling await this.setInputValueSafely( 'bm-inputBgColorPicker', stylingData.input_bg_color || '#ffffff' ); await this.setInputValueSafely( 'bm-inputBorderColorPicker', stylingData.input_border_color || '#e2e5e9' ); await this.setInputValueSafely( 'bm-inputTextColorPicker', stylingData.input_text_color || '#333333' ); // Load interaction colors await this.setInputValueSafely( 'bm-buttonHoverColorPicker', stylingData.button_hover_color || '#4a7bc8' ); await this.setInputValueSafely( 'bm-scrollbarColorPicker', stylingData.scrollbar_color || '#d0d4da' ); await this.setInputValueSafely( 'bm-shadowColorPicker', stylingData.shadow_color || '#000000' ); // Load table overflow colors await this.setInputValueSafely( 'bm-tableOverflowBgColorPicker', stylingData.table_overflow_bg_color || '#fff3cd' ); await this.setInputValueSafely( 'bm-tableOverflowBorderColorPicker', stylingData.table_overflow_border_color || '#ffeaa7' ); await this.setInputValueSafely( 'bm-tableOverflowTextColorPicker', stylingData.table_overflow_text_color || '#856404' ); // Load typography and layout await this.setInputValueSafely( 'bm-fontFamilySelect', stylingData.font_family || 'system' ); await this.setInputValueSafely( 'bm-widgetSizeSelect', stylingData.widget_size || 'medium' ); await this.setInputValueSafely( 'bm-logoSizeSelect', stylingData.logo_size || 'medium' ); await this.setInputValueSafely( 'bm-messageFontSizeSelect', stylingData.message_font_size || 'medium' ); await this.setInputValueSafely( 'bm-chatBubbleStyleSelect', stylingData.chat_bubble_style || 'modern' ); await this.setInputValueSafely( 'bm-typingIndicatorStyleSelect', stylingData.typing_indicator_style || 'dots' ); await this.setCheckboxValueSafely( 'bm-closeOnOutsideClickCheckbox', stylingData.close_on_outside_click !== undefined ? stylingData.close_on_outside_click : true ); // Load logo if available if (stylingData.logo_url) { this.setLogoPreview(stylingData.logo_url); this.logoData = { base64: stylingData.logo_url, name: 'Uploaded Logo', size: 0, // Size unknown for existing logos }; } // Load custom CSS if available await this.setInputValueSafely( 'bm-customCssEditor', stylingData.custom_css ); console.log('[SUCCESS] Styling data loaded successfully'); // Verify that values were actually set by checking a few key elements (without setTimeout) await this.verifyStylingValuesLoaded(stylingData); } /** * Verify styling values were loaded correctly */ async verifyStylingValuesLoaded(stylingData) { try { const primaryColorElement = await waitForElement( '#bm-primaryColorPicker', this.widget, 1000 ); const fontFamilyElement = await waitForElement( '#bm-fontFamilySelect', this.widget, 1000 ); const widgetSizeElement = await waitForElement( '#bm-widgetSizeSelect', this.widget, 1000 ); const primaryColor = primaryColorElement?.value; const fontFamily = fontFamilyElement?.value; const widgetSize = widgetSizeElement?.value; console.log('🔍 Verification of loaded styling values:'); console.log( ' Primary color:', primaryColor, '(expected:', stylingData.primary_color || '#5897de', ')' ); console.log( ' Font family:', fontFamily, '(expected:', stylingData.font_family || 'system', ')' ); console.log( ' Widget size:', widgetSize, '(expected:', stylingData.widget_size || 'medium', ')' ); // Check if we're seeing only defaults when we expected custom values const hasCustomPrimary = stylingData.primary_color && stylingData.primary_color !== '#5897de'; const hasCustomFont = stylingData.font_family && stylingData.font_family !== 'system'; const hasCustomSize = stylingData.widget_size && stylingData.widget_size !== 'medium'; if ( (hasCustomPrimary && primaryColor === '#5897de') || (hasCustomFont && fontFamily === 'system') || (hasCustomSize && widgetSize === 'medium') ) { console.warn( '[WARN] Expected custom values but got defaults - styling may not have loaded properly' ); console.warn( " Elements were found but values didn't match expected custom values" ); } else { console.log('[SUCCESS] Styling values loaded correctly'); } } catch (error) { console.warn('[WARN] Could not verify styling values:', error.message); } } /** * Reset styling to default values */ resetToDefaults() { // Default styling values from backend const defaultStyling = { primary_color: '#5897de', secondary_color: '#022d54', accent_color: '#ffbc82', user_bubble_bg_color: '#5897de', user_bubble_text_color: '#ffffff', bot_bubble_bg_color: '#f0f4f8', bot_bubble_text_color: '#333333', header_bg_color: '#ffffff', header_text_color: '#333333', chat_bg_color: '#ffffff', input_bg_color: '#ffffff', input_border_color: '#e2e5e9', input_text_color: '#333333', button_hover_color: '#4a7bc8', scrollbar_color: '#d0d4da', shadow_color: '#000000', table_overflow_bg_color: '#fff3cd', table_overflow_border_color: '#ffeaa7', table_overflow_text_color: '#856404', font_family: 'system', widget_size: 'medium', logo_size: 'medium', message_font_size: 'medium', chat_bubble_style: 'modern', typing_indicator_style: 'dots', close_on_outside_click: true, custom_css: '', }; this.loadStylingData(defaultStyling); } /** * Set input value helper with enhanced element finding */ setInputValue(elementId, value) { // Use robust element finding strategy let element = null; // Strategy 1: Widget-scoped search if (this.widget && typeof this.widget.querySelector === 'function') { element = this.widget.querySelector(`#${elementId}`); if (element) { if (value !== undefined && value !== null && value !== '') { element.value = value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via widget - element=${!!element}, value="${value}" (set)` ); } } else { // Preserve existing value if new value is empty const currentValue = element.value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via widget - preserving current value="${currentValue}" (value="${value}" was empty)` ); } } return; } } // Strategy 2: Widget-scoped attribute search if (this.widget && this.widget.querySelectorAll) { const candidates = this.widget.querySelectorAll(`[id="${elementId}"]`); if (candidates.length > 0) { element = candidates[0]; if (value !== undefined && value !== null && value !== '') { element.value = value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via widget.querySelectorAll - element=${!!element}, value="${value}" (set)` ); } } else { const currentValue = element.value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via widget.querySelectorAll - preserving current value="${currentValue}"` ); } } return; } } // Strategy 3: Document fallback element = document.getElementById(elementId); if (element) { if (value !== undefined && value !== null && value !== '') { element.value = value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via document.getElementById - element=${!!element}, value="${value}" (set)` ); } } else { const currentValue = element.value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via document.getElementById - preserving current value="${currentValue}"` ); } } return; } // Strategy 4: Document attribute search fallback const candidates = document.querySelectorAll(`[id="${elementId}"]`); if (candidates.length > 0) { element = candidates[0]; if (value !== undefined && value !== null && value !== '') { element.value = value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via document.querySelectorAll - element=${!!element}, value="${value}" (set)` ); } } else { const currentValue = element.value; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): via document.querySelectorAll - preserving current value="${currentValue}"` ); } } return; } // If no element found, log warning for important fields and try retry for critical elements if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(elementId) ) { console.log( ` setInputValue(${elementId}): [ERROR] element not found with any strategy! Will retry in 200ms...` ); // Retry once after a delay for critical styling elements setTimeout(() => { console.log( ` Retrying setInputValue(${elementId}) with value: ${value}` ); this.setInputValue(elementId, value); }, 200); } } /** * Set checkbox value helper with enhanced element finding */ setCheckboxValue(elementId, value) { // Use robust element finding strategy let element = null; // Strategy 1: Widget-scoped search if (this.widget && typeof this.widget.querySelector === 'function') { element = this.widget.querySelector(`#${elementId}`); if (element) { element.checked = Boolean(value); return; } } // Strategy 2: Widget-scoped attribute search if (this.widget && this.widget.querySelectorAll) { const candidates = this.widget.querySelectorAll(`[id="${elementId}"]`); if (candidates.length > 0) { element = candidates[0]; element.checked = Boolean(value); return; } } // Strategy 3: Document fallback element = document.getElementById(elementId); if (element) { element.checked = Boolean(value); return; } // Strategy 4: Document attribute search fallback const candidates = document.querySelectorAll(`[id="${elementId}"]`); if (candidates.length > 0) { element = candidates[0]; element.checked = Boolean(value); return; } console.log( ` setCheckboxValue(${elementId}): [ERROR] checkbox not found with any strategy!` ); } /** * Populate styling data * @param {Object} styling - Styling data to populate */ populateData(styling) { if (!styling) return; // Set color values this.setInputValue('bm-primaryColorPicker', styling.primaryColor); this.setInputValue('bm-secondaryColorPicker', styling.secondaryColor); this.setInputValue('bm-userBubbleBgColorPicker', styling.userBubbleBgColor); this.setInputValue( 'bm-userBubbleTextColorPicker', styling.userBubbleTextColor ); this.setInputValue('bm-botBubbleBgColorPicker', styling.botBubbleBgColor); this.setInputValue( 'bm-botBubbleTextColorPicker', styling.botBubbleTextColor ); this.setInputValue( 'bm-tableOverflowBgColorPicker', styling.tableOverflowBgColor ); this.setInputValue( 'bm-tableOverflowBorderColorPicker', styling.tableOverflowBorderColor ); this.setInputValue( 'bm-tableOverflowTextColorPicker', styling.tableOverflowTextColor ); this.setInputValue( 'bm-tableOverflowBgColorHoverPicker', styling.tableOverflowBgColorHover ); this.setInputValue( 'bm-tableOverflowBorderColorHoverPicker', styling.tableOverflowBorderColorHover ); // Set select values this.setInputValue('bm-fontFamilySelect', styling.fontFamily); this.setInputValue('bm-widgetSizeSelect', styling.widgetSize); this.setInputValue('bm-logoSizeSelect', styling.logoSize); this.setInputValue('bm-messageFontSizeSelect', styling.messageFontSize); this.setInputValue('bm-chatBubbleStyleSelect', styling.chatBubbleStyle); this.setInputValue( 'bm-typingIndicatorStyleSelect', styling.typingIndicatorStyle ); // Set checkbox values this.setCheckboxValue( 'bm-closeOnOutsideClickCheckbox', styling.closeOnOutsideClick ); // Set custom CSS this.setInputValue('bm-customCssEditor', styling.customCss); } /** * Get input value safely with enhanced element finding * @param {string} id - Input ID * @returns {string} Input value */ getInputValue(id) { // Use robust element finding strategy similar to BehaviourTab let input = null; // Strategy 1: Widget-scoped search if (this.widget && typeof this.widget.querySelector === 'function') { input = this.widget.querySelector(`#${id}`); if (input) { const value = input.value || ''; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(id) ) { console.log( ` getInputValue(${id}): via widget - element=${!!input}, value="${value}"` ); } return value; } } // Strategy 2: Widget-scoped attribute search if (this.widget && this.widget.querySelectorAll) { const candidates = this.widget.querySelectorAll(`[id="${id}"]`); if (candidates.length > 0) { input = candidates[0]; const value = input.value || ''; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(id) ) { console.log( ` getInputValue(${id}): via widget.querySelectorAll - element=${!!input}, value="${value}"` ); } return value; } } // Strategy 3: Document fallback input = document.getElementById(id); if (input) { const value = input.value || ''; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(id) ) { console.log( ` getInputValue(${id}): via document.getElementById - element=${!!input}, value="${value}"` ); } return value; } // Strategy 4: Document attribute search fallback const candidates = document.querySelectorAll(`[id="${id}"]`); if (candidates.length > 0) { input = candidates[0]; const value = input.value || ''; if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(id) ) { console.log( ` getInputValue(${id}): via document.querySelectorAll - element=${!!input}, value="${value}"` ); } return value; } // Strategy 5: Alternative class-based search for color inputs if (id.includes('ColorPicker')) { const className = id.replace('bm-', '').replace('Picker', ''); const classSelector = `.${className}, .color-input[data-color="${className}"], input[data-id="${id}"]`; const colorInput = this.widget ? this.widget.querySelector(classSelector) : document.querySelector(classSelector); if (colorInput) { const value = colorInput.value || ''; console.log( ` getInputValue(${id}): via alternative class search - element=${!!colorInput}, value="${value}"` ); return value; } } // If no element found, log warning for important fields if ( [ 'bm-primaryColorPicker', 'bm-widgetSizeSelect', 'bm-fontFamilySelect', ].includes(id) ) { console.log( ` getInputValue(${id}): [ERROR] element not found with any strategy!` ); } return ''; } /** * Get checkbox value safely with enhanced element finding * @param {string} id - Checkbox ID * @returns {boolean} Checkbox value */ getCheckboxValue(id) { // Use similar robust strategy for checkboxes let checkbox = null; // Strategy 1: Widget-scoped search if (this.widget && typeof this.widget.querySelector === 'function') { checkbox = this.widget.querySelector(`#${id}`); if (checkbox) { return checkbox.checked || false; } } // Strategy 2: Widget-scoped attribute search if (this.widget && this.widget.querySelectorAll) { const candidates = this.widget.querySelectorAll(`[id="${id}"]`); if (candidates.length > 0) { checkbox = candidates[0]; return checkbox.checked || false; } } // Strategy 3: Document fallback checkbox = document.getElementById(id); if (checkbox) { return checkbox.checked || false; } // Strategy 4: Document attribute search fallback const candidates = document.querySelectorAll(`[id="${id}"]`); if (candidates.length > 0) { checkbox = candidates[0]; return checkbox.checked || false; } console.log( ` getCheckboxValue(${id}): [ERROR] checkbox not found with any strategy!` ); return false; } /** * Safely set in