`;
// Reset submission state and retry through queue
setTimeout(() => {
this.clearSubmissionState();
this.handleFormCompletion(payload);
}, 500);
});
}
});
}
// Handle form completion with debouncing and queueing
handleFormCompletion(payload) {
console.log('Form completion triggered for payload:', payload?.message_id);
// Additional protection: check if form already submitted
if (this.hasSubmittedSuccessfully) {
console.log('Form already submitted successfully, ignoring completion trigger');
return;
}
// Check if this is a duplicate based on message_id
if (payload?.message_id && this.processedSubmissions.has(`msg_${payload.message_id}`)) {
console.log('Duplicate form completion detected for message_id:', payload.message_id);
return;
}
// Mark this message_id as being processed
if (payload?.message_id) {
this.processedSubmissions.add(`msg_${payload.message_id}`);
}
// Use queueing system to prevent duplicates
this.queueSubmission(payload);
}
// Create debounced version for auto-advance
createDebouncedHandleNext() {
return this.debounce(() => {
if (!this.hasSubmittedSuccessfully && !this.isSubmitting) {
this.handleNext();
}
}, 300);
}
// Handle error
handleError(payload) {
// Cancel any pending loading timers
this.cancelLoadingTimer();
console.error('Chat error:', payload.message);
this.showErrorMessage(payload.message || 'Ein Fehler ist aufgetreten.');
}
showLoadingIndicator(message = 'Lädt...') {
// Clear any existing timer
if (this.loadingTimer) {
clearTimeout(this.loadingTimer);
this.loadingTimer = null;
}
// For known slow operations, show immediately
const isKnownSlowOperation = this.knownSlowOperations.includes(message);
if (isKnownSlowOperation) {
this._displayLoadingAnimation(message);
} else {
// Otherwise, set a timer to show loading only if operation takes longer than threshold
this.loadingTimer = setTimeout(() => {
this._displayLoadingAnimation(message);
this.loadingTimer = null;
}, this.loadingDelay);
}
}
// Private method to actually display the loading animation
_displayLoadingAnimation(message) {
// First animate out current module if it exists
if (this.currentModule) {
this.currentModule.classList.add('fade-out-up');
// Wait briefly for animation to start before replacing
setTimeout(() => {
const loadingModule = document.createElement('div');
loadingModule.className = 'form-module loading-module fade-in-up';
loadingModule.dataset.moduleType = 'loading';
loadingModule.innerHTML = `
${message}
`;
this.container.innerHTML = '';
this.container.appendChild(loadingModule);
this.currentModule = loadingModule;
}, 150);
} else {
// If no current module, just show the loading immediately
const loadingModule = document.createElement('div');
loadingModule.className = 'form-module loading-module fade-in-up';
loadingModule.dataset.moduleType = 'loading';
loadingModule.innerHTML = `
${message}
`;
this.container.innerHTML = '';
this.container.appendChild(loadingModule);
this.currentModule = loadingModule;
}
}
// Cancel loading timer
cancelLoadingTimer() {
if (this.loadingTimer) {
clearTimeout(this.loadingTimer);
this.loadingTimer = null;
}
}
// Enable next button (active state)
enableNextButton() {
this.nextButton.classList.remove('inactive');
// Update arrow icon
const arrowIcon = this.nextButton.querySelector('img');
if (arrowIcon) {
arrowIcon.src = "https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/66ea8c6584c7e035cee00be0_right-arrow-wedge-next-white.svg";
}
}
updateNextButtonState() {
// Skip if no chat history
if (this.chatHistory.length === 0) return;
const currentMessage = this.chatHistory[this.chatHistory.length - 1];
// Always enable for responder types
if (currentMessage.type === 'responder') {
this.enableNextButton();
return;
}
// If message is required, check if it's complete
if (currentMessage.required) {
if (this.isMessageComplete(currentMessage)) {
this.enableNextButton();
} else {
this.disableNextButton();
}
} else {
// Not required, always enable
this.enableNextButton();
}
}
// Disable next button (inactive state)
disableNextButton() {
this.nextButton.classList.add('inactive');
// Update arrow icon
const arrowIcon = this.nextButton.querySelector('img');
if (arrowIcon) {
arrowIcon.src = "https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/6560739fd4d1fc9cf500057e_right-arrow-icon-mygesuch.svg";
}
}
showErrorMessage(message, isAuthError = false) {
console.error("Error occurred:", message);
if (isAuthError) {
// For auth errors, show a specific error message
const errorModule = document.createElement('div');
errorModule.className = 'form-module error-module fade-in-up';
errorModule.innerHTML = `
${message}
`;
// Add to container
this.container.innerHTML = '';
this.container.appendChild(errorModule);
this.currentModule = errorModule;
// Add retry button handler
const retryButton = errorModule.querySelector('.error-retry-button');
if (retryButton) {
retryButton.addEventListener('click', () => {
// Go back to the previous step
this.handleBack();
});
}
} else {
// For non-auth errors, use existing behavior (reset session)
this.resetAndStartNewSession().catch(error => {
console.error("Failed to create new session after error:", error);
window.location.reload();
});
}
}
async displayMessage(message) {
this.isAnimating = true;
// Create module container
const moduleEl = document.createElement('div');
moduleEl.className = 'form-module fade-in-up';
moduleEl.dataset.messageId = message.message_id;
moduleEl.dataset.moduleType = 'message';
// Add title/heading if message has a title
if (message.title) {
const titleEl = document.createElement('h2');
titleEl.className = 'agent-create-ms-text title-text';
// Add special class if there's no message to add margin below
if (!message.message || message.title === message.message) {
titleEl.classList.add('title-no-message');
}
moduleEl.appendChild(titleEl);
// Will be animated later
titleEl.textContent = '';
}
// Add explanatory message if present
if (message.message && message.title !== message.message) { // Avoid duplication if title and message are the same
const messageEl = document.createElement('div');
messageEl.className = 'message-text';
moduleEl.appendChild(messageEl);
// Always set to empty for consistent animation
messageEl.textContent = '';
}
// Add content container but initially hidden
const contentEl = document.createElement('div');
contentEl.className = 'step-content';
contentEl.style.opacity = '0'; // Always start hidden
moduleEl.appendChild(contentEl);
// Generate specific content based on type
switch (message.type) {
case 'message-only':
// No additional content
break;
case 'text-input':
// Check if this is a specialized input type
if (message.authStepType === 'password_creation') {
contentEl.appendChild(this.createPasswordInput(message));
} else {
contentEl.appendChild(this.createTextInput(message));
}
break;
case 'stacked-text-input':
contentEl.appendChild(this.createStackedTextInput(message));
break;
case 'address-input':
contentEl.appendChild(this.createAddressInput(message));
break;
case 'date-input':
contentEl.appendChild(this.createDateInput(message));
break;
case 'money-input':
contentEl.appendChild(this.createMoneyInput(message));
break;
case 'select':
contentEl.appendChild(this.createSelect(message));
// Add class for sequential animation of options
const selectEl = contentEl.querySelector('select');
if (selectEl) {
selectEl.classList.add('animate-select');
// Hide all options initially
Array.from(selectEl.options).forEach((option, index) => {
option.style.opacity = '0';
option.style.transition = 'opacity 0.3s ease-out';
option.dataset.animationIndex = index;
});
}
break;
case 'checkbox-list':
contentEl.appendChild(this.createCheckboxList(message));
// Add classes for sequential animation
const checkboxes = contentEl.querySelectorAll('.selection-option');
checkboxes.forEach((option, index) => {
option.classList.add('animate-option');
option.style.opacity = '0';
option.style.transform = 'translateY(10px)';
option.dataset.animationIndex = index;
});
break;
case 'radio-list':
contentEl.appendChild(this.createRadioList(message));
// Add classes for sequential animation
const radioOptions = contentEl.querySelectorAll('.selection-option');
radioOptions.forEach((option, index) => {
option.classList.add('animate-option');
option.style.opacity = '0';
option.style.transform = 'translateY(10px)';
option.dataset.animationIndex = index;
});
break;
case 'photo-upload':
contentEl.appendChild(this.createPhotoUpload(message));
break;
case 'responder':
contentEl.appendChild(this.createResponder(message));
break;
case 'loading':
contentEl.appendChild(this.createLoadingAnimation());
break;
default:
console.warn(`Unknown message type: ${message.type}`);
}
if (message.explainer) {
const explainerEl = document.createElement('p');
explainerEl.className = 'par-form gray explainer-fade';
explainerEl.style.marginTop = '48px';
// Set content immediately instead of empty
explainerEl.textContent = message.explainer;
// Start with hidden state for fade-in animation
explainerEl.style.opacity = '0';
explainerEl.style.transform = 'translateY(15px)';
moduleEl.appendChild(explainerEl);
}
// Add to container
this.container.appendChild(moduleEl);
this.currentModule = moduleEl;
// Ensure animation CSS classes take effect
await this.wait(10); // Brief microtask delay
// CONSISTENT ANIMATION SEQUENCE FOR ALL TYPES:
this.updateNextButtonState();
// 1. Animate the title first
if (message.title) {
const titleEl = moduleEl.querySelector('.title-text');
if (titleEl) {
// Ensure the title animation completes fully first
await TextAnimator.animate(titleEl, message.title);
// Add a more pronounced delay after title animation
await this.wait(350); // Increased delay for more noticeable separation
}
}
// 2. Animate the message next (if it exists and is different from title)
if (message.message && message.title !== message.message) {
const messageEl = moduleEl.querySelector('.message-text');
if (messageEl) {
// Now that title is fully complete, animate the message
await TextAnimator.animate(messageEl, message.message);
await this.wait(300); // Slightly longer pause after message animation
}
}
// 3. For responder types, handle responder text
if (message.type === 'responder' && message.responderText) {
const responderEl = contentEl.querySelector('.agent-create-ms-text');
if (responderEl) {
contentEl.style.opacity = '1';
await TextAnimator.animate(responderEl, message.responderText);
}
}
// For all other types, fade in content
else {
// First show content container
contentEl.style.opacity = '1';
await this.wait(100);
// Then animate select options or list items sequentially if applicable
if (['select', 'radio-list', 'checkbox-list'].includes(message.type)) {
await this.animateOptionsSequentially(contentEl, message.type);
}
}
// 4. Animate the explainer text AFTER content if present
if (message.explainer) {
const explainerEl = moduleEl.querySelector('.par-form.gray');
if (explainerEl) {
// Animate the explainer text after the content is shown
await this.wait(300); // Wait a bit before showing explainer
// Use CSS transition for fade in and up instead of text animation
explainerEl.style.transition = 'opacity 0.8s ease-out, transform 0.8s ease-out';
explainerEl.style.opacity = '1';
explainerEl.style.transform = 'translateY(0)';
}
}
// Remove animation classes to prevent interference
moduleEl.classList.remove('fade-in-up');
this.isAnimating = false;
}
// New method to animate options sequentially
async animateOptionsSequentially(container, type) {
if (type === 'select') {
const select = container.querySelector('select.animate-select');
if (select) {
const options = Array.from(select.options);
// Skip the first option if it's a placeholder (usually is)
const startIndex = options[0].disabled ? 1 : 0;
for (let i = startIndex; i < options.length; i++) {
options[i].style.opacity = '1';
await this.wait(70); // Brief delay between each option
}
// Remove animation class after completion
select.classList.remove('animate-select');
}
} else if (type === 'radio-list' || type === 'checkbox-list') {
const options = container.querySelectorAll('.selection-option.animate-option');
for (let i = 0; i < options.length; i++) {
options[i].style.opacity = '1';
options[i].style.transform = 'translateY(0)';
await this.wait(70); // Brief delay between each option
}
// Remove animation classes after completion
options.forEach(option => {
option.classList.remove('animate-option');
});
}
}
// Animate a message out
async animateMessageOut(messageId) {
const messageEl = this.container.querySelector(`[data-message-id="${messageId}"]`);
if (!messageEl) return;
this.isAnimating = true;
// Animate out
messageEl.classList.add('fade-out-up');
await this.wait(500); // Wait for animation to complete
// Remove from DOM
if (messageEl.parentNode) {
messageEl.parentNode.removeChild(messageEl);
}
this.isAnimating = false;
}
getUserInput(message) {
if (!message) return null;
const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`);
if (!messageEl) return null;
switch (message.type) {
case 'text-input':
// For password input
if (message.authStepType === 'password_creation') {
const passwordInput = messageEl.querySelector(`input.cg-input-morph.pw[id="${message.message_id}"]`);
return passwordInput ? passwordInput.value : null;
}
// For regular text input
const textInput = messageEl.querySelector(`input[type="text"][id="${message.message_id}"], input[type="password"][id="${message.message_id}"]`);
return textInput ? textInput.value : null;
case 'date-input':
// For custom date input, return the hidden input value
const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`);
return hiddenDateInput ? hiddenDateInput.value : null;
case 'money-input':
const moneyInput = messageEl.querySelector(`#${message.message_id}`);
const currencyInput = messageEl.querySelector('#currency');
if (!moneyInput) return null;
// Get the numeric value normalized to American decimal format
const amount = this.parseMoneyValue(moneyInput.value);
const currency = currencyInput ? currencyInput.value : '€';
// Return both currency and amount as JSON string
return amount !== null ? JSON.stringify({
amount: amount, // Already normalized to American decimal format (dots)
currency: currency
}) : null;
case 'select':
const selectEl = messageEl.querySelector(`#${message.message_id}`);
return selectEl ? selectEl.value : null;
case 'radio-list':
const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`);
return selectedRadio ? selectedRadio.value : null;
case 'checkbox-list':
const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`);
return selectedCheckboxes.length > 0 ? Array.from(selectedCheckboxes).map(cb => cb.value) : null;
case 'stacked-text-input':
// For stacked text inputs, return a JSON string of all field values
const stackedInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
const stackedResult = {};
stackedInputs.forEach(input => {
const fieldId = input.id.replace(`${message.message_id}_`, '');
stackedResult[fieldId] = input.value;
});
return Object.keys(stackedResult).length > 0 ? JSON.stringify(stackedResult) : null;
case 'address-input':
// For address inputs, return a JSON string of all address field values
const addressInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
const addressResult = {};
addressInputs.forEach(input => {
const fieldId = input.id.replace(`${message.message_id}_`, '');
addressResult[fieldId] = input.value;
});
return Object.keys(addressResult).length > 0 ? JSON.stringify(addressResult) : null;
default:
return null;
}
}
// Cancel ongoing animations
cancelOngoingAnimations() {
// Clear auto-advance timer if it exists
if (this.autoAdvanceTimer) {
clearTimeout(this.autoAdvanceTimer);
this.autoAdvanceTimer = null;
}
// Force completion of text animations
if (this.currentModule) {
// Find any elements being animated and reveal them instantly
const textElements = this.currentModule.querySelectorAll('.agent-create-ms-text, .message-text, .explainer-text');
textElements.forEach(element => {
// Get the message ID
const messageId = this.currentModule.dataset.messageId;
if (!messageId) return;
// Find message in history
const message = this.chatHistory.find(msg => msg.message_id === messageId);
if (!message) return;
if (element.closest('.step-content')) {
// It's a responder text
element.textContent = message.responderText || '';
} else if (element.classList.contains('explainer-text')) {
// It's the explainer text
element.textContent = message.explainer || '';
} else if (element.classList.contains('message-text')) {
// It's the main message
element.textContent = message.message || '';
} else if (element.classList.contains('title-text')) {
// It's the title
element.textContent = message.title || '';
}
});
// Show content elements that might be waiting for animation
const contentEl = this.currentModule.querySelector('.step-content');
if (contentEl) {
contentEl.style.opacity = '1';
// Also reveal any options that might be animating
const selectOptions = contentEl.querySelectorAll('select.animate-select option');
selectOptions.forEach(option => {
option.style.opacity = '1';
});
const listOptions = contentEl.querySelectorAll('.selection-option.animate-option');
listOptions.forEach(option => {
option.style.opacity = '1';
option.style.transform = 'translateY(0)';
});
}
// Remove animation classes
this.currentModule.classList.remove('fade-in-up', 'fade-in-down');
const selects = this.currentModule.querySelectorAll('select.animate-select');
selects.forEach(select => select.classList.remove('animate-select'));
const options = this.currentModule.querySelectorAll('.selection-option.animate-option');
options.forEach(option => option.classList.remove('animate-option'));
// Set isAnimating to false so we can proceed
this.isAnimating = false;
}
}
// Validate the current message
validateMessage(message) {
if (!message) return true;
// No validation needed for certain types
if (['message-only', 'responder'].includes(message.type)) return true;
const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`);
if (!messageEl) return true;
let isValid = true;
// Check for required fields based on type
if (message.required) {
switch (message.type) {
case 'text-input':
const textInput = messageEl.querySelector(`#${message.message_id}`);
const textValidationMessage = messageEl.querySelector('.validation-error');
if (!textInput || !textInput.value.trim()) {
if (textValidationMessage) {
textValidationMessage.textContent = 'Dieses Feld ist erforderlich';
textValidationMessage.style.display = 'block';
textValidationMessage.style.color = '#ff3b30';
textValidationMessage.style.fontSize = '0.875rem';
textValidationMessage.style.marginTop = '0.5rem';
// Highlight the input
textInput.style.borderColor = '#ff3b30';
}
isValid = false;
}
break;
case 'date-input':
// For custom date input, validate the hidden input value
const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`);
const dateInputGroup = messageEl.querySelector('.date-input-holder-group');
const validationMessage = messageEl.querySelector('.validation-error');
if (!hiddenDateInput || !hiddenDateInput.value) {
if (validationMessage && dateInputGroup) {
this.showCustomValidationMessage(
validationMessage,
dateInputGroup,
'Dieses Feld ist erforderlich'
);
}
isValid = false;
} else if (validationMessage && dateInputGroup) {
// Validate the date
isValid = this.validateCustomDate(
hiddenDateInput.value,
message,
validationMessage,
dateInputGroup
);
}
break;
case 'money-input':
const moneyInput = messageEl.querySelector(`#${message.message_id}`);
const moneyValidationMessage = messageEl.querySelector('.validation-error');
if (!moneyInput || !moneyInput.value.trim()) {
if (moneyValidationMessage) {
moneyValidationMessage.textContent = 'Bitte gib einen Betrag ein';
moneyValidationMessage.style.display = 'block';
moneyInput.style.borderColor = '#ff3b30';
}
isValid = false;
} else {
// Check if it's a valid number
const numericValue = this.parseMoneyValue(moneyInput.value);
if (numericValue === null) {
if (moneyValidationMessage) {
moneyValidationMessage.textContent = 'Bitte gib einen gültigen Betrag ein';
moneyValidationMessage.style.display = 'block';
moneyInput.style.borderColor = '#ff3b30';
}
isValid = false;
} else if (message.minValue !== undefined && numericValue < message.minValue) {
if (moneyValidationMessage) {
moneyValidationMessage.textContent = `Der Mindestbetrag ist ${this.formatMoneyValue(message.minValue)}€`;
moneyValidationMessage.style.display = 'block';
moneyInput.style.borderColor = '#ff3b30';
}
isValid = false;
} else if (message.maxValue !== undefined && numericValue > message.maxValue) {
if (moneyValidationMessage) {
moneyValidationMessage.textContent = `Der Höchstbetrag ist ${this.formatMoneyValue(message.maxValue)}€`;
moneyValidationMessage.style.display = 'block';
moneyInput.style.borderColor = '#ff3b30';
}
isValid = false;
}
}
break;
case 'select':
const selectEl = messageEl.querySelector(`#${message.message_id}`);
if (!selectEl || !selectEl.value) {
this.showValidationError(selectEl, 'Bitte wähle eine Option');
isValid = false;
}
break;
case 'radio-list':
const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`);
if (!selectedRadio) {
const listEl = messageEl.querySelector('.selection-list');
if (listEl) {
this.showValidationError(listEl, 'Bitte wähle eine Option');
}
isValid = false;
}
break;
case 'checkbox-list':
const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`);
if (selectedCheckboxes.length === 0) {
const listEl = messageEl.querySelector('.selection-list');
if (listEl) {
this.showValidationError(listEl, 'Bitte wähle mindestens eine Option');
}
isValid = false;
}
break;
case 'stacked-text-input':
const stackedInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
const stackedValidationMessage = messageEl.querySelector('.validation-error');
for (const input of stackedInputs) {
if (input.dataset.required === 'true' && input.value.trim() === '') {
if (stackedValidationMessage) {
stackedValidationMessage.textContent = 'Alle erforderlichen Felder müssen ausgefüllt werden';
stackedValidationMessage.style.display = 'block';
stackedValidationMessage.style.color = '#ff3b30';
stackedValidationMessage.style.fontSize = '0.875rem';
stackedValidationMessage.style.marginTop = '0.5rem';
// Highlight the input
input.style.borderColor = '#ff3b30';
}
isValid = false;
}
}
break;
case 'address-input':
const addressInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
const addressValidationMessage = messageEl.querySelector('.validation-error');
for (const input of addressInputs) {
if (input.dataset.required === 'true' && input.value.trim() === '') {
if (addressValidationMessage) {
addressValidationMessage.textContent = 'All required address fields must be filled out';
addressValidationMessage.style.display = 'block';
addressValidationMessage.style.color = '#ff3b30';
addressValidationMessage.style.fontSize = '0.875rem';
addressValidationMessage.style.marginTop = '0.5rem';
// Highlight the input
input.style.borderColor = '#ff3b30';
}
isValid = false;
}
}
break;
}
}
return isValid;
}
isMessageComplete(message) {
if (!message) return true;
// No validation needed for certain types
if (['message-only', 'responder'].includes(message.type)) return true;
const messageEl = this.container.querySelector(`[data-message-id="${message.message_id}"]`);
if (!messageEl) return true;
switch (message.type) {
case 'text-input':
const textInputEl = messageEl.querySelector(`#${message.message_id}`);
return textInputEl && textInputEl.value.trim() !== '';
case 'date-input':
// For our custom date input, check the hidden input value
const hiddenDateInput = messageEl.querySelector(`input[type="hidden"][id="${message.message_id}"]`);
return hiddenDateInput && hiddenDateInput.value !== '';
case 'money-input':
const moneyInputEl = messageEl.querySelector(`#${message.message_id}`);
const isComplete = moneyInputEl && moneyInputEl.value.trim() !== '' &&
this.parseMoneyValue(moneyInputEl.value) !== null;
return isComplete;
case 'select':
const selectEl = messageEl.querySelector(`#${message.message_id}`);
return selectEl && selectEl.value !== '';
case 'radio-list':
const selectedRadio = messageEl.querySelector(`input[name="${message.message_id}"]:checked`);
return !!selectedRadio;
case 'checkbox-list':
const selectedCheckboxes = messageEl.querySelectorAll(`input[name="${message.message_id}"]:checked`);
return selectedCheckboxes.length > 0;
case 'photo-upload':
const photoContainer = messageEl.querySelector('.photo-upload-container');
return photoContainer && photoContainer._hasPhoto && photoContainer._hasPhoto();
case 'stacked-text-input':
// Check if all required stacked inputs have values
const stackedInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
for (const input of stackedInputs) {
if (input.dataset.required === 'true' && input.value.trim() === '') {
return false;
}
}
return stackedInputs.length > 0;
case 'address-input':
// Check if all required address inputs have values
const addressInputs = messageEl.querySelectorAll(`input[id^="${message.message_id}_"]`);
for (const input of addressInputs) {
if (input.dataset.required === 'true' && input.value.trim() === '') {
return false;
}
}
return addressInputs.length > 0;
default:
return true;
}
}
// Show validation error message
showValidationError(element, message) {
if (!element) return;
// Remove existing error messages
const existingError = element.parentNode.querySelector('.validation-error');
if (existingError) {
existingError.parentNode.removeChild(existingError);
}
// Create and add error message
const errorEl = document.createElement('div');
errorEl.className = 'validation-error';
errorEl.style.color = '#ff3b30';
errorEl.style.fontSize = '0.875rem';
errorEl.style.marginTop = '0.5rem';
errorEl.textContent = message;
element.parentNode.appendChild(errorEl);
// Highlight the input
element.style.borderColor = '#ff3b30';
element.addEventListener('input', function onInput() {
element.style.borderColor = '';
if (errorEl.parentNode) {
errorEl.parentNode.removeChild(errorEl);
}
element.removeEventListener('input', onInput);
});
}
// Helper methods to create UI components
// Create text input
createTextInput(message) {
const container = document.createElement('div');
container.className = 'cont-morph-input';
// Create the input with the specified classes
const input = document.createElement('input');
input.type = 'text';
input.className = 'cg-input-morph w-input'; // Add the required classes
input.id = message.message_id;
input.name = message.message_id;
input.placeholder = message.placeholder || 'Schreibe hier...';
// input.autofocus = true; // Set autofocus attribute
input.maxLength = 256; // Add maxlength as per your example
// Set value from stored input if available
if (message.input !== null) {
input.value = message.input;
}
// Add event listener for input changes
input.addEventListener('input', () => this.updateNextButtonState());
// Create the line element
const lineElement = document.createElement('div');
lineElement.className = 'line-input-bottom-new';
// Add validation message container (initially hidden)
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
// Append elements to container
container.appendChild(input);
container.appendChild(lineElement);
container.appendChild(validationMessage);
return container;
}
// Create stacked text input (multiple text fields stacked vertically)
createStackedTextInput(message) {
const container = document.createElement('div');
container.className = 'stacked-input-container';
// Default fields if not specified
const defaultFields = [
{ id: 'first_name', placeholder: 'Vorname', maxLength: 100, required: true },
{ id: 'last_name', placeholder: 'Nachname', maxLength: 100, required: true }
];
const fields = message.fields || defaultFields;
const inputs = [];
fields.forEach((field, index) => {
const inputContainer = document.createElement('div');
inputContainer.className = 'cont-morph-input stacked-input-item';
const input = document.createElement('input');
input.type = 'text';
input.className = 'cg-input-morph w-input';
input.id = `${message.message_id}_${field.id}`;
input.name = `${message.message_id}_${field.id}`;
input.placeholder = field.placeholder;
input.maxLength = field.maxLength || 256;
if (field.required) {
input.setAttribute('required', 'true');
input.dataset.required = 'true';
}
// Set value from stored input if available
if (message.inputs && message.inputs[field.id] !== undefined) {
input.value = message.inputs[field.id];
}
// Add event listener for input changes
input.addEventListener('input', () => this.updateNextButtonState());
// Create the line element
const lineElement = document.createElement('div');
lineElement.className = 'line-input-bottom-new';
inputs.push(input);
// Append elements to input container
inputContainer.appendChild(input);
inputContainer.appendChild(lineElement);
// Add margin between stacked inputs (except for the last one)
if (index < fields.length - 1) {
inputContainer.style.marginBottom = '20px';
}
container.appendChild(inputContainer);
});
// Add validation message container (initially hidden)
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
container.appendChild(validationMessage);
return container;
}
// Create address input (specialized multi-field input for addresses)
createAddressInput(message) {
// Main container
const container = document.createElement('div');
container.className = 'cont-address-input-holder-group';
// Get address fields from server configuration or use defaults
const addressFields = message.addressFields || [
{ id: 'street', placeholder: 'Straße & Hausnummer', maxLength: 256, required: true },
{ id: 'postal_code', placeholder: 'PLZ', maxLength: 20, required: true },
{ id: 'city', placeholder: 'Stadt', maxLength: 100, required: true },
{ id: 'country', placeholder: 'Land', maxLength: 100, required: false },
{ id: 'state', placeholder: 'Bundesland', maxLength: 100, required: false }
];
// Map server field IDs to frontend field names for compatibility
const fieldMapping = {
'street': 'streetname',
'postal_code': 'zip',
'city': 'city',
'country': 'country',
'state': 'state'
};
// Create field containers based on layout
const streetSpreader = document.createElement('div');
streetSpreader.className = 'address-street-spreader-input-morph';
const citySpreader = document.createElement('div');
citySpreader.className = 'city-street-spreader-input-morph';
const countrySpreader = document.createElement('div');
countrySpreader.className = 'country-spreader-input-morph-copy';
// Store input references for later use
const inputElements = {};
// Create address fields dynamically based on server configuration
addressFields.forEach(fieldConfig => {
const mappedName = fieldMapping[fieldConfig.id] || fieldConfig.id;
// Create input container
const morphCont = document.createElement('div');
morphCont.className = 'date-morph-cont';
// Create input element
const input = document.createElement('input');
input.className = 'cg-input-morph w-input';
input.maxLength = fieldConfig.maxLength || 256;
input.name = mappedName;
input.setAttribute('data-name', mappedName);
input.placeholder = fieldConfig.placeholder;
input.type = 'text';
input.id = `${message.message_id}_${mappedName}`;
// Set required attribute based on server configuration
if (fieldConfig.required) {
input.dataset.required = 'true';
input.setAttribute('required', 'true');
}
// Create line element
const line = document.createElement('div');
line.className = 'line-input-bottom-new';
// Assemble input container
morphCont.appendChild(input);
morphCont.appendChild(line);
// Add to appropriate spreader based on field type
if (fieldConfig.id === 'street') {
streetSpreader.appendChild(morphCont);
} else if (fieldConfig.id === 'city' || fieldConfig.id === 'postal_code') {
citySpreader.appendChild(morphCont);
} else if (fieldConfig.id === 'country' || fieldConfig.id === 'state') {
countrySpreader.appendChild(morphCont);
}
// Store reference for later use
inputElements[mappedName] = input;
});
// Assemble the final structure
container.appendChild(streetSpreader);
container.appendChild(citySpreader);
container.appendChild(countrySpreader);
// Add validation message container
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
validationMessage.style.color = '#ff4444';
validationMessage.style.fontSize = '14px';
validationMessage.style.marginTop = '8px';
container.appendChild(validationMessage);
// Set values from stored input if available
if (message.addressInputs) {
Object.keys(inputElements).forEach(fieldName => {
if (message.addressInputs[fieldName]) {
inputElements[fieldName].value = message.addressInputs[fieldName];
}
});
}
// Add event listeners for input changes
Object.values(inputElements).forEach(input => {
input.addEventListener('input', () => this.updateNextButtonState());
});
return container;
}
// Create month suggestions dropdown
createMonthSuggestions(input, suggestions) {
// Remove existing suggestions
this.removeMonthSuggestions();
if (suggestions.length === 0) return;
const dropdown = document.createElement('div');
dropdown.className = 'month-suggestions';
dropdown.id = 'monthSuggestions';
suggestions.forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'month-suggestion-item';
item.textContent = suggestion.display;
item.dataset.value = suggestion.value;
item.dataset.type = suggestion.type;
// Highlight first item
if (index === 0) {
item.classList.add('highlighted');
}
item.addEventListener('click', () => {
this.selectMonthSuggestion(input, suggestion);
});
dropdown.appendChild(item);
});
// Position dropdown below the input
const rect = input.getBoundingClientRect();
dropdown.style.position = 'absolute';
dropdown.style.top = (rect.bottom + window.scrollY) + 'px';
dropdown.style.left = rect.left + 'px';
dropdown.style.width = rect.width + 'px';
dropdown.style.zIndex = '1000';
dropdown.style.backgroundColor = 'white';
dropdown.style.border = '1px solid #ddd';
dropdown.style.borderRadius = '4px';
dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
dropdown.style.maxHeight = '200px';
dropdown.style.overflowY = 'auto';
document.body.appendChild(dropdown);
}
// Remove month suggestions dropdown
removeMonthSuggestions() {
const existing = document.getElementById('monthSuggestions');
if (existing) {
existing.remove();
}
}
// Select a month suggestion
selectMonthSuggestion(input, suggestion) {
const monthNum = findClosestMonth(suggestion.value);
const formattedMonth = fullMonthNames.en[monthNum - 1];
input.value = formattedMonth;
this.removeMonthSuggestions();
// Move to year input
const yearInput = input.parentElement.parentElement.querySelector('.year input');
if (yearInput) {
setTimeout(() => {
yearInput.focus();
this.animatePulse(yearInput);
}, 10);
}
// Update hidden value
const hiddenInput = input.closest('.input-field').querySelector('input[type="hidden"]');
if (hiddenInput) {
const dayInput = input.parentElement.parentElement.querySelector('.day input');
const message = { message_id: hiddenInput.dataset.messageId };
this.updateHiddenDateValue(message.message_id, dayInput, input, yearInput, hiddenInput);
}
}
// Create date input
createDateInput(message) {
// Create date input structure with 3 fields
const dateInputGroup = document.createElement('div');
dateInputGroup.className = 'date-input-holder-group';
// Create day input
const dayInputHolder = document.createElement('div');
dayInputHolder.className = 'date-input-holder';
const dayMorphCont = document.createElement('div');
dayMorphCont.className = 'date-morph-cont day';
const dayInput = document.createElement('input');
dayInput.type = 'text';
dayInput.className = 'cg-input-morph w-input date-input-transition';
dayInput.id = 'day';
dayInput.name = 'day';
dayInput.placeholder = 'Tag';
dayInput.maxLength = 256;
dayInput.setAttribute('data-name', 'day');
dayInput.autofocus = true;
const dayLine = document.createElement('div');
dayLine.className = 'line-input-bottom-new';
const dayError = document.createElement('div');
dayError.className = 'date-input-error';
dayError.id = 'dayError';
dayError.style.display = 'none';
dayError.textContent = 'Please input a valid day 1-31';
// Create month input
const monthInputHolder = document.createElement('div');
monthInputHolder.className = 'date-input-holder';
const monthMorphCont = document.createElement('div');
monthMorphCont.className = 'date-morph-cont month';
const monthInput = document.createElement('input');
monthInput.type = 'text';
monthInput.className = 'cg-input-morph w-input date-input-transition';
monthInput.id = 'month';
monthInput.name = 'month';
monthInput.placeholder = 'Monat';
monthInput.maxLength = 256;
monthInput.setAttribute('data-name', 'month');
const monthLine = document.createElement('div');
monthLine.className = 'line-input-bottom-new';
const monthError = document.createElement('div');
monthError.className = 'date-input-error';
monthError.id = 'monthError';
monthError.style.display = 'none';
monthError.textContent = 'Please input a valid month';
// Create year input
const yearInputHolder = document.createElement('div');
yearInputHolder.className = 'date-input-holder';
const yearMorphCont = document.createElement('div');
yearMorphCont.className = 'date-morph-cont year';
const yearInput = document.createElement('input');
yearInput.type = 'text';
yearInput.className = 'cg-input-morph w-input date-input-transition';
yearInput.id = 'year';
yearInput.name = 'year';
yearInput.placeholder = 'Jahr';
yearInput.maxLength = 256;
yearInput.setAttribute('data-name', 'year');
const yearLine = document.createElement('div');
yearLine.className = 'line-input-bottom-new';
const yearError = document.createElement('div');
yearError.className = 'date-input-error';
yearError.id = 'yearError';
yearError.style.display = 'none';
yearError.textContent = 'Please input a valid year';
// Add inputs to their holders
dayMorphCont.appendChild(dayInput);
dayMorphCont.appendChild(dayLine);
dayMorphCont.appendChild(dayError);
dayInputHolder.appendChild(dayMorphCont);
monthMorphCont.appendChild(monthInput);
monthMorphCont.appendChild(monthLine);
monthMorphCont.appendChild(monthError);
monthInputHolder.appendChild(monthMorphCont);
yearMorphCont.appendChild(yearInput);
yearMorphCont.appendChild(yearLine);
yearMorphCont.appendChild(yearError);
yearInputHolder.appendChild(yearMorphCont);
// Add all holders to the group
dateInputGroup.appendChild(dayInputHolder);
dateInputGroup.appendChild(monthInputHolder);
dateInputGroup.appendChild(yearInputHolder);
// Add hidden input to store the full date value (YYYY-MM-DD format)
const hiddenDateInput = document.createElement('input');
hiddenDateInput.type = 'hidden';
hiddenDateInput.id = message.message_id;
hiddenDateInput.name = message.message_id;
// Set value from stored input if available
if (message.input !== null) {
hiddenDateInput.value = message.input;
this.populateDateFields(message.input); // Note the "this." prefix
}
const container = document.createElement('div');
container.className = 'date-input-container';
container.appendChild(dateInputGroup);
container.appendChild(hiddenDateInput);
// Set up event listeners for all date fields
this.setupDateFieldEvents(message, container, dateInputGroup); // Note the "this." prefix
return container;
}
// Create password input with toggle visibility
createPasswordInput(message) {
// Main container
const holderMorphPw = document.createElement('div');
holderMorphPw.className = 'holder-morph-pw-input';
// Password input container
const contMorphPassword = document.createElement('div');
contMorphPassword.className = 'cont-morph-password';
holderMorphPw.appendChild(contMorphPassword);
// Create password input
const input = document.createElement('input');
input.type = 'password'; // Start as password type
input.className = 'cg-input-morph pw w-input';
input.id = message.message_id;
input.name = 'field-2';
input.dataset.name = 'Field 2';
input.placeholder = '*******';
input.autofocus = true;
input.maxLength = 256;
input.required = message.required || false;
// Set value from stored input if available
if (message.input !== null) {
input.value = message.input;
}
// Add event listener for input changes
input.addEventListener('input', () => this.updateNextButtonState());
// Create toggle visibility button
const toggleButton = document.createElement('div');
toggleButton.id = 'toggleaddPasswordVisibility';
toggleButton.className = 'cta-hide-show-password-lp morph';
// Create eye icon
const eyeIcon = document.createElement('img');
eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg';
eyeIcon.loading = 'lazy';
eyeIcon.id = 'eyeicon';
eyeIcon.alt = '';
eyeIcon.className = 'icon-hideshow-password-lp';
// Add click event to toggle password visibility
toggleButton.addEventListener('click', () => {
if (input.type === 'password') {
input.type = 'text';
eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg'; // Use crossed-eye icon for visible password
} else {
input.type = 'password';
eyeIcon.src = 'https://cdn.prod.website-files.com/61918430ea8005fe5b5d3b6c/65d8bdc4be42fe7bc323935c_eye-icon-MyGesuch-big-filled-gray-01.svg'; // Use regular eye icon for hidden password
}
});
// Append elements
toggleButton.appendChild(eyeIcon);
contMorphPassword.appendChild(input);
contMorphPassword.appendChild(toggleButton);
// Add bottom line
const lineElement = document.createElement('div');
lineElement.className = 'line-input-bottom-new';
holderMorphPw.appendChild(lineElement);
// Add validation message container (initially hidden)
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
holderMorphPw.appendChild(validationMessage);
return holderMorphPw;
}
// Helper method to create individual date fields
createDateField(id, placeholder, autofocus = false) {
const input = document.createElement('input');
input.className = 'date-field';
input.type = 'text';
input.maxLength = '1';
input.placeholder = placeholder;
input.id = id;
input.name = id;
input.dataset.name = id;
if (autofocus) {
input.autofocus = true;
}
// Allow only numbers
input.addEventListener('keypress', (e) => {
const keyCode = e.which || e.keyCode;
if (keyCode < 48 || keyCode > 57) {
e.preventDefault();
}
});
return input;
}
// Create money input with currency selection
createMoneyInput(message) {
const container = document.createElement('div');
container.className = 'holder-morph-figure-input';
// Create the main input wrapper
const inputWrapper = document.createElement('div');
inputWrapper.className = 'cont-morph-password';
// Create the income input
const incomeInput = document.createElement('input');
incomeInput.className = 'cg-input-morph figure w-input';
incomeInput.autofocus = true;
incomeInput.maxLength = 256;
incomeInput.name = message.message_id || 'income';
incomeInput.setAttribute('data-name', message.message_id || 'income');
incomeInput.placeholder = message.placeholder || '0.000';
incomeInput.type = 'text';
incomeInput.id = message.message_id || 'income';
incomeInput.required = true;
// Set value from stored input if available
if (message.input !== null) {
incomeInput.value = this.formatMoneyValue(message.input);
}
// Create the currency selector
const currencyToggle = document.createElement('div');
currencyToggle.id = 'toggleaddPasswordVisibility';
currencyToggle.className = 'cta-change-currency-figure';
const currencyInput = document.createElement('input');
currencyInput.className = 'input-currency-select w-input';
currencyInput.maxLength = 256;
currencyInput.name = 'currency';
currencyInput.setAttribute('data-name', 'currency');
currencyInput.placeholder = '';
currencyInput.type = 'text';
currencyInput.id = 'currency';
currencyInput.required = true;
currencyInput.setAttribute('data-prev', '$');
currencyInput.value = '€'; // Default to Euro
// Assemble the input structure
currencyToggle.appendChild(currencyInput);
inputWrapper.appendChild(incomeInput);
inputWrapper.appendChild(currencyToggle);
// Create the line element
const lineElement = document.createElement('div');
lineElement.className = 'line-input-bottom-new';
// Add validation message container
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
// Assemble the container
container.appendChild(inputWrapper);
container.appendChild(lineElement);
container.appendChild(validationMessage);
// Add currency validation and formatting logic
this.addCurrencyLogic(incomeInput, currencyInput);
return container;
}
// Add currency validation and formatting logic
addCurrencyLogic(incomeInput, currencyInput) {
// Currency normalization function
const normalizeCurrency = (input) => {
const val = (input || "").trim().toUpperCase();
if (["EUR", "€", "DE"].includes(val)) return "€";
if (["GBP", "£", "UK"].includes(val)) return "£";
if (["USD", "$", "US"].includes(val)) return "$";
return "$"; // fallback
};
// Get locale based on currency symbol
const getLocale = (symbol) => {
switch (symbol) {
case "€": return "de-DE";
case "£": return "en-GB";
case "$":
default: return "en-US";
}
};
// Parse localized number
const parseLocalizedNumber = (inputStr, locale) => {
if (!inputStr) return NaN;
let normalized = inputStr;
// Handle different number formats intelligently based on locale
if (locale === "de-DE") {
// German/European format: 1.234,56
if (normalized.includes(',')) {
const parts = normalized.split(',');
if (parts.length === 2 && parts[1].length <= 2) {
// Has decimal comma - remove dots (thousands) and convert comma to dot
normalized = parts[0].replace(/\./g, '') + '.' + parts[1];
} else {
// Multiple commas - treat as thousands separators
normalized = normalized.replace(/,/g, '');
}
} else if (normalized.includes('.')) {
// Only dots - could be thousands separators or decimal
const parts = normalized.split('.');
if (parts.length === 2 && parts[1].length <= 2) {
// Likely misused decimal format - keep as is
} else {
// Multiple dots - thousands separators
normalized = normalized.replace(/\./g, '');
}
}
} else {
// US/UK format: 1,234.56
if (normalized.includes('.')) {
const parts = normalized.split('.');
if (parts.length === 2 && parts[1].length <= 2) {
// Has decimal point - remove commas (thousands)
normalized = parts[0].replace(/,/g, '') + '.' + parts[1];
} else {
// Multiple dots - unusual, remove all
normalized = normalized.replace(/\./g, '');
}
} else {
// Only commas - thousands separators
normalized = normalized.replace(/,/g, '');
}
}
return parseFloat(normalized);
};
// Format number with locale
const formatNumber = (number, locale) => {
if (isNaN(number)) return "";
return new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(number);
};
// Reformat income after currency change
const reformatIncomeAfterCurrencyChange = (prevCurrency) => {
const prevLocale = getLocale(prevCurrency);
const newCurrency = normalizeCurrency(currencyInput.value);
const newLocale = getLocale(newCurrency);
// Parse based on *old* locale (before switching)
const parsedNumber = parseLocalizedNumber(incomeInput.value, prevLocale);
const formatted = formatNumber(parsedNumber, newLocale);
currencyInput.value = newCurrency;
incomeInput.value = formatted;
};
// When income loses focus — reformat in-place
incomeInput.addEventListener("blur", () => {
const currentLocale = getLocale(normalizeCurrency(currencyInput.value));
const parsed = parseLocalizedNumber(incomeInput.value, currentLocale);
incomeInput.value = formatNumber(parsed, currentLocale);
// Update next button state
this.updateNextButtonState();
});
// When currency field gains focus — remember what it was
currencyInput.addEventListener("focus", () => {
currencyInput.setAttribute("data-prev", normalizeCurrency(currencyInput.value));
});
// When currency field loses focus — reformat with proper conversion
currencyInput.addEventListener("blur", () => {
const prev = currencyInput.getAttribute("data-prev") || "$";
reformatIncomeAfterCurrencyChange(prev);
// Update next button state
this.updateNextButtonState();
});
// Initialize currency
currencyInput.value = normalizeCurrency(currencyInput.value);
}
// Helper to format money value for display
formatMoneyValue(value) {
if (value === null || value === undefined || value === '') {
return '';
}
// If value is a string with a comma, parse it first
if (typeof value === 'string' && value.includes(',')) {
value = this.parseMoneyValue(value);
if (value === null) return '';
}
// Format the number with German/European formatting (1.234,56)
return new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
}).format(value);
}
// Helper to parse money value from formatted string
parseMoneyValue(value) {
if (!value) return null;
// Remove any currency symbols (€, £, $), spaces
value = value.replace(/[€£$\s]/g, '');
// Handle different number formats intelligently
if (value.includes(',')) {
// If there's a comma, it could be European decimal or thousands separator
const parts = value.split(',');
if (parts.length === 2 && parts[1].length <= 2) {
// Likely European decimal format (1.234,56)
// Remove dots (thousands separators) and convert comma to dot
value = parts[0].replace(/\./g, '') + '.' + parts[1];
} else {
// Likely American thousands separator (1,234,567)
value = value.replace(/,/g, '');
}
} else if (value.includes('.')) {
// If there's a dot, check if it's decimal or thousands separator
const parts = value.split('.');
if (parts.length === 2 && parts[1].length <= 2) {
// Likely decimal format (1234.56)
// Keep as is
} else {
// Likely thousands separator (1.234.567)
value = value.replace(/\./g, '');
}
}
// If no punctuation, it's just a whole number
const numValue = parseFloat(value);
return isNaN(numValue) ? null : numValue;
}
// Add validation for money input
validateMoneyInput(message) {
const messageEl = document.querySelector(`[data-message-id="${message.message_id}"]`);
if (!messageEl) return true;
const input = messageEl.querySelector(`#${message.message_id}`);
const validationMessage = messageEl.querySelector('.validation-error');
if (!input || !validationMessage) return true;
// Clear previous validation
validationMessage.style.display = 'none';
input.style.borderColor = '';
// Skip validation if empty and not required
if (!input.value.trim() && !message.required) {
return true;
}
// Required check
if (message.required && !input.value.trim()) {
validationMessage.textContent = 'Bitte gib einen Betrag ein';
validationMessage.style.display = 'block';
input.style.borderColor = '#ff3b30';
return false;
}
// Parse the value
const numericValue = this.parseMoneyValue(input.value);
// Check if valid number
if (numericValue === null) {
validationMessage.textContent = 'Bitte gib einen gültigen Betrag ein';
validationMessage.style.display = 'block';
input.style.borderColor = '#ff3b30';
return false;
}
// Check minimum value if specified
if (message.minValue !== undefined && numericValue < message.minValue) {
validationMessage.textContent = `Der Mindestbetrag ist ${this.formatMoneyValue(message.minValue)} €`;
validationMessage.style.display = 'block';
input.style.borderColor = '#ff3b30';
return false;
}
// Check maximum value if specified
if (message.maxValue !== undefined && numericValue > message.maxValue) {
validationMessage.textContent = `Der Höchstbetrag ist ${this.formatMoneyValue(message.maxValue)} €`;
validationMessage.style.display = 'block';
input.style.borderColor = '#ff3b30';
return false;
}
return true;
}
animateShake(element) {
if (!element) return;
element.classList.remove('date-input-shake');
void element.offsetWidth; // Trigger reflow
element.classList.add('date-input-shake');
setTimeout(() => {
if (element) element.classList.remove('date-input-shake');
}, 600);
}
animatePulse(element) {
if (!element) return;
element.classList.remove('date-input-pulse');
void element.offsetWidth; // Trigger reflow
element.classList.add('date-input-pulse');
setTimeout(() => {
if (element) element.classList.remove('date-input-pulse');
}, 1600);
}
animateHighlight(element) {
if (!element) return;
element.classList.remove('date-input-highlight');
void element.offsetWidth; // Trigger reflow
element.classList.add('date-input-highlight');
setTimeout(() => {
if (element) element.classList.remove('date-input-highlight');
}, 1100);
}
// Setup event listeners for date fields
setupDateFieldEvents(message, container, dateInputGroup) {
// Get the 3 input fields
const dayInput = container.querySelector('#day');
const monthInput = container.querySelector('#month');
const yearInput = container.querySelector('#year');
const hiddenInput = container.querySelector(`input[id="${message.message_id}"]`);
const validationMessage = document.createElement('div');
validationMessage.className = 'validation-error';
validationMessage.style.display = 'none';
container.appendChild(validationMessage);
// Current date for reference
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1; // JavaScript months are 0-based
// Track if backward navigation is in progress
let backNavigationInProgress = false;
// Track month input for text entry
let lastMonthInputValue = '';
// Basic validation limits
const yearMin = 1900;
const yearMax = currentMonth >= 10 ? currentYear + 1 : currentYear;
// COMMON KEYBOARD NAVIGATION
// Day input keyboard navigation
dayInput.addEventListener('keydown', (e) => {
// Right arrow to month
if (e.key === 'ArrowRight' && dayInput.selectionStart === dayInput.value.length) {
e.preventDefault();
monthInput.focus();
monthInput.setSelectionRange(0, 0);
}
});
// Month input keyboard navigation
monthInput.addEventListener('keydown', (e) => {
const dropdown = document.getElementById('monthSuggestions');
// Handle dropdown navigation
if (dropdown) {
const items = dropdown.querySelectorAll('.month-suggestion-item');
const highlighted = dropdown.querySelector('.month-suggestion-item.highlighted');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (highlighted) {
highlighted.classList.remove('highlighted');
const next = highlighted.nextElementSibling || items[0];
next.classList.add('highlighted');
} else if (items.length > 0) {
items[0].classList.add('highlighted');
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (highlighted) {
highlighted.classList.remove('highlighted');
const prev = highlighted.previousElementSibling || items[items.length - 1];
prev.classList.add('highlighted');
} else if (items.length > 0) {
items[items.length - 1].classList.add('highlighted');
}
} else if (e.key === 'Enter' && highlighted) {
e.preventDefault();
const suggestion = {
display: highlighted.textContent,
value: highlighted.dataset.value,
type: highlighted.dataset.type
};
this.selectMonthSuggestion(monthInput, suggestion);
} else if (e.key === 'Escape') {
e.preventDefault();
this.removeMonthSuggestions();
}
}
// Left arrow to day
if (e.key === 'ArrowLeft' && monthInput.selectionStart === 0) {
e.preventDefault();
this.removeMonthSuggestions();
backNavigationInProgress = true;
dayInput.focus();
dayInput.setSelectionRange(dayInput.value.length, dayInput.value.length);
setTimeout(() => {
backNavigationInProgress = false;
}, 100);
}
// Right arrow to year
else if (e.key === 'ArrowRight' && monthInput.selectionStart === monthInput.value.length) {
e.preventDefault();
this.removeMonthSuggestions();
yearInput.focus();
yearInput.setSelectionRange(0, 0);
}
// Backspace in empty field - go back to day
else if (e.key === 'Backspace' && monthInput.value === '') {
e.preventDefault();
this.removeMonthSuggestions();
backNavigationInProgress = true;
dayInput.focus();
dayInput.setSelectionRange(dayInput.value.length, dayInput.value.length);
setTimeout(() => {
backNavigationInProgress = false;
}, 100);
}
});
// Year input keyboard navigation
yearInput.addEventListener('keydown', (e) => {
// Left arrow to month
if (e.key === 'ArrowLeft' && yearInput.selectionStart === 0) {
e.preventDefault();
backNavigationInProgress = true;
monthInput.focus();
monthInput.setSelectionRange(monthInput.value.length, monthInput.value.length);
setTimeout(() => {
backNavigationInProgress = false;
}, 100);
}
// Backspace in empty field - go back to month
else if (e.key === 'Backspace' && yearInput.value === '') {
e.preventDefault();
backNavigationInProgress = true;
monthInput.focus();
monthInput.setSelectionRange(monthInput.value.length, monthInput.value.length);
setTimeout(() => {
backNavigationInProgress = false;
}, 100);
}
});
// DAY INPUT HANDLING
dayInput.addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, ''); // Remove non-digits
// Limit to 2 digits
if (value.length > 2) {
value = value.slice(0, 2);
}
// Cap at 31
if (parseInt(value, 10) > 31) {
value = '31';
this.animateShake(dayInput);
}
e.target.value = value;
// Clear validation message
validationMessage.style.display = 'none';
dateInputGroup.style.borderColor = '';
// Update hidden field
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
// Update next button state
this.updateNextButtonState();
// Auto-advance to month if valid 2-digit day
if (value.length === 2 && parseInt(value, 10) >= 1 && parseInt(value, 10) <= 31) {
setTimeout(() => {
monthInput.focus();
this.animatePulse(monthInput);
}, 10);
}
});
// Validate and format day on blur
dayInput.addEventListener('blur', (e) => {
const value = e.target.value.trim();
if (value === '') {
return; // Don't validate empty field
}
// Validate day
const day = parseInt(value, 10);
let month = 0;
// Try to get month value if available
if (monthInput.value) {
if (/^\d+$/.test(monthInput.value)) {
month = parseInt(monthInput.value, 10);
} else {
const monthNum = findClosestMonth(monthInput.value);
if (monthNum) month = monthNum;
}
}
const year = parseInt(yearInput.value, 10) || 0;
const isValid = !isNaN(day) && isValidDay(day, month, year);
if (!isValid) {
validationMessage.textContent = 'Ungültiger Tag';
validationMessage.style.display = 'block';
this.animateShake(dayInput);
} else {
// Pad single digit with zero
if (value.length === 1) {
e.target.value = value.padStart(2, '0');
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
}
}
});
// MONTH INPUT HANDLING
monthInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
// Track if user is deleting
const isDeleting = value.length < lastMonthInputValue.length;
lastMonthInputValue = value;
// Clear validation
validationMessage.style.display = 'none';
dateInputGroup.style.borderColor = '';
// Numeric input handling
if (/^\d+$/.test(value)) {
let numValue = value;
// Cap at 12
if (parseInt(numValue, 10) > 12) {
numValue = '12';
e.target.value = numValue;
this.animateShake(monthInput);
}
// Auto-advance after valid 2-digit month
if (numValue.length === 2 && parseInt(numValue, 10) >= 1 && parseInt(numValue, 10) <= 12) {
setTimeout(() => {
yearInput.focus();
this.animatePulse(yearInput);
}, 10);
}
// Update hidden value
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
this.updateNextButtonState();
return;
}
// Text-based month handling
if (!isDeleting && value.length > 0) {
// Show autocomplete suggestions
const suggestions = getMonthSuggestions(value);
this.createMonthSuggestions(monthInput, suggestions);
// Try to match to a month name
const monthNum = findClosestMonth(value);
// If we have a full valid match, move to year
if (value.length >= 3 && monthNum !== null) {
// Check for exact match
const matchesExactly = Object.keys(monthsLookup).some(month =>
month === value.toLowerCase() && monthsLookup[month] === monthNum
);
if (matchesExactly) {
this.removeMonthSuggestions();
setTimeout(() => {
yearInput.focus();
this.animatePulse(yearInput);
}, 10);
}
}
} else if (isDeleting || value.length === 0) {
// Remove suggestions when deleting or empty
this.removeMonthSuggestions();
}
// Update hidden value
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
this.updateNextButtonState();
});
// Format and validate month on blur
monthInput.addEventListener('blur', (e) => {
// Delay removal to allow click on suggestion
setTimeout(() => {
this.removeMonthSuggestions();
}, 150);
lastMonthInputValue = ''; // Reset tracker
const value = e.target.value.trim();
if (value === '') {
return; // Don't validate empty
}
// For numeric input
if (/^\d+$/.test(value)) {
const monthNum = parseInt(value, 10);
const isValid = !isNaN(monthNum) && isValidMonth(monthNum);
if (!isValid) {
validationMessage.textContent = 'Ungültiger Monat';
validationMessage.style.display = 'block';
this.animateShake(monthInput);
} else {
// Format single-digit months
if (value.length === 1) {
e.target.value = value.padStart(2, '0');
}
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
}
return;
}
// For text input
const monthNum = findClosestMonth(value);
const isValid = monthNum !== null && isValidMonth(monthNum);
if (!isValid) {
validationMessage.textContent = 'Ungültiger Monat';
validationMessage.style.display = 'block';
this.animateShake(monthInput);
} else {
// Format as full month name
const formattedMonth = fullMonthNames.en[monthNum - 1];
e.target.value = formattedMonth;
this.animateHighlight(monthInput);
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
}
});
// YEAR INPUT HANDLING
yearInput.addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, ''); // Remove non-digits
// Limit to 4 digits
if (value.length > 4) {
value = value.slice(0, 4);
}
e.target.value = value;
// Clear validation
validationMessage.style.display = 'none';
dateInputGroup.style.borderColor = '';
// Update hidden value
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
this.updateNextButtonState();
// Auto-validate when 4 digits are entered
if (value.length === 4) {
const year = parseInt(value, 10);
if (year >= yearMin && year <= yearMax) {
setTimeout(() => {
yearInput.blur();
this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup);
}, 300);
}
}
});
// Auto-fill year on blur
yearInput.addEventListener('blur', (e) => {
// Don't auto-fill if navigating back
if (backNavigationInProgress) {
return;
}
const value = e.target.value.trim();
if (value === '') {
// Auto-fill current year if day & month are filled
if (dayInput.value && monthInput.value) {
// Get month value to determine if we should use next year
let monthValue = monthInput.value;
let monthNum;
if (/^\d+$/.test(monthValue)) {
monthNum = parseInt(monthValue, 10);
} else {
monthNum = findClosestMonth(monthValue);
}
// For late-year months (Nov/Dec), consider next year
if ((monthNum === 11 || monthNum === 12) && currentMonth >= 10) {
e.target.value = (currentYear + 1).toString();
} else {
e.target.value = currentYear.toString();
}
this.animateHighlight(yearInput);
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
}
return;
}
// Validate year
const year = parseInt(value, 10);
if (!isNaN(year)) {
if (year < yearMin) {
e.target.value = yearMin.toString();
this.animateShake(yearInput);
} else if (year > yearMax) {
e.target.value = yearMax.toString();
this.animateShake(yearInput);
} else {
// Ensure 4 digits
e.target.value = year.toString().padStart(4, '0');
}
this.updateHiddenDateValue(message.message_id, dayInput, monthInput, yearInput, hiddenInput);
// Validate if this completes the date
if (dayInput.value && monthInput.value) {
this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup);
}
}
});
// Debounced validation function
const validateDateDelayed = this.debounce(() => {
if (dayInput.value && monthInput.value && yearInput.value) {
this.validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup);
}
}, 800);
// Attach validation to all fields
dayInput.addEventListener('input', validateDateDelayed);
monthInput.addEventListener('input', validateDateDelayed);
yearInput.addEventListener('input', validateDateDelayed);
}
// Populate date fields from a date string (YYYY-MM-DD)
populateDateFields(dateString) {
if (!dateString || dateString.length !== 10) return;
try {
// Parse YYYY-MM-DD format
const year = dateString.substring(0, 4);
const month = dateString.substring(5, 7);
const day = dateString.substring(8, 10);
const dayInput = document.querySelector('#day');
const monthInput = document.querySelector('#month');
const yearInput = document.querySelector('#year');
if (dayInput) {
dayInput.value = parseInt(day, 10).toString().padStart(2, '0');
}
if (monthInput) {
monthInput.value = parseInt(month, 10).toString().padStart(2, '0');
}
if (yearInput) {
yearInput.value = year;
}
} catch (e) {
console.error('Error populating date fields:', e);
}
}
validateDate(dayInput, monthInput, yearInput, validationMessage, dateInputGroup) {
const day = parseInt(dayInput.value, 10);
let month = monthInput.value.trim();
const year = parseInt(yearInput.value, 10);
// Try to parse month as number or name
let monthNum;
if (/^\d+$/.test(month)) {
monthNum = parseInt(month, 10);
} else {
monthNum = findClosestMonth(month);
}
// Check if all fields are valid
const dayValid = !isNaN(day) && isValidDay(day, monthNum, year);
const monthValid = monthNum !== null && isValidMonth(monthNum);
const yearValid = !isNaN(year) && year >= 1900 && year <= (new Date().getFullYear() + 1);
// If any part is invalid, show validation message
if (!dayValid) {
validationMessage.textContent = 'Ungültiger Tag für diesen Monat';
validationMessage.style.display = 'block';
this.animateShake(dayInput);
return false;
}
if (!monthValid) {
validationMessage.textContent = 'Ungültiger Monat';
validationMessage.style.display = 'block';
this.animateShake(monthInput);
return false;
}
if (!yearValid) {
validationMessage.textContent = 'Ungültiges Jahr';
validationMessage.style.display = 'block';
this.animateShake(yearInput);
return false;
}
// If all valid, clear validation and return true
validationMessage.style.display = 'none';
dateInputGroup.style.borderColor = '';
return true;
}
// Check if all date fields are filled
isDateInputComplete(dateFields) {
for (let i = 0; i < dateFields.length; i++) {
if (!dateFields[i].value) {
return false;
}
}
return true;
}
// Update hidden input with the complete date value
updateHiddenDateValue(messageId, dayInput, monthInput, yearInput, hiddenInput) {
// Get values
const dayValue = dayInput.value.trim();
const monthValue = monthInput.value.trim();
const yearValue = yearInput.value.trim();
// Only update if we have at least partial values in all fields
if (dayValue && monthValue && yearValue) {
// Parse month value - could be text or number
let monthNum;
if (/^\d+$/.test(monthValue)) {
monthNum = parseInt(monthValue, 10);
} else {
monthNum = findClosestMonth(monthValue);
}
// Only set if we have valid numbers
const day = parseInt(dayValue, 10);
const year = parseInt(yearValue, 10);
if (!isNaN(day) && monthNum !== null && !isNaN(year)) {
// Format as YYYY-MM-DD
const formattedMonth = monthNum.toString().padStart(2, '0');
const formattedDay = day.toString().padStart(2, '0');
hiddenInput.value = `${year}-${formattedMonth}-${formattedDay}`;
}
} else {
// Clear if incomplete
hiddenInput.value = '';
}
}
// Validate the custom date input
validateCustomDate(dateValue, message, validationEl, dateInputGroup) {
// Check if input is empty when required
if (message.required && !dateValue) {
this.showCustomValidationMessage(validationEl, dateInputGroup, 'Dieses Feld ist erforderlich');
this.animateShake(dateInputGroup);
return false;
}
// If input has a value, validate date format and constraints
if (dateValue) {
const selectedDate = new Date(dateValue);
// Check if date is valid
if (isNaN(selectedDate.getTime())) {
this.showCustomValidationMessage(validationEl, dateInputGroup, 'Ungültiges Datum');
this.animateShake(dateInputGroup);
return false;
}
// Extract date parts for advanced validation
const [year, month, day] = dateValue.split('-').map(Number);
// Validate day for this specific month/year
if (!isValidDay(day, month, year)) {
this.showCustomValidationMessage(validationEl, dateInputGroup, 'Ungültiger Tag für diesen Monat');
this.animateShake(dateInputGroup);
return false;
}
// Check min date constraint
if (message.minDate && new Date(dateValue) < new Date(message.minDate)) {
this.showCustomValidationMessage(
validationEl,
dateInputGroup,
`Datum muss nach ${this.formatDate(message.minDate)} sein`
);
this.animateShake(dateInputGroup);
return false;
}
// Check max date constraint
if (message.maxDate && new Date(dateValue) > new Date(message.maxDate)) {
this.showCustomValidationMessage(
validationEl,
dateInputGroup,
`Datum muss vor ${this.formatDate(message.maxDate)} sein`
);
this.animateShake(dateInputGroup);
return false;
}
// Custom validation logic from message if provided
if (message.validate && typeof message.validate === 'function') {
const customValidation = message.validate(dateValue);
if (customValidation !== true) {
this.showCustomValidationMessage(
validationEl,
dateInputGroup,
customValidation || 'Ungültiges Datum'
);
this.animateShake(dateInputGroup);
return false;
}
}
}
return true;
}
isDateInputComplete(dateFields) {
for (let i = 0; i < dateFields.length; i++) {
if (!dateFields[i].value) {
return false;
}
}
return true;
}
showCustomValidationMessage(validationEl, dateInputGroup, message) {
// Check if validationEl exists before using it
if (!validationEl) {
console.warn('Validation element is null. Cannot display validation message.');
return;
}
// Check if dateInputGroup exists before using it
if (!dateInputGroup) {
console.warn('Date input group is null. Cannot highlight date fields.');
} else {
dateInputGroup.style.borderColor = '#ff3b30';
}
validationEl.textContent = message;
validationEl.style.display = 'block';
validationEl.style.color = '#ff3b30';
validationEl.style.fontSize = '0.875rem';
validationEl.style.marginTop = '0.5rem';
}
// Add a specific validation method for date inputs
validateDateInput(input, message, validationEl) {
// Check if input is empty when required
if (message.required && (!input.value || input.value.trim() === '')) {
this.showValidationMessage(validationEl, input, 'Dieses Feld ist erforderlich');
return false;
}
// If input has a value, validate date format and constraints
if (input.value) {
const selectedDate = new Date(input.value);
// Check if date is valid
if (isNaN(selectedDate.getTime())) {
this.showValidationMessage(validationEl, input, 'Ungültiges Datum');
return false;
}
// Check min date constraint
if (input.min && new Date(input.value) < new Date(input.min)) {
this.showValidationMessage(validationEl, input, `Datum muss nach ${this.formatDate(input.min)} sein`);
return false;
}
// Check max date constraint
if (input.max && new Date(input.value) > new Date(input.max)) {
this.showValidationMessage(validationEl, input, `Datum muss vor ${this.formatDate(input.max)} sein`);
return false;
}
// Custom validation logic from message if provided
if (message.validate && typeof message.validate === 'function') {
const customValidation = message.validate(input.value);
if (customValidation !== true) {
this.showValidationMessage(validationEl, input, customValidation || 'Ungültiges Datum');
return false;
}
}
}
return true;
}
// Helper method to show validation messages
showValidationMessage(validationEl, input, message) {
validationEl.textContent = message;
validationEl.style.display = 'block';
validationEl.style.color = '#ff3b30';
validationEl.style.fontSize = '0.875rem';
validationEl.style.marginTop = '0.5rem';
// Highlight the input
input.style.borderColor = '#ff3b30';
}
// Helper method to format dates for error messages
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
// Create select dropdown
createSelect(message) {
const container = document.createElement('div');
container.className = 'input-field';
const select = document.createElement('select');
select.id = message.message_id;
select.name = message.message_id;
// Add placeholder option
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = message.placeholder || 'Bitte wählen';
placeholderOption.disabled = true;
placeholderOption.selected = message.input === null;
select.appendChild(placeholderOption);
// Add options
if (message.options && Array.isArray(message.options)) {
message.options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.value = option.value;
optionEl.textContent = option.label;
// Set selected if it matches saved input
if (message.input === option.value) {
optionEl.selected = true;
}
select.appendChild(optionEl);
});
}
input.addEventListener('change', () => this.updateNextButtonState());
container.appendChild(select);
return container;
}
// Create checkbox list
createCheckboxList(message) {
const container = document.createElement('div');
container.className = 'selection-list';
// Add options
if (message.options && Array.isArray(message.options)) {
message.options.forEach(option => {
const optionContainer = document.createElement('div');
optionContainer.className = 'selection-option';
const labelEl = document.createElement('span');
labelEl.className = 'selection-option-label';
labelEl.textContent = option.label;
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `${message.message_id}_${option.value}`;
input.name = message.message_id;
input.value = option.value;
// Add custom checkbox visual
const checkboxEl = document.createElement('span');
checkboxEl.className = 'selection-checkbox';
// Check if this option is already selected
if (message.input && Array.isArray(message.input) && message.input.includes(option.value)) {
input.checked = true;
}
// Check if this option should be disabled/dimmed
if (option.disabled) {
optionContainer.classList.add('selection-option-dimmed');
input.disabled = true;
}
input.addEventListener('change', () => this.updateNextButtonState());
optionContainer.appendChild(labelEl);
optionContainer.appendChild(input);
optionContainer.appendChild(checkboxEl);
container.appendChild(optionContainer);
// Make entire option clickable
optionContainer.addEventListener('click', (e) => {
if (!option.disabled && e.target !== input) {
input.checked = !input.checked;
// Trigger a change event so any listeners can respond
input.dispatchEvent(new Event('change'));
}
});
});
}
return container;
}
// Create radio list
createRadioList(message) {
const container = document.createElement('div');
container.className = 'selection-list';
// Add options
if (message.options && Array.isArray(message.options)) {
message.options.forEach(option => {
const optionContainer = document.createElement('div');
optionContainer.className = 'selection-option';
const labelEl = document.createElement('span');
labelEl.className = 'selection-option-label';
labelEl.textContent = option.label;
const input = document.createElement('input');
input.type = 'radio';
input.id = `${message.message_id}_${option.value}`;
input.name = message.message_id;
input.value = option.value;
// Add custom radio visual
const radioEl = document.createElement('span');
radioEl.className = 'selection-radio';
// Check if this option is already selected
if (message.input === option.value) {
input.checked = true;
}
// Check if this option should be disabled/dimmed
if (option.disabled) {
optionContainer.classList.add('selection-option-dimmed');
input.disabled = true;
}
optionContainer.appendChild(labelEl);
optionContainer.appendChild(input);
optionContainer.appendChild(radioEl);
container.appendChild(optionContainer);
// Make entire option clickable
optionContainer.addEventListener('click', (e) => {
if (!option.disabled && e.target !== input) {
input.checked = true;
// Trigger a change event so any listeners can respond
input.dispatchEvent(new Event('change'));
}
});
input.addEventListener('change', () => this.updateNextButtonState());
// Add auto-advance functionality for all radio lists
input.addEventListener('change', (e) => {
if (e.target.checked) {
// Use debounced handler to prevent multiple rapid submissions
this.debouncedHandleNext();
}
});
});
}
return container;
}
// Create photo upload
createPhotoUpload(message) {
const container = document.createElement('div');
container.className = 'photo-upload-container';
// Hidden file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = `photo_input_${message.message_id}`;
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
// Main circular photo area
const photoCircle = document.createElement('div');
photoCircle.className = 'photo-circle';
// Upload trigger (initially visible)
const uploadTrigger = document.createElement('div');
uploadTrigger.className = 'photo-upload-trigger circular';
uploadTrigger.innerHTML = `