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
JavaScript
/**
* 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();
}
}
}