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
JavaScript
/**
* 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