UNPKG

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
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); }; } }