1// v1.6 2(($, Drupal, once) => { 3 let ajaxBeforeRedirectProcessed = []; 4 let beforeRedirectProcessed = []; 5 6 Drupal.behaviors.analyticsHelpersForm = { 7 attach: (context) => { 8 Drupal.analytics = Drupal?.analytics ?? {}; 9 Drupal.analytics.formData = Drupal.analytics?.formData ?? {}; 10 11 Drupal.analytics.initForm = (formsList) => { 12 formsList.forEach((options) => { 13 if (options?.externalForm) { 14 // Wait for form and handle, and track. 15 waitForElement(options.formSelector, () => { 16 handleForm(options); 17 }); 18 } else { 19 handleForm(options); 20 } 21 22 Drupal.analytics.lsNewPageConfirmation.check(options); 23 }); 24 } 25 26 function waitForElement(selector, callback) { 27 const $body = $('body'); 28 29 const observer = new MutationObserver((mutationsList, observerInstance) => { 30 if ($body.find(selector).length) { 31 callback($body.find(selector)); 32 } 33 }); 34 35 observer.observe(document.body, { childList: true, subtree: true }); 36 } 37 38 /** 39 * Form handler. 40 * 41 * @param {Object} options 42 */ 43 function handleForm(options) { 44 $(once('form-listener', options.formSelector, document)).each(() => { 45 const form = getForm(options); 46 const $form = $(form); 47 const $submitButton = $form.find(options?.submitButtonSelector ?? 'input[type="submit"]'); 48 49 form.setAttribute('data-form-name', options.formName); 50 form.setAttribute('data-form-type', options.formType); 51 52 $submitButton.addClass('data-analytics-form-submit data-analytics-click-link'); 53 54 $submitButton.each(function () { 55 let name = Drupal.t('Submit'); 56 if (this.value) { 57 name = this.value; 58 } else if (this.innerText) { 59 name = this.innerText; 60 } 61 62 Drupal.analytics.addDataAnalyticsLink(this, { 63 name: name, 64 }); 65 }); 66 67 if (options?.addCustomClasses) { 68 for (const [key, value] of Object.entries(options.addCustomClasses)) { 69 $form.find(key).addClass(value); 70 } 71 } 72 73 // Check result on a new page after redirect. 74 if (options?.newPageConfirmation) { 75 window.onload = () => collectAndExecute(options); 76 Drupal.analytics.lsNewPageConfirmation.init(form, options); 77 } 78 79 $submitButton.on('click', () => { 80 Drupal.analytics.formData = formValues(form); 81 collectAndExecute(options); 82 }); 83 $submitButton.on('keydown', (event) => { 84 if (event.key === 'Enter') { 85 Drupal.analytics.formData = formValues(form); 86 collectAndExecute(options); 87 } 88 }); 89 }); 90 } 91 92 /** 93 * Get the form element. 94 * 95 * @param {Object} options 96 * @return {Element} 97 */ 98 function getForm(options) { 99 let form = document.querySelector(`${options.formSelector}`); 100 if (form.tagName.toLowerCase() !== 'form') { 101 form = document.querySelector(`${options.formSelector} form`); 102 } 103 return form; 104 } 105 106 /** 107 * Collect all function calls for delay timeout and execute with final 108 * results. 109 * 110 * @param {(Object|undefined)} options 111 * @returns 112 */ 113 function collectAndExecute(options = undefined) { 114 const delay = options?.delay ?? 100; 115 116 if (options?.formCompleteBeforeRedirect) { 117 if (options?.ajax) { 118 $(document).off('ajaxComplete').on('ajaxComplete', (event, xhr, settings) => { 119 setTimeout(() => formError(options), delay); 120 }); 121 formCompleteBeforeAjaxRedirect(options); 122 123 if (options?.jsValidation) { 124 setTimeout(() => formError(options), delay); 125 } 126 } else { 127 if (options?.jsValidation) { 128 formCompleteBeforeRedirect(options); 129 } 130 } 131 } else { 132 if (options?.ajax) { 133 $(document).off('ajaxComplete').on('ajaxComplete', (event, xhr, settings) => { 134 setTimeout(() => checkForm(options), delay); 135 }); 136 } 137 138 if (options?.jsValidation) { 139 setTimeout(() => checkForm(options), delay); 140 } 141 } 142 143 if (!options?.ajax && !options?.jsValidation) { 144 setTimeout(() => checkForm(options), delay); 145 } 146 } 147 148 /** 149 * Check form. 150 * 151 * @param {Object} options 152 */ 153 function checkForm(options) { 154 const errors = formError(options); 155 if (!errors) { 156 formComplete(options); 157 } 158 } 159 160 /** 161 * Trigger form_complete event and redirect after delay for Ajax form. 162 * 163 * @param {Object} options 164 */ 165 function formCompleteBeforeAjaxRedirect(options) { 166 if (!ajaxBeforeRedirectProcessed.includes(options.formSelector)) {
167 const redirectDelay = options?.formCompleteBeforeRedirectSettings?.delay ?? 2000; 168 const originalWebformRefresh = Drupal.AjaxCommands.prototype.webformRefresh; 169 170 Drupal.AjaxCommands.prototype.webformRefresh = function (ajax, response, status) { 171 const self = this; 172 173 formComplete(options); 174 175 setTimeout(() => { 176 // Call the original webformRefresh command to proceed with the default behavior. 177 originalWebformRefresh.call(self, ajax, response, status); 178 }, redirectDelay); 179 }; 180 181 ajaxBeforeRedirectProcessed = [ ...ajaxBeforeRedirectProcessed, options.formSelector ]; 182 } 183 } 184 185 /** 186 * Trigger form_complete event and redirect after delay for regular form. 187 * 188 * @param {Object} options 189 */ 190 function formCompleteBeforeRedirect(options) { 191 if (!beforeRedirectProcessed.includes(options.formSelector)) { 192 const form = getForm(options); 193 form.addEventListener('submit', function(event) { 194 event.preventDefault(); 195 196 setTimeout(() => { 197 const errors = formError(options); 198 if (!errors) { 199 formComplete(options); 200 201 const redirectDelay = options?.formCompleteBeforeRedirectSettings?.delay ?? 2000; 202 203 setTimeout(() => { 204 this.submit(); 205 }, redirectDelay); 206 } 207 }, options?.delay ?? 100); 208 }); 209 210 beforeRedirectProcessed = [ ...beforeRedirectProcessed, options.formSelector ]; 211 } 212 } 213 214 /** 215 * Collect form values. 216 * 217 * @param {Element} form 218 */ 219 function formValues(form) { 220 const elements = form.elements; 221 const result = {}; 222 223 for (let i = 0; i < elements.length; i++) { 224 const element = elements[i]; 225 const name = element.name; 226 227 // Skip elements without a name or disabled elements 228 if (!name || element.disabled) continue; 229 230 switch (element.type) { 231 case 'checkbox': 232 result[name] = element.checked; 233 break; 234 case 'radio': 235 if (element.checked) { 236 result[name] = element.value; 237 } else if (!(name in result)) { 238 result[name] = null; 239 } 240 break; 241 case 'select-multiple': 242 result[name] = Array.from(element.selectedOptions).map(option => option.value); 243 break; 244 default: 245 result[name] = element.value; 246 } 247 } 248 return result; 249 } 250 251 /** 252 * Find form. 253 * 254 * @param {Element} element 255 */ 256 function findForm(element) { 257 while (true) { 258 element = element.parentElement; 259 const form = element.querySelector('form'); 260 if (form) return form; 261 } 262 } 263 264 /** 265 * Through event of form successful submission. 266 * 267 * @param {Object} options 268 */ 269 function formComplete(options) { 270 const getData = () => { 271 const data = { 272 form_name: options.formName, 273 form_type: options.formType, 274 }; 275 276 if (options?.formFieldPopulated) { 277 const formFieldPopulated = []; 278 options.formFieldPopulated.forEach((field) => { 279 if (Drupal.analytics.formData[field]) { 280 formFieldPopulated.push(`${field}:${Drupal.analytics.formData[field]}`); 281 } 282 }); 283 if (formFieldPopulated.length) { 284 data.form_field_populated = formFieldPopulated.join(); 285 } 286 } 287 288 return data; 289 }; 290 291 if (options?.resultSelector) { 292 const selector = `${options.formSelector} ${options.resultSelector}`; 293 const $formResultEl = $(`${selector}:visible`); 294 295 if ($formResultEl.length) { 296 const form = findForm($formResultEl[0]); 297 const results = formValues(form); 298 const data = getData(); 299 300 if (options?.formResult) { 301 data.form_result = $(form).find(options.formResult).first().text(); 302 } 303 304 $(document).trigger('form_complete', { data }); 305 } 306 } else if (options?.formCompleteBeforeRedirect) { 307 const data = getData(); 308 $(document).trigger('form_complete', { data }); 309 } 310 } 311 312 /** 313 * Through event of form error submission. 314 * 315 * @param {Object} options 316 * @return boolean 317 */ 318 function formError(options) { 319 if (options?.errorSelector) { 320 const $errors = $(options.formSelector).find(options.errorSelector); 321 let errors = []; 322 323 $errors.each(function () { 324 const $item = $(this); 325 const item = $item[0]; 326 const isErrorVisible = $item.is(':visible'); 327 let errorMessage; 328 329 if (isErrorVisible) { 330 if (item.tagName.toLowerCase() === 'select') { 331 for (const option of item.options) { 332 if (option.defaultSelected) { 333 errorMessage = option.text; 334 break; 335 } 336 } 337 } else { 338 errorMessage = item.innerText.replace(/["'`\n\r\t\0\v]/g, ''); 339 } 340 if (!errorMessage) { 341 errorMessage = item.getAttribute('placeholder'); 342 } 343 errorMessage = errorMessage?.trim(); 344 345 let name = ''; 346 const $formItem = $item.closest('.form-item'); 347 const $fieldset = $item.closest('fieldset'); 348 349 if ($formItem.hasClass('js-form-type-checkbox')) { 350 name = $formItem.find('input').attr('name'); 351 errorMessage ??= Drupal.t('Required'); 352 } else if ($fieldset.hasClass('radios--wrapper')) {
353 name = $fieldset.find('input').attr('name'); 354 errorMessage ??= Drupal.t('Required'); 355 } else if (!options?.simpleErrorsFormat) { 356 name = $item.parent().find('input, select, textarea').attr('name'); 357 } 358 359 if (errorMessage) { 360 if (name) { 361 errors.push(`${name}^${errorMessage}`); 362 } else { 363 errors.push(errorMessage); 364 } 365 } 366 } 367 }); 368 369 if (options?.captcha) { 370 const $captchaErrorWrapper = $(options.formSelector).find(options.captcha.errorSelector); 371 if ($captchaErrorWrapper.length) { 372 errors.push(options.captcha.errorMessage); 373 } 374 } 375 376 if (errors.length) { 377 errors = [...new Set(errors)]; // Remove duplicates 378 const $form = $(options.formSelector); 379 380 let step; 381 if (options?.stepsSelectors) { 382 for (const [key, stepSelector] of Object.entries(options.stepsSelectors)) { 383 const $stepWrapper = $form.find(`${stepSelector}:visible`); 384 385 if ($stepWrapper.length) { 386 step = key; 387 break; 388 } 389 } 390 } 391 392 // Replace errors if it's needed. 393 if (options?.replaceErrors) { 394 options.replaceErrors.forEach((replace) => { 395 errors = errors.map(error => { 396 return error === replace.from ? replace.to : error; 397 }); 398 }); 399 } 400 401 const data = { 402 form_name: options.formName, 403 form_type: options.formType, 404 form_error_messages: errors, 405 }; 406 407 if (step) { 408 data.form_step = step; 409 } 410 411 $(document).trigger('form_error', { data }); 412 413 Drupal.analytics.lsNewPageConfirmation.deleteKey(options); 414 } 415 416 return (!!errors.length); 417 } 418 419 // No errors. 420 return false; 421 } 422 } 423 }; 424})(jQuery, Drupal, once);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.