UNPKG

besper-frontend-site-dev-main

Version:

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

397 lines (349 loc) 13.6 kB
/** * Chat Input Component * Handles the input area for sending messages in the chat widget */ import { getLocalizedText } from '../../utils/i18n.js'; export class ChatInput { constructor(widget, state, options = {}) { this.widget = widget; this.state = state; this.options = options; } /** * Generate the HTML for the chat input area * @returns {string} Input area HTML string */ getHTML() { const placeholder = getLocalizedText( 'typeYourMessage', 'en', this.state.userLanguage ); return ` <div class="besper-chat-input-container"> <form class="besper-chat-input-form" id="besper-chat-form"> <div class="besper-input-wrapper"> <textarea class="besper-chat-input" id="besper-message-input" placeholder="${placeholder}" rows="1" maxlength="2000" ></textarea> </div> <button type="submit" class="besper-chat-send-btn" id="besper-send-btn"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="22" y1="2" x2="11" y2="13"></line> <polygon points="22,2 15,22 11,13 2,9 22,2"></polygon> </svg> </button> </form> </div> `; } /** * Setup event listeners for the input area * @param {Object} callbacks - Callback functions for events */ setupEventListeners(callbacks) { const sendBtn = this.widget.querySelector('#besper-send-btn'); const messageInput = this.widget.querySelector('#besper-message-input'); const chatForm = this.widget.querySelector('#besper-chat-form'); // Send button click handler if (sendBtn && callbacks.onSend) { sendBtn.addEventListener('click', e => { e.preventDefault(); callbacks.onSend(); }); } // Form submit handler if (chatForm && callbacks.onSend) { chatForm.addEventListener('submit', e => { e.preventDefault(); callbacks.onSend(); }); } // Keyboard event handler if (messageInput && callbacks.onKeyPress) { messageInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); callbacks.onSend(); } }); } // Auto-resize textarea if (messageInput) { messageInput.addEventListener('input', () => { this.autoResizeTextarea(messageInput); }); } // Mobile-specific input handling if (this.isMobileDevice() && messageInput) { this.setupMobileInputHandling(messageInput); } } /** * Auto-resize the textarea based on content * @param {HTMLElement} textarea - The textarea element */ autoResizeTextarea(textarea) { textarea.style.height = 'auto'; const maxHeight = 120; const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = newHeight + 'px'; } /** * Mobile device detection * @returns {boolean} Whether the device is mobile */ isMobileDevice() { return ( window.innerWidth <= 480 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ) ); } /** * Setup mobile-specific input handling for better keyboard experience * @param {HTMLElement} messageInput - The message input element */ setupMobileInputHandling(messageInput) { // Handle keyboard visibility on mobile if (window.visualViewport) { const handleViewportChange = () => { const inputContainer = this.widget.querySelector( '.besper-chat-input-container' ); const messagesContainer = this.widget.querySelector( '.besper-chat-messages' ); const headerContainer = this.widget.querySelector( '.besper-chat-header' ); const chatContainer = this.widget.querySelector( '.besper-chat-container' ); if (inputContainer && messagesContainer && chatContainer) { const keyboardHeight = window.innerHeight - window.visualViewport.height; // Store current scroll position to maintain it during transitions const currentScrollRatio = messagesContainer.scrollTop / (messagesContainer.scrollHeight - messagesContainer.clientHeight || 1); if (keyboardHeight > 150) { // Only consider keyboard visible if it's significant // Position input above keyboard using CSS custom property inputContainer.style.setProperty( '--input-bottom-offset', `${keyboardHeight}px` ); // Calculate available space for messages // Account for: keyboard height + input container height + header height const inputHeight = inputContainer.offsetHeight || 80; const headerHeight = headerContainer ? headerContainer.offsetHeight : 56; const availableHeight = window.visualViewport.height - headerHeight - inputHeight; // Ensure minimum height for messages container const minMessagesHeight = Math.max(200, availableHeight - 50); // Adjust messages container to fit available space while keeping it scrollable messagesContainer.style.maxHeight = `${minMessagesHeight}px`; messagesContainer.style.height = `${minMessagesHeight}px`; messagesContainer.style.paddingBottom = '16px'; // Minimal padding when keyboard is open // Ensure header remains visible and accessible if (headerContainer) { headerContainer.style.position = 'sticky'; headerContainer.style.top = '0'; headerContainer.style.zIndex = '1001'; headerContainer.style.background = 'rgba(255, 255, 255, 0.98)'; headerContainer.style.backdropFilter = 'blur(10px)'; headerContainer.style.borderBottom = '1px solid rgba(0, 0, 0, 0.05)'; } // Ensure chat container uses the full available viewport chatContainer.style.height = `${window.visualViewport.height}px`; chatContainer.style.maxHeight = `${window.visualViewport.height}px`; // Add class for additional styling if needed this.widget.classList.add('besper-keyboard-visible'); // Ensure messages container is scrollable messagesContainer.style.overflowY = 'auto'; messagesContainer.style.webkitOverflowScrolling = 'touch'; // Force a reflow to ensure proper layout messagesContainer.offsetHeight; } else { // Keyboard is hidden - restore normal layout inputContainer.style.setProperty('--input-bottom-offset', '0px'); messagesContainer.style.maxHeight = ''; messagesContainer.style.height = ''; messagesContainer.style.paddingBottom = '100px'; // Original padding // Restore header to normal state if (headerContainer) { headerContainer.style.position = ''; headerContainer.style.top = ''; headerContainer.style.zIndex = ''; headerContainer.style.background = ''; headerContainer.style.backdropFilter = ''; headerContainer.style.borderBottom = ''; } // Restore chat container to normal state chatContainer.style.height = ''; chatContainer.style.maxHeight = ''; this.widget.classList.remove('besper-keyboard-visible'); } // Restore scroll position after layout change, but prefer bottom if user was at bottom setTimeout(() => { const wasAtBottom = currentScrollRatio > 0.9; // User was near bottom if (wasAtBottom) { // Keep at bottom - new messages should be visible messagesContainer.scrollTop = messagesContainer.scrollHeight; } else { // Restore previous scroll ratio const newScrollTop = currentScrollRatio * (messagesContainer.scrollHeight - messagesContainer.clientHeight); messagesContainer.scrollTop = newScrollTop; } }, 100); // Increased delay for better layout stability } }; window.visualViewport.addEventListener('resize', handleViewportChange); // Cleanup on component destroy this.visualViewportCleanup = () => { window.visualViewport.removeEventListener( 'resize', handleViewportChange ); }; } else { // Fallback for browsers without visualViewport API // Use window resize events and focus/blur for basic keyboard handling let isKeyboardVisible = false; const fallbackKeyboardHandler = () => { const messagesContainer = this.widget.querySelector( '.besper-chat-messages' ); const inputContainer = this.widget.querySelector( '.besper-chat-input-container' ); const headerContainer = this.widget.querySelector( '.besper-chat-header' ); const chatContainer = this.widget.querySelector( '.besper-chat-container' ); if (messagesContainer && inputContainer && chatContainer) { if (isKeyboardVisible) { // Estimate keyboard height as 40% of screen height for mobile devices const estimatedKeyboardHeight = window.innerHeight * 0.4; const inputHeight = inputContainer.offsetHeight || 80; const headerHeight = headerContainer ? headerContainer.offsetHeight : 56; const availableHeight = window.innerHeight - estimatedKeyboardHeight - inputHeight - headerHeight; const minMessagesHeight = Math.max(200, availableHeight - 50); messagesContainer.style.maxHeight = `${minMessagesHeight}px`; messagesContainer.style.height = `${minMessagesHeight}px`; messagesContainer.style.paddingBottom = '16px'; // Ensure header remains visible if (headerContainer) { headerContainer.style.position = 'sticky'; headerContainer.style.top = '0'; headerContainer.style.zIndex = '1001'; headerContainer.style.background = 'rgba(255, 255, 255, 0.98)'; headerContainer.style.backdropFilter = 'blur(10px)'; headerContainer.style.borderBottom = '1px solid rgba(0, 0, 0, 0.05)'; } chatContainer.style.height = `${window.innerHeight - estimatedKeyboardHeight}px`; chatContainer.style.maxHeight = `${window.innerHeight - estimatedKeyboardHeight}px`; messagesContainer.style.overflowY = 'auto'; messagesContainer.style.webkitOverflowScrolling = 'touch'; this.widget.classList.add('besper-keyboard-visible'); } else { messagesContainer.style.maxHeight = ''; messagesContainer.style.height = ''; messagesContainer.style.paddingBottom = '100px'; if (headerContainer) { headerContainer.style.position = ''; headerContainer.style.top = ''; headerContainer.style.zIndex = ''; headerContainer.style.background = ''; headerContainer.style.backdropFilter = ''; headerContainer.style.borderBottom = ''; } chatContainer.style.height = ''; chatContainer.style.maxHeight = ''; this.widget.classList.remove('besper-keyboard-visible'); } } }; messageInput.addEventListener('focus', () => { if (this.isMobileDevice()) { isKeyboardVisible = true; setTimeout(fallbackKeyboardHandler, 300); // Delay for keyboard animation } }); messageInput.addEventListener('blur', () => { if (this.isMobileDevice()) { isKeyboardVisible = false; setTimeout(fallbackKeyboardHandler, 300); } }); } } /** * Get the current message text * @returns {string} The current message text */ getMessage() { const messageInput = this.widget.querySelector('#besper-message-input'); return messageInput ? messageInput.value.trim() : ''; } /** * Clear the input field */ clearInput() { const messageInput = this.widget.querySelector('#besper-message-input'); if (messageInput) { messageInput.value = ''; this.autoResizeTextarea(messageInput); } } /** * Focus the input field */ focus() { const messageInput = this.widget.querySelector('#besper-message-input'); if (messageInput) { messageInput.focus(); } } /** * Set the input value * @param {string} value - The value to set */ setValue(value) { const messageInput = this.widget.querySelector('#besper-message-input'); if (messageInput) { messageInput.value = value; this.autoResizeTextarea(messageInput); } } /** * Cleanup method for component destruction */ destroy() { if (this.visualViewportCleanup) { this.visualViewportCleanup(); } } }