/** * HubSpot Form Utilities * * Common utilities for HubSpot forms across multiple pages * Includes: emoji removal, form submission handling, referrer population, dependency checking * * Usage: * * * HubSpotFormUtils.setupEmojiRemoval($form); * HubSpotFormUtils.setupFormSubmission($form, { * formSelector: '#form1', * thankYouSelector: '#thank-you', * hideSelectors: ['.toggle-div'], * addClassToSelector: '.form-holder', * classToAdd: 'padding-bottom-0' * }); * HubSpotFormUtils.populateReferrerField($form); */ class HubSpotFormUtils { /** * Remove emojis and zero-width characters from form fields * @param {jQuery} $form - jQuery form element * @param {Object} options - Configuration options */ static setupEmojiRemoval($form, options = {}) { const config = { onInput: true, // Remove emojis as user types onSubmit: true, // Remove emojis on form submission ...options }; if (config.onInput) { $form.find('input, textarea').on('input', function () { const noEmoji = this.value.replace(/[\p{Emoji_Presentation}\u200B\u200D\uFE0F]/gu, ''); if (this.value !== noEmoji) { this.value = noEmoji; } }); } if (config.onSubmit) { $form.on('submit', function() { $form.find('input, textarea').each(function () { this.value = this.value.replace(/[\p{Emoji_Presentation}\u200B\u200D\uFE0F]/gu, ''); }); }); } } /** * Setup form submission handler to hide form and show thank you message * @param {jQuery} $form - jQuery form element * @param {Object} config - Configuration object * @param {string} config.formSelector - Selector for the form container to hide * @param {string} config.thankYouSelector - Selector for the thank you message to show * @param {Array} config.hideSelectors - Additional selectors to hide * @param {string} config.addClassToSelector - Selector to add class to * @param {string} config.classToAdd - Class name to add * @param {boolean} config.scrollToTop - Whether to scroll to top after submission (default: true) */ static setupFormSubmission($form, config = {}) { const defaultConfig = { formSelector: '#form1', thankYouSelector: '#thank-you', hideSelectors: [], addClassToSelector: null, classToAdd: null, scrollToTop: true }; const finalConfig = { ...defaultConfig, ...config }; $form.on('submit', function() { // Hide form if (finalConfig.formSelector) { $(finalConfig.formSelector).hide(); } // Hide additional elements finalConfig.hideSelectors.forEach(selector => { $(selector).hide(); }); // Add class to element if (finalConfig.addClassToSelector && finalConfig.classToAdd) { $(finalConfig.addClassToSelector).addClass(finalConfig.classToAdd); } // Show thank you message if (finalConfig.thankYouSelector) { $(finalConfig.thankYouSelector).show(); } // Scroll to top if (finalConfig.scrollToTop) { window.scrollTo(0, 0); } return false; }); } /** * Populate referrer field with browser referrer * @param {jQuery} $form - jQuery form element * @param {Object} options - Configuration options * @param {string} options.fieldName - Field name to populate (default: 'previous_page_url') */ static populateReferrerField($form, options = {}) { const config = { fieldName: 'previous_page_url', ...options }; const referrerValue = document.referrer || ''; if (!referrerValue) { return; // No referrer to populate } // Try multiple methods to find and set the field let referrerField = $form.find(`input[name="${config.fieldName}"]`); // If not found, try with different attribute selectors if (!referrerField.length) { referrerField = $form.find(`input[data-name="${config.fieldName}"]`); } if (!referrerField.length) { referrerField = $form.find(`input[name*="previous_page"]`); } if (!referrerField.length) { // Try finding by label text $form.find('label').each(function () { if ($(this).text().toLowerCase().includes('previous page')) { const fieldId = $(this).attr('for'); if (fieldId) { referrerField = $form.find('#' + fieldId); if (referrerField.length) { return false; // Break the loop } } } }); } if (referrerField.length) { referrerField.val(referrerValue); referrerField.trigger('change'); } } /** * Wait for dependencies to load before executing callback * @param {Array} dependencies - Array of global variable names to wait for * @param {Function} callback - Callback function to execute when all dependencies are loaded * @param {Object} options - Configuration options * @param {number} options.checkInterval - Interval in ms to check for dependencies (default: 100) * @param {number} options.maxWaitTime - Maximum time to wait in ms (default: 10000) */ static waitForDependencies(dependencies, callback, options = {}) { const config = { checkInterval: 100, maxWaitTime: 15000, // Increased to 15 seconds for async scripts ...options }; const startTime = Date.now(); function checkDependencies() { // Check if max wait time exceeded if (Date.now() - startTime > config.maxWaitTime) { // Check which dependencies are actually missing const missingDeps = dependencies.filter(dep => { const parts = dep.split('.'); let obj = window; for (const part of parts) { if (obj[part] === undefined) { return true; } obj = obj[part]; } return false; }); // Only warn if critical dependencies are missing (not just hbspt which loads async) if (missingDeps.length > 0 && !(missingDeps.length === 1 && missingDeps[0] === 'hbspt')) { console.warn('Timeout waiting for dependencies:', missingDeps); } if (callback) callback(); return; } // Check if all dependencies are available const allLoaded = dependencies.every(dep => { // Handle nested properties (e.g., 'window.jQuery') const parts = dep.split('.'); let obj = window; for (const part of parts) { if (obj[part] === undefined) { return false; } obj = obj[part]; } return true; }); if (allLoaded) { if (callback) callback(); } else { setTimeout(checkDependencies, config.checkInterval); } } checkDependencies(); } /** * Setup global error handlers for subscription errors * Prevents console spam from expected subscription-related errors */ static setupErrorHandlers() { // Global error handler to catch unhandled errors window.addEventListener('error', function(event) { // Filter out subscription errors - these are expected and already handled const errorMsg = event.message || ''; const isSubscriptionError = errorMsg.includes('SUBSCRIPTION') || errorMsg.includes('SUBSCRIPTION ISSUE') || errorMsg.includes('subscription is inactive') || errorMsg.includes('subscription is expired'); if (isSubscriptionError) { // Log as info, not error, and prevent default error logging event.preventDefault(); return true; } }); // Handle unhandled promise rejections window.addEventListener('unhandledrejection', function(event) { // Filter out subscription errors - these are expected const errorMsg = (event.reason && event.reason.message) ? event.reason.message : ''; const isSubscriptionError = errorMsg.includes('SUBSCRIPTION') || errorMsg.includes('SUBSCRIPTION ISSUE') || errorMsg.includes('subscription is inactive') || errorMsg.includes('subscription is expired') || (event.reason && (event.reason.isSubscriptionError || event.reason.name === 'SubscriptionError')); if (isSubscriptionError) { // Log as info, not error, and prevent default error logging event.preventDefault(); return true; } }); } /** * Initialize form with common utilities * @param {jQuery} $form - jQuery form element * @param {Object} config - Configuration object */ static initForm($form, config = {}) { const defaultConfig = { removeEmojis: true, populateReferrer: true, setupSubmission: false, // Set to true and provide config if you want auto submission handling submissionConfig: {} }; const finalConfig = { ...defaultConfig, ...config }; // Setup emoji removal if (finalConfig.removeEmojis) { this.setupEmojiRemoval($form); } // Populate referrer field if (finalConfig.populateReferrer) { this.populateReferrerField($form, finalConfig.referrerOptions || {}); } // Setup form submission handler if (finalConfig.setupSubmission) { this.setupFormSubmission($form, finalConfig.submissionConfig); } } } // Export for use in other files if (typeof module !== 'undefined' && module.exports) { module.exports = HubSpotFormUtils; }