besper-frontend-site-dev-main
Version:
Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment
517 lines (431 loc) • 14.4 kB
JavaScript
import { StyleSanitizer } from '../utils/sanitizer.js';
/**
* CustomStylingManager - Visual Custom Styling Editor for Bot Management
* Provides comprehensive styling interface with live preview and security
*/
export class CustomStylingManager {
constructor(botManagement) {
this.botManagement = botManagement;
this.sanitizer = new StyleSanitizer();
this.mode = 'visual';
this.previewWidget = null;
this.customStyles = {
css: '',
html: '',
visual: {},
};
this.setupEventListeners();
}
setupEventListeners() {
// Mode switching
document.querySelectorAll('.bm-mode-btn').forEach(btn => {
btn.addEventListener('click', e => {
this.switchMode(e.target.dataset.mode);
});
});
// Visual controls
this.setupVisualControls();
// Code editor tabs
document.querySelectorAll('.bm-code-tab').forEach(tab => {
tab.addEventListener('click', e => {
this.switchCodeTab(e.target.dataset.lang);
});
});
// Real-time preview updates
this.setupRealtimePreview();
// Import/Export
document
.getElementById('bm-exportStyles')
?.addEventListener('click', () => {
this.exportStyles();
});
document
.getElementById('bm-importStyles')
?.addEventListener('click', () => {
this.importStyles();
});
document.getElementById('bm-applyStyles')?.addEventListener('click', () => {
this.applyStyles();
});
// Code tools
document.getElementById('bm-formatCss')?.addEventListener('click', () => {
this.formatCSS();
});
document.getElementById('bm-validateCss')?.addEventListener('click', () => {
this.validateCSS();
});
// Preview controls
document.querySelectorAll('.bm-preview-btn').forEach(btn => {
btn.addEventListener('click', e => {
if (e.target.dataset.theme) {
this.switchPreviewTheme(e.target.dataset.theme);
} else if (e.target.dataset.device) {
this.switchPreviewDevice(e.target.dataset.device);
}
});
});
}
switchMode(mode) {
this.mode = mode;
// Update button states
document.querySelectorAll('.bm-mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// Show/hide panels
document.getElementById('bm-visual-panel').style.display =
mode === 'visual' ? 'block' : 'none';
document.getElementById('bm-code-panel').style.display =
mode === 'code' ? 'block' : 'none';
if (mode === 'visual') {
this.generateCSSFromVisual();
}
}
switchCodeTab(lang) {
// Update tab states
document.querySelectorAll('.bm-code-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.lang === lang);
});
// Show/hide content
document.getElementById('bm-css-editor').style.display =
lang === 'css' ? 'block' : 'none';
document.getElementById('bm-html-editor').style.display =
lang === 'html' ? 'block' : 'none';
}
setupVisualControls() {
// Range inputs
const rangeInputs = [
{ id: 'bm-bubbleRadius', property: 'borderRadius', unit: 'px' },
{ id: 'bm-messagePadding', property: 'messagePadding', unit: 'px' },
{ id: 'bm-shadowIntensity', property: 'shadowIntensity', unit: '' },
{ id: 'bm-lineHeight', property: 'lineHeight', unit: '' },
];
rangeInputs.forEach(({ id, property, unit }) => {
const input = document.getElementById(id);
const valueDisplay = input?.nextElementSibling;
input?.addEventListener('input', e => {
const value = e.target.value;
if (valueDisplay) {
valueDisplay.textContent = value + unit;
}
this.updateVisualProperty(property, value + unit);
});
});
// Select inputs
document.getElementById('bm-fontWeight')?.addEventListener('change', e => {
this.updateVisualProperty('fontWeight', e.target.value);
});
document
.getElementById('bm-animationSpeed')
?.addEventListener('change', e => {
this.updateVisualProperty('animationSpeed', e.target.value);
});
// Checkboxes
document
.getElementById('bm-enableAnimations')
?.addEventListener('change', e => {
this.updateVisualProperty('enableAnimations', e.target.checked);
});
document
.getElementById('bm-enableHoverEffects')
?.addEventListener('change', e => {
this.updateVisualProperty('enableHoverEffects', e.target.checked);
});
// Text inputs
document
.getElementById('bm-customClasses')
?.addEventListener('input', e => {
this.updateVisualProperty('customClasses', e.target.value);
});
}
updateVisualProperty(property, value) {
this.customStyles.visual[property] = value;
this.generateCSSFromVisual();
this.updatePreview();
}
generateCSSFromVisual() {
const visual = this.customStyles.visual;
let css = '';
// Message bubbles
if (visual.borderRadius) {
css += `.besper-message-content { border-radius: ${visual.borderRadius}; }\n`;
}
if (visual.messagePadding) {
css += `.besper-message-content { padding: ${visual.messagePadding}; }\n`;
}
if (visual.shadowIntensity && visual.shadowIntensity > 0) {
const shadow = `0 ${visual.shadowIntensity}px ${visual.shadowIntensity * 2}px rgba(0,0,0,0.1)`;
css += `.besper-message-content { box-shadow: ${shadow}; }\n`;
}
// Typography
if (visual.lineHeight) {
css += `.besper-message-content { line-height: ${visual.lineHeight}; }\n`;
}
if (visual.fontWeight) {
css += `.besper-message-content { font-weight: ${visual.fontWeight}; }\n`;
}
// Animations
if (visual.enableAnimations === false) {
css += `.besper-message { animation: none !important; }\n`;
}
if (visual.animationSpeed) {
const durations = { fast: '150ms', normal: '300ms', slow: '500ms' };
css += `:root { --animation-duration: ${durations[visual.animationSpeed]}; }\n`;
}
// Hover effects
if (visual.enableHoverEffects === false) {
css += `.besper-message:hover { transform: none !important; }\n`;
}
// Custom classes
if (visual.customClasses) {
css += `/* Custom classes: ${visual.customClasses} */\n`;
}
// Update CSS editor if in visual mode
if (this.mode === 'visual') {
const cssEditor = document.getElementById('bm-customCss');
if (cssEditor) {
cssEditor.value = css;
}
}
this.customStyles.css = css;
}
setupRealtimePreview() {
// CSS editor
const cssEditor = document.getElementById('bm-customCss');
cssEditor?.addEventListener(
'input',
this.debounce(e => {
this.validateAndPreviewCSS(e.target.value);
}, 300)
);
// HTML editor
const htmlEditor = document.getElementById('bm-customHtml');
htmlEditor?.addEventListener(
'input',
this.debounce(e => {
this.validateAndPreviewHTML(e.target.value);
}, 300)
);
}
validateAndPreviewCSS(css) {
const sanitized = this.sanitizer.sanitizeCSS(css);
const status = document.getElementById('bm-cssStatus');
if (sanitized !== css) {
status.textContent = '[WARN] Some CSS rules were removed for security';
status.className = 'bm-code-status warning';
} else {
status.textContent = '✓ CSS is valid';
status.className = 'bm-code-status success';
}
this.customStyles.css = sanitized;
this.updatePreview();
}
validateAndPreviewHTML(html) {
const sanitized = this.sanitizer.sanitizeHTML(html);
const status = document.getElementById('bm-htmlStatus');
if (sanitized !== html) {
status.textContent = '[WARN] Some HTML was sanitized for security';
status.className = 'bm-code-status warning';
} else {
status.textContent = '✓ HTML is valid';
status.className = 'bm-code-status success';
}
this.customStyles.html = sanitized;
this.updatePreview();
}
updatePreview() {
const preview = document.getElementById('bm-stylePreview');
if (!preview) return;
// Remove existing custom styles
const existingStyle = preview.querySelector('#preview-custom-styles');
existingStyle?.remove();
// Apply custom CSS
if (this.customStyles.css) {
const style = document.createElement('style');
style.id = 'preview-custom-styles';
style.textContent = this.scopeCSS(
this.customStyles.css,
'#bm-stylePreview'
);
preview.appendChild(style);
}
// Apply visual properties directly
this.applyVisualToPreview(preview);
}
scopeCSS(css, scope) {
// Prefix all selectors with the scope
return css.replace(/([^{]+)\{/g, (match, selector) => {
const scopedSelector = selector
.split(',')
.map(s => `${scope} ${s.trim()}`)
.join(', ');
return `${scopedSelector} {`;
});
}
applyVisualToPreview(preview) {
// Apply custom classes if specified
if (this.customStyles.visual.customClasses) {
const classes = this.customStyles.visual.customClasses
.split(' ')
.filter(c => c.trim());
classes.forEach(className => {
preview.classList.add(className);
});
}
}
switchPreviewTheme(theme) {
const preview = document.getElementById('bm-stylePreview');
const buttons = document.querySelectorAll('.bm-preview-btn[data-theme]');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === theme);
});
preview.classList.toggle('dark-theme', theme === 'dark');
}
switchPreviewDevice(device) {
const preview = document.getElementById('bm-stylePreview');
const buttons = document.querySelectorAll('.bm-preview-btn[data-device]');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.device === device);
});
preview.classList.toggle('mobile-view', device === 'mobile');
}
applyStyles() {
// Collect all custom styling
const customStyling = {
custom_css: this.customStyles.css,
custom_html_template: this.customStyles.html,
visual_config: this.customStyles.visual,
applied_at: new Date().toISOString(),
};
// Add to bot configuration
if (!this.botManagement.state.config) {
this.botManagement.state.config = {};
}
if (!this.botManagement.state.config.styling) {
this.botManagement.state.config.styling = {};
}
this.botManagement.state.config.styling.custom_styling = customStyling;
// Save configuration
this.botManagement
.saveConfiguration()
.then(() => {
this.botManagement.showNotification(
'Custom styles applied successfully!',
'success'
);
// Update live bot preview if enabled
if (this.botManagement.botWidget) {
this.botManagement.botWidget.setCustomStyling({
customCSS: this.customStyles.css,
customHTML: this.customStyles.html,
});
}
})
.catch(() => {
this.botManagement.showNotification(
'Failed to apply custom styles',
'error'
);
});
}
exportStyles() {
const exportData = {
version: '1.0',
timestamp: new Date().toISOString(),
styles: this.customStyles,
botId: this.botManagement.credentials.botId,
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `besper-custom-styles-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
importStyles() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const data = JSON.parse(e.target.result);
if (data.styles) {
this.customStyles = data.styles;
this.applyImportedStyles();
this.botManagement.showNotification(
'Styles imported successfully!',
'success'
);
}
} catch (error) {
this.botManagement.showNotification(
'Failed to import styles: Invalid file format',
'error'
);
}
};
reader.readAsText(file);
});
input.click();
}
applyImportedStyles() {
// Update visual controls
Object.entries(this.customStyles.visual).forEach(([property, value]) => {
const element = document.getElementById(`bm-${property}`);
if (element) {
element.value = value;
if (element.type === 'checkbox') {
element.checked = value;
}
}
});
// Update code editors
const cssEditor = document.getElementById('bm-customCss');
const htmlEditor = document.getElementById('bm-customHtml');
if (cssEditor) cssEditor.value = this.customStyles.css || '';
if (htmlEditor) htmlEditor.value = this.customStyles.html || '';
// Update preview
this.updatePreview();
}
formatCSS() {
const cssEditor = document.getElementById('bm-customCss');
if (!cssEditor) return;
let css = cssEditor.value;
if (!css.trim()) return;
// Basic CSS formatting
css = css
.replace(/\s*{\s*/g, ' {\n ')
.replace(/;\s*/g, ';\n ')
.replace(/\s*}\s*/g, '\n}\n\n')
.replace(/\n\s*\n\s*\n/g, '\n\n')
.trim();
cssEditor.value = css;
this.validateAndPreviewCSS(css);
}
validateCSS() {
const cssEditor = document.getElementById('bm-customCss');
if (!cssEditor) return;
const css = cssEditor.value;
this.validateAndPreviewCSS(css);
}
// Utility method for debouncing
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}