1"use strict"; 2 3let cf7signature_resized = 0; // for compatibility with contact-form-7-signature-addon 4 5let wpcf7cf_timeout; 6let wpcf7cf_change_time_ms = 100; // the timeout after a change in the form is detected 7 8// needed for multistep validation 9if (window.wpcf7 && !wpcf7.setStatus) { 10 wpcf7.setStatus = ( form, status ) => { 11 form = form.length ? form[0] : form; // if form is a jQuery object, only grab te html-element 12 const defaultStatuses = new Map( [ 13 // 0: Status in API response, 1: Status in HTML class 14 [ 'init', 'init' ], 15 [ 'validation_failed', 'invalid' ], 16 [ 'acceptance_missing', 'unaccepted' ], 17 [ 'spam', 'spam' ], 18 [ 'aborted', 'aborted' ], 19 [ 'mail_sent', 'sent' ], 20 [ 'mail_failed', 'failed' ], 21 [ 'submitting', 'submitting' ], 22 [ 'resetting', 'resetting' ], 23 ] ); 24 25 if ( defaultStatuses.has( status ) ) { 26 status = defaultStatuses.get( status ); 27 } 28 29 if ( ! Array.from( defaultStatuses.values() ).includes( status ) ) { 30 status = status.replace( /[^0-9a-z]+/i, ' ' ).trim(); 31 status = status.replace( /\s+/, '-' ); 32 status = `custom-${ status }`; 33 } 34 35 const prevStatus = form.getAttribute( 'data-status' ); 36 37 form.wpcf7.status = status; 38 form.setAttribute( 'data-status', status ); 39 form.classList.add( status ); 40 41 if ( prevStatus && prevStatus !== status ) { 42 form.classList.remove( prevStatus ); 43 } 44 45 return status; 46 }; 47} 48 49if (window.wpcf7cf_running_tests) { 50 document.querySelectorAll('input[name="_wpcf7cf_options"]').forEach(input => { 51 const opt = JSON.parse(input.value); 52 opt.settings.animation_intime = 0; 53 opt.settings.animation_outtime = 0; 54 input.value = JSON.stringify(opt); 55 }); 56 wpcf7cf_change_time_ms = 0; 57} 58 59const wpcf7cf_show_animation = { "height": "show", "marginTop": "show", "marginBottom": "show", "paddingTop": "show", "paddingBottom": "show" }; 60const wpcf7cf_hide_animation = { "height": "hide", "marginTop": "hide", "marginBottom": "hide", "paddingTop": "hide", "paddingBottom": "hide" }; 61 62const wpcf7cf_show_step_animation = { "opacity": "show" }; 63const wpcf7cf_hide_step_animation = { "opacity": "hide" }; 64 65const wpcf7cf_change_events = 'input.wpcf7cf paste.wpcf7cf change.wpcf7cf click.wpcf7cf propertychange.wpcf7cf changedisabledprop.wpcf7cf'; 66 67const wpcf7cf_forms = []; 68 69const Wpcf7cfForm = function(formElement) { 70 71 const $form = jQuery(formElement); 72 73 const options_element = $form.find('input[name="_wpcf7cf_options"]').eq(0); 74 if (!options_element.length || !options_element.val()) { 75 // doesn't look like a CF7 form created with conditional fields plugin enabled. 76 return false; 77 } 78 79 const form = this; 80 81 // always wait until groups are updated before submitting the form 82 formElement.addEventListener('submit', function(e) { 83 if (window.wpcf7cf_updatingGroups) { 84 e.preventDefault(); 85 e.stopImmediatePropagation(); 86 87 // Wait until updatingGroups is false, then submit again 88 const retry = () => { 89 if (!window.wpcf7cf_updatingGroups) { 90 $form.off('.wpcf7cf-autosubmit'); // prevent duplicates 91 formElement.requestSubmit(); // safe submit 92 } else { 93 requestAnimationFrame(retry); 94 } 95 }; 96 97 // Attach listener to prevent loop if user resubmits manually 98 $form.on('submit.wpcf7cf-autosubmit', (e) => e.preventDefault()); 99 100 retry(); // start retry loop 101 return false; 102 } 103 }, true); // use capture to run before WP's own handler 104 105 // Disable submit buttons only during the actual submit, then restore prior state â don't clobber CF7's acceptance gating or any custom disabling (#136) 106 const submitButtons = formElement.querySelectorAll('button[type=submit], input[type=submit]'); 107 const prevDisabled = new WeakMap(); 108 let wasSubmitting = formElement.classList.contains('submitting'); 109 const observer = new MutationObserver(() => { 110 const isSubmitting = formElement.classList.contains('submitting'); 111 if (isSubmitting === wasSubmitting) return; // only act on the submitting transition 112 wasSubmitting = isSubmitting; 113 114 submitButtons.forEach(button => { 115 if (isSubmitting) { 116 prevDisabled.set(button, button.disabled); 117 button.disabled = true; 118 button.classList.add('is-disabled'); 119 } else { 120 button.disabled = prevDisabled.get(button) || false; 121 button.classList.remove('is-disabled'); 122 } 123 }); 124 }); 125 observer.observe(formElement, { attributes: true, attributeFilter: ['class'] }); 126 127 const form_options = JSON.parse(options_element.val()); 128 129 form.$form = $form; 130 form.formElement = formElement; 131 form.$input_hidden_group_fields = $form.find('[name="_wpcf7cf_hidden_group_fields"]'); 132 form.$input_hidden_groups = $form.find('[name="_wpcf7cf_hidden_groups"]'); 133 form.$input_visible_groups = $form.find('[name="_wpcf7cf_visible_groups"]'); 134 form.$input_repeaters = $form.find('[name="_wpcf7cf_repeaters"]'); 135 form.$input_steps = $form.find('[name="_wpcf7cf_steps"]'); 136 137 form.unit_tag = $form.closest('.wpcf7').attr('id'); 138 form.conditions = form_options['conditions']; 139 140 form.simpleDom = null; 141 142 form.reloadSimpleDom = function() { 143 form.simpleDom = wpcf7cf.get_simplified_dom_model(form.formElement); 144 } 145 146 // quicker than reloading the simpleDom completely with reloadSimpleDom 147 form.updateSimpleDom = function() { 148 if (!form.simpleDom) { 149 form.reloadSimpleDom(); 150 } 151 const inputs = Object.values(form.simpleDom).filter(item => item.type === 'input'); 152 const formdata = new FormData(form.formElement); 153
154 let formdataEntries = [... formdata.entries()].map(entry => [ entry[0], entry[1].name ?? entry[1] ]); 155 const buttonEntries = Array.from(form.formElement.querySelectorAll('button'), b => [b.name, b.value]); 156 formdataEntries = formdataEntries.concat(buttonEntries); 157 158 inputs.forEach(simpleDomItem => { 159 const newValue = form.getNewDomValueIfChanged(simpleDomItem, formdataEntries); 160 if (newValue !== null) { 161 form.simpleDom[simpleDomItem.name].val = newValue; 162 } 163 }); 164 165 } 166 167 form.isDomMatch = function(simpleDomItem, formDataEntries) { 168 const simpleDomItemName = simpleDomItem.name; 169 const simpleDomItemValues = simpleDomItem.val; 170 const currentValues = formDataEntries.filter(entry => entry[0] === simpleDomItemName).map(entry => entry[1]); 171 return currentValues.join('|') === simpleDomItemValues.join('|'); 172 } 173 174 /** 175 * 176 * @param {*} simpleDomItem 177 * @param {*} formDataEntries 178 * @returns the new value, or NULL if no change 179 */ 180 form.getNewDomValueIfChanged = function(simpleDomItem, formDataEntries) { 181 const simpleDomItemName = simpleDomItem.name; 182 const simpleDomItemValues = simpleDomItem.val; 183 const currentValues = formDataEntries.filter(entry => entry[0] === simpleDomItemName).map(entry => entry[1]); 184 return currentValues.join('|') === simpleDomItemValues.join('|') ? null : currentValues; 185 } 186 187 // Wrapper around jQuery(selector, form.$form) 188 form.get = function (selector) { 189 // TODO: implement some caching here. 190 return jQuery(selector, form.$form); 191 } 192 193 form.getFieldByName = function(name) { 194 return form.simpleDom[name] || form.simpleDom[name+'[]']; 195 } 196 197 // compatibility with conditional forms created with older versions of the plugin ( < 1.4 ) 198 for (let i=0; i < form.conditions.length; i++) { 199 const condition = form.conditions[i]; 200 if (!('and_rules' in condition)) { 201 condition.and_rules = [{'if_field':condition.if_field,'if_value':condition.if_value,'operator':condition.operator}]; 202 } 203 } 204 205 form.initial_conditions = form.conditions; 206 form.settings = form_options['settings']; 207 208 form.$groups = jQuery(); // empty jQuery set 209 form.repeaters = []; 210 form.multistep = null; 211 form.fields = []; 212 213 form.settings.animation_intime = parseInt(form.settings.animation_intime); 214 form.settings.animation_outtime = parseInt(form.settings.animation_outtime); 215 216 if (form.settings.animation === 'no') { 217 form.settings.animation_intime = 0; 218 form.settings.animation_outtime = 0; 219 } 220 221 form.updateGroups(); 222 form.updateEventListeners(); 223 form.displayFields(); 224 225 // bring form in initial state if the reset event is fired on it. 226 // (CF7 triggers the 'reset' event by default on each successfully submitted form) 227 form.$form.on('reset.wpcf7cf', form, function(e) { 228 const form = e.data; 229 setTimeout(function(){ 230 form.reloadSimpleDom(); 231 form.displayFields(); 232 form.resetRepeaters(); 233 if (form.multistep != null) { 234 form.multistep.moveToStep(1, false); 235 } 236 setTimeout(function(){ 237 if (form.$form.hasClass('sent')) { 238 jQuery('.wpcf7-response-output', form.$form)[0].scrollIntoView({behavior: "smooth", block:"nearest", inline:"nearest"}); 239 } 240 }, 400); 241 },200); 242 }); 243 244 // PRO ONLY 245 246 form.get('.wpcf7cf_repeater:not(.wpcf7cf_repeater .wpcf7cf_repeater)').each(function(){ 247 form.repeaters.push(new Wpcf7cfRepeater(jQuery(this),form)); 248 }); 249 250 form.$input_repeaters.val(JSON.stringify(form.repeaters.map((item)=>item.params.$repeater.id))); 251 252 const $multistep = form.get('.wpcf7cf_multistep'); 253 254 if ($multistep.length) { 255 form.multistep = new Wpcf7cfMultistep($multistep, form); 256 // window.wpcf7cf.updateMultistepState(form.multistep); 257 } 258 259 // END PRO ONLY 260 261} 262 263/** 264 * reset initial number of subs for each repeater. 265 * (does not clear values) 266 */ 267Wpcf7cfForm.prototype.resetRepeaters = function() { 268 const form = this;
269 form.repeaters.forEach(repeater => { 270 repeater.updateSubs( repeater.params.$repeater.initial_subs ); 271 }); 272} 273 274Wpcf7cfForm.prototype.displayFields = function() { 275 276 const form = this; 277 278 const wpcf7cf_conditions = this.conditions; 279 const wpcf7cf_settings = this.settings; 280 281 //for compatibility with contact-form-7-signature-addon 282 if (cf7signature_resized === 0 && typeof signatures !== 'undefined' && signatures.constructor === Array && signatures.length > 0 ) { 283 for (let i = 0; i < signatures.length; i++) { 284 if (signatures[i].canvas.width === 0) { 285 286 const $sig_canvas = jQuery(".wpcf7-form-control-signature-body>canvas"); 287 const $sig_wrap = jQuery(".wpcf7-form-control-signature-wrap"); 288 $sig_canvas.eq(i).attr('width', $sig_wrap.width()); 289 $sig_canvas.eq(i).attr('height', $sig_wrap.height()); 290 291 cf7signature_resized = 1; 292 } 293 } 294 } 295 296 // 'novalidate' makes CF7 skip inline validation inside hidden groups (issue #137) 297 form.$groups.addClass('wpcf7cf-hidden novalidate'); 298 299 for (let i=0; i < wpcf7cf_conditions.length; i++) { 300 301 const condition = wpcf7cf_conditions[i]; 302 303 const show_group = window.wpcf7cf.should_group_be_shown(condition, form); 304 305 if (show_group) { 306 form.get('[data-id="'+condition.then_field+'"]').removeClass('wpcf7cf-hidden novalidate'); 307 } 308 } 309 310 311 const animation_intime = wpcf7cf_settings.animation_intime; 312 const animation_outtime = wpcf7cf_settings.animation_outtime; 313 314 form.$groups.each(function (index) { 315 const $group = jQuery(this); 316 if ($group.is(':animated')) { 317 $group.finish(); // stop any current animations on the group 318 } 319 if ($group.css('display') === 'none' && !$group.hasClass('wpcf7cf-hidden')) { 320 if ($group.prop('tagName') === 'SPAN') { 321 $group.show().trigger('wpcf7cf_show_group'); // show instantly 322 } else { 323 $group.animate(wpcf7cf_show_animation, animation_intime).trigger('wpcf7cf_show_group'); // show with animation 324 } 325 326 if($group.attr('data-disable_on_hide') !== undefined) { 327 $group.find(':input').prop('disabled', false).trigger('changedisabledprop.wpcf7cf'); 328 $group.find('.wpcf7-form-control-wrap').removeClass('wpcf7cf-disabled'); 329 } 330 331 } else if ($group.css('display') !== 'none' && $group.hasClass('wpcf7cf-hidden')) { 332 333 if ($group.attr('data-clear_on_hide') !== undefined) { 334 const $inputs = jQuery(':input', $group).not(':button, :submit, :reset, :hidden'); 335 336 $inputs.each(function(){ 337 const $this = jQuery(this); 338 $this.val(this.defaultValue); 339 $this.prop('checked', this.defaultChecked); 340 }); 341 342 jQuery('option', $group).each(function() { 343 this.selected = this.defaultSelected; 344 }); 345 346 jQuery('select', $group).each(function() { 347 const $select = jQuery(this); 348 if ($select.val() === null) { 349 $select.val(jQuery("option:first",$select).val()); 350 } 351 }); 352 353 $inputs.each(function(){this.dispatchEvent(new Event("change",{"bubbles":true}))}); 354 } 355 356 if ($group.prop('tagName') === 'SPAN') { 357 $group.hide().trigger('wpcf7cf_hide_group'); 358 } else { 359 $group.animate(wpcf7cf_hide_animation, animation_outtime).trigger('wpcf7cf_hide_group'); // hide 360 } 361 } 362 }); 363 364 form.updateHiddenFields(); 365 // PRO ONLY 366 form.updateCalculatedFields(); 367 // END PRO ONLY 368 form.updateSummaryFields(); 369}; 370 371// PRO ONLY 372Wpcf7cfForm.prototype.updateCalculatedFields = function() { 373 const form = this; 374 const $calculatedFields = form.get('.wpcf7cf-calculated-field'); 375 376 if ($calculatedFields.length === 0) { 377 return; 378 } 379 380 $calculatedFields.each(function() { 381 const $field = jQuery(this); 382 const formula = $field.attr('data-formula'); 383 const func = $field.attr('data-func'); 384 385 // Determine repeater suffix for this calculated field 386 let suffix = ''; 387 if ($field.is('input')) { 388 // input or hidden: name gets renamed by repeater, data-orig_name stays original 389 const currentName = $field.attr('name'); 390 const origName = $field.attr('data-orig_name') || currentName; 391 const nameBase = currentName.replace('[]', ''); 392 const origBase = origName.replace('[]', ''); 393 suffix = nameBase.replace(origBase, ''); 394 } else { 395 // span display: the sibling hidden input gets renamed 396 const $hiddenInput = $field.siblings('input[type="hidden"]').first(); 397 if ($hiddenInput.length) { 398 const currentName = $hiddenInput.attr('name'); 399 const origName = $hiddenInput.attr('data-orig_name') || currentName; 400 suffix = currentName.replace('[]', '').replace(origName.replace('[]', ''), ''); 401 } 402 } 403 404 let result; 405 406 if (func && typeof window[func] === 'function') { 407 // Call user-defined function with a suffix-aware fields helper 408 const fieldsHelper = wpcf7cf.calculatedfieldsCreateFieldsHelper(form, suffix
408); 409 try { 410 result = window[func](fieldsHelper); 411 } catch(e) { 412 result = 0; 413 } 414 if (typeof result !== 'number' || !isFinite(result)) { 415 result = isFinite(parseFloat(result)) ? parseFloat(result) : 0; 416 } 417 } else if (formula) { 418 result = wpcf7cf.calculatedfieldsEvaluateFormula(formula, form, suffix); 419 } else { 420 return; 421 } 422 423 const decimals = $field.attr('data-decimals'); 424 425 let displayValue = result; 426 let rawValue = result; 427 if (decimals !== undefined && decimals !== '') { 428 const numDecimals = parseInt(decimals); 429 rawValue = parseFloat(result).toFixed(numDecimals); 430 431 // Locale-aware formatting for display 432 const locale = document.documentElement.lang || 'en-US'; 433 try { 434 displayValue = new Intl.NumberFormat(locale, { 435 minimumFractionDigits: numDecimals, 436 maximumFractionDigits: numDecimals, 437 }).format(result); 438 } catch(e) { 439 displayValue = rawValue; 440 } 441 } 442 443 // Determine the field name for simpleDom update 444 let fieldName; 445 if ($field.is('input')) { 446 fieldName = $field.attr('name'); 447 // For input display, show locale-formatted but keep raw value in data attribute 448 $field.val(displayValue); 449 $field.attr('data-raw-value', rawValue); 450 } else { 451 // span display: show locale-formatted text, store raw value in hidden input 452 $field.text(displayValue); 453 const $hidden = $field.siblings('input[type="hidden"]').first(); 454 $hidden.val(rawValue); 455 fieldName = $hidden.attr('name'); 456 } 457 458 // Update simpleDom so subsequent calculated fields see the fresh value 459 if (fieldName && form.simpleDom && form.simpleDom[fieldName]) { 460 form.simpleDom[fieldName].val = [String(rawValue)]; 461 } 462 }); 463}; 464// END PRO ONLY 465 466Wpcf7cfForm.prototype.updateSummaryFields = function() { 467 const form = this; 468 const $summary = form.get('.wpcf7cf-summary'); 469 470 if ($summary.length == 0 || !$summary.is(':visible')) { 471 return; 472 } 473 474 const fd = new FormData(); 475 476 const formdata = form.$form.serializeArray(); 477 jQuery.each(formdata,function(key, input){ 478 fd.append(input.name, input.value); 479 }); 480 481 // Make sure to add file fields to FormData 482 jQuery.each(form.$form.find('input[type="file"]'), function(index, el) { 483 if (! el.files.length) return true; // continue 484 const fieldName = el.name; 485 fd.append(fieldName, new Blob() , Array.from(el.files).map(file => file.name).join(', ')); 486 }); 487 488 // add file fields to form-data 489 490 jQuery.ajax({ 491 url: wpcf7cf_global_settings.ajaxurl + '?action=wpcf7cf_get_summary', 492 type: 'POST', 493 data: fd, 494 processData: false, 495 contentType: false, 496 dataType: 'json', 497 success: function(json) { 498 $summary.html(json.summaryHtml); 499 } 500 }); 501}; 502 503Wpcf7cfForm.prototype.updateHiddenFields = function() { 504 505 const form = this; 506 507 const hidden_fields = []; 508 const hidden_groups = []; 509 const visible_groups = []; 510 511 form.$groups.each(function () { 512 const $group = jQuery(this); 513 if ($group.hasClass('wpcf7cf-hidden')) { 514 hidden_groups.push($group.attr('data-id')); 515 if($group.attr('data-disable_on_hide') !== undefined) { 516 // fields inside hidden disable_on_hide group 517 $group.find('input,select,textarea').each(function(){ 518 const $this = jQuery(this); 519 if (!$this.prop('disabled')) { 520 $this.prop('disabled', true).trigger('changedisabledprop.wpcf7cf'); 521 } 522 523 // if there's no other field with the same name visible in the form 524 // then push this field to hidden_fields 525 if (form.$form.find(`[data-class="wpcf7cf_group"]:not(.wpcf7cf-hidden) [name='${$this.attr('name')}']`).length === 0) { 526 hidden_fields.push($this.attr('name')); 527 } 528 }) 529 $group.find('.wpcf7-form-control-wrap').addClass('wpcf7cf-disabled'); 530 } else { 531 // fields inside regular hidden group are all pushed to hidden_fields 532 $group.find('input,select,textarea').each(function () { 533 hidden_fields.push(jQuery(this).attr('name')); 534 }); 535 } 536 } else { 537 visible_groups.push($group.attr('data-id')); 538 } 539 }); 540 541 form.hidden_fields = hidden_fields; 542 form.hidden_groups = hidden_groups; 543 form.visible_groups = visible_groups; 544 545 form.$input_hidden_group_fields.val(JSON.stringify(hidden_fields)); 546 form.$input_hidden_groups.val(JSON.stringify(hidden_groups)); 547 form.$input_visible_groups.val(JSON.stringify(visible_groups)); 548 549 return true; 550}; 551Wpcf7cfForm.prototype.updateGroups = function() { 552 const form = this; 553 form.$groups = form.$form.find('[data-class="wpcf7cf_group"]'); 554 form.$groups.height('auto'); 555 form.conditions = window.wpcf7cf.get_nested_conditions(form); 556 557}; 558Wpcf7cfForm.prototype.updateEventListeners = function() { 559 560 const form = this; 561 562 // monitor input changes, and call displayFields() if something has changed 563 form.get('input, select, textarea, button').not('.wpcf7cf_add, .wpcf7cf_remove').off(wpcf7cf_change_events).on(wpcf7cf_change_events,form, function(e) { 564 window.wpcf7cf_updatingGroups = true; // while this is true, don't allow the form to be submitted. 565 const form = e.data; 566 clearTimeout(wpcf7cf_timeout); 567 wpcf7cf_timeout = setTimeout(function() { 568 window.wpcf7cf.updateMultistepState(form.multistep); 569 form.updateSimpleDom(); 570 form.displayFields(); 571 window.wpcf7cf_updatingGroups = false;
572 }, wpcf7cf_change_time_ms); 573 }); 574 575 // PRO ONLY 576 form.get('.wpcf7cf-togglebutton').off('click.toggle_wpcf7cf').on('click.toggle_wpcf7cf',function() { 577 const $this = jQuery(this); 578 if ($this.text() === $this.attr('data-val-1')) { 579 $this.text($this.attr('data-val-2')); 580 $this.val($this.attr('data-val-2')); 581 } else { 582 $this.text($this.attr('data-val-1')); 583 $this.val($this.attr('data-val-1')); 584 } 585 }); 586 // END PRO ONLY 587}; 588 589// PRO ONLY 590function Wpcf7cfRepeater($repeater, form) { 591 const $ = jQuery; 592 593 let thisRepeater = this; 594 595 const wpcf7cf_settings = form.settings; 596 597 thisRepeater.form = form; 598 599 $repeater.parentRepeaters = Array.from($repeater.parents('.wpcf7cf_repeater').map(function() { 600 return this.getAttribute('data-id'); 601 } )).reverse(); 602 603 $repeater.num_subs = 0; 604 $repeater.id = $repeater.attr('data-id'); 605 $repeater.orig_id = $repeater.attr('data-orig_data_id'); 606 $repeater.min = typeof( $repeater.attr('data-min')) !== 'undefined' ? parseInt($repeater.attr('data-min')) : 1; 607 $repeater.max = typeof( $repeater.attr('data-max')) !== 'undefined' ? parseInt($repeater.attr('data-max')) : 200; 608 $repeater.initial_subs = typeof( $repeater.attr('data-initial')) !== 'undefined' ? parseInt($repeater.attr('data-initial')) : $repeater.min; 609 if ($repeater.initial_subs > $repeater.max) { 610 $repeater.initial_subs = $repeater.max; 611 } 612 613 $repeater.remove_button = $repeater.attr('data-remove_button') === 'per_entry' ? 'per_entry' : 'bottom'; 614 $repeater.show_sub_remove = $repeater.remove_button === 'per_entry'; 615 616 const $repeater_sub = $repeater.children('.wpcf7cf_repeater_sub').eq(0); 617 const $repeater_controls = $repeater.children('.wpcf7cf_repeater_controls').eq(0); 618 619 // Inject per-sub Remove control into the template sub 620 // so it gets cloned along with each new sub-entry. 621 if ($repeater.show_sub_remove) { 622 // Reuse the bottom Remove button's label so there's a single source of truth. 623 const removeLabel = $repeater.children('.wpcf7cf_repeater_controls').find('.wpcf7cf_remove').eq(0).text() || 'Remove'; 624 const $sub_controls = $('<span class="wpcf7cf_repeater_sub_controls" aria-hidden="false"></span>'); 625 $sub_controls.append($('<button type="button" class="wpcf7cf_sub_remove"></button>').text(removeLabel)); 626 $repeater_sub.append($sub_controls); 627 } 628 629 const $repeater_sub_clone = $repeater_sub.clone(); 630 631 $repeater_sub_clone.find('.wpcf7cf_repeater_sub').addBack('.wpcf7cf_repeater_sub').each(function() { 632 const $this = jQuery(this); 633 const prev_suffix = $this.attr('data-repeater_sub_suffix'); 634 const new_suffix = prev_suffix+'__{{repeater_sub_suffix}}'; 635 $this.attr('data-repeater_sub_suffix', new_suffix); 636 }); 637 638 $repeater_sub_clone.find('[name]').each(function() { 639 const $this = jQuery(this); 640 const prev_name = $this.attr('name'); 641 const new_name = thisRepeater.getNewName(prev_name); 642 643 const orig_name = $this.attr('data-orig_name') != null ? $this.attr('data-orig_name') : prev_name; 644 645 $this.attr('name', new_name); 646 $this.attr('data-orig_name', orig_name); 647 $this.closest('.wpcf7-form-control-wrap').attr('data-name', new_name.replace('[]','')); 648 }); 649 650 $repeater_sub_clone.find('.wpcf7cf_repeater,[data-class="wpcf7cf_group"]').each(function() { 651 const $this = jQuery(this); 652 const prev_data_id = $this.attr('data-id'); 653 const orig_data_id = $this.attr('data-orig_data_id') != null ? $this.attr('data-orig_data_id') : prev_data_id; 654 let new_data_id = thisRepeater.getNewName(prev_data_id); 655 656 if(prev_data_id.endsWith('_count')) { 657 new_data_id = prev_data_id.replace('_count','__{{repeater_sub_suffix}}_count'); 658 } 659 660 $this.attr('data-id', new_data_id); 661 $this.attr('data-orig_data_id', orig_data_id); 662 }); 663 664 $repeater_sub_clone.find('[id]').each(function() { 665 const $this = jQuery(this); 666 const prev_id = $this.attr('id'); 667 const orig_id = $this.attr('data-orig_id') != null ? $this.attr('data-orig_id') : prev_id; 668 const new_id = thisRepeater.getNewName(prev_id); 669 670 $this.attr('id', new_id); 671 $this.attr('data-orig_id', orig_id); 672 }); 673 674 $repeater_sub_clone.find('[for]').each(function() { 675 const $this = jQuery(this); 676 const prev_for = $this.attr('for'); 677 const orig_for = $this.attr('data-orig_for') != null ? $this.attr('data-orig_for') : prev_for; 678 const new_for = thisRepeater.getNewName(prev_for); 679 680 $this.attr('for', new_for); 681 $this.attr('data-orig_for', orig_for); 682 }); 683 684 const repeater_sub_html = $repeater_sub_clone[0].outerHTML; 685 686 const $repeater_count_field = $repeater.find('[name='+$repeater.id+'_count]').eq(0); 687 const $button_add = $repeater_controls.find('.wpcf7cf_add').eq(0); 688 const $button_remove = $repeater_controls.find('.wpcf7cf_remove').eq(0); 689 690 const params = { 691 $repeater: $repeater, 692 $repeater_count_field: $repeater_count_field, 693 repeater_sub_html: repeater_sub_html, 694 $repeater_controls: $repeater_controls, 695 $button_add: $button_add, 696 $button_remove: $button_remove, 697 wpcf7cf_settings: wpcf7cf_settings 698 }; 699 700 thisRepeater.params = params; 701 702 $button_add.on('click', null, thisRepeater, function(e) { 703 thisRepeater = e.data; 704 thisRepeater.updateSubs(params.$repeater.num_subs+1); 705 }); 706 707 $button_remove.on('click', null, thisRepeater,function(e) { 708 thisRepeater = e.data; 709 thisRepeater.updateSubs(params.$repeater.num_subs-1); 710 }); 711 712 // Per-sub Remove buttons (delegated, only top-level subs of this repeater). 713 if ($repeater.show_sub_remove) { 714 $repeater.on('click.wpcf7cf_sub_remove', '> .wpcf7cf_repeater_sub > .wpcf7cf_repeater_sub_controls > .wpcf7cf_sub_remove', function(e) { 715 e.preventDefault(); 716 const idx = $(this).closest('.wpcf7cf_repeater_sub').index(); 717 thisRepeater.removeSubs(1, idx); 718 }); 719 } 720 721 jQuery('> .wpcf7cf_repeater_sub',params.$repeater).eq(0).remove(); // remove the first sub, it's just a template. 722 723 thisRepeater.updateSubs($repeater.initial_subs); 724 thisRepeater.updateButtons(); 725 726} 727 728Wpcf7cfRepeater.prototype.getNewName = function(previousName) { 729 const prev_parts = previousName.split('['); 730 previousName = prev_parts[0]; 731 const prev_suff = prev_parts.length > 1 ? '['+prev_parts.splice(1).join('[') : ''; 732 let newName = previousName+'__{{repeater_sub_suffix}}'+prev_suff; 733 734 if(previousName.endsWith('_count')) { 735 newName = previousName.replace('_count','__{{repeater_sub_suffix}}_count'); 736 } 737 738 return newName; 739} 740 741Wpcf7cfRepeater.prototype.updateButtons = function() { 742 const repeater = this; 743 const params = repeater.params; 744 const num_subs = params.$repeater.num_subs; 745 746 let showButtonRemove = false;
747 let showButtonAdd = false; 748 749 if (params.$repeater.num_subs < params.$repeater.max) { 750 showButtonAdd = true; 751 } 752 if (params.$repeater.num_subs > params.$repeater.min) { 753 showButtonRemove = true; 754 } 755 756 // In 'per_entry' mode, the per-entry remove buttons replace the global Remove button. 757 if (params.$repeater.remove_button === 'per_entry') { 758 showButtonRemove = false; 759 } 760 761 if (showButtonAdd) { 762 params.$button_add.show(); 763 } else { 764 params.$button_add.hide(); 765 766 } 767 768 if (showButtonRemove) { 769 params.$button_remove.show(); 770 } else { 771 params.$button_remove.hide(); 772 } 773 774 // Per-sub controls visibility 775 if (params.$repeater.show_sub_remove) { 776 const $deletes = jQuery('> .wpcf7cf_repeater_sub > .wpcf7cf_repeater_sub_controls > .wpcf7cf_sub_remove', params.$repeater); 777 if (params.$repeater.num_subs > params.$repeater.min) { 778 $deletes.show(); 779 } else { 780 $deletes.hide(); 781 } 782 } 783 784 params.$repeater_count_field.val(num_subs); 785} 786 787Wpcf7cfRepeater.prototype.updateSubs = function(subs_to_show) { 788 const repeater = this; 789 const params = repeater.params; 790 791 // make sure subs_to_show is a valid number 792 subs_to_show = subs_to_show < params.$repeater.min ? params.$repeater.min : subs_to_show 793 subs_to_show = subs_to_show > params.$repeater.max ? params.$repeater.max : subs_to_show 794 795 const subs_to_add = subs_to_show - params.$repeater.num_subs; 796 797 if (subs_to_add < 0) { 798 repeater.removeSubs(-subs_to_add); 799 } else if (subs_to_add > 0) { 800 repeater.addSubs(subs_to_add); 801 } 802}; 803/** 804 * add Subs to repeater 805 * @param {Number} subs_to_add 806 * @param {Number} index - zero-based. leave blank (or null) to append at the end 807 */ 808Wpcf7cfRepeater.prototype.addSubs = function(subs_to_add, index=null) { 809 810 const $ = jQuery; 811 const params = this.params; 812 const repeater = this; 813 const form = repeater.form; 814 815 const $repeater = params.$repeater; 816 const $repeater_controls = params.$repeater_controls; 817 818 if (subs_to_add + $repeater.num_subs > $repeater.max) { 819 subs_to_add = $repeater.max - $repeater.num_subs; 820 } 821 822 let html_str = ''; 823 824 for(let i=1; i<=subs_to_add; i++) { 825 const sub_suffix = $repeater.num_subs+i; 826 html_str += params.repeater_sub_html.replace(/\{\{repeater_sub_suffix\}\}
826/g,sub_suffix) 827 .replace(new RegExp('\{\{'+$repeater.orig_id+'_index\}\}','g'),'<span class="wpcf7cf-index wpcf7cf__'+$repeater.orig_id+'">'+sub_suffix+'</span>'); 828 } 829 830 831 const $html = $(html_str); 832 833 $('> .wpcf7cf_repeater_sub',$repeater).finish(); // finish any currently running animations immediately. 834 835 // Add the newly created fields to the form 836 if (index === null) { 837 $html.hide().insertBefore($repeater_controls).animate(wpcf7cf_show_animation, params.wpcf7cf_settings.animation_intime).trigger('wpcf7cf_repeater_added'); 838 } else { 839 $html.hide().insertBefore($('> .wpcf7cf_repeater_sub', $repeater).eq(index)).animate(wpcf7cf_show_animation, params.wpcf7cf_settings.animation_intime).trigger('wpcf7cf_repeater_added'); 840 } 841 842 // enable all new fields 843 $html.find('.wpcf7cf-disabled :input').prop('disabled', false).trigger('changedisabledprop.wpcf7cf'); 844 $html.find('.wpcf7-form-control-wrap').removeClass('wpcf7cf-disabled'); 845 846 $('.wpcf7cf_repeater', $html).each(function(){ 847 form.repeaters.push(new Wpcf7cfRepeater($(this),form)); 848 }); 849 850 form.$input_repeaters.val(JSON.stringify(form.repeaters.map((item)=>item.params.$repeater.id))); 851 852 $repeater.num_subs+= subs_to_add; 853 854 if (index !== null) { 855 repeater.updateSuffixes(); 856 } 857 858 window.wpcf7cf.updateMultistepState(form.multistep); 859 form.updateGroups(); 860 form.updateEventListeners(); 861 form.displayFields(); 862 863 repeater.updateButtons(); 864 865 // Exclusive Checkbox 866 $html.on( 'click', '.wpcf7-exclusive-checkbox input:checkbox', function() { 867 const name = $( this ).attr( 'name' ); 868 $html.find( 'input:checkbox[name="' + name + '"]' ).not( this ).prop( 'checked', false ); 869 } ); 870 871 //basic compatibility with material-design-for-contact-form-7 872 if (typeof window.cf7mdInit === "function") { 873 window.cf7mdInit(); 874 } 875 876 return false; 877}; 878 879Wpcf7cfRepeater.prototype.updateSuffixes = function() { 880 881 // Loop trough all subs 882 // -- 1. update all fields, groups and repeaters names, id's, for's, ... 883 // -- 2. loop trough all repeaters 884 // -- update sub_html template for nested repeater 885 // -- call updateSuffixes() for nested repeater 886 887 const $repeater = this.params.$repeater; 888 const num_subs = this.params.$repeater.num_subs; 889 const form = this.form; 890 const orig_id = $repeater.attr('data-orig_data_id'); 891 const repeater_id = $repeater.attr('data-id'); 892 const repeater_suffix = repeater_id.replace(orig_id,''); 893 894 let simplifiedDomArray = Object.values(wpcf7cf.get_simplified_dom_model(form.formElement)); 895 896 for (let i = 0; i < num_subs; i++) { 897 898 const $sub = jQuery('> .wpcf7cf_repeater_sub', $repeater).eq(i); 899 900 const newIndex = i+1; 901 const currentSuffix = $sub.attr('data-repeater_sub_suffix'); 902 const newSuffix = repeater_suffix+'__'+newIndex; 903 904 $sub.attr('data-repeater_sub_suffix', newSuffix); // update sub attr 905 $sub.find('.wpcf7cf__'+orig_id).html(newIndex); // update {{r_index}} parts 906 907 simplifiedDomArray.forEach(function(el) { 908 909 if (el.suffix !== currentSuffix) return; 910 911 // TODO: may need an extra check to verify that the element is inside the current repeater 912 // (orig_id) . Otherwise problems may occur if there are repeaters on the same level. 913 914 const newName = el.name.replace(currentSuffix, newSuffix); 915 916 const pureElName = el.name.replace('[]',''); 917 const pureNewName = newName.replace('[]',''); 918 919 jQuery('[name="'+el.name+'"]', $sub).attr('name', newName); 920 jQuery('[id="'+el.name+'"]', $sub).attr('id', newName); 921 jQuery('label[for="'+el.name+'"]', $sub).attr('for', newName); 922 const $nested_repeater = jQuery('[data-id="'+el.name+'"]', $sub); 923 $nested_repeater.attr('data-id', newName); 924 jQuery(`.wpcf7-form-control-wrap[data-name="${pureElName}"]`,$sub).attr('data-name', pureNewName); 925 926 if (el.type === 'repeater') { 927 const nested_repeater = form.repeaters.find( function(repeater) { 928 return repeater.params.$repeater.get(0) === $nested_repeater.get(0); 929 }); 930 931 if (!nested_repeater) return; 932 933 nested_repeater.params.repeater_sub_html = wpcf7cf.updateRepeaterSubHTML( 934 nested_repeater.params.repeater_sub_html, 935 currentSuffix, 936 newSuffix, 937 nested_repeater.params.$repeater.parentRepeaters 938 ); 939 940 nested_repeater.updateSuffixes(); 941 942 } 943 944 }); 945 } 946 947}; 948 949/** 950 * Return the parent repeaters, order is not guaranteed. 951 */ 952Wpcf7cfRepeater.prototype.getParentRepeaters = function() { 953 const simplifiedDomArray = Object.values(wpcf7cf.get_simplified_dom_model(form.formElement)); 954 form.repeaters.map(repeater => { 955 956 }); 957}; 958 959Wpcf7cfRepeater.prototype.removeSubs = function(subs_to_remove, index=null) { 960 const $ = jQuery; 961 const repeater = this; 962 const params = repeater.params; 963 const form = repeater.form; 964 const $repeater = params.$repeater; 965 966 if ($repeater.num_subs - subs_to_remove < $repeater.min) { 967 subs_to_remove = $repeater.num_subs - $repeater.min; 968 } 969 970 if (index===null) { 971 index = $repeater.num_subs-subs_to_remove; 972 } 973 $repeater.num_subs-= subs_to_remove; 974 975 jQuery('> .wpcf7cf_repeater_sub',$repeater).finish(); // finish any currently running animations immediately. 976 977 jQuery('> .wpcf7cf_repeater_sub',$repeater).slice(index,index+subs_to_remove).animate(wpcf7cf_hide_animation, {duration:params.wpcf7cf_settings.animation_intime, done:function() { 978 const $this = jQuery(this); 979 //remove the actual fields from the form 980 $this.remove(); 981 params.$repeater.trigger('wpcf7cf_repeater_removed'); 982 window.wpcf7cf.updateMultistepState(form.multistep); 983 form.updateGroups(); 984 form.updateEventListeners(); 985 form.displayFields(); 986 987 repeater.updateButtons(); 988 989 if (index !== null) { 990 repeater.updateSuffixes(); 991 } 992 }}); 993 994 return false;
995}; 996 997function Wpcf7cfMultistep($multistep, form) { 998 const multistep = this; 999 multistep.$multistep = $multistep; 1000 multistep.form = form; 1001 multistep.$steps = $multistep.find('.wpcf7cf_step'); 1002 multistep.$btn_next = $multistep.find('.wpcf7cf_next'); 1003 multistep.$btn_prev = $multistep.find('.wpcf7cf_prev'); 1004 multistep.$dots = $multistep.find('.wpcf7cf_steps-dots'); 1005 multistep.currentStep = 0; 1006 multistep.numSteps = multistep.$steps.length; 1007 1008 1009 multistep.$dots.html(''); 1010 for (let i = 1; i <= multistep.numSteps; i++) { 1011 multistep.$dots.append(` 1012 <div class="dot" data-step="${i}"> 1013 <div class="step-index">${i}</div> 1014 <div class="step-title">${multistep.$steps.eq(i-1).attr('data-title')}</div> 1015 </div> 1016 `); 1017 } 1018 1019 multistep.$btn_next.on('click.wpcf7cf_step', async function() { 1020 1021 multistep.$btn_next.addClass('disabled').attr('disabled', true); 1022 multistep.form.$form.addClass('submitting'); 1023 const result = await multistep.validateStep(multistep.currentStep); 1024 multistep.form.$form.removeClass('submitting'); 1025 1026 if (result === 'success') { 1027 multistep.moveToStep(multistep.currentStep+1); 1028 } 1029 1030 }); 1031 1032 // If form is submitted (by pressing Enter for example), and if we are not on the last step, 1033 // then trigger click event on the $btn_next button instead. 1034 multistep.form.$form.on('submit.wpcf7cf_step', function(e) { 1035 1036 if (multistep.form.$form.hasClass('submitting')) { // prevent double submit 1037 e.stopImmediatePropagation(); 1038 return false; 1039 } 1040 1041 if (multistep.currentStep !== multistep.numSteps) { 1042 multistep.$btn_next.trigger('click.wpcf7cf_step'); 1043 1044 e.stopImmediatePropagation(); 1045 return false; 1046 } 1047 }); 1048 1049 multistep.$btn_prev.on( 'click', function() { 1050 multistep.moveToStep(multistep.currentStep-1); 1051 }); 1052 1053 multistep.moveToStep(1); 1054} 1055 1056Wpcf7cfMultistep.prototype.validateStep = function(step_index) { 1057 1058 const multistep = this; 1059 const $multistep = multistep.$multistep; 1060 const $form = multistep.form.$form; 1061 const form = multistep.form; 1062 1063 $form.find('.wpcf7-response-output').addClass('wpcf7-display-none'); 1064 1065 return new Promise(resolve => { 1066 1067 const fd = new FormData(); 1068 1069 // Make sure to add file fields to FormData 1070 jQuery.each($form.find('[data-id="step-'+step_index+'"] input[type="file"]'), function(index, el) { 1071 if (! el.files.length) return true; // = continue 1072 const file = el.files[0]; 1073 const fieldName = el.name; 1074 fd.append(fieldName, file); 1075 }); 1076 1077 const formdata = $form.serializeArray(); 1078 jQuery.each(formdata,function(key, input){ 1079 fd.append(input.name, input.value); 1080 }); 1081 1082 jQuery.ajax({ 1083 url: wpcf7cf_global_settings.ajaxurl + '?action=wpcf7cf_validate_step', 1084 type: 'POST', 1085 data: fd, 1086 processData: false, 1087 contentType: false, 1088 dataType: 'json', 1089 }).done(function(json) { 1090 1091 $multistep.find('.wpcf7-form-control-wrap .wpcf7-not-valid-tip').remove(); 1092 $multistep.find('.wpcf7-not-valid').removeClass('wpcf7-not-valid'); 1093 $multistep.find('.wpcf7-response-output.wpcf7-validation-errors').removeClass('wpcf7-validation-errors'); 1094 1095 multistep.$btn_next.removeClass('disabled').attr('disabled', false); 1096 1097 if (!json.success) { 1098 let checkError = 0; 1099 1100 jQuery.each(json.invalid_fields, function(index, el) { 1101 if ($multistep.find('input[name="'+index+'"]').length || 1102 $multistep.find('input[name="'+index+'[]"]').length || 1103 $multistep.find('select[name="'+index+'"]').length || 1104 $multistep.find('select[name="'+index+'[]"]').length || 1105 $multistep.find('textarea[name="'+index+'"]').length || 1106 $multistep.find('textarea[name="'+index+'[]"]').length 1107 ) { 1108 checkError = checkError + 1; 1109 1110 const controlWrap = form.get(`.wpcf7-form-control-wrap[data-name="${index}"]`); 1111 controlWrap.find('.wpcf7-form-control').addClass('wpcf7-not-valid'); 1112 controlWrap.find('span.wpcf7-not-valid-tip').remove(); 1113 controlWrap.append('<span role="alert" class="wpcf7-not-valid-tip">' + el.reason + '</span>'); 1114 1115 } 1116 }); 1117 1118 resolve('failed'); 1119 1120 $multistep.parent().find('.wpcf7-response-output').removeClass('wpcf7-display-none').html(json.message); 1121 1122 wpcf7.setStatus( $form, 'invalid' );
1123 multistep.$steps.trigger('wpcf7cf_step_invalid'); 1124 1125 // wpcf7.triggerEvent( data.into, 'invalid', detail ); 1126 1127 } else if (json.success) { 1128 1129 wpcf7.setStatus( $form, 'init' ); 1130 1131 resolve('success'); 1132 return false; 1133 } 1134 1135 }).fail(function() { 1136 resolve('error'); 1137 }).always(function() { 1138 // do nothing 1139 }); 1140 }); 1141 1142}; 1143Wpcf7cfMultistep.prototype.moveToStep = function(step_index, scrollToTop = true) { 1144 const multistep = this; 1145 const previousStep = multistep.currentStep; 1146 1147 multistep.currentStep = step_index > multistep.numSteps ? multistep.numSteps 1148 : step_index < 1 ? 1 1149 : step_index; 1150 1151 // ANIMATION DISABLED FOR NOW cause it's ugly 1152 // multistep.$steps.animate(wpcf7cf_hide_step_animation, multistep.form.settings.animation_outtime); 1153 // multistep.$steps.eq(multistep.currentStep-1).animate(wpcf7cf_show_step_animation, multistep.form.settings.animation_intime); 1154 1155 multistep.$multistep.attr('data-current_step', multistep.currentStep); 1156 multistep.$steps.hide(); 1157 multistep.$steps 1158 .eq(multistep.currentStep-1) 1159 .show() 1160 .trigger('wpcf7cf_change_step', [previousStep, multistep.currentStep]); 1161 1162 if (scrollToTop) { 1163 const formEl = multistep.form.formElement; 1164 const topOffset = formEl.getBoundingClientRect().top; 1165 if (topOffset < 0 && previousStep > 0) { 1166 formEl.scrollIntoView({behavior: "smooth"}); 1167 } 1168 } 1169 1170 multistep.form.updateSummaryFields(); 1171 1172 window.wpcf7cf.updateMultistepState(multistep); 1173}; 1174 1175Wpcf7cfMultistep.prototype.getFieldsInStep = function(step_index) { 1176 this.form.reloadSimpleDom(); 1177 let inStep = false; 1178 return Object.values(this.form.simpleDom).filter(function(item, i) { 1179 if(item.type == 'step') { 1180 inStep = item.val == step_index+''; 1181 } 1182 return inStep && item.type == 'input'; 1183 }).map(function(item) { 1184 return item.name; 1185 }); 1186}; 1187 1188// END PRO ONLY 1189 1190// matches an ISO date (YYYY-MM-DD) as submitted by CF7's [date] field 1191const wpcf7cf_date_regex = /^\d{4}-\d{2}-\d{2}$/; 1192function wpcf7cf_isDateString(v) { 1193 return typeof v === 'string' && wpcf7cf_date_regex.test(v) && !isNaN(Date.parse(v)); 1194} 1195 1196/** 1197 * @global 1198 * @namespace wpcf7cf 1199 */ 1200window.wpcf7cf = { 1201 1202 hideGroup : function($group, animate) { 1203 1204 }, 1205 1206 showGroup : function($group, animate) { 1207 1208 }, 1209 1210 updateRepeaterSubHTML : function(html, oldSuffix, newSuffix, parentRepeaters) { 1211 const oldIndexes = oldSuffix.split('__'); 1212 oldIndexes.shift(); // remove first empty element 1213 const newIndexes = newSuffix.split('__'); 1214 newIndexes.shift(); // remove first empty element 1215 1216 let returnHtml = html; 1217 1218 if ( 1219 oldIndexes && newIndexes && 1220 oldIndexes.length === parentRepeaters.length && 1221 newIndexes.length === parentRepeaters.length 1222 ) { 1223 1224 const parentRepeatersInfo = parentRepeaters.map((repeaterId, i) => { 1225 return {[repeaterId.split('__')[0]]: [oldIndexes[i], newIndexes[i]]}; 1226 }); 1227 1228 const length = parentRepeatersInfo.length; 1229 1230 let replacements = oldIndexes.map( (oldIndex, i) => { 1231 return [ 1232 '__'+oldIndexes.slice(0,length-i).join('__'), 1233 '__'+newIndexes.slice(0,length-i).join('__'), 1234 ]; 1235 }); 1236 1237 1238 for (let i=0; i<length ; i++) { 1239 const id = Object.keys(parentRepeatersInfo[i])[0]; 1240 const find = parentRepeatersInfo[i][id][0]; 1241 const repl = parentRepeatersInfo[i][id][1]; 1242 replacements.push([ 1243 `<span class="wpcf7cf-index wpcf7cf__${id}">${find}<\\/span>`, 1244 `<span class="wpcf7cf-index wpcf7cf__${id}">${repl}</span>` 1245 ]); 1246 } 1247 1248 replacements.forEach( ([oldSuffix, newSuffix]) => { 1249 returnHtml = returnHtml.replace(new RegExp(oldSuffix,'g'), newSuffix); 1250 }); 1251 1252 } 1253 1254 return returnHtml ; 1255 }, 1256 1257 // keep this for backwards compatibility 1258 initForm : function($forms) { 1259 $forms.each(function(){ 1260 const formElement = this; 1261 // only add form is its class is "wpcf7-form" and if the form was not previously added 1262 if ( 1263 formElement.classList.contains('wpcf7-form') && 1264 !wpcf7cf_forms.some((form)=>{ return form.$form.get(0) === formElement; }) 1265 ) { 1266 wpcf7cf_forms.push(new Wpcf7cfForm(formElement)); 1267 } 1268 }); 1269 }, 1270 1271 getWpcf7cfForm : function ($form) { 1272 const matched_forms = wpcf7cf_forms.filter((form)=>{ 1273 return form.$form.get(0) === $form.get(0); 1274 }); 1275 if (matched_forms.length) { 1276 return matched_forms[0]; 1277 } 1278 return false;
1279 }, 1280 1281 get_nested_conditions : function(form) { 1282 const conditions = form.initial_conditions; 1283 //loop trough conditions. Then loop trough the dom, and each repeater we pass we should update all sub_values we encounter with __index 1284 form.reloadSimpleDom(); 1285 const groups = Object.values(form.simpleDom).filter(function(item, i) { 1286 return item.type==='group'; 1287 }); 1288 1289 let sub_conditions = []; 1290 1291 for(let i = 0; i < groups.length; i++) { 1292 const g = groups[i]; 1293 let relevant_conditions = conditions.filter(function(condition, i) { 1294 return condition.then_field === g.original_name; 1295 }); 1296 1297 relevant_conditions = relevant_conditions.map(function(item,i) { 1298 return { 1299 then_field : g.name, 1300 and_rules : item.and_rules.map(function(and_rule, i) { 1301 return { 1302 if_field : and_rule.if_field+g.suffix, 1303 if_value : and_rule.if_value, 1304 operator : and_rule.operator 1305 }; 1306 }) 1307 } 1308 }); 1309 1310 sub_conditions = sub_conditions.concat(relevant_conditions); 1311 } 1312 return sub_conditions; 1313 }, 1314 1315 get_simplified_dom_model : function(currentNode, simplified_dom = {}, parentGroups = [], parentRepeaters = []) { 1316 1317 1318 const type = currentNode.classList && currentNode.classList.contains('wpcf7cf_repeater') ? 'repeater' : 1319 currentNode.dataset.class == 'wpcf7cf_group' ? 'group' : 1320 currentNode.className == 'wpcf7cf_step' ? 'step' : 1321 currentNode.hasAttribute('name') ? 'input' : false; 1322 1323 let newParentRepeaters = [...parentRepeaters]; 1324 let newParentGroups = [...parentGroups]; 1325 1326 if (type) { 1327 1328 const name = type === 'input' ? currentNode.getAttribute('name') : currentNode.dataset.id; 1329 1330 if (type === 'repeater') { 1331 newParentRepeaters.push(name); 1332 } 1333 if (type === 'group') { 1334 newParentGroups.push(name); 1335 } 1336 1337 // skip _wpcf7 hidden fields 1338 if (name.substring(0,6) === '_wpcf7') return {}; 1339 1340 const original_name = type === 'repeater' || type === 'group' ? currentNode.dataset.orig_data_id 1341 : type === 'input' ? (currentNode.getAttribute('data-orig_name') || name) 1342 : name; 1343 1344 const nameWithoutBrackets = name.replace('[]',''); 1345 const originalNameWithoutBrackets = original_name.replace('[]',''); 1346 1347 const val = type === 'step' ? [currentNode.dataset.id.substring(5)] : []; 1348 1349 const suffix = nameWithoutBrackets.replace(originalNameWithoutBrackets, ''); 1350 1351 if (!simplified_dom[name]) { 1352 // init entry 1353 simplified_dom[name] = {name, type, original_name, suffix, val, parentGroups, parentRepeaters} 1354 } 1355 1356 if (type === 'input') { 1357 1358 // skip unchecked checkboxes and radiobuttons 1359 if ( (currentNode.type === 'checkbox' || currentNode.type === 'radio') && !currentNode.checked ) return {}; 1360 1361 // if multiselect, make sure to add all the values 1362 if ( currentNode.multiple && currentNode.options ) { 1363 simplified_dom[name].val = Object.values(currentNode.options).filter(o => o.selected).map(o => o.value) 1364 } else { 1365 simplified_dom[name].val.push(currentNode.value); 1366 } 1367 } 1368 } 1369 1370 // can't use currentNode.children (because then field name cannot be "children") 1371 const getter = Object.getOwnPropertyDescriptor(Element.prototype, "children").get; 1372 const children = getter.call(currentNode); 1373 1374 Array.from(children).forEach(childNode => { 1375 const dom = wpcf7cf.get_simplified_dom_model(childNode, simplified_dom, newParentGroups, newParentRepeaters); 1376 simplified_dom = {...dom, ...simplified_dom} ; 1377 }); 1378 1379 return simplified_dom; 1380 }, 1381 1382 updateMultistepState: function (multistep) { 1383 if (multistep == null) return; 1384 if (multistep.form.$form.hasClass('submitting')) return; 1385 1386 // update hidden input field 1387 1388 const stepsData = { 1389 currentStep : multistep.currentStep, 1390 numSteps : multistep.numSteps, 1391 fieldsInCurrentStep : multistep.getFieldsInStep(multistep.currentStep) 1392 }; 1393 multistep.form.$input_steps.val(JSON.stringify(stepsData)); 1394 1395 // update Buttons 1396 multistep.$btn_prev.removeClass('disabled').attr('disabled', false); 1397 multistep.$btn_next.removeClass('disabled').attr('disabled', false); 1398 if (multistep.currentStep == multistep.numSteps) { 1399 multistep.$btn_next.addClass('disabled').attr('disabled', true); 1400 } 1401 if (multistep.currentStep == 1) { 1402 multistep.$btn_prev.addClass('disabled').attr('disabled', true); 1403 } 1404 1405 const $current_step = multistep.$steps.eq(multistep.currentStep - 1); 1406 const prev_text = $current_step.attr('data-prev-button-text'); 1407 const next_text = $current_step.attr('data-next-button-text'); 1408 if (prev_text !== undefined) multistep.$btn_prev.text(prev_text); 1409 if (next_text !== undefined) multistep.$btn_next.text(next_text); 1410 1411 // replace next button with submit button on last step.
1412 // TODO: make this depend on a setting 1413 const $submit_button = multistep.form.$form.find('input[type="submit"]:last').eq(0); 1414 const $ajax_loader = multistep.form.$form.find('.wpcf7-spinner').eq(0); 1415 1416 $submit_button.detach().prependTo(multistep.$btn_next.parent()); 1417 $ajax_loader.detach().prependTo(multistep.$btn_next.parent()); 1418 1419 if (multistep.currentStep == multistep.numSteps) { 1420 multistep.$btn_next.hide(); 1421 $submit_button.show(); 1422 } else { 1423 $submit_button.hide(); 1424 multistep.$btn_next.show(); 1425 } 1426 1427 // update dots 1428 const $dots = multistep.$dots.find('.dot'); 1429 $dots.removeClass('active').removeClass('completed'); 1430 for(let step = 1; step <= multistep.numSteps; step++) { 1431 if (step < multistep.currentStep) { 1432 $dots.eq(step-1).addClass('completed'); 1433 } else if (step == multistep.currentStep) { 1434 $dots.eq(step-1).addClass('active'); 1435 } 1436 } 1437 1438 }, 1439 1440 should_group_be_shown : function(condition, form) { 1441 1442 let show_group = true; 1443 let atLeastOneFieldFound = false; 1444 1445 for (let and_rule_i = 0; and_rule_i < condition.and_rules.length; and_rule_i++) { 1446 1447 let condition_ok = false; 1448 1449 const condition_and_rule = condition.and_rules[and_rule_i]; 1450 1451 const inputField = form.getFieldByName(condition_and_rule.if_field); 1452 1453 if (!inputField) continue; // field not found 1454 1455 atLeastOneFieldFound = true; 1456 1457 const if_val = condition_and_rule.if_value; 1458 let operator = condition_and_rule.operator; 1459 1460 //backwards compat 1461 operator = operator === 'â¤' ? 'less than or equals' : operator; 1462 operator = operator === 'â¥' ? 'greater than or equals' : operator; 1463 operator = operator === '>' ? 'greater than' : operator; 1464 operator = operator === '<' ? 'less than' : operator; 1465 1466 const $field = operator === 'function' && jQuery(`[name="${inputField.name}"]`).eq(0); 1467 1468 condition_ok = this.isConditionTrue(inputField.val,operator,if_val, $field); 1469 1470 show_group = show_group && condition_ok; 1471 } 1472 1473 return show_group && atLeastOneFieldFound; 1474 1475 }, 1476 1477 isConditionTrue(values, operator, testValue='', $field=jQuery()) { 1478 1479 if (!Array.isArray(values)) { 1480 values = [values]; 1481 } 1482 1483 let condition_ok = false; // start by assuming that the condition is not met 1484 1485 // Considered EMPTY: [] [''] [null] ['',null] [,,''] 1486 // Considered NOT EMPTY: [0] ['ab','c'] ['',0,null] 1487 const valuesAreEmpty = values.length === 0 || values.every((v) => !v&&v!==0); // 0 is not considered empty 1488 1489 // special cases: [] equals '' => TRUE; [] not equals '' => FALSE 1490 if (operator === 'equals' && testValue === '' && valuesAreEmpty) { 1491 return true; 1492 } 1493 if (operator === 'not equals' && testValue === '' && valuesAreEmpty) { 1494 return false; 1495 } 1496 1497 if (valuesAreEmpty) { 1498 if (operator === 'is empty') { 1499 condition_ok = true; 1500 } 1501 } else { 1502 if (operator === 'not empty') { 1503 condition_ok = true; 1504 } 1505 } 1506 1507 const testValueNumber = isFinite(parseFloat(testValue)) ? parseFloat(testValue) : NaN; 1508 const testValueIsDate = wpcf7cf_isDateString(testValue); 1509 1510 1511 if (operator === 'not equals' || operator === 'not equals (regex)') { 1512 // start by assuming that the condition is met 1513 condition_ok = true; 1514 } 1515 1516 if ( 1517 operator === 'function' 1518 && typeof window[testValue] == 'function' 1519 && window[testValue]($field) // here we call the actual user defined function 1520 ) { 1521 condition_ok = true; 1522 } 1523 1524 let regex_patt = /.*/i; // fallback regex pattern 1525 let isValidRegex = true; 1526 if (operator === 'equals (regex)' || operator === 'not equals (regex)') { 1527 try { 1528 regex_patt = new RegExp(testValue, 'i'); 1529 } catch(e) { 1530 isValidRegex = false;
1531 } 1532 } 1533 1534 1535 for(let i = 0; i < values.length; i++) { 1536 1537 const value = values[i]; 1538 1539 const valueNumber = isFinite(parseFloat(value)) ? parseFloat(value) : NaN; 1540 const valsAreNumbers = !isNaN(valueNumber) && !isNaN(testValueNumber); 1541 1542 // when both sides are ISO dates, compare chronologically instead of by parseFloat (which reads only the year) 1543 const valueIsDate = wpcf7cf_isDateString(value); 1544 const compareAsDates = testValueIsDate && valueIsDate; 1545 const left = compareAsDates ? Date.parse(value) : valueNumber; 1546 const right = compareAsDates ? Date.parse(testValue) : testValueNumber; 1547 // a date on one side and a plain number on the other are not comparable 1548 const comparable = compareAsDates || (valsAreNumbers && testValueIsDate === valueIsDate); 1549 1550 if ( 1551 1552 operator === 'equals' && value === testValue || 1553 operator === 'equals (regex)' && regex_patt.test(value) || 1554 operator === 'greater than' && comparable && left > right || 1555 operator === 'less than' && comparable && left < right || 1556 operator === 'greater than or equals' && comparable && left >= right || 1557 operator === 'less than or equals' && comparable && left <= right 1558 1559 ) { 1560 1561 condition_ok = true; 1562 break; 1563 1564 } else if ( 1565 1566 operator === 'not equals' && value === testValue || 1567 operator === 'not equals (regex)' && regex_patt.test(value) 1568 1569 ) { 1570 1571 condition_ok = false; 1572 break; 1573 1574 } 1575 } 1576 1577 return condition_ok; 1578 1579 }, 1580 1581 getFormObj($form) { 1582 if (typeof $form === 'string') { 1583 $form = jQuery($form).eq(0); 1584 } 1585 return wpcf7cf.getWpcf7cfForm($form); 1586 }, 1587 1588 getRepeaterObj($form, repeaterDataId) { 1589 const form = wpcf7cf.getFormObj($form); 1590 const repeater = form.repeaters.find( repeater => repeater.params.$repeater.attr('data-id') === repeaterDataId ); 1591 1592 return repeater; 1593 1594 }, 1595 1596 getMultiStepObj($form){ 1597 const form = wpcf7cf.getFormObj($form); 1598 return form.multistep; 1599 }, 1600 1601 /** 1602 * Append a new sub-entry to the repeater with the name `repeaterDataId` inside the form `$form` 1603 * @memberof wpcf7cf 1604 * @function wpcf7cf.repeaterAddSub 1605 * @link 1606 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1607 * @param {String} repeaterDataId - *data-id* attribute of the repeater. Normally this is simply the name of the repeater. However, in case of a nested repeater you need to append the name with the correct suffix. For example `my-nested-repeater__1__3`. Hint (check the `data-id` attribute in the HTML code to find the correct suffix) 1608 */ 1609 repeaterAddSub($form,repeaterDataId) { 1610 const repeater = wpcf7cf.getRepeaterObj($form, repeaterDataId); 1611 repeater.updateSubs(repeater.params.$repeater.num_subs+1); 1612 }, 1613 1614 /** 1615 * Insert a new sub-entry at the given `index` of the repeater with the name `repeaterDataId` inside the form `$form` 1616 * @memberof wpcf7cf 1617 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1618 * @param {String} repeaterDataId - *data-id* attribute of the repeater. 1619 * @param {Number} index - position where to insert the new sub-entry within the repeater 1620 */ 1621 repeaterAddSubAtIndex($form,repeaterDataId,index) { 1622 const repeater = wpcf7cf.getRepeaterObj($form, repeaterDataId); 1623 repeater.addSubs(1, index); 1624 }, 1625 1626 /** 1627 * Remove the sub-entry at the given `index` of the repeater with the *data-id* attribute of `repeaterDataId` inside the form `$form` 1628 * @memberof wpcf7cf 1629 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1630 * @param {String} repeaterDataId - *data-id* attribute of the repeater. 1631 * @param {Number} index - position where to insert the new sub-entry within the repeater 1632 */ 1633 repeaterRemoveSubAtIndex($form,repeaterDataId,index) { 1634 const repeater = wpcf7cf.getRepeaterObj($form, repeaterDataId); 1635 repeater.removeSubs(1, index); 1636 }, 1637 1638 /** 1639 * Remove the last sub-entry from the repeater with the *data-id* attribute of `repeaterDataId` inside the form `$form` 1640 * @memberof wpcf7cf 1641 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1642 * @param {String} repeaterDataId - *data-id* attribute of the repeater. 1643 * @param {Number} index - position where to insert the new sub-entry within the repeater 1644 */ 1645 repeaterRemoveSub($form,repeaterDataId) { 1646 const repeater = wpcf7cf.getRepeaterObj($form, repeaterDataId); 1647 repeater.updateSubs(repeater.params.$repeater.num_subs-1); 1648 }, 1649 1650 /** 1651 * Set the number of subs for the repeater with the *data-id* attribute of `repeaterDataId` inside the form `$form`. 1652 * Subs are either appended to or removed from the end of the repeater. 1653 * @memberof wpcf7cf 1654 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1655 * @param {String} repeaterDataId - *data-id* attribute of the repeater. 1656 * @param {Number} numberOfSubs - position where to insert the new sub-entry within the repeater 1657 */ 1658 repeaterSetNumberOfSubs($form, repeaterDataId, numberOfSubs) { 1659 const repeater = wpcf7cf.getRepeaterObj($form, repeaterDataId); 1660 repeater.updateSubs(numberOfSubs); 1661 }, 1662 1663 /** 1664 * Move to step number `step`, ignoring any validation. 1665 * @memberof wpcf7cf 1666 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1667 * @param {*} step 1668 */ 1669 multistepMoveToStep($form, step) { 1670 const multistep = wpcf7cf.getMultiStepObj($form); 1671 multistep.moveToStep(step); 1672 }, 1673 1674 /** 1675 * Validate the current step, and move to step number `step` if val
1675idation passes. 1676 * @memberof wpcf7cf 1677 * @param {String|JQuery} $form - JQuery object or css-selector representing the form 1678 * @param {Number} step 1679 */ 1680 async multistepMoveToStepWithValidation($form, step) { 1681 const multistep = wpcf7cf.getMultiStepObj($form); 1682 1683 const result = await multistep.validateStep(multistep.currentStep); 1684 if (result === 'success') { 1685 multistep.moveToStep(step); 1686 } 1687 }, 1688 1689 // PRO ONLY 1690 1691 /** 1692 * Create a fields helper object for use in user-defined calculation functions. 1693 * Provides get(), raw(), all(), and sum() methods. 1694 * When suffix is provided, get() and raw() auto-resolve to the same repeater sub. 1695 * @memberof wpcf7cf 1696 * @param {Wpcf7cfForm} form - The form object 1697 * @param {String} suffix - Repeater suffix (e.g. '__2' or '__1__3'), empty string if not in repeater 1698 * @returns {Object} The fields helper 1699 */ 1700 calculatedfieldsCreateFieldsHelper(form, suffix) { 1701 suffix = suffix || ''; 1702 1703 function resolveField(name) { 1704 // Try with suffix first (same repeater sub), then without 1705 if (suffix) { 1706 const suffixed = form.getFieldByName(name + suffix) || form.getFieldByName(name + suffix + '[]'); 1707 if (suffixed) return suffixed; 1708 } 1709 return form.getFieldByName(name) || form.getFieldByName(name + '[]'); 1710 } 1711 1712 function getNumericValue(field) { 1713 if (!field || !field.val || !field.val.length) return 0; 1714 1715 // Check for data-calc-value on the DOM element(s) 1716 const calcVal = wpcf7cf.calculatedfieldsGetCalcValue(field.name, form); 1717 if (calcVal !== null) return calcVal; 1718 1719 let sum = 0; 1720 for (let i = 0; i < field.val.length; i++) { 1721 const numericStr = String(field.val[i]).replace(/[^0-9.\-]/g, ''); 1722 const num = parseFloat(numericStr); 1723 if (isFinite(num)) sum += num; 1724 } 1725 return sum; 1726 } 1727 1728 return { 1729 /** 1730 * Get the numeric value of a field. Auto-resolves repeater suffix. 1731 * @param {String} name - The original field name 1732 * @returns {Number} 1733 */ 1734 get(name) { 1735 return getNumericValue(resolveField(name)); 1736 }, 1737 /** 1738 * Get the raw value of a field. Auto-resolves repeater suffix. 1739 * @param {String} name - The original field name 1740 * @returns {String|Array} 1741 */ 1742 raw(name) { 1743 const field = resolveField(name); 1744 if (!field || !field.val) return ''; 1745 return field.val.length === 1 ? field.val[0] : field.val; 1746 }, 1747 /** 1748 * Get all field values as an object { fieldName: numericValue }. 1749 * @returns {Object} 1750 */ 1751 all() { 1752 const result = {}; 1753 if (!form.simpleDom) return result; 1754 for (const key in form.simpleDom) { 1755 const item = form.simpleDom[key]; 1756 if (item.type === 'input') { 1757 result[item.name.replace('[]', '')] = getNumericValue(item); 1758 } 1759 } 1760 return result; 1761 }, 1762 /** 1763 * Sum a field's numeric value across all repeater subs. 1764 * E.g. fields.sum('quantity') sums quantity__1, quantity__2, etc. 1765 * @param {String} name - The original (unsuffixed) field name 1766 * @returns {Number} 1767 */ 1768 sum(name) { 1769 if (!form.simpleDom) return 0; 1770 let total = 0; 1771 for (const key in form.simpleDom) { 1772 const item = form.simpleDom[key]; 1773 if (item.type !== 'input') continue; 1774 const origName = item.original_name ? item.original_name.replace('[]', '') : ''; 1775 if (origName === name || origName === name + '[]') { 1776 // Check for data-calc-value first 1777 const calcVal = wpcf7cf.calculatedfieldsGetCalcValue(item.name, form); 1778 if (calcVal !== null) { 1779 total += calcVal; 1780 } else { 1781 total += getNumericValue(item); 1782 } 1783 } 1784 } 1785 return total; 1786 }, 1787 /** 1788 * Get the repeater suffix for this calculated field's context. 1789 * @returns {String} 1790 */ 1791 suffix() { 1792 return suffix; 1793 } 1794 }; 1795 }, 1796 1797 /** 1798 * Evaluate a formula string by replacing [field-name] references with actual field values. 1799 * Supports: +, -, *, /, parentheses, and nested field references. 1800 * When suffix is provided, field references auto-resolve to the same repeater sub. 1801 * @memberof wpcf7cf 1802 * @param {String} formula - The formula string, e.g. "[quantity] * [price] + 10" 1803 * @param {Wpcf7cfForm} form - The form object 1804 * @param {String} suffix - Repeater suffix (e.g. '__2'), empty string if not in repeater 1805 * @returns {Number} The calculated result 1806 */
1807 calculatedfieldsEvaluateFormula(formula, form, suffix) { 1808 suffix = suffix || ''; 1809 1810 // Helper: sum a field across all repeater subs 1811 function sumAcrossSubs(fieldName) { 1812 if (!form.simpleDom) return 0; 1813 let total = 0; 1814 for (const key in form.simpleDom) { 1815 const item = form.simpleDom[key]; 1816 if (item.type !== 'input') continue; 1817 const origName = item.original_name ? item.original_name.replace('[]', '') : ''; 1818 if (origName === fieldName || origName === fieldName + '[]') { 1819 // Check for data-calc-value first 1820 const calcVal = wpcf7cf.calculatedfieldsGetCalcValue(item.name, form); 1821 if (calcVal !== null) { 1822 total += calcVal; 1823 } else if (item.val && item.val.length) { 1824 for (let i = 0; i < item.val.length; i++) { 1825 const num = parseFloat(String(item.val[i]).replace(/[^0-9.\-]/g, '')); 1826 if (isFinite(num)) total += num; 1827 } 1828 } 1829 } 1830 } 1831 return total; 1832 } 1833 1834 // Replace [sum:field-name] and [field-name] references with their numeric values 1835 const expression = formula.replace(/\[([^\]]+)\]/g, function(match, ref) { 1836 // [sum:field-name] â sum across all repeater subs 1837 if (ref.indexOf('sum:') === 0) { 1838 const fieldName = ref.substring(4); 1839 return String(sumAcrossSubs(fieldName)); 1840 } 1841 1842 // [field-name] â resolve with suffix awareness 1843 let field = null; 1844 if (suffix) { 1845 field = form.getFieldByName(ref + suffix) || form.getFieldByName(ref + suffix + '[]'); 1846 } 1847 if (!field) { 1848 field = form.getFieldByName(ref) || form.getFieldByName(ref + '[]'); 1849 } 1850 if (!field) return '0'; 1851 1852 // Check for data-calc-value first 1853 const calcVal = wpcf7cf.calculatedfieldsGetCalcValue(field.name, form); 1854 if (calcVal !== null) return String(calcVal); 1855 1856 const values = field.val; 1857 if (!values || !values.length) return '0'; 1858 1859 // Sum all values (handles checkboxes, multi-selects) 1860 let sum = 0; 1861 for (let i = 0; i < values.length; i++) { 1862 const numericStr = String(values[i]).replace(/[^0-9.\-]/g, ''); 1863 const num = parseFloat(numericStr); 1864 if (isFinite(num)) { 1865 sum += num; 1866 } 1867 } 1868 return String(sum); 1869 }); 1870 1871 return wpcf7cf.calculatedfieldsSafeMathEval(expression); 1872 }, 1873 1874 /** 1875 * Get the numeric calc value for a field by reading data-calc-value from DOM elements. 1876 * Returns the sum of data-calc-value attributes from selected options or checked inputs. 1877 * Returns null if no data-calc-value attributes are found (field is not a calc_select/radio/checkbox). 1878 * @memberof wpcf7cf 1879 * @param {String} fieldName - The field name (possibly with [] suffix) 1880 * @param {Wpcf7cfForm} form - The form object 1881 * @returns {Number|null} 1882 */ 1883 calculatedfieldsGetCalcValue(fieldName, form) { 1884 const cleanName = fieldName.replace('[]', ''); 1885 const $formEl = form.$form; 1886 1887 // Try select element 1888 const $select = $formEl.find('select[name="' + fieldName + '"], select[name="' + cleanName + '"]'); 1889 if ($select.length) { 1890 const $selected = $select.find('option:selected[data-calc-value]'); 1891 if ($selected.length) { 1892 let sum = 0; 1893 $selected.each(function() { 1894 const num = parseFloat(jQuery(this).attr('data-calc-value')); 1895 if (isFinite(num)) sum += num; 1896 }); 1897 return sum; 1898 } 1899 // Select exists but no data-calc-value â not a calc_select 1900 if ($select.find('option[data-calc-value]').length === 0) return null; 1901 return 0; 1902 } 1903 1904 // Try checkbox/radio inputs
1905 const $inputs = $formEl.find('input[name="' + fieldName + '"][data-calc-value], input[name="' + cleanName + '"][data-calc-value], input[name="' + cleanName + '[]"][data-calc-value]'); 1906 if ($inputs.length) { 1907 let sum = 0; 1908 $inputs.filter(':checked').each(function() { 1909 const num = parseFloat(jQuery(this).attr('data-calc-value')); 1910 if (isFinite(num)) sum += num; 1911 }); 1912 return sum; 1913 } 1914 1915 return null; // not a calc field 1916 }, 1917 1918 /** 1919 * Safely evaluate a mathematical expression string. 1920 * Only allows numbers, +, -, *, /, parentheses, and decimal points. 1921 * Does NOT use eval(). 1922 * @memberof wpcf7cf 1923 * @param {String} expr - The mathematical expression 1924 * @returns {Number} The result, or 0 on error 1925 */ 1926 calculatedfieldsSafeMathEval(expr) { 1927 // Remove whitespace 1928 expr = expr.replace(/\s+/g, ''); 1929 1930 // Validate - only allow safe characters 1931 if (!/^[\d\+\-\*\/\(\)\.]+$/.test(expr)) { 1932 return 0; 1933 } 1934 1935 // Tokenize 1936 const tokens = []; 1937 let num = ''; 1938 for (let i = 0; i < expr.length; i++) { 1939 const c = expr[i]; 1940 if ((c >= '0' && c <= '9') || c === '.') { 1941 num += c; 1942 } else { 1943 if (num !== '') { 1944 tokens.push(parseFloat(num)); 1945 num = ''; 1946 } 1947 // Handle unary minus 1948 if (c === '-' && (i === 0 || expr[i - 1] === '(' || '+-*/'.includes(expr[i - 1]))) { 1949 num = '-'; 1950 } else { 1951 tokens.push(c); 1952 } 1953 } 1954 } 1955 if (num !== '') { 1956 tokens.push(parseFloat(num)); 1957 } 1958 1959 // Shunting-yard algorithm (infix to postfix) 1960 const output = []; 1961 const ops = []; 1962 const prec = {'+': 1, '-': 1, '*': 2, '/': 2}; 1963 1964 for (let i = 0; i < tokens.length; i++) { 1965 const t = tokens[i]; 1966 if (typeof t === 'number') { 1967 output.push(t); 1968 } else if (prec[t] !== undefined) { 1969 while (ops.length && ops[ops.length - 1] !== '(' && (prec[ops[ops.length - 1]] || 0) >= prec[t]) { 1970 output.push(ops.pop()); 1971 } 1972 ops.push(t); 1973 } else if (t === '(') { 1974 ops.push(t); 1975 } else if (t === ')') { 1976 while (ops.length && ops[ops.length - 1] !== '(') { 1977 output.push(ops.pop()); 1978 } 1979 if (ops.length) ops.pop(); // remove '(' 1980 } 1981 } 1982 while (ops.length) { 1983 output.push(ops.pop()); 1984 } 1985 1986 // Evaluate postfix 1987 const stack = []; 1988 for (let i = 0; i < output.length; i++) { 1989 const t = output[i]; 1990 if (typeof t === 'number') { 1991 stack.push(t); 1992 } else { 1993 if (stack.length < 2) return 0; 1994 const b = stack.pop(); 1995 const a = stack.pop(); 1996 switch (t) { 1997 case '+': stack.push(a + b); break; 1998 case '-': stack.push(a - b); break; 1999 case '*': stack.push(a * b); break; 2000 case '/': stack.push(b !== 0 ? a / b : 0); break; 2001 } 2002 } 2003 } 2004 2005 return stack.length ? stack[stack.length - 1] : 0; 2006 }, 2007 2008 // END PRO ONLY 2009 2010}; 2011 2012document.querySelectorAll('.wpcf7-form').forEach(function(formElement){ 2013 wpcf7cf_forms.push(new Wpcf7cfForm(formElement)); 2014}); 2015 2016// Call displayFields again on all forms 2017// Necessary in case some theme or plugin changed a form value by the time the entire page is fully loaded. 2018document.addEventListener('DOMContentLoaded', function () { 2019 wpcf7cf_forms.forEach(function(f){ 2020 f.displayFields(); 2021 }); 2022});
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.