1function frmProFormJS() { 2 /* globals frm_js, frmFrontForm, google, jQuery */ 3 /* globals __frmDatepicker, __frmDropzone, __frmUniqueTimes */ 4 /* globals __FRMLOOKUP, __FRMCALC, __FRMRULES, __FRMCURR */ 5 /* globals __frmChosen, __frmHideOrShowFields, __frmDepDynamicFields */ 6 /* globals __frmDepLookupFields, __frmMasks, __FRMTABLES */ 7 /* globals frmCheckboxI18n */ 8 9 'use strict'; 10 let currentlyAddingRow = false; 11 let action = ''; 12 let processesRunning = 0; 13 const lookupQueues = {}; 14 let hiddenSubmitButtons = []; 15 let pendingDynamicFieldAjax = []; 16 const frmMaskInstances = new Map(); 17 const pendingLookupFieldAjax = []; 18 let listWrappersOriginal = {}; 19 const intlPhoneInputs = {}; 20 let autoId = 0; 21 22 function setNextPage( e ) { 23 var closestButton; 24 if ( this.className.includes('frm_rootline_title') ) { 25 closestButton = this.previousElementSibling; 26 closestButton.click(); 27 return; 28 } 29 30 if ( this.className.includes('frm_rootline_single') ) { 31 this.querySelector( 'input' ).click(); 32 return; 33 } 34 35 /*jshint validthis:true */ 36 var $thisObj = jQuery( this ); 37 var thisType = $thisObj.attr( 'type' ); 38 if ( thisType !== 'submit' ) { 39 e.preventDefault(); 40 } 41 42 var f = $thisObj.parents( 'form' ).first(), 43 v = '', 44 d = '', 45 thisName = this.name; 46 47 if ( thisName === 'frm_prev_page' || this.className.includes('frm_prev_page') ) { 48 v = jQuery( f ).find( '.frm_next_page' ).attr( 'id' ).replace( 'frm_next_p_', '' ); 49 if ( f.length ) { 50 maybeAddEmptyHiddenInputsForCheckboxes( f.get( 0 ) ); 51 } 52 } else if ( thisName === 'frm_save_draft' || this.className.includes('frm_save_draft') ) { 53 d = 1; 54 } else if ( this.className.includes('frm_page_skip') ) { 55 var goingTo = $thisObj.data( 'page' ); 56 var formId = jQuery( f ).find( 'input[name="form_id"]' ).val(); 57 var orderField = jQuery( f ).find( 'input[name="frm_page_order_' + formId + '"]' ); 58 jQuery( f ).append( '<input name="frm_last_page" type="hidden" value="' + orderField.val() + '" />' ); 59 60 if ( goingTo === '' ) { 61 orderField.remove(); 62 } else { 63 orderField.val( goingTo ); 64 } 65 } else if ( this.className.includes('frm_page_back') ) { 66 v = $thisObj.data( 'page' ); 67 } 68 69 if ( 1 === d ) { 70 resetTinyMceOnDraftSave(); 71 } else { 72 resetTinyMceOnPageTurn(); 73 } 74 75 jQuery( '.frm_next_page' ).val( v ); 76 jQuery( '.frm_saving_draft' ).val( d ); 77 78 if ( thisType !== 'submit' ) { 79 f.trigger( 'submit' ); 80 } 81 } 82 83 /** 84 * When no checkboxes are checked, no $_POST data is sent. 85 * This causes issues when going back when drafts are enabled.
86 * The draft gets saved when the next button is pressed, and then 87 * gets reloaded after being unchecked because there is no $_POST data. 88 * This adds a hidden input with no value in its place so $_POST data does get sent. 89 * 90 * @since 6.8.3 91 * 92 * @param {HTMLElement} form 93 * @returns {void} 94 */ 95 function maybeAddEmptyHiddenInputsForCheckboxes( form ) { 96 form.querySelectorAll( '.frm_opt_container' ).forEach( 97 ( optContainer ) => { 98 var checkboxes, hiddenInput; 99 checkboxes = optContainer.querySelectorAll( 'input[type="checkbox"]' ); 100 if ( checkboxes.length && ! jQuery( checkboxes ).filter( ':checked' ).length ) { 101 hiddenInput = document.createElement( 'input' ); 102 hiddenInput.setAttribute( 'type', 'hidden' ); 103 hiddenInput.setAttribute( 'name', checkboxes[ 0 ].getAttribute( 'name' ) ); 104 optContainer.append( hiddenInput ); 105 } 106 } 107 ); 108 } 109 110 function resetTinyMceOnDraftSave() { 111 jQuery( document ).one( 'frmFormComplete', () => { 112 jQuery( '.wp-editor-area' ).each( function() { 113 reInitializeRichText( this.id ); 114 }); 115 }); 116 } 117 118 function resetTinyMceOnPageTurn() { 119 var removeIds = []; 120 jQuery( '.frm_form_field .wp-editor-area' ).each( 121 function() { 122 removeIds.push( this.id ); 123 } 124 ); 125 jQuery( document ).one( 'frmPageChanged', () => { 126 var removeIndex, removeId; 127 // Remove tinymce from RTE fields on page update so they can initialize properly when the page becomes active again 128 for ( removeIndex = 0; removeIndex < removeIds.length; ++removeIndex ) { 129 removeId = removeIds[ removeIndex ]; 130 removeRichText( removeId ); 131 } 132 checkConditionalLogic(); 133 }); 134 } 135 136 function toggleSection( e ) { 137 var $toggleContainer, togglingOn; 138 139 if ( e.key !== undefined ) { 140 if ( e.key !== ' ' && e.key !== 'Enter' ) { 141 return; 142 } 143 } else if ( e.keyCode !== undefined && e.keyCode !== 32 && e.keyCode !== 13 ) { 144 return; 145 } 146 147 e.preventDefault(); 148 149 /*jshint validthis:true */ 150 $toggleContainer = jQuery( this ).parent().children( '.frm_toggle_container' ); 151 togglingOn = 'none' === $toggleContainer.get( 0 ).style.display; 152 153 if ( togglingOn ) { 154 $toggleContainer.show(); 155 } 156 157 triggerEvent( document, 'frmBeforeToggleSection', { 158 toggleButton: this 159 }); 160 161 if ( togglingOn ) { 162 $toggleContainer.hide(); 163 } 164 165 $toggleContainer.slideToggle( 'fast' ); 166 167 if ( togglingOn ) { 168 this.className += ' active'; 169 this.setAttribute( 'aria-expanded', 'true' ); 170 } else { 171 this.className = this.className.replace( ' active', '' ); 172 this.setAttribute( 'aria-expanded', 'false' ); 173 } 174 } 175 176 function loadDateFields() { 177 jQuery( document ).on( 'focusin', '.frm_date', triggerDateField ); 178 document.addEventListener( 'focusout', keepFloatingLabelOnDateBlur, true ); 179 180 loadUniqueTimeFields(); 181 } 182 183 function frmDatepickerPro( input, settings ) { 184 185 if ( input._flatpickr ) { 186 return input._flatpickr; 187 } 188 189 const _this = this; 190 191 // Init the static methods 192 this.initAccessiblity = frmDatepickerPro.initAccessiblity; 193 this.initMonthSelector = frmDatepickerPro.initMonthSelector; 194 this.initYearSelector = frmDatepickerPro.initYearSelector; 195 this.parseFunctionConfig = frmDatepickerPro.parseFunctionConfig; 196 this.getDateRange = frmDatepickerPro.getDateRange; 197 this.callbacks = frmDatepickerPro.callbacks; 198 this.getDefaultConfigs = frmDatepickerPro.getDefaultConfigs; 199 this.isUsingACustomTheme = frmDatepickerPro.isUsingACustomTheme; 200 201 this.getConfigs = function() { 202 if ( 'undefined' === typeof settings.datepickerJsOptions ) { 203 return _this.getDefaultConfigs( settings, input ); 204 } 205 return { ..._this.getDefaultConfigs( settings, input ), ..._this.parseFunctionConfig( settings.datepickerJsOptions ) }; 206 }; 207 return flatpickr( input, this.getConfigs() ); 208 } 209 210 frmDatepickerPro.getThemeType = function() { 211 const themeFile = document.querySelector( 'link[href*="flatpickr/dist/themes/"]' ); 212 const themeSelectors = [ 213 { name: 'frm-dark', selector: 'dark.css' }, 214 { name: 'frm-material-blue', selector: 'material_blue.css' }, 215 { name: 'frm-material-green', selector: 'material_green.css' }, 216 { name: 'frm-material-orange', selector: 'material_orange.css' }, 217 { name: 'frm-material-red', selector: 'material_red.css' }, 218 { name: 'frm-airbnb', selector: 'airbnb.css' }, 219 { name: 'frm-confetti', selector: 'confetti.css' } 220 ]; 221 222 if ( themeFile && themeFile.href ) { 223 for ( const theme of themeSelectors ) { 224 if ( themeFile.href.includes( theme.selector ) ) { 225 return theme.name; 226 } 227 } 228 } 229 230 return ''; 231 }; 232 frmDatepickerPro.themeType = frmDatepickerPro.getThemeType(); 233 frmDatepickerPro.isUsingACustomTheme = frmDatepickerPro.themeType !== ''; 234 frmDatepickerPro.callbacks = {}; 235 236 /** 237 * Get the style classes to copy from the form onto the calendar. 238 * 239 * A popup calendar is appended to the body, so it sits outside of the form and 240 * cannot inherit either the style class or the CSS variables scoped to it. The 241 * jQuery UI datepicker copies the form classes for the same reason, in 242 * addFormidableClassToDatepicker. 243 * 244 * @since 6.35 245 * 246 * @param {HTMLElement} input The date input the calendar belongs to. 247 * @returns {Array} Class names to add to the calendar container. 248 */ 249 frmDatepickerPro.getStyleClasses = function( input ) { 250 const formContainer = input.closest( '.with_frm_style' ); 251 if ( ! formContainer ) { 252 return [ 'with_frm_style' ]; 253 } 254
255 return [ 'with_frm_style' ].concat( 256 Array.prototype.filter.call( 257 formContainer.classList, 258 ( className ) => className.startsWith( 'frm_style_' ) 259 ) 260 ); 261 }; 262 263 /** 264 * Gets the repeater row an element sits in, so a lookup cannot escape into another row. 265 * 266 * Every row renders the same ids and field ids, so a document wide lookup always answers with 267 * the first row and leaves every later row pointing at it. 268 * 269 * @since 6.24 270 * 271 * @param {HTMLElement} element An element inside the row. 272 * @return {Element|Document} The row, or the document when the element is not in a repeater. 273 */ 274 frmDatepickerPro.getRepeaterRow = function( element ) { 275 return element.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document; 276 }; 277 278 /** 279 * Finds the hidden input an inline picker writes its value into. 280 * 281 * The alt field selector is built from the field key alone, so inside a repeater it matches 282 * nothing: every row appends its own index to the id. Falling back to the element's own id 283 * covers that, scoped to the row so one row cannot answer for another. 284 * 285 * @since 6.24 286 * 287 * @param {HTMLElement} input The element the picker is built on. 288 * @param {string} altSelector The alt field selector from the field settings. 289 * @return {HTMLElement|null} The alt input, or null when there is none. 290 */ 291 frmDatepickerPro.getAltInputElement = function( input, altSelector ) { 292 const scope = frmDatepickerPro.getRepeaterRow( input ); 293 294 if ( input.id ) { 295 const rowAltInput = scope.querySelector( '#' + CSS.escape( input.id + '_alt' ) ); 296 if ( rowAltInput ) { 297 return rowAltInput; 298 } 299 } 300 301 return altSelector ? scope.querySelector( altSelector ) : null; 302 }; 303 304 frmDatepickerPro.callbacks.onOpen = function( selectedDates, dateStr, instance ) { 305 instance.frmStyleClasses = frmDatepickerPro.getStyleClasses( instance.input ); 306 instance.calendarContainer.classList.add( 'frm-datepicker', ...instance.frmStyleClasses ); 307 if ( frmDatepickerPro.isUsingACustomTheme ) { 308 instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); 309 } 310 }; 311 frmDatepickerPro.callbacks.onClose = function( selectedDates, dateStr, instance ) { 312 if ( ! instance.config.inline ) { 313 instance.calendarContainer.classList.remove( 'frm-datepicker', ...( instance.frmStyleClasses || [ 'with_frm_style' ] ) ); 314 if ( frmDatepickerPro.isUsingACustomTheme ) { 315 instance.calendarContainer.classList.remove( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); 316 } 317 } 318 319 triggerEvent( instance.input, 'frmFlatpickrClosed', { 320 instance, 321 }); 322 }; 323 324 /** 325 * Get the default configs for the datepicker. 326 * 327 * @since 6.19 328 * 329 * @returns {object} 330 */ 331 frmDatepickerPro.getDefaultConfigs = function( config, input ) { 332 333 this.getSettings = function() { 334 const result = {}; 335 if ( config.formidable_dates || config.options ) { 336 return config; 337 } 338 339 result.formidable_dates = config; 340 result.options = config.datepickerOptions; 341 return result; 342 }; 343 344 this.getDatesDisabled = function( settings ) { 345 const disabledDates = settings?.formidable_dates?.datesDisabled || settings?.options?.datesDisabled; 346 347 if ( ! Array.isArray( disabledDates ) ) { 348 return []; 349 } 350 351 return disabledDates.map( date => new Date( date + 'T00:00:00' ) ); 352 }; 353 354 /** 355 * Gets the input a picker's value belongs in. 356 * 357 * An inline picker is built on a div, so the value it produces has to be written to the 358 * hidden input the field actually submits. That input is the picker's alt input, which is 359 * resolved per repeater row, so prefer it over looking one up by name. 360 * 361 * @since 6.24 362 * 363 * @param {Object} instance The Flatpickr instance. 364 * @return {HTMLElement} The input to write to, or the element the picker was built on. 365 */ 366 this.getInstanceElement = ( instance ) => { 367 if ( ! instance.config.inline || 'INPUT' === instance.element.nodeName ) { 368 return instance.element; 369 } 370 371 if ( instance.config.altInputElement ) { 372 return instance.config.altInputElement; 373 } 374
375 const fieldId = instance.element.dataset.fieldId; 376 377 // A repeater row names its inputs item_meta[section][row][field], so an exact 378 // item_meta[field] match only ever finds a field that is not in a repeater, and a 379 // document wide lookup would answer with the first row for every row. 380 const scope = frmDatepickerPro.getRepeaterRow( instance.element ); 381 382 return scope.querySelector( `input[name="item_meta[${ fieldId }]"], input[name$="][${ fieldId }]"]` ) || instance.element; 383 }; 384 385 386 /** 387 * COMPATIBILITY PATH. Do not fix bugs here. 388 * 389 * Date ranges belong to the Dates add-on, which now owns this as 390 * frmDatepickerInstance.syncRangeFields. This copy only runs for a site that has updated 391 * Pro but not yet the add-on, so that its ranges keep working rather than breaking on 392 * upgrade. Treat it as frozen: fix range behaviour in the add-on, and delete this once the 393 * add-on version that owns it has been out long enough. 394 * 395 * @since 6.24 396 * 397 * @param {string} mode The mode of the datepicker. 398 * @param {object} instance The instance of the datepicker. 399 * @param {string} dateStr The date string. 400 * @param {array} selectedDates The selected dates. 401 * @returns {void} 402 */ 403 this.updateRangeFieldsOnChange = ( mode, instance, dateStr, selectedDates ) => { 404 if ( 'range' !== mode ) { 405 return; 406 } 407 408 // The pair is keyed by the start field's id. On the end field that is 409 // data-range-start-field-id, so it must win over the field's own id. 410 const fieldId = input.dataset.rangeStartFieldId || input.dataset.fieldId; 411 const [ startDate, endDate ] = selectedDates; 412 const instanceElement = this.getInstanceElement( instance ); 413 414 // Every row of a repeater renders the same data-field-id, so a document wide lookup would 415 // always resolve to the first row and leave every later row unsplit. Search inside the row 416 // when there is one. 417 const scope = instanceElement.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document; 418 419 // The name suffix match is the fallback for a half of the pair that is carried over as a 420 // hidden input, which is how an earlier page of a multi page form is preserved. It covers 421 // both item_meta[123] and the item_meta[section][row][123] form a repeater uses. 422 const startDateField = scope.querySelector( `input[data-field-id="${ fieldId }"]` ) 423 || scope.querySelector( `input[type="hidden"][name$="[${ fieldId }]"]` ); 424 425 // Resolve the end field whatever the selection state is. Skipping it while only one date is 426 // picked used to leave the two fields holding different ranges. The name suffix match is 427 // the same multi page fallback the start field gets, keyed off the end field id the start 428 // field carries, since a half preserved from another page has no data attributes. 429 const endFieldId = instanceElement.dataset.rangeEndFieldId; 430 const endDateField = scope.querySelector( `input[data-range-start-field-id="${ fieldId }"]` ) 431 || ( endFieldId ? scope.querySelector( `input[type="hidden"][name$="[${ endFieldId }]"]` ) : null ); 432 433 // A range field is not always paired, and the partner is not always rendered yet. Split 434 // this field's own value anyway, so a visible input never keeps the joined range string. 435 if ( ! startDateField || ! endDateField ) { 436 if ( 'undefined' !== typeof endDate ) { 437 instanceElement.value = flatpickr.formatDate( input.dataset.rangeStartFieldId ? endDate : startDate, settings.options.fpDateFormat ); 438 } 439 return; 440 } 441 442 /** 443 * Points one half of the pair at its own date. 444 * 445 * The partner does not always have a picker. On a multi page form the other half arrives as 446 * a plain hidden input, and reading .setDate off it used to throw before the value was 447 * written, which left a stale date behind and saved a range nobody picked. 448 * 449 * @param {HTMLElement} field The field to write to. 450 * @param {Date} date The date that half represents. 451 * @returns {void} 452 */ 453 const setHalf = ( field, date ) => { 454 if ( field._flatpickr ) { 455 field._flatpickr.setDate( dateStr ); 456 } 457 458 field.value = flatpickr.formatDate( date, settings.options.fpDateFormat ); 459 }; 460 461 instanceElement.dataset.rangeValue = dateStr; 462 463 // The range is only half picked. Point both halves of the pair at that one date, so 464 // neither field is left holding a range from an earlier selection. Blanking the end field 465 // instead would not survive, since the add-on refreshes every picker after this runs and 466 // Flatpickr rewrites each input from its own selected dates. 467 if ( 'undefined' === typeof endDate ) { 468 const partialValue = flatpickr.formatDate( startDate, settings.options.fpDateFormat ); 469 470 startDateField.dataset.rangeValue = dateStr; 471 endDateField.dataset.rangeValue = dateStr; 472 473 if ( instanceElement !== startDateField && startDateField._flatpickr ) { 474 startDateField._flatpickr.setDate( startDate ); 475 } 476 if ( instanceElement !== endDateField && endDateField._flatpickr ) { 477 endDateField._flatpickr.setDate( startDate ); 478 } 479 480 startDateField.value = partialValue; 481 endDateField.value = partialValue; 482 return; 483 } 484 485 if ( instanceElement === startDateField ) { 486 instanceElement.value = flatpickr.formatDate( startDate, settings.options.fpDateFormat ); 487 endDateField.dataset.rangeValue = dateStr; 488 489 setHalf( endDateField, endDate ); 490 491 // Trigger the change event to end range field to update the calculation 492 endDateField.dispatchEvent( new Event( 'change', { bubbles: true } ) ); 493 } 494 495 if ( instanceElement === endDateField ) { 496 instanceElement.value = flatpickr.formatDate( endDate, settings.options.fpDateFormat ); 497 startDateField.dataset.rangeValue = dateStr; 498 499 setHalf( startDateField, startDate ); 500 } 501 }; 502
503 const settings = this.getSettings(); 504 const mode = input.classList.contains( 'frm_date_range' ) || ( settings.formidable_dates && settings.formidable_dates.isRangeEnabled ) ? 'range' : 'single'; 505 const inline = settings.formidable_dates && settings.formidable_dates.inline; 506 const showMonths = inline && 'range' === mode ? 2 : 1; 507 508 const { min, max } = frmDatepickerPro.getDateRange( settings ); 509 const disabledDates = this.getDatesDisabled( settings ); 510 const firstDay = settings.options.firstDay || 1; 511 512 const flatpickrOptions = { 513 dateFormat: settings.options.fpDateFormat, 514 inline: settings.formidable_dates && settings.formidable_dates.inline, 515 minDate: min, 516 maxDate: max, 517 monthSelectorType: 'true' === settings.options.changeMonth ? 'dropdown' : 'static', 518 changeYear: settings.options.changeYear, 519 closeOnSelect: true, 520 showMonths, 521 locale: { 522 ...flatpickr.l10ns[ frmDatepickerPro.getFlatpickrLocaleKey( settings.locale || settings.options.locale ) ], 523 firstDayOfWeek: firstDay, 524 }, 525 mode, 526 disable: [ 527 function( date ) { 528 if ( ! settings.formidable_dates ) { 529 return false; 530 } 531 532 // The Dates add-on owns these settings and ranks its three rules in one place, so 533 // defer to it when it is active. Listing blackout dates as separate disable entries 534 // could not work with it, since Flatpickr treats each entry as its own veto and an 535 // excepted date would never be able to re-enable a blacked out day. 536 if ( 'function' === typeof window.frmDatepickerInstance?.isDateAllowed ) { 537 return ! window.frmDatepickerInstance.isDateAllowed( date, settings.formidable_dates ); 538 } 539 540 return settings.formidable_dates.daysEnabled && -1 === settings.formidable_dates.daysEnabled.indexOf( date.getDay() ); 541 }, 542 function( date ) { 543 // The frm_selectable_dates and frm_dates_selectable_response filters supply a 544 // JavaScript expression that only jQuery UI's beforeShowDay used to evaluate, so a 545 // site relying on either filter lost it the moment Flatpickr was switched on. 546 if ( ! settings.formidable_dates || 'function' !== typeof window.frmDatepickerInstance?.passesSelectableResponse ) { 547 return false; 548 } 549 550 return ! window.frmDatepickerInstance.passesSelectableResponse( date, settings.formidable_dates ); 551 }, 552 ...( 'function' === typeof window.frmDatepickerInstance?.isDateAllowed ? [] : ( disabledDates ? disabledDates : [] ) ) 553 ], 554 onReady( selectedDates, dateStr, instance ) { 555 // The Dates add-on owns this. Its own copy is only missing on a site that has not 556 // updated it yet, which still has to get its saved ranges back. 557 const setDefaultRange = 'function' === typeof window.frmDatepickerInstance?.setDefaultRangeValue 558 ? window.frmDatepickerInstance.setDefaultRangeValue 559 : frmDatepickerPro.setDefaultRangeValue; 560 561 setTimeout( () => setDefaultRange( instance.element ), 0 ); 562 if ( instance.config.inline ) { 563 instance.calendarContainer.classList.add( 'frm-datepicker', 'with_frm_style', 'frm_date_inline' ); 564 if ( frmDatepickerPro.isUsingACustomTheme ) { 565 instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); 566 } 567 } 568 frmDatepickerPro.initMonthSelector( instance ); 569 frmDatepickerPro.initYearSelector( instance ); 570 frmDatepickerPro.initAccessiblity( instance ); 571 }, 572 onChange: ( selectedDates, dateStr, instance ) => { 573 if ( instance.config.inline ) { 574 const altInput = instance.config.altInputElement || instance.altInput; 575 if ( altInput ) { 576 altInput.value = dateStr; 577 altInput.dispatchEvent( new Event( 'change', { bubbles: true } ) ); 578 } 579 } 580 581 // Flatpickr only keeps the hooks built in this object, so an onChange handler passed in 582 // through the field settings (the Dates add-on sets one) never reaches it. Call it 583 // explicitly. It runs before the range sync because refreshing a picker makes Flatpickr 584 // rewrite the input from its own selected dates, which for a range is the whole range 585 // string. Syncing afterwards puts the single date back on each half of the pair. 586 // 587 // The add-on's handler was unreachable until this call existed, so the version that 588 // predates it reads a variable that is out of scope there and throws every time. That 589 // version never ran this code at all, so skip it rather than start raising errors on a 590 // site whose only change was updating Pro. getFieldIdFromField is what it reaches for, 591 // and the add-on that owns it is the first one whose handler survives being called. 592 const addOnHandlesOnChange = 'function' === typeof window.frmDatepickerInstance?.getFieldIdFromField; 593 594 if ( addOnHandlesOnChange && settings.options && 'function' === typeof settings.options.onChange ) { 595 try { 596 settings.options.onChange( selectedDates, dateStr, instance ); 597 } catch ( error ) { 598 // Nothing below depends on it, so a handler that throws must not take the range 599 // syncing down with it. 600 if ( window.console && window.console.error ) { 601 window.console.error( error ); 602 } 603 } 604 } 605 606 if ( 'range' === mode ) { 607 // Same story as onReady: the add-on owns the split, this falls back to Pro's own 608 // copy only while the add-on is behind.
609 if ( 'function' === typeof window.frmDatepickerInstance?.syncRangeFields ) { 610 window.frmDatepickerInstance.syncRangeFields( { 611 input, 612 instanceElement: this.getInstanceElement( instance ), 613 dateStr, 614 selectedDates, 615 dateFormat: settings.options.fpDateFormat 616 } ); 617 } else { 618 this.updateRangeFieldsOnChange( mode, instance, dateStr, selectedDates ); 619 } 620 } 621 }, 622 onOpen( selectedDates, dateStr, instance ) { 623 frmDatepickerPro.callbacks.onOpen( selectedDates, dateStr, instance ); 624 }, 625 onClose: ( selectedDates, dateStr, instance ) => { 626 frmDatepickerPro.callbacks.onClose( selectedDates, dateStr, instance ); 627 628 // Closing on an unfinished range is Flatpickr discarding the selection: it empties 629 // its own input and forgets the date. The other half of the pair is not its input 630 // though, so without this it keeps a date the range never ended up using. The add-on 631 // owns both halves, so hand it the close. 632 if ( 'range' === mode && 'function' === typeof window.frmDatepickerInstance?.clearIncompleteRange ) { 633 window.frmDatepickerInstance.clearIncompleteRange( { 634 input, 635 instanceElement: this.getInstanceElement( instance ), 636 selectedDates 637 } ); 638 } 639 }, 640 // jQuery UI drew the same calendar on every device. Flatpickr hands a phone the browser's 641 // native date input instead, and that input carries over only minDate and maxDate, so the 642 // disable predicate above and range mode would both be silently dropped there. 643 disableMobile: true, 644 shorthandCurrentMonth: true, 645 altInputClass: '' 646 }; 647 648 if ( settings.formidable_dates && settings.formidable_dates.inline ) { 649 flatpickrOptions.altInput = true; 650 flatpickrOptions.altInputElement = frmDatepickerPro.getAltInputElement( input, settings.options.altField ); 651 } 652 653 return flatpickrOptions; 654 }; 655 656 657 /** 658 * Locale codes whose Flatpickr l10n key is not the code the date field stores. 659 * 660 * The field's locale setting uses jQuery UI's codes, and Flatpickr names a number of the same 661 * languages differently, so a straight lookup silently falls back to English for these. The 662 * value is the key the matching l10n file registers on flatpickr.l10ns, which is not always the 663 * file's own name either. Entries with no Flatpickr equivalent at all are left out on purpose, 664 * since English is all that is available for them. 665 * 666 * @since 6.35 667 * 668 * @type {object} 669 */ 670 frmDatepickerPro.localeKeyMap = { 671 'ar-DZ': 'ar', 672 'ca': 'ca', 673 'cy-GB': 'cy', 674 'de-AT': 'at', 675 'el': 'gr', 676 'fr-CA': 'fr', 677 'fr-CH': 'fr', 678 'kk': 'kz', 679 'nb': 'no', 680 'pt-BR': 'pt', 681 'sr': 'sr', 682 'sr-SR': 'sr', 683 'vi': 'vn', 684 'zh-CN': 'zh', 685 'zh-HK': 'zh_tw', 686 'zh-TW': 'zh_tw' 687 }; 688 689 /** 690 * Gets the key a locale's translations are registered under on flatpickr.l10ns. 691 * 692 * @since 6.35 693 * 694 * @param {string} locale The locale stored on the date field. 695 * @returns {string} 696 */ 697 frmDatepickerPro.getFlatpickrLocaleKey = function( locale ) { 698 if ( ! locale ) { 699 return 'default'; 700 } 701 702 return frmDatepickerPro.localeKeyMap[ locale ] || locale; 703 }; 704 705 /** 706 * COMPATIBILITY PATH. Do not fix bugs here. 707 * 708 * Superseded by frmDatepickerInstance.setDefaultRangeValue in the Dates add-on. Kept so a site 709 * running an older add-on against this version of Pro still gets its saved ranges back onto the 710 * calendar. Frozen, see updateRangeFieldsOnChange above. 711 * 712 * @since 6.24 713 * 714 * @param {object} input 715 * @returns {void} 716 */ 717 frmDatepickerPro.setDefaultRangeValue = function( instanceElement ) { 718 let input = instanceElement; 719 720 if ( instanceElement._flatpickr.config.inline ) { 721 const fieldId = instanceElement.dataset.fieldId; 722 input = document.querySelector( `input[name="item_meta[${ fieldId }]"]` ); 723 if ( ! input ) { 724 const container = instanceElement.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ); 725 if ( container ) { 726 input = container.querySelector( `input[name^="item_meta[${ fieldId }]["]` ); 727 } 728 } 729 if ( ! input ) { 730 return; 731 }
732 input.dataset.fieldId = fieldId; 733 } 734 735 if ( ! input.classList.contains( 'frm_date_range' ) ) { 736 return null; 737 } 738 739 // Scope to the repeater row for the same reason updateRangeFieldsOnChange does: every row 740 // carries the same data-field-id, so a document wide lookup resolves to the first row. 741 const scope = input.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ) || document; 742 743 // On a multi page form the half that lives on another page is preserved as a bare hidden 744 // input carrying neither data attribute, so fall back to matching its name. Without this the 745 // pair cannot be resolved and the date already picked on the other page is not shown here. 746 const findHalf = ( selector, fieldId ) => scope.querySelector( selector ) || ( fieldId ? scope.querySelector( `input[type="hidden"][name$="[${ fieldId }]"]` ) : null ); 747 748 const startDate = input.dataset.rangeStartFieldId ? findHalf( `input[data-field-id="${ input.dataset.rangeStartFieldId }"]`, input.dataset.rangeStartFieldId ) : input; 749 const endDate = input.dataset.rangeStartFieldId ? input : findHalf( `input[data-range-start-field-id="${ input.dataset.fieldId }"]`, input.dataset.rangeEndFieldId ); 750 751 // A range field can be left unpaired, for instance when its partner field was deleted. Reading 752 // a value off either half then throws and the datepicker never finishes setting up. 753 if ( ! startDate || ! endDate ) { 754 return; 755 } 756 757 const startDateValue = startDate.value; 758 const endDateValue = endDate.value; 759 760 // Flatpickr parses a range string by splitting it on the separator of the active locale, so a 761 // hardcoded ' to ' is unreadable in every other language. 762 const rangeValue = startDateValue + frmDatepickerPro.getRangeSeparator( instanceElement._flatpickr ) + endDateValue; 763 764 if ( instanceElement._flatpickr.config.inline ) { 765 instanceElement._flatpickr.setDate( rangeValue ); 766 startDate.value = startDateValue; 767 endDate.value = endDateValue; 768 return; 769 } 770 771 // When the inline datepicker is enabled, the rangeâs default value is controlled by the âStart Dateâ field. 772 // If the Flatpickr instance for âStart Dateâ isnât available, setDefaultRangeValue is executed for the âEnd Dateâ field when the "Display Inline" is activated - the default dates are already updated by the "Start Date" field 773 if ( 'undefined' === typeof startDate._flatpickr ) { 774 return; 775 } 776 777 startDate._flatpickr.setDate( rangeValue ); 778 startDate.value = startDateValue; 779 if ( endDate._flatpickr ) { 780 endDate._flatpickr.setDate( rangeValue ); 781 } 782 endDate.value = endDateValue; 783 }; 784 785 /** 786 * Gets the range separator Flatpickr uses to join and split a range string. 787 * 788 * @since 6.35 789 * 790 * @param {object} instance The Flatpickr instance. 791 * @returns {string} 792 */ 793 frmDatepickerPro.getRangeSeparator = function( instance ) { 794 return instance?.l10n?.rangeSeparator || ' to '; 795 }; 796 797 /** 798 * Initialize the month selector for the datepicker. 799 * 800 * @since 6.19 801 * 802 * @param {object} instance 803 * @returns {void} 804 */ 805 frmDatepickerPro.initMonthSelector = function( instance ) { 806 if ( 'static' !== instance.config.monthSelectorType ) { 807 return; 808 } 809 instance.calendarContainer.classList.add( 'frm-date-no-month-select' ); 810 }; 811 812 /** 813 * Initialize the year selector for the datepicker. 814 * 815 * @since 6.19 816 * 817 * @param {object} instance 818 * @returns {void} 819 */ 820 frmDatepickerPro.initYearSelector = function( instance ) { 821 if ( 'false' !== instance.config.changeYear ) { 822 return; 823 } 824 instance.calendarContainer.classList.add( 'frm-date-no-year-select' ); 825 }; 826 827 /** 828 * Initialize the accessibility for the datepicker. 829 * 830 * @since 6.19 831 * 832 * @param {object} instance 833 * @returns {void} 834 */ 835 frmDatepickerPro.initAccessiblity = function( instance ) { 836 837 /** 838 * Initialize the accessibility for the datepicker. 839 * 840 * @since 6.19 841 * 842 * @returns {void} 843 */ 844 this.init = function() { 845 instance.calendarContainer.setAttribute( 'role', 'dialog' ); 846 instance.calendarContainer.setAttribute( 'tabindex', '0' ); 847 this.prevButton(); 848 this.nextButton(); 849 }; 850 851 /** 852 * Initialize the previous button for the datepicker. 853 * 854 * @since 6.19 855 * 856 * @returns {void} 857 */ 858 this.prevButton = function( ) { 859 const prevArrow = instance.calendarContainer.querySelector( '.flatpickr-prev-month' ); 860 if ( null === prevArrow ) { 861 return; 862 } 863 prevArrow.setAttribute( 'tabindex', '0' ); 864 prevArrow.setAttribute( 'role', 'button' ); 865 prevArrow.addEventListener( 'keydown', ( event ) => { 866 if ( event.key === 'Enter' ) { 867 prevArrow.click(); 868 } 869 }); 870 }; 871 872 /** 873 * Initialize the next button for the datepicker. 874 * 875 * @since 6.19 876 * 877 * @returns {void} 878 */ 879 this.nextButton = function( ) { 880 const nextArrow = instance.calendarContainer.querySelector( '.flatpickr-next-month' ); 881 if ( null === nextArrow ) { 882 return; 883 } 884 nextArrow.setAttribute( 'tabindex', '0' ); 885 nextArrow.setAttribute( 'role', 'button' ); 886 nextArrow.addEventListener( 'keydown', ( event ) => { 887 if ( event.key === 'Enter' ) { 888 nextArrow.click(); 889 } 890 }); 891 }; 892 893 this.init(); 894 }; 895 896 /** 897 * Get the date range for the datepicker. 898 * 899 * @since 6.19 900 * 901 * @param {object} optionsData 902 * @returns {object} 903 */ 904 frmDatepickerPro.getDateRange = function( optionsData ) { 905 const dates = { 906 min: null, 907 max: null, 908 }; 909 const [ minYear, maxYear ] = optionsData.options.yearRange.split( ':' ); 910 911 dates.min = new Date( minYear + '-01-01T00:00:00' ); 912 dates.max = new Date( maxYear + '-12-31T23:59:59' ); 913 914 if ( ! optionsData.formidable_dates ) { 915 return dates; 916 } 917 918 if ( optionsData.formidable_dates.maximum_date_cond ) { 919 if ( optionsData.options.maxDate ) { 920 dates.max = optionsData.options.maxDate; 921 } else { 922 dates.max = frmDatepickerPro.resolveDateLimit( optionsData.formidable_dates.maximum_date_cond, optionsData.formidable_dates.maximum_date_val, dates.max ); 923 } 924 } 925 if ( optionsData.formidable_dates.minimum_date_cond ) { 926 if ( optionsData.options.minDate ) { 927 dates.min = optionsData.options.minDate; 928 } else { 929 dates.min = frmDatepickerPro.resolveDateLimit( optionsData.formidable_dates.minimum_date_cond, optionsData.formidable_dates.minimum_date_val, dates.min ); 930 } 931 } 932 933 return dates; 934 }; 935 936 /** 937 * Resolves a minimum or maximum limit from the Dates add-on settings. 938 * 939 * Only reached when the add-on has not already worked the limit out, which happens when the 940 * datepicker is built before the add-on has run over the field. 941 * 942 * @since 6.35 943 * 944 * @param {string} condition How the limit is set: a specific date, today, or field_<key>. 945 * @param {string} offsetValue The specific date, or the offset to apply to today. 946 * @param {object} fallback Limit to keep when the condition cannot be resolved here. 947 * @returns {object|string} 948 */ 949 frmDatepickerPro.resolveDateLimit = function( condition, offsetValue, fallback ) { 950 // A limit measured from another date field depends on what that field currently holds, which 951 // only the Dates add-on tracks. Guessing an offset from today would invent a wrong limit. 952 if ( 0 === condition.indexOf( 'field_' ) ) { 953 return fallback; 954 } 955 956 // A specific date is already a date, there is no offset to apply. 957 if ( 'today' !== condition ) { 958 return offsetValue; 959 } 960 961 // Offsets are the add-on's own setting, so prefer its parser and keep one definition of what 962 // "1 week" means. Older versions of the add-on do not expose it, so fall back to the local 963 // copy below rather than dropping the limit for anyone who updated only this plugin. 964 if ( 'function' === typeof window.frmDatepickerInstance?.applyDateOffset ) { 965 // An offset carrying no unit, an empty one included, means today itself. 966 return window.frmDatepickerInstance.applyDateOffset( new Date(), offsetValue ); 967 } 968 969 const offsetDate = frmDatepickerPro.parseOffsetDate( offsetValue ); 970 971 return offsetDate instanceof Date ? offsetDate : new Date(); 972 }; 973 974 /** 975 * Parse the offset date from the datepickerJsOptions. 976 *
977 * Kept only for the case where the Dates add-on is older than the version that shares its own 978 * parser. When both plugins are current, resolveDateLimit uses the add-on's instead, so the two 979 * cannot disagree about what an offset means. 980 * 981 * @since 6.19 982 * 983 * @param {string} date 984 * @returns {object|string} 985 */ 986 frmDatepickerPro.parseOffsetDate = function( date ) { 987 if ( ! date ) { 988 return date; 989 } 990 991 // Sign optional, and weeks and single letter units accepted, matching what the add-on allows. 992 const match = String( date ).toLowerCase().replace( /\s+/g, '' ) 993 .match( /^([+-]?\d+)(d|day|days|w|week|weeks|m|month|months|y|year|years)$/ ); 994 995 if ( ! match ) { 996 return date; 997 } 998 999 const amount = parseInt( match[ 1 ], 10 ); 1000 const unit = match[ 2 ]; 1001 const currentDate = new Date(); 1002 1003 if ( unit.startsWith( 'd' ) ) { 1004 currentDate.setDate( currentDate.getDate() + amount ); 1005 } else if ( unit.startsWith( 'w' ) ) { 1006 currentDate.setDate( currentDate.getDate() + ( 7 * amount ) ); 1007 } else if ( unit.startsWith( 'm' ) ) { 1008 currentDate.setMonth( currentDate.getMonth() + amount ); 1009 } else if ( unit.startsWith( 'y' ) ) { 1010 currentDate.setFullYear( currentDate.getFullYear() + amount ); 1011 } 1012 1013 return currentDate; 1014 }; 1015 1016 /** 1017 * Parse the function config from the datepickerJsOptions. 1018 * 1019 * @since 6.19 1020 * 1021 * @param {string} config 1022 * @returns {object} 1023 */ 1024 frmDatepickerPro.parseFunctionConfig = function( config ) { 1025 const parsed = JSON.parse( config ); 1026 1027 const stringToFunction = ( str ) => { 1028 if ( typeof str === 'string' && str.startsWith( 'function' ) ) { 1029 return new Function( 'return ' + str )(); 1030 } 1031 return str; 1032 }; 1033 1034 Object.keys( parsed ).forEach( key => { 1035 if ( Array.isArray( parsed[ key ] ) ) { 1036 parsed[ key ] = parsed[ key ].map( item => stringToFunction( item ) ); 1037 } else { 1038 parsed[ key ] = stringToFunction( parsed[ key ] ); 1039 } 1040 }); 1041 1042 return parsed; 1043 }; 1044 1045 /** 1046 * Check if the flatpickr library is available. 1047 * 1048 * @since 6.19 1049 */ 1050 frmDatepickerPro.useFlatpickr = () => window.frm_js && window.frm_js.datepickerLibrary === 'flatpickr'; 1051 1052 function triggerDateField() { 1053 /*jshint validthis:true */ 1054 if ( this.className.includes('frm_custom_date') || typeof __frmDatepicker === 'undefined' ) { 1055 return; 1056 } 1057 1058 var dateFields = __frmDatepicker, 1059 id = this.id, 1060 idParts = id.split( '-' ), 1061 altID = ''; 1062 1063 if ( isRepeatingFieldByName( this.name ) ) { 1064 altID = 'input[id^="' + idParts[ 0 ] + '"]'; 1065 } else { 1066 altID = 'input[id^="' + idParts.join( '-' ) + '"]'; 1067 } 1068 1069 var optKey = 0; 1070 for ( var i = 0; i < dateFields.length; i++ ) { 1071 if ( dateFields[ i ].triggerID === '#' + id || dateFields[ i ].triggerID == altID ) { 1072 optKey = i; 1073 break; 1074 } 1075 } 1076 1077 if ( dateFields[ optKey ].options.defaultDate !== '' ) { 1078 dateFields[ optKey ].options.defaultDate = new Date( dateFields[ optKey ].options.defaultDate ); 1079 } 1080 1081 if ( frmDatepickerPro.useFlatpickr() ) { 1082 new frmDatepickerPro( this, dateFields[ optKey ] ); 1083 } else { 1084 // Legacy datepicker. 1085 // TO DO: Remove this once the legacy datepicker is removed. 1086 dateFields[ optKey ].options.beforeShow = frmProForm.addFormidableClassToDatepicker; 1087 dateFields[ optKey ].options.onClose = frmProForm.removeFormidableClassFromDatepicker; 1088 1089 jQuery( this ).datepicker( jQuery.extend( 1090 {}, 1091 jQuery.datepicker.regional[ dateFields[ optKey ].locale ], 1092 dateFields[ optKey ].options 1093 )); 1094 } 1095 } 1096 1097 function loadDropzones( repeatRow ) { 1098 if ( typeof __frmDropzone === 'undefined' ) { 1099 return; 1100 } 1101 1102 var uploadFields = __frmDropzone; 1103 for ( var i = 0; i < uploadFields.length; i++ ) { 1104 loadDropzone( i, repeatRow ); 1105 } 1106 } 1107 1108 function loadDropzone( i, repeatRow ) { 1109 var field, max, uploadedCount, form, 1110 uploadFields = __frmDropzone, 1111 uploadField = uploadFields[ i ], 1112 selector = '#' + uploadField.htmlID + '_dropzone', 1113 fieldName = uploadField.fieldName; 1114 1115 let label = uploadField.label; 1116 1117 if ( repeatRow !== undefined && selector.includes('-0_dropzone') ) { 1118 selector = selector.replace( '-0_dropzone', '-' + repeatRow + '_dropzone' ); 1119 fieldName = fieldName.replace( '[0]', '[' + repeatRow + ']' ); 1120 label = label.replace( /-0$/, '-' + repeatRow ); 1121 delete uploadField.mockFiles; 1122 } 1123
1124 field = jQuery( selector ); 1125 if ( field.length < 1 || field.hasClass( 'dz-clickable' ) || field.hasClass( 'dz-started' ) ) { 1126 return; 1127 } 1128 1129 max = uploadField.maxFiles; 1130 if ( uploadField.mockFiles !== undefined ) { 1131 uploadedCount = uploadField.mockFiles.length; 1132 if ( max > 0 ) { 1133 max = max - uploadedCount; 1134 } 1135 } 1136 1137 form = field.closest( 'form' ); 1138 uploadField = uploadFields[ i ]; 1139 1140 const dropzoneConfig = { 1141 url: getAjaxUrl( form.get( 0 ) ), 1142 headers: { 1143 'Frm-Dropzone': 1 1144 }, 1145 addRemoveLinks: false, 1146 paramName: field.attr( 'id' ).replace( '_dropzone', '' ), 1147 maxFilesize: uploadField.maxFilesize, 1148 minFilesize: uploadField.minFilesize, 1149 maxFiles: max, 1150 uploadMultiple: uploadField.uploadMultiple, 1151 hiddenInputContainer: field.parent()[0], 1152 dictDefaultMessage: uploadField.defaultMessage, 1153 dictFallbackMessage: uploadField.fallbackMessage, 1154 dictFallbackText: uploadField.fallbackText, 1155 dictFileTooBig: uploadField.fileTooBig, 1156 dictFileTooSmall: uploadField.fileTooSmall, 1157 dictInvalidFileType: uploadField.invalidFileType, 1158 dictResponseError: uploadField.responseError, 1159 dictCancelUpload: uploadField.cancel, 1160 dictCancelUploadConfirmation: uploadField.cancelConfirm, 1161 dictRemoveFile: uploadField.remove, 1162 dictMaxFilesExceeded: uploadField.maxFilesExceeded, 1163 resizeMethod: 'contain', 1164 resizeWidth: uploadField.resizeWidth, 1165 resizeHeight: uploadField.resizeHeight, 1166 thumbnailWidth: 60, 1167 thumbnailHeight: 60, 1168 timeout: uploadField.timeout, 1169 previewTemplate: filePreviewHTML( uploadField ), 1170 acceptedFiles: uploadField.acceptedFiles, 1171 fallback() { 1172 // Force ajax submit to turn off 1173 jQuery( this.element ).closest( 'form' ).removeClass( 'frm_ajax_submit' ); 1174 }, 1175 init() { 1176 var hidden, mockFileIndex, mockFileData, mockFile; 1177 1178 hidden = field.parent().find( '.dz-hidden-input' ); 1179 1180 if ( hidden.attr( 'id' ) === undefined ) { 1181 hidden.attr( 'id', label ); 1182 } 1183 1184 this.on( 'thumbnail', function( file ) { 1185 if ( file.size < 1024 * 1024 * ( this.options.minFilesize ) ) { 1186 if ( 'function' === typeof file.rejectSize ) { 1187 file.rejectSize(); 1188 } 1189 } 1190 }); 1191 1192 this.on( 'sending', function( file, xhr, formData ) { 1193 1194 if ( isSpam( uploadFields[ i ].parentFormID, uploadField.checkHoneypot ) ) { 1195 this.removeFile( file ); 1196 alert( frm_js.file_spam ); 1197 return false; 1198 } 1199 1200 formData.append( 'action', 'frm_submit_dropzone' ); 1201 formData.append( 'field_id', uploadFields[ i ].fieldID ); 1202 formData.append( 'form_id', uploadFields[ i ].formID ); 1203 formData.append( 'nonce', frm_js.nonce ); 1204 1205 if ( form.get( 0 ).hasAttribute( 'data-token' ) ) { 1206 formData.append( 'antispam_token', form.get( 0 ).getAttribute( 'data-token' ) ); 1207 } 1208 }); 1209 1210 this.on( 'processing', function() { 1211 if ( ! this.options.uploadMultiple ) { 1212 this.removeEventListeners(); 1213 } 1214 }); 1215 1216 this.on( 'success', function( file, response ) { 1217 var mediaIDs, m, mediaID; 1218 1219 mediaIDs = JSON.parse( response ); 1220 for ( m = 0; m < mediaIDs.length; m++ ) { 1221 if ( uploadFields[ i ].uploadMultiple !== true ) { 1222 mediaID = mediaIDs[ m ]; 1223 jQuery( 'input[name="' + fieldName + '"]' ).val( mediaID ); 1224 } 1225 } 1226 1227 if ( this.options.uploadMultiple === false ) { 1228 this.disable(); 1229 } 1230 1231 // Clear the errors here so it only happens on upload. 1232 // complete and addedfile both get triggered when reloading a form with a file ID. 1233 clearErrorsOnUpload( file.previewElement ); 1234 }); 1235 1236 this.on( 'successmultiple', ( files, response ) => { 1237 var mediaIDs = JSON.parse( response ); 1238 for ( var m = 0; m < files.length; m++ ) { 1239 jQuery( files[ m ].previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], mediaIDs[ m ], fieldName ) ); 1240 } 1241 }); 1242 1243 this.on( 'complete', ( file ) => { 1244 var fileName, node, img, thumbnail; 1245 1246 processesRunning--; 1247 1248 if ( form.length ) { 1249 removeSubmitLoading( form.get( 0 ), uploadFields[ i ].formID, processesRunning ); 1250 } 1251 1252 if ( file.mediaID === undefined ) { 1253 return; 1254 } 1255 1256 if ( uploadFields[ i ].uploadMultiple ) { 1257 jQuery( file.previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], file.mediaID, fieldName ) ); 1258 } 1259 1260 // Add download link to the file 1261 fileName = file.previewElement.querySelectorAll( '[data-dz-name]' ); 1262 for ( var _i = 0, _len = fileName.length; _i < _len; _i++ ) { 1263 node = fileName[ _i ]; 1264 if ( file.accessible ) { 1265 node.innerHTML = '<a href="' + file.url + '" target="_blank" rel="noopener">' + file.name + '</a>'; 1266 } else { 1267 node.innerHTML = file.name; 1268 } 1269 1270 if ( file.ext ) { 1271 img = file.previewElement.querySelector( '.dz-image img' ); 1272 if ( null !== img ) { 1273 thumbnail = maybeGetExtensionThumbnail( file.ext ); 1274 if ( false !== thumbnail ) { 1275 img.setAttribute( 'src', thumbnail ); 1276 } 1277 } 1278 } 1279 } 1280 }); 1281 1282 this.on( 'addedfile', ( file ) => { 1283 var ext, thumbnail; 1284 ext = file.name.split( '.' ).pop(); 1285 thumbnail = maybeGetExtensionThumbnail( ext ); 1286 processesRunning++; 1287 frmFrontForm.showSubmitLoading( form ); 1288 if ( false !== thumbnail ) { 1289 jQuery( file.previewElement ).find( '.dz-image img' ).attr( 'src', thumbnail ); 1290 } 1291 }); 1292 1293 function clearErrorsOnUpload( fileElement ) { 1294 var container = fileElement.closest( '.frm_form_field' ); 1295 if ( ! container ) { 1296 return; 1297 } 1298 1299 container.classList.remove( 'frm_blank_field', 'has-error' ); 1300 container.querySelectorAll( '.form-field .frm_error, .frm_error_style' ).forEach( 1301 ( error ) => { 1302 if ( error.parentNode ) { 1303 error.parentNode.removeChild( error ); 1304 } 1305 } 1306 ); 1307 } 1308 1309 this.on( 'removedfile', function( file ) { 1310 var fileCount = this.files.length; 1311 1312 if ( this.options.uploadMultiple === false && fileCount < 1 ) { 1313 this.enable(); 1314 } 1315 1316 if ( file.accepted !== false && uploadFields[ i ].uploadMultiple !== true ) { 1317 jQuery( 'input[name="' + fieldName + '"]' ).val( '' ); 1318 } 1319 1320 if ( file.accepted !== false && file.mediaID !== undefined ) { 1321 jQuery( file.previewElement ).remove(); 1322 fileCount = this.files.length; 1323 this.options.maxFiles = uploadFields[ i ].maxFiles - fileCount; 1324 } 1325 }); 1326 1327 if ( uploadFields[ i ].mockFiles !== undefined ) { 1328 for ( mockFileIndex = 0; mockFileIndex < uploadFields[ i ].mockFiles.length; mockFileIndex++ ) { 1329 mockFileData = uploadFields[ i ].mockFiles[ mockFileIndex ]; 1330 mockFile = { 1331 name: mockFileData.name, 1332 size: mockFileData.size, 1333 url: mockFileData.file_url, 1334 mediaID: mockFileData.id, 1335 accessible: mockFileData.accessible, 1336 ext: mockFileData.ext, 1337 type: mockFileData.type 1338 }; 1339
1340 this.emit( 'addedfile', mockFile ); 1341 if ( mockFile.accessible && 'string' === typeof mockFile.type && 0 === mockFile.type.indexOf( 'image/' ) ) { 1342 this.emit( 'thumbnail', mockFile, mockFileData.url ); 1343 } 1344 1345 this.emit( 'complete', mockFile ); 1346 this.files.push( mockFile ); 1347 } 1348 } 1349 }, 1350 accept( file, done ) { 1351 var message = this.options.dictFileTooSmall.replace( '{{minFilesize}}', this.options.minFilesize ); 1352 file.rejectSize = function() { 1353 done( message ); 1354 }; 1355 1356 return done(); 1357 } 1358 }; 1359 1360 /** 1361 * Fixes Chrome Android bug where cloud storage files (Google Drive, etc.) fail to upload. 1362 * 1363 * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1063576 1364 */ 1365 if ( /Android/i.test( navigator.userAgent ) && /Chrome/i.test( navigator.userAgent ) ) { 1366 /** 1367 * Transforms a file before Dropzone uploads it on Android Chrome. 1368 * 1369 * @param {File} file The file selected by the user. 1370 * @param {Function} done Callback invoked with the file or Blob to upload. 1371 * @returns {void} 1372 */ 1373 dropzoneConfig.transformFile = function( file, done ) { 1374 /** 1375 * Forwards the file through Dropzone's resize when configured for images. 1376 * 1377 * @param {File|Blob} fileToUpload The file or in-memory Blob ready to upload. 1378 * @returns {void} 1379 */ 1380 const maybeResize = ( fileToUpload ) => { 1381 if ( ( this.options.resizeWidth || this.options.resizeHeight ) && fileToUpload.type && fileToUpload.type.match( /image.*/ ) ) { 1382 this.resizeImage( fileToUpload, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done ); 1383 return; 1384 } 1385 done( fileToUpload ); 1386 }; 1387 1388 const reader = new FileReader(); 1389 reader.onload = () => { 1390 const blob = new Blob( [ reader.result ], { type: file.type } ); 1391 blob.name = file.name; 1392 blob.lastModified = file.lastModified; 1393 maybeResize( blob ); 1394 }; 1395 reader.onerror = () => maybeResize( file ); 1396 reader.readAsArrayBuffer( file ); 1397 }; 1398 } 1399 1400 field.dropzone( dropzoneConfig ); 1401 } 1402 1403 /** 1404 * Load imask for the given container. 1405 * 1406 * @since 6.23 1407 * 1408 * @param {string|undefined} containerId 1409 * @returns {void} 1410 */ 1411 function loadImask( containerId ) { 1412 if ( ! window.IMask ) { 1413 return; 1414 } 1415 1416 let inputs; 1417 if ( containerId ) { 1418 inputs = document.querySelectorAll( '#' + containerId + ' input[data-frmmask]' ); 1419 } else { 1420 inputs = document.querySelectorAll( 'input[data-frmmask]' ); 1421 } 1422 1423 inputs.forEach( 1424 ( input ) => { 1425 if ( input.getAttribute( 'data-frm-imask-initialized' ) ) { 1426 return; 1427 } 1428 1429 input.setAttribute( 'data-frm-imask-initialized', 1 ); 1430 1431 const mask = IMask( 1432 input, 1433 { 1434 mask: input.dataset.frmmask, 1435 lazy: true, 1436 definitions: { 1437 '*': /[a-zA-Z0-9]/ 1438 } 1439 } 1440 ); 1441 1442 // Store the mask instance for later access 1443 frmMaskInstances.set( input, mask ); 1444 1445 input.addEventListener( 'change', () => mask.updateValue() ); 1446 1447 // Show placeholder on focus 1448 input.addEventListener( 'focus', () => { 1449 mask.updateOptions({ lazy: false }); 1450 1451 // Extend the maxlength by 1 to fix an issue where the placeholder characters would prevent 1452 // characters from being added. 1453 if ( input.hasAttribute( 'maxlength' ) && input.value.length >= input.maxLength ) { 1454 if ( ! input.hasAttribute( 'original-maxlength' ) ) { 1455 input.setAttribute( 'original-maxlength', input.maxLength ); 1456 } 1457 input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) ) + 1; 1458 } 1459 1460 if ( '' === mask.unmaskedValue ) { 1461 // This fixes an issue in Chrome, where it 1462 // automatically would move the cursor to the end of the input. 1463 mask.alignCursor( 0 ); 1464 } 1465 } ); 1466 1467 input.addEventListener( 'blur', () => { 1468 input.value = input.value.trim(); 1469 1470 if ( '' === mask.unmaskedValue ) { 1471 mask.updateOptions({ lazy: true }); 1472 } 1473 1474 if ( input.hasAttribute( 'original-maxlength' ) ) { 1475 input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) ); 1476 } 1477 } ); 1478 } 1479 ); 1480 } 1481 1482 /** 1483 * Remove the submit loading class from a form and 1484 * only enable the submit button if it is not conditionally disabled. 1485 * 1486 * @param {HTMLElement} form 1487 * @param {string} formId 1488 * @param {number} processesRunning 1489 * @returns {void} 1490 */ 1491 function removeSubmitLoading( form, formId, processesRunning ) { 1492 var isFinalSubmitButton, enable; 1493 1494 isFinalSubmitButton = form.querySelector( '.frm_submit .frm_button_submit.frm_final_submit' ); 1495 enable = ! isFinalSubmitButton || ! submitButtonIsConditionallyDisabled( formId ) ? 'enable' : ''; 1496 1497 if ( '' === enable ) { 1498 // If "enable" is not passed, the save draft button stays disabled. 1499 // This code enables the save draft button. 1500 jQuery( '.frm_loading_form' ).find( 'a.frm_save_draft' ).css( 'pointer-events', '' ); 1501 } 1502 1503 frmFrontForm.removeSubmitLoading( jQuery( form ), enable, processesRunning ); 1504 } 1505 1506 /** 1507 * Check if the submit button is conditionally disabled. 1508 * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements. 1509 * 1510 * @since 6.7.1 1511 * 1512 * @param {String} formId 1513 * @returns {bool} 1514 */ 1515 function submitButtonIsConditionallyDisabled( formId ) { 1516 return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ 'submit_' + formId ].hideDisable; 1517 } 1518 1519 /** 1520 * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled. 1521 * 1522 * @since 6.7.1 1523 * 1524 * @param {String} formId 1525 * @returns bool 1526 */ 1527 function submitButtonIsConditionallyNotAvailable( formId ) { 1528 var hideFields = document.getElementById( 'frm_hide_fields_' + formId ); 1529 return hideFields && -1 !== hideFields.value.indexOf( '"frm_form_' + formId + '_container .frm_final_submit"' ); 1530 } 1531 1532 function maybeGetExtensionThumbnail( ext ) { 1533 if ( -1 !== [ 'jpg', 'jpeg', 'png' ].indexOf( ext ) ) { 1534 return false;
1535 } 1536 1537 if ( 'pdf' === ext ) { 1538 return getProPluginUrl() + '/images/pdf.svg'; 1539 } 1540 1541 if ( -1 !== ext.indexOf( 'xls' ) ) { 1542 return getProPluginUrl() + '/images/xls.svg'; 1543 } 1544 1545 return getProPluginUrl() + '/images/doc.svg'; 1546 } 1547 1548 function getProPluginUrl() { 1549 var freePluginUrlSplitBySlashes = frm_js.images_url.split( '/' ); 1550 freePluginUrlSplitBySlashes.pop(); 1551 freePluginUrlSplitBySlashes.pop(); 1552 freePluginUrlSplitBySlashes.push( 'formidable-pro' ); 1553 return freePluginUrlSplitBySlashes.join( '/' ); 1554 } 1555 1556 function filePreviewHTML( field ) { 1557 return '<div class="dz-preview dz-file-preview">\n' + 1558 '<div class="dz-image"><img data-dz-thumbnail /></div>\n' + 1559 '<div class="dz-column">\n' + 1560 '<div class="dz-details">\n' + 1561 '<div class="dz-filename"><span data-dz-name></span></div>\n' + 1562 ' ' + // add white space between file name and file size. 1563 '<div class="dz-size"><span data-dz-size></span></div>\n' + 1564 '<a class="dz-remove frm_remove_link" href="javascript:undefined;" data-dz-remove title="' + field.remove + '">' + 1565 '<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="m11.5 4.5-7 7M4.5 4.5l7 7" stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/></svg>' + 1566 '</a>' + 1567 '</div>\n' + 1568 '<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>\n' + 1569 '<div class="dz-error-message"><span data-dz-errormessage></span></div>\n' + 1570 '</div>\n' + 1571 '</div>'; 1572 } 1573 1574 function getHiddenUploadHTML( field, mediaID, fieldName ) { 1575 return '<input name="' + fieldName + '[]" type="hidden" value="' + mediaID + '" data-frmfile="' + field.fieldID + '" />'; 1576 } 1577 1578 function removeFile() { 1579 /*jshint validthis:true */ 1580 var fieldName = jQuery( this ).data( 'frm-remove' ); 1581 fadeOut( jQuery( this ).closest( '.dz-preview' ) ); 1582 var singleField = jQuery( 'input[name="' + fieldName + '"]' ); 1583 if ( singleField.length ) { 1584 singleField.val( '' ); 1585 } 1586 } 1587 1588 function postToAjaxUrl( form, data, success, error, extraParams ) { 1589 var ajaxParams = 'object' === typeof extraParams ? extraParams : {}; 1590 1591 ajaxParams.type = 'POST'; 1592 ajaxParams.url = getAjaxUrl( form ); 1593 ajaxParams.data = data; 1594 ajaxParams.success = success; 1595 1596 if ( 'function' === typeof error ) { 1597 ajaxParams.error = error; 1598 } 1599 1600 jQuery.ajax( ajaxParams ); 1601 } 1602 1603 function getAjaxUrl( form ) { 1604 var ajaxUrl, action; 1605 1606 ajaxUrl = frm_js.ajax_url; 1607 action = form.getAttribute( 'action' ); 1608 1609 if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) { 1610 ajaxUrl = action.split( '?action=frm_forms_preview' )[0]; 1611 } 1612 1613 return ajaxUrl; 1614 } 1615 1616 function isSpam( formID, checkHoneypot ) { 1617 if ( isHeadless() ) { 1618 return true; 1619 } 1620 return checkHoneypot && isHoneypotSpam( formID ); 1621 } 1622 1623 /** 1624 * Check if submission is Honeypot spam 1625 * 1626 * @since 2.03.08 1627 * 1628 * @returns {boolean} 1629 */ 1630 function isHoneypotSpam( formID ) { 1631 var honeypotField = document.getElementById( 'frm_email_' + formID ); 1632 if ( honeypotField === null ) { 1633 honeypotField = document.getElementById( 'frm_form_' + formID + '_container' )?.querySelector( '.frm_verify[id^="field_"]' ); 1634 } 1635 return honeypotField !== null && honeypotField.value !== ''; 1636 } 1637 1638 function isHeadless() { 1639 return ( 1640 window._phantom || window.callPhantom || //phantomjs 1641 window.__phantomas || //PhantomJS-based web perf metrics 1642 window.Buffer || //nodejs 1643 window.emit || //couchjs 1644 window.spawn //rhino 1645 ); 1646 } 1647 1648 /** 1649 * Show "Other" text box when Other item is checked/selected 1650 * Hide and clear text box when item is unchecked/unselected 1651 */ 1652 function showOtherText() { 1653 /*jshint validthis:true */ 1654 var type = this.type, 1655 other = false, 1656 select = false; 1657 1658 // Dropdowns 1659 if ( type === 'select-one' ) { 1660 select = true; 1661 var curOpt = this.options[ this.selectedIndex ]; 1662 if ( curOpt !== undefined && curOpt.className === 'frm_other_trigger' ) { 1663 other = true; 1664 } 1665 } else if ( type === 'select-multiple' ) { 1666 select = true; 1667 var allOpts = this.options; 1668 other = false;
1669 for ( var i = 0; i < allOpts.length; i++ ) { 1670 if ( allOpts[ i ].className === 'frm_other_trigger' ) { 1671 if ( allOpts[ i ].selected ) { 1672 other = true; 1673 break; 1674 } 1675 } 1676 } 1677 } 1678 if ( select ) { 1679 var otherField = jQuery( this ).parent().children( '.frm_other_input' ); 1680 1681 if ( otherField.length ) { 1682 if ( other ) { 1683 // Remove frm_pos_none 1684 otherField[ 0 ].className = otherField[ 0 ].className.replace( 'frm_pos_none', '' ); 1685 } else { 1686 // Add frm_pos_none 1687 if ( otherField[ 0 ].className.indexOf( 'frm_pos_none' ) < 1 ) { 1688 otherField[ 0 ].className = otherField[ 0 ].className + ' frm_pos_none'; 1689 } 1690 otherField[ 0 ].value = ''; 1691 } 1692 } 1693 1694 // Radio 1695 } else if ( type === 'radio' ) { 1696 if ( jQuery( this ).is( ':checked' ) ) { 1697 jQuery( this ).closest( '.frm_radio' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' ); 1698 jQuery( this ).closest( '.frm_radio' ).siblings().children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' ); 1699 } 1700 // Checkboxes 1701 } else if ( type === 'checkbox' ) { 1702 if ( this.checked ) { 1703 jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' ); 1704 } else { 1705 jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' ); 1706 } 1707 } 1708 } 1709 1710 function setToggleAriaChecked() { 1711 this.nextElementSibling.setAttribute( 'aria-checked', this.checked ? 'true' : 'false' ); 1712 } 1713 1714 function maybeCheckDependent( _, field, fieldId, e ) { 1715 var $field, originalEvent; 1716 1717 $field = jQuery( field ); 1718 checkFieldsWithConditionalLogicDependentOnThis( fieldId, $field ); 1719 1720 originalEvent = getOriginalEvent( e ); 1721 checkFieldsWatchingLookup( fieldId, $field, originalEvent ); 1722 doCalculation( fieldId, $field ); 1723 } 1724 1725 function getOriginalEvent( e ) { 1726 var originalEvent; 1727 if ( e.originalEvent !== undefined || e.currentTarget.className.includes('frm_chzn') || e.currentTarget.className.includes('frm_slimselect') ) { 1728 originalEvent = 'value changed'; 1729 } else { 1730 originalEvent = 'other'; 1731 } 1732 return originalEvent; 1733 } 1734 1735 /***************************************************** 1736 Conditional Logic Functions 1737 ******************************************************/ 1738 1739 /** 1740 * Check if a changed field has other fields depending on it. 1741 */ 1742 function checkFieldsWithConditionalLogicDependentOnThis( fieldId, changedInput ) { 1743 if ( typeof __FRMRULES === 'undefined' || 1744 __FRMRULES[ fieldId ] === undefined || 1745 __FRMRULES[ fieldId ].dependents.length < 1 || 1746 changedInput === null || 1747 changedInput === undefined 1748 ) { 1749 return; 1750 } 1751 1752 var triggerFieldArgs = __FRMRULES[ fieldId ]; 1753 var repeatArgs = getRepeatArgsFromFieldName( changedInput[ 0 ].name ); 1754 1755 pendingDynamicFieldAjax = []; 1756 1757 for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) { 1758 hideOrShowFieldById( triggerFieldArgs.dependents[ i ], repeatArgs ); 1759 } 1760 1761 processPendingAjax(); 1762 } 1763 1764 function processPendingAjax() { 1765 var fieldsToProcess, postData, data, formId, form; 1766 1767 if ( ! pendingDynamicFieldAjax.length ) { 1768 return; 1769 } 1770 1771 // Copy pendingDynamicFieldAjax as it gets reset to [] every time checkFieldsWithConditionalLogicDependentOnThis is called. 1772 fieldsToProcess = pendingDynamicFieldAjax.slice(); 1773 postData = []; 1774 1775 for ( data in fieldsToProcess ) { 1776 postData.push( fieldsToProcess[ data ].data ); 1777 } 1778 1779 formId = fieldsToProcess[0].args.depFieldArgs.formId; 1780 1781 function processDynamicField( html, depFieldArgs, onCurrentPage ) { 1782 var $fieldDiv, $optContainer, $listInputs, listVal; 1783 1784 if ( onCurrentPage ) { 1785 $fieldDiv = jQuery( '#' + depFieldArgs.containerId ); 1786 addLoadingIcon( $fieldDiv ); 1787 1788 $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); 1789 $optContainer.html( html ); 1790 $listInputs = $optContainer.children( 'input' ); 1791 listVal = $listInputs.val(); 1792 removeLoadingIcon( $optContainer ); 1793 if ( '' === html || '' === listVal ) { 1794 hideDynamicField( depFieldArgs ); 1795 } else { 1796 showDynamicField( depFieldArgs, $fieldDiv, $listInputs, true ); 1797 } 1798 } else { 1799 updateHiddenDynamicListField( depFieldArgs, html ); 1800 } 1801 } 1802 1803 function ajaxHandler( response ) { 1804 var i; 1805 for ( i = 0; i < fieldsToProcess.length; i++ ) { 1806 processDynamicField( 1807 'undefined' === typeof response[ i ] ? '' : response[ i ], 1808 fieldsToProcess[ i ].args.depFieldArgs, 1809 fieldsToProcess[ i ].args.onCurrentPage 1810 ); 1811 } 1812 } 1813 1814 form = getFormById( formId ); 1815 if ( form ) { 1816 postToAjaxUrl( 1817 form, 1818 { 1819 action: 'frm_fields_ajax_get_data_arr', 1820 postData 1821 }, 1822 ajaxHandler, 1823 ( response ) => { 1824 console.error( response ); 1825 }, 1826 { 1827 dataType: 'json' 1828 } 1829 ); 1830 } 1831 } 1832
1833 /** 1834 * Hide or show all instances of a field using the field ID 1835 * 1836 * @param fieldId 1837 * @param triggerFieldRepeatArgs 1838 */ 1839 function hideOrShowFieldById( fieldId, triggerFieldRepeatArgs ) { 1840 var depFieldArgs = getRulesForSingleField( fieldId ); 1841 1842 if ( depFieldArgs === false || depFieldArgs.conditions.length < 1 ) { 1843 // If field has no logic on it, stop now 1844 return; 1845 } 1846 1847 var childFieldDivIds = getAllFieldDivIds( depFieldArgs, triggerFieldRepeatArgs ); 1848 1849 var childFieldNum = childFieldDivIds.length; 1850 for ( var i = 0; i < childFieldNum; i++ ) { 1851 depFieldArgs.containerId = childFieldDivIds[ i ]; 1852 addRepeatRow( depFieldArgs, childFieldDivIds[ i ]); 1853 hideOrShowSingleField( depFieldArgs ); 1854 } 1855 } 1856 1857 /** 1858 * Get all the field divs that should be hidden or shown, regardless of whether they're on the current page 1859 * 1860 * @param {Object} depFieldArgs 1861 * @param {bool} depFieldArgs.isRepeating 1862 * @param {string} depFieldArgs.fieldId 1863 * @param {object} triggerFieldArgs 1864 * @param {string} triggerFieldArgs.repeatingSection 1865 * @param {string} triggerFieldArgs.repeatRow 1866 * @param {string} depFieldArgs.inEmbedForm 1867 * @returns {Array} 1868 */ 1869 function getAllFieldDivIds( depFieldArgs, triggerFieldArgs ) { 1870 var childFieldDivs = []; 1871 1872 if ( depFieldArgs.isRepeating ) { 1873 if ( triggerFieldArgs.repeatingSection !== '' ) { 1874 // If trigger field is repeating/embedded, use its section row in selector 1875 var container = 'frm_field_' + depFieldArgs.fieldId + '-'; 1876 container += triggerFieldArgs.repeatingSection + '-' + triggerFieldArgs.repeatRow + '_container'; 1877 childFieldDivs.push( container ); 1878 } else { 1879 // If trigger field is not repeating/embedded, get all repeating/embedded field divs 1880 childFieldDivs = getAllRepeatingFieldDivIds( depFieldArgs ); 1881 } 1882 } else if ( depFieldArgs.fieldType === 'submit' ) { 1883 childFieldDivs.push( getSubmitButtonContainerID( depFieldArgs ) ); 1884 } else { 1885 childFieldDivs.push( 'frm_field_' + depFieldArgs.fieldId + '_container' ); 1886 } 1887 1888 return childFieldDivs; 1889 } 1890 1891 /** 1892 * Return selector for submit button for use in Conditional Logic. 1893 * 1894 * @param depFieldArgs 1895 * @returns {string} 1896 */ 1897 function getSubmitButtonContainerID( depFieldArgs ) { 1898 return 'frm_form_' + depFieldArgs.formId + '_container .frm_final_submit'; 1899 } 1900 1901 /** 1902 * Get all instances of a repeating field 1903 * 1904 * @since 2.01.0 1905 * @param {Object} depFieldArgs 1906 * @param {string} depFieldArgs.fieldId 1907 */ 1908 function getAllRepeatingFieldDivIds( depFieldArgs ) { 1909 var childFieldDivs = [], 1910 containerFieldId = getContainerFieldId( depFieldArgs ); 1911 1912 // TODO: what if section is inside embedded form? 1913 1914 if ( isFieldDivOnPage( 'frm_field_' + containerFieldId + '_container' ) ) { 1915 childFieldDivs = getRepeatingFieldDivIdsOnCurrentPage( depFieldArgs.fieldId ); 1916 } else { 1917 childFieldDivs = getRepeatingFieldDivIdsAcrossPage( depFieldArgs ); 1918 } 1919 1920 return childFieldDivs; 1921 } 1922 1923 /** 1924 * Get all repeating field divs on the current page 1925 * 1926 * @since 2.02.06 1927 * @param string fieldId 1928 * @returns {Array} 1929 */ 1930 function getRepeatingFieldDivIdsOnCurrentPage( fieldId ) { 1931 var childFieldDivs = [], 1932 childFields = document.querySelectorAll( '.frm_field_' + fieldId + '_container' ); 1933 1934 for ( var i = 0, l = childFields.length; i < l; i++ ) { 1935 childFieldDivs.push( childFields[ i ].id ); 1936 } 1937 1938 return childFieldDivs; 1939 } 1940 1941 /** 1942 * Get the field divs for repeating fields across a page 1943 * 1944 * @param {Object} depFieldArgs 1945 * @param {string} depFieldArgs.fieldId 1946 * @returns {Array} 1947 */ 1948 function getRepeatingFieldDivIdsAcrossPage( depFieldArgs ) { 1949 var childFieldDivs = [], 1950 containerFieldId = getContainerFieldId( depFieldArgs ), 1951 fieldDiv = 'frm_field_' + depFieldArgs.fieldId + '-' + containerFieldId + '-', 1952 allRows = document.querySelectorAll( '[name="item_meta[' + containerFieldId + '][row_ids][]"]' ); 1953 1954 for ( var i = 0, l = allRows.length; i < l; i++ ) { 1955 if ( allRows[ i ].value !== '' ) { 1956 childFieldDivs.push( fieldDiv + allRows[ i ].value + '_container' ); 1957 } 1958 } 1959 1960 if ( childFieldDivs.length < 1 ) { 1961 childFieldDivs.push( fieldDiv + '0_container' ); 1962 } 1963 1964 return childFieldDivs; 1965 } 1966 1967 /** 1968 * 1969 * @param {Object} depFieldArgs 1970 * @param {string} depFieldArgs.inSection 1971 * @param {string} depFieldArgs.inEmbedForm 1972 * @returns {string} 1973 */ 1974 function getContainerFieldId( depFieldArgs ) { 1975 var containerFieldId = '';
1976 if ( depFieldArgs.inEmbedForm !== '0' ) { 1977 containerFieldId = depFieldArgs.inEmbedForm; 1978 } else if ( depFieldArgs.inSection !== '0' ) { 1979 containerFieldId = depFieldArgs.inSection; 1980 } 1981 1982 return containerFieldId; 1983 } 1984 1985 /** 1986 * @param depFieldArgs 1987 * @param {bool} depFieldArgs.isRepeating 1988 * @param childFieldDivId 1989 */ 1990 function addRepeatRow( depFieldArgs, childFieldDivId ) { 1991 if ( depFieldArgs.isRepeating ) { 1992 var divParts = childFieldDivId.replace( '_container', '' ).split( '-' ); 1993 depFieldArgs.repeatRow = divParts[ 2 ]; 1994 } else { 1995 depFieldArgs.repeatRow = ''; 1996 } 1997 } 1998 1999 /** 2000 * @param {object} depFieldArgs 2001 * @return {void} 2002 */ 2003 function hideOrShowSingleField( depFieldArgs ) { 2004 var i, add, 2005 logicOutcomes = [], 2006 len = depFieldArgs.conditions.length; 2007 2008 for ( i = 0; i < len; i++ ) { 2009 add = checkLogicCondition( depFieldArgs.conditions[ i ], depFieldArgs ); 2010 if ( add !== null ) { 2011 // Prevent fields not on the page from being hidden without a chance. 2012 logicOutcomes.push( add ); 2013 } 2014 } 2015 2016 if ( logicOutcomes.length ) { 2017 // Only continue if the field was found on the page. 2018 routeToHideOrShowField( depFieldArgs, logicOutcomes ); 2019 } 2020 } 2021 2022 function getRulesForSingleField( fieldId ) { 2023 if ( typeof __FRMRULES === 'undefined' || __FRMRULES[ fieldId ] === undefined ) { 2024 return false; 2025 } 2026 2027 return __FRMRULES[ fieldId ]; 2028 } 2029 2030 /** 2031 * @param {Array} logicCondition 2032 * @param {string} logicCondition.fieldId 2033 * @param depFieldArgs 2034 * @returns {*} 2035 */ 2036 function checkLogicCondition( logicCondition, depFieldArgs ) { 2037 var fieldId = logicCondition.fieldId, 2038 logicFieldArgs = getRulesForSingleField( fieldId ), 2039 fieldValue = getFieldValue( logicFieldArgs, depFieldArgs ); 2040 2041 if ( fieldValue === null ) { 2042 // The field wasn't found. 2043 return null; 2044 } 2045 2046 return getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs ); 2047 } 2048 2049 /** 2050 * Get the value from any field. 2051 * 2052 * @param {object} logicFieldArgs 2053 * @param {string} logicFieldArgs.inputType 2054 * @param {object} depFieldArgs 2055 * @returns {string} 2056 */ 2057 function getFieldValue( logicFieldArgs, depFieldArgs ) { 2058 var fieldValue = ''; 2059 2060 if ( 'name' === logicFieldArgs.fieldType ) { 2061 fieldValue = getValueFromNameField( logicFieldArgs, depFieldArgs ); 2062 } else if ( logicFieldArgs.inputType === 'radio' || logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) { 2063 fieldValue = getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs ); 2064 } else if ( logicFieldArgs.inputType === 'date' ) { 2065 fieldValue = getValueForDateField( logicFieldArgs, depFieldArgs ); 2066 } else { 2067 fieldValue = getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ); 2068 } 2069 2070 fieldValue = cleanFinalFieldValue( fieldValue ); 2071 2072 return fieldValue; 2073 } 2074 2075 /** 2076 * @since 6.21 2077 * 2078 * @param {object} logicFieldArgs 2079 * @param {object} depFieldArgs 2080 * @return {string} 2081 */ 2082 function getValueForDateField( logicFieldArgs, depFieldArgs ) { 2083 const fieldId = logicFieldArgs.fieldId; 2084 const fieldContainer = document.getElementById( 'frm_field_' + fieldId + '_container' ); 2085 2086 if ( fieldContainer && fieldContainer.querySelector( '.frm_date_inline' ) ) { 2087 const altField = fieldContainer.querySelector( '#field_' + logicFieldArgs.fieldKey + '_alt' ); 2088 if ( altField ) { 2089 return altField.value; 2090 } 2091 } 2092 2093 return getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ); 2094 } 2095 2096 function getValueFromNameField( logicFieldArgs, depFieldArgs ) { 2097 var inputName, inputs, nameValues; 2098 2099 inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ); 2100 inputs = document.querySelectorAll( '[name^="' + inputName + '"]' ); 2101 nameValues = []; 2102 2103 inputs.forEach( 2104 ( input ) => { 2105 nameValues.push( input.value ); 2106 } 2107 ); 2108 2109 return nameValues.join( ' ' ); 2110 } 2111 2112 /** 2113 * Get the value from a Text or Dropdown field 2114 * 2115 * @param logicFieldArgs 2116 * @param {string} logicFieldArgs.fieldKey 2117 * @param {bool} logicFieldArgs.isRepeating 2118 * @param {bool} logicFieldArgs.isMultiSelect 2119 * @param depFieldArgs 2120 * @param {string} depFieldArgs.repeatRow 2121 */ 2122 function getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ) { 2123 var logicFieldValue = ''; 2124 2125 if ( logicFieldArgs.isMultiSelect === true ) { 2126 return getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ); 2127 } 2128 2129 var fieldCall = 'field_' + logicFieldArgs.fieldKey; 2130 if ( logicFieldArgs.isRepeating ) { 2131 // If trigger field is repeating, dependent field is repeating too 2132 fieldCall += '-' + depFieldArgs.repeatRow; 2133 } 2134 2135 var logicFieldInput = document.getElementById( fieldCall ); 2136 2137 if ( logicFieldInput === null ) { 2138 logicFieldValue = parseTimeValue( logicFieldArgs, fieldCall ); 2139 if ( logicFieldValue === '' ) { 2140 // Check for hidden fields. 2141 logicFieldValue = getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ); 2142 } 2143 } else { 2144 logicFieldValue = logicFieldInput.value; 2145 } 2146 2147 return logicFieldValue; 2148 } 2149 2150 function parseTimeValue( logicFieldArgs, fieldCall ) { 2151 var logicFieldValue = ''; 2152 if ( logicFieldArgs.fieldType === 'time' ) { 2153 var hour = document.getElementById( fieldCall + '_H' ); 2154 if ( hour !== null ) { 2155 var minute = document.getElementById( fieldCall + '_m' ); 2156 logicFieldValue = hour.value + ':' + minute.value; 2157 2158 var pm = document.getElementById( fieldCall + '_A' ); 2159 if ( logicFieldValue == ':' ) { 2160 logicFieldValue = ''; 2161 } else if ( pm !== null ) { 2162 logicFieldValue += ' ' + pm.value; 2163 } 2164 } 2165 } 2166 return logicFieldValue; 2167 } 2168 2169 function getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ) { 2170 var inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ), 2171 logicFieldInputs = document.querySelectorAll( '[name^="' + inputName + '"]' ), 2172 selectedVals = []; 2173 2174 // TODO: What about if it's read-only? 2175 2176 if ( logicFieldInputs.length == 1 && logicFieldInputs[ 0 ].type !== 'hidden' ) { 2177 selectedVals = jQuery( '[name^="' + inputName + '"]' ).val(); 2178 if ( selectedVals === null ) { 2179 selectedVals = ''; 2180 } 2181 } else { 2182 selectedVals = getValuesFromCheckboxInputs( logicFieldInputs ); 2183 } 2184 2185 return selectedVals; 2186 } 2187 2188 /** 2189 * Get the value from a Radio or Checkbox field trigger field 2190 * 2191 * @param {Object} logicFieldArgs 2192 * @param {string} logicFieldArgs.inputType 2193 * @param {Object} depFieldArgs 2194 * @returns {String|Array} 2195 */ 2196 function getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs ) { 2197 var logicFieldValue, 2198 inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ), 2199 logicFieldInputs = document.querySelectorAll( 'input[name^="' + inputName + '"]' ); 2200 2201 if ( logicFieldInputs.length === 0 ) { 2202 // Don't continue if the field doesn't exist. 2203 return null; 2204 } 2205 2206 if ( logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) { 2207 logicFieldValue = getValuesFromCheckboxInputs( logicFieldInputs ); 2208 } else { 2209 logicFieldValue = getValueFromRadioInputs( logicFieldInputs ); 2210 } 2211 2212 return logicFieldValue; 2213 } 2214 2215 /** 2216 * Build a logic field's input name 2217 * Does not include full name for checkbox, address, or multi-select fields 2218 * 2219 * @param {object} logicFieldArgs 2220 * @param {boolean} logicFieldArgs.isRepeating 2221 * @param {string} logicFieldArgs.fieldId 2222 * @param {object} depFieldArgs 2223 * @param {string} depFieldArgs.inEmbedForm 2224 * @param {string} depFieldArgs.inSection 2225 * @param {string} depFieldArgs.repeatRow 2226 * @returns {string} 2227 */ 2228 function buildLogicFieldInputName( logicFieldArgs, depFieldArgs ) { 2229 var inputName = ''; 2230 2231 if ( logicFieldArgs.isRepeating ) { 2232 // If the trigger field is repeating, the child must be repeating as well 2233 var sectionId = '';
2234 if ( depFieldArgs.inEmbedForm !== '0' ) { 2235 sectionId = depFieldArgs.inEmbedForm; 2236 } else { 2237 sectionId = depFieldArgs.inSection; 2238 } 2239 var rowId = depFieldArgs.repeatRow; 2240 inputName = 'item_meta[' + sectionId + '][' + rowId + '][' + logicFieldArgs.fieldId + ']'; 2241 } else { 2242 inputName = 'item_meta[' + logicFieldArgs.fieldId + ']'; 2243 } 2244 2245 return inputName; 2246 } 2247 2248 function getValuesFromCheckboxInputs( inputs ) { 2249 var checkedVals = []; 2250 2251 for ( var i = 0, l = inputs.length; i < l; i++ ) { 2252 if ( inputs[ i ].type === 'hidden' || inputs[ i ].checked ) { 2253 checkedVals.push( inputs[ i ].value ); 2254 } else if ( inputs[ i ].dataset.off !== undefined ) { 2255 checkedVals.push( inputs[ i ].dataset.off ); 2256 } 2257 } 2258 2259 if ( checkedVals.length === 0 ) { 2260 checkedVals = false; 2261 } 2262 2263 return checkedVals; 2264 } 2265 2266 function cleanFinalFieldValue( fieldValue ) { 2267 if ( fieldValue === undefined ) { 2268 fieldValue = ''; 2269 } else if ( typeof fieldValue === 'string' ) { 2270 fieldValue = fieldValue.trim(); 2271 } 2272 2273 return fieldValue; 2274 } 2275 2276 /** 2277 * Check whether a particular conditional logic condition is true or false 2278 * 2279 * @param {Array} logicCondition 2280 * @param {operator:string} logicCondition.operator 2281 * @param {value:string} logicCondition.value 2282 * @param {String|Array} fieldValue 2283 * @param {Object} depFieldArgs 2284 * @param {string} depFieldArgs.fieldType 2285 * @param {Object} logicFieldArgs 2286 * @param {fieldType:string} logicFieldArgs.fieldType 2287 * @returns {Boolean} 2288 */ 2289 function getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs ) { 2290 var outcome; 2291 2292 if ( depFieldArgs.fieldType === 'data' && logicFieldArgs.fieldType === 'data' ) { 2293 // If dep field is Dynamic and logic field is Dynamic 2294 outcome = getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs ); 2295 } else { 2296 const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId ); 2297 outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue ); 2298 } 2299 2300 return outcome; 2301 } 2302 2303 /** 2304 * @param {Array} logicCondition 2305 * @param {string} logicCondition.operator 2306 * @param {string} logicCondition.value 2307 * @param {string|Array} fieldValue 2308 * @param {object} depFieldArgs 2309 * @param {Array} depFieldArgs.dataLogic 2310 * @returns {boolean} 2311 */ 2312 function getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs ) { 2313 var outcome = false; 2314 if ( logicCondition.value === '' ) { 2315 // Logic: "Dynamic field is equal to/not equal to anything" 2316 2317 if ( fieldValue === '' || ( fieldValue.length == 1 && fieldValue[ 0 ] === '' ) ) { 2318 outcome = false; 2319 } else { 2320 outcome = true; 2321 } 2322 } else { 2323 const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId ); 2324 // Logic: "Dynamic field is equal to/not equal to specific option" 2325 outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue ); 2326 } 2327 depFieldArgs.dataLogic = logicCondition; 2328 depFieldArgs.dataLogic.actualValue = fieldValue; 2329 2330 return outcome; 2331 } 2332 2333 /** 2334 * @since 6.19 2335 * 2336 * @param {object} logicCondition 2337 * @param {number|string} fieldValue 2338 * @param {string} formId 2339 * @returns {object} 2340 */ 2341 function maybePrepareValuesCompared( logicCondition, fieldValue, formId ) { 2342 let logicValue = logicCondition.value; 2343 if ( 'undefined' === typeof __FRMRULES || 'undefined' === typeof __FRMRULES[ logicCondition.fieldId ] ) { 2344 return { logicValue, fieldValue }; 2345 } 2346 const logicConditionFieldType = __FRMRULES[ logicCondition.fieldId ].fieldType; 2347 if ( 'total' === logicConditionFieldType ) { 2348 const currency = getCurrency( formId ); 2349 logicValue = preparePrice( logicCondition.value, currency ); 2350 fieldValue = preparePrice( fieldValue, currency ); 2351 } else if ( 'date' === logicConditionFieldType ) { 2352 const dateFormat = getDateFormatForField( logicCondition.fieldId ); 2353 if ( false !== dateFormat ) { 2354 logicValue = prepareDate( logicValue, dateFormat ); 2355 fieldValue = prepareDate( fieldValue, dateFormat ); 2356 } 2357 } 2358 return { logicValue, fieldValue }; 2359 } 2360 2361 /** 2362 * Change date to Y-m-d format if the current format is not sortable. 2363 * 2364 * @since 6.21 2365 * 2366 * @param {string} date 2367 * @param {string} format 2368 * @return {string} 2369 */ 2370 function prepareDate( date, format ) { 2371 if ( 'Y-m-d' === format ) { 2372 return date; 2373 } 2374 2375 const possibleSeparators = [ '/', '.', '-' ]; 2376 let separator = false;
2377 possibleSeparators.forEach( ( sep ) => { 2378 if ( format.includes( sep ) ) { 2379 separator = sep; 2380 } 2381 } ); 2382 if ( separator === false ) { 2383 return date; 2384 } 2385 2386 const split = date.split( separator ); 2387 if ( 3 !== split.length ) { 2388 return date; 2389 } 2390 2391 let yearPart, monthPart, dayPart; 2392 2393 switch ( format ) { 2394 case 'Y/m/d': 2395 yearPart = split[ 0 ]; 2396 monthPart = split[ 1 ]; 2397 dayPart = split[ 2 ]; 2398 break; 2399 case 'n/j/Y': 2400 case 'm/d/Y': 2401 monthPart = split[ 0 ]; 2402 dayPart = split[ 1 ]; 2403 yearPart = split[ 2 ]; 2404 break; 2405 case 'd/m/Y': 2406 case 'd.m.Y': 2407 case 'j/m/Y': 2408 case 'j/n/Y': 2409 case 'j-m-Y': 2410 default: 2411 dayPart = split[ 0 ]; 2412 monthPart = split[ 1 ]; 2413 yearPart = split[ 2 ]; 2414 break; 2415 } 2416 2417 if ( yearPart.length !== 4 ) { 2418 return date; 2419 } 2420 2421 if ( monthPart.length < 2 ) { 2422 monthPart = '0' + monthPart; 2423 } 2424 2425 if ( dayPart.length < 2 ) { 2426 dayPart = '0' + dayPart; 2427 } 2428 2429 return yearPart + '-' + monthPart + '-' + dayPart; 2430 } 2431 2432 /** 2433 * @since 6.21 2434 * 2435 * @param {string} fieldId 2436 * @return {string|false} 2437 */ 2438 function getDateFormatForField( fieldId ) { 2439 if ( ! window.__frmDatepicker ) { 2440 return false; 2441 } 2442 2443 let fieldRule = false; 2444 __frmDatepicker.forEach( ( rule ) => { 2445 if ( ! rule.fieldId || rule.fieldId === fieldId ) { 2446 fieldRule = rule; 2447 } 2448 } ); 2449 2450 if ( false === fieldRule ) { 2451 return false; 2452 } 2453 2454 return fieldRule.options.fpDateFormat; 2455 } 2456 2457 function operators( op, a, b ) { 2458 var theOperators; 2459 2460 a = prepareLogicValueForComparison( a ); 2461 b = prepareEnteredValueForComparison( a, b ); 2462 2463 if ( typeof a === 'string' && a.indexOf( '"' ) != '-1' && operators( op, a.replace( '"', '"' ), b ) ) { 2464 return true; 2465 } 2466 2467 theOperators = { 2468 '=='( c, d ) { 2469 return c === d; 2470 }, 2471 '!='( c, d ) { 2472 return c !== d; 2473 }, 2474 '<'( c, d ) { 2475 return c > d; 2476 }, 2477 '<='( c, d ) { 2478 return c >= d; 2479 }, 2480 '>'( c, d ) { 2481 return c < d; 2482 }, 2483 '>='( c, d ) { 2484 return c <= d; 2485 }, 2486 'LIKE'( c, d ) { 2487 if ( ! d ) { 2488 /* If no value, then assume no match */ 2489 return false; 2490 } 2491 2492 c = prepareLogicValueForLikeComparison( c ); 2493 d = prepareEnteredValueForLikeComparison( c, d ); 2494 2495 return d.includes(c); 2496 }, 2497 'not LIKE'( c, d ) { 2498 if ( ! d ) { 2499 /* If no value, then assume no match */ 2500 return true; 2501 } 2502 2503 c = prepareLogicValueForLikeComparison( c ); 2504 d = prepareEnteredValueForLikeComparison( c, d ); 2505 2506 return !d.includes(c); 2507 }, 2508 // Starts with 2509 'LIKE%'( c, d ) { 2510 if ( ! d ) { 2511 /* If no value, then assume no match */ 2512 return false; 2513 } 2514 2515 c = prepareLogicValueForLikeComparison( c ); 2516 d = prepareEnteredValueForLikeComparison( c, d ); 2517 2518 if ( Array.isArray( d ) ) { 2519 // If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match. 2520 return false; 2521 } 2522 2523 return d.substr( 0, c.length ) === c; 2524 }, 2525 // Ends with 2526 '%LIKE'( c, d ) { 2527 if ( ! d ) { 2528 /* If no value, then assume no match */ 2529 return false; 2530 } 2531 2532 c = prepareLogicValueForLikeComparison( c ); 2533 d = prepareEnteredValueForLikeComparison( c, d ); 2534 2535 if ( Array.isArray( d ) ) { 2536 // If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match. 2537 return false; 2538 } 2539 2540 return d.substr( -c.length ) === c; 2541 } 2542 }; 2543 2544 if ( 'function' !== typeof theOperators[ op ]) { 2545 op = '=='; // Fallback to Equals to. 2546 } 2547 2548 return theOperators[ op ]( a, b ); 2549 } 2550 2551 function prepareLogicValueForComparison( a ) { 2552 if ( shouldParseFloat( a ) ) { 2553 a = parseFloat( a ); 2554 } else if ( typeof a === 'string' ) { 2555 a = a.trim(); 2556 } 2557 2558 return a; 2559 } 2560 2561 /** 2562 * @param {string} value 2563 * @returns {bool} 2564 */ 2565 function shouldParseFloat( value ) { 2566 return String( value ).search( /^\s*(\+|-)?((\d+(\.\d+)?)|(\.\d+))\s*$/ ) !== -1; 2567 } 2568 2569 function prepareEnteredValueForComparison( a, b ) { 2570 if ( b === undefined || b === null || b === false ) { 2571 b = ''; 2572 } 2573 2574 if ( Array.isArray( b ) && jQuery.inArray( String( a ), b ) > -1 ) { 2575 b = a; 2576 } 2577 2578 if ( typeof a === 'number' && typeof b === 'string' && shouldParseFloat( b ) ) { 2579 b = parseFloat( b ); 2580 } 2581 2582 if ( typeof b === 'string' ) { 2583 b = b.trim(); 2584 } 2585 2586 return b; 2587 } 2588 2589 function prepareLogicValueForLikeComparison( val ) { 2590 return prepareValueForLikeComparison( val ); 2591 } 2592 2593 function prepareEnteredValueForLikeComparison( logicValue, enteredValue ) { 2594 enteredValue = prepareValueForLikeComparison( enteredValue ); 2595 2596 var currentValue = ''; 2597 if ( Array.isArray( enteredValue ) ) { 2598 for ( var i = 0, l = enteredValue.length; i < l; i++ ) { 2599 currentValue = enteredValue[ i ].toLowerCase(); 2600 if ( currentValue.includes(logicValue) ) { 2601 enteredValue = logicValue; 2602 break; 2603 } 2604 } 2605 } 2606 2607 return enteredValue; 2608 } 2609 2610 function prepareValueForLikeComparison( val ) { 2611 if ( typeof val === 'string' ) { 2612 val = val.toLowerCase(); 2613 } else if ( typeof val === 'number' ) { 2614 val = val.toString(); 2615 } 2616 return val; 2617 } 2618 2619 /** 2620 * 2621 * @param {Object} depFieldArgs 2622 * @param {string} depFieldArgs.containerId 2623 * @param {string} depFieldArgs.fieldType 2624 * @param logicOutcomes 2625 */ 2626 function routeToHideOrShowField( depFieldArgs, logicOutcomes ) { 2627 var onCurrentPage, 2628 action = getHideOrShowAction( depFieldArgs, logicOutcomes ); 2629
2630 if ( depFieldArgs.fieldType === 'submit' ) { 2631 onCurrentPage = isSubmitButtonOnPage( depFieldArgs.containerId ); 2632 } else { 2633 onCurrentPage = isFieldDivOnPage( depFieldArgs.containerId ); 2634 } 2635 2636 if ( action === 'show' ) { 2637 if ( depFieldArgs.fieldType === 'data' && depFieldArgs.hasOwnProperty( 'dataLogic' ) ) { 2638 // Only update dynamic field options/value if it is dependent on another Dynamic field 2639 updateDynamicField( depFieldArgs, onCurrentPage ); 2640 } else { 2641 showFieldAndSetValue( depFieldArgs, onCurrentPage ); 2642 } 2643 } else { 2644 hideFieldAndClearValue( depFieldArgs, onCurrentPage ); 2645 } 2646 } 2647 2648 function isFieldDivOnPage( containerId ) { 2649 var fieldDiv = document.getElementById( containerId ); 2650 2651 return fieldDiv !== null; 2652 } 2653 2654 /** 2655 * Checks if specified submit button is on the current page. 2656 * 2657 * @param depFieldArgs 2658 * @returns {boolean} 2659 */ 2660 function isSubmitButtonOnPage( container ) { 2661 var submitButton = document.querySelector( '#' + container ); 2662 2663 return submitButton != null; 2664 } 2665 2666 /** 2667 * @param {Object} depFieldArgs 2668 * @param {string} depFieldArgs.anyAll 2669 * @param {string} depFieldArgs.showHide 2670 * @param {Array} logicOutcomes 2671 * @returns {string} 2672 */ 2673 function getHideOrShowAction( depFieldArgs, logicOutcomes ) { 2674 if ( depFieldArgs.anyAll === 'any' ) { 2675 // If any of the following match logic 2676 if ( logicOutcomes.includes(true) ) { 2677 action = depFieldArgs.showHide; 2678 } else { 2679 action = reverseAction( depFieldArgs.showHide ); 2680 } 2681 } else { 2682 // If all of the following match logic 2683 if ( logicOutcomes.includes(false) ) { 2684 action = reverseAction( depFieldArgs.showHide ); 2685 } else { 2686 action = depFieldArgs.showHide; 2687 } 2688 } 2689 2690 return action; 2691 } 2692 2693 function reverseAction( action ) { 2694 if ( action === 'show' ) { 2695 action = 'hide'; 2696 } else { 2697 action = 'show'; 2698 } 2699 return action; 2700 } 2701 2702 /** 2703 * @param {Object} depFieldArgs 2704 * @param {string} depFieldArgs.containerId 2705 * @param {string} depFieldArgs.formId 2706 * @param {bool} onCurrentPage 2707 */ 2708 function showFieldAndSetValue( depFieldArgs, onCurrentPage ) { 2709 if ( isFieldCurrentlyShown( depFieldArgs.containerId, depFieldArgs.formId ) ) { 2710 return; 2711 } 2712 2713 removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); 2714 2715 if ( depFieldArgs.fieldType === 'submit' ) { 2716 if ( onCurrentPage ) { 2717 showOrEnableSubmitButton( depFieldArgs ); 2718 } 2719 return; 2720 } 2721 if ( onCurrentPage ) { 2722 // Set value, then show field 2723 setValuesInsideFieldOnPage( depFieldArgs.containerId, depFieldArgs ); 2724 showFieldContainer( depFieldArgs.containerId ); 2725 triggerEvent( document, 'frmShowField' ); 2726 if ( depFieldArgs.inputType === 'rte' ) { 2727 reInitializeRichText( 'field_' + depFieldArgs.fieldKey ); 2728 } 2729 } else { 2730 setValuesInsideFieldAcrossPage( depFieldArgs ); 2731 } 2732 } 2733 2734 /** 2735 * @param {string} fieldId 2736 */ 2737 function reInitializeRichText( fieldId ) { 2738 var isVisible = 'undefined' !== typeof tinyMCE.editors[ fieldId ] && ! tinyMCE.editors[ fieldId ].isHidden(); 2739 if ( ! isVisible ) { 2740 return; 2741 } 2742 removeRichText( fieldId ); 2743 initRichText( fieldId ); 2744 } 2745 2746 /** 2747 * Show or enable submit button, as appropriate based on submit Conditional Logic. 2748 * 2749 * @param depFieldArgs 2750 */ 2751 function showOrEnableSubmitButton( depFieldArgs ) { 2752 if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) { 2753 enableButton( '#' + depFieldArgs.containerId ); 2754 } else { 2755 showFieldContainer( depFieldArgs.containerId ); 2756 } 2757 2758 removeSubmitButtonFromHiddenList( depFieldArgs ); 2759 } 2760 2761 /** 2762 * Remove submit button from list of hidden submit buttons on page 2763 * 2764 * @param depFieldArgs 2765 */ 2766 function removeSubmitButtonFromHiddenList( depFieldArgs ) { 2767 hiddenSubmitButtons = hiddenSubmitButtons.filter( ( button ) => { 2768 return button !== depFieldArgs.formKey; 2769 }); 2770 } 2771 2772 /** 2773 * Enable button with given selector. 2774 * 2775 * @param {string} buttonSelector 2776 * 2777 * @returns {void} 2778 */ 2779 function enableButton( buttonSelector ) { 2780 var button = document.querySelector( buttonSelector ); 2781 if ( button && ! button.closest( '.frm_loading_form' ) ) { 2782 button.disabled = false;
2783 } 2784 } 2785 2786 /** 2787 * Set the value for all inputs inside of a field div on the current page 2788 * 2789 * @param {string} container 2790 * @param {Object} depFieldArgs 2791 * @param {string} depFieldArgs.fieldType 2792 * @param {string} depFieldArgs.formId 2793 */ 2794 function setValuesInsideFieldOnPage( container, depFieldArgs ) { 2795 var inputs = getInputsInFieldOnPage( container ), 2796 inContainer = ( depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' ); 2797 2798 setValueForInputs( inputs, inContainer, depFieldArgs.formId, 'required' ); 2799 } 2800 2801 /** 2802 * Set the value for all inputs inside of a field across a page 2803 * 2804 * @param {Object} depFieldArgs 2805 * @param {string} depFieldArgs.fieldType 2806 * @param {string} depFieldArgs.formId 2807 */ 2808 function setValuesInsideFieldAcrossPage( depFieldArgs ) { 2809 var inputs = getInputsInFieldAcrossPage( depFieldArgs ), 2810 inContainer = depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' || depFieldArgs.isRepeating; 2811 2812 setValueForInputs( inputs, inContainer, depFieldArgs.formId ); 2813 } 2814 2815 /**
2816 * Gets inputs in field on page. 2817 * 2818 * @since 5.4 Accepted HTML element as param. 2819 * 2820 * @param {String|HTMLElement} containerId ID of container or container element. 2821 * @return {Array} 2822 */ 2823 function getInputsInFieldOnPage( containerId ) { 2824 var container = 'string' === typeof containerId ? document.getElementById( containerId ) : containerId; 2825 return container.querySelectorAll( 'select[name^="item_meta"], textarea[name^="item_meta"], input[name^="item_meta"]' ); 2826 } 2827 2828 /** 2829 * @param {Object} depFieldArgs 2830 * @param {string} depFieldArgs.fieldType 2831 * @returns {Array} 2832 */ 2833 function getInputsInFieldAcrossPage( depFieldArgs ) { 2834 var inputs = []; 2835 2836 if ( depFieldArgs.fieldType === 'divider' ) { 2837 inputs = getInputsInHiddenSection( depFieldArgs ); 2838 } else if ( depFieldArgs.fieldType === 'form' ) { 2839 inputs = getInputsInHiddenEmbeddedForm( depFieldArgs ); 2840 } else { 2841 inputs = getHiddenInputs( depFieldArgs ); 2842 } 2843 2844 return inputs; 2845 } 2846 2847 /** 2848 * Get the inputs for a non-repeating field that is type=hidden 2849 * @param {object} depFieldArgs 2850 * @param {bool} depFieldArgs.isRepeating 2851 * @param {string} depFieldArgs.inSection 2852 * @param {string} depFieldArgs.repeatRow 2853 * @param {string} depFieldArgs.fieldId 2854 * @returns {NodeList} 2855 */ 2856 function getHiddenInputs( depFieldArgs ) { 2857 var name = ''; 2858 if ( depFieldArgs.isRepeating ) { 2859 //item_meta[section-id][row-id][field-id] 2860 var containerFieldId = getContainerFieldId( depFieldArgs ); 2861 name = 'item_meta[' + containerFieldId + '][' + depFieldArgs.repeatRow + '][' + depFieldArgs.fieldId + ']'; 2862 } else { 2863 // item_meta[field-id] 2864 name = 'item_meta[' + depFieldArgs.fieldId + ']'; 2865 } 2866 return document.querySelectorAll( '[name^="' + name + '"]' ); 2867 } 2868 2869 function setValueForInputs( inputs, inContainer, formId, setRequired ) { 2870 var input, prevInput, i; 2871 2872 if ( ! inputs.length ) { 2873 return; 2874 } 2875 2876 for ( i = 0; i < inputs.length; i++ ) { 2877 input = inputs[ i ]; 2878 2879 // Don't set the value if the field is in a section and it's conditionally hidden 2880 if ( inContainer && isChildInputConditionallyHidden( input, formId ) ) { 2881 continue; 2882 } 2883 2884 if ( setRequired === 'required' ) { 2885 maybeAddRequiredTag( input ); 2886 } 2887 2888 if ( skipSetValue( i, prevInput, inputs ) ) { 2889 continue; 2890 } 2891 2892 setDefaultValue( input, inContainer ); 2893 maybeSetWatchingFieldValue( input ); 2894 setShownProduct( input ); 2895 maybeDoCalcForSingleField( input ); 2896 2897 prevInput = input; 2898 } 2899 } 2900 2901 /** 2902 * When a field is shown with logic, add the html and aria 2903 * required attributes if the field is required. 2904 */ 2905 function maybeAddRequiredTag( input ) { 2906 var isRequired, isOptional; 2907 2908 if ( input.type === 'checkbox' || input.type === 'radio' || input.type === 'file' ) { 2909 return; 2910 } 2911 2912 isRequired = input.parentElement.className.indexOf( 'frm_required_field' ); 2913 isOptional = input.className.indexOf( 'frm_optional' ); 2914 2915 if ( isRequired > -1 && isOptional === -1 ) { 2916 input.setAttribute( 'aria-required', true ); 2917 } 2918 } 2919 2920 /** 2921 * Don't loop through every input in a radio/checkbox/other field 2922 * TODO: Improve this for checkboxes and address fields 2923 */ 2924 function skipSetValue( i, prevInput, inputs ) { 2925 var typeArray = [ 'checkbox', 'radio' ]; 2926 2927 if ( i < 1 || prevInput === undefined ) { 2928 return false; 2929 } 2930 2931 if ( null !== inputs[ i ].getAttribute( 'data-frmprice' ) ) { 2932 // never skip calculating a product price. 2933 return false; 2934 } 2935 2936 var isOther = inputs[ i ].className.includes('frm_other_input'); 2937 return isOther || ( prevInput.name == inputs[ i ].name && typeArray.includes(prevInput.type) ); 2938 } 2939 2940 // Check if a field input inside of a section or embedded form is conditionally hidden 2941 function isChildInputConditionallyHidden( input, formId ) { 2942 var fieldDivPart = frmFrontForm.getFieldId( input, true ), 2943 fieldDivId = 'frm_field_' + fieldDivPart + '_container'; 2944 2945 return isFieldConditionallyHidden( fieldDivId, formId ); 2946 } 2947 2948 function showFieldContainer( containerId ) { 2949 var $container = jQuery( '#' + containerId ).show(); 2950 if ( $container.hasClass( 'frm_inside_container' ) && null === $container.find( 'select' ).val() ) { 2951 $container.find( 'select' ).val( '' ).trigger( 'change' ); 2952 } 2953 } 2954
2955 /** 2956 * @param {Object} depFieldArgs 2957 * @param {string} depFieldArgs.containerId 2958 * @param {string} depFieldArgs.formId 2959 * @param onCurrentPage 2960 */ 2961 function hideFieldAndClearValue( depFieldArgs, onCurrentPage ) { 2962 if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { 2963 return; 2964 } 2965 2966 addToHideFields( depFieldArgs.containerId, depFieldArgs.formId ); 2967 2968 if ( depFieldArgs.fieldType === 'submit' ) { 2969 if ( onCurrentPage ) { 2970 hideOrDisableSubmitButton( depFieldArgs ); 2971 } 2972 return; 2973 } 2974 2975 if ( onCurrentPage ) { 2976 hideFieldContainer( depFieldArgs.containerId ); 2977 clearInputsInFieldOnPage( depFieldArgs.containerId ); 2978 } else { 2979 clearInputsInFieldAcrossPage( depFieldArgs ); 2980 } 2981 } 2982 2983 /** 2984 * Hide or disable given submit button as appropriate, based on submit Conditional Logic 2985 * 2986 * @param depFieldArgs 2987 */ 2988 function hideOrDisableSubmitButton( depFieldArgs ) { 2989 if ( depFieldArgs.containerId == undefined ) { 2990 depFieldArgs.containerId = getSubmitButtonContainerID( depFieldArgs ); 2991 } 2992 2993 addSubmitButtonToHiddenList( depFieldArgs ); 2994 2995 if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) { 2996 disableButton( '#' + depFieldArgs.containerId ); 2997 } else { 2998 hideFieldContainer( depFieldArgs.containerId ); 2999 } 3000 } 3001 3002 /** 3003 * Add submit button to list of hidden submit buttons on page 3004 * 3005 * @param depFieldArgs 3006 */ 3007 function addSubmitButtonToHiddenList( depFieldArgs ) { 3008 hiddenSubmitButtons.push( depFieldArgs.formKey ); 3009 } 3010 3011 /** 3012 * Checks if a particular submit button on the current page is hidden 3013 * 3014 * @param {string} formKey 3015 * @returns {boolean} 3016 */ 3017 function isOnPageSubmitButtonHidden( formKey ) { 3018 return hiddenSubmitButtons.includes(formKey); 3019 } 3020 3021 /** 3022 * Hide submit button with specified container ID that was previously hidden. 3023 * 3024 * @param submitContainerID 3025 */ 3026 function hidePreviouslyHiddenSubmitButton( submitContainerID ) { 3027 var formId = submitContainerID.replace( 'frm_form_', '' ); 3028 formId = formId.replace( '_container .frm_final_submit', '' ); 3029 3030 var depFieldArgs = getRulesForSingleField( 'submit_' + formId ); 3031 if ( depFieldArgs ) { 3032 hideOrDisableSubmitButton( depFieldArgs ); 3033 } 3034 } 3035 3036 /** 3037 * Get the form key (e.g. t3cq5) from the form element id (e.g. form_t3cq5) 3038 * 3039 * @param elementId 3040 * @returns {string} form key 3041 */ 3042 function getFormKeyFromFormElementID( elementId ) { 3043 return elementId.replace( 'form_', '' ); 3044 } 3045 3046 function hideFieldContainer( containerId ) { 3047 jQuery( '#' + containerId ).hide(); 3048 } 3049 3050 /** 3051 * Disable button with specified selector 3052 * 3053 * @param buttonSelector 3054 */ 3055 function disableButton( buttonSelector ) { 3056 jQuery( buttonSelector ).prop( 'disabled', true ); 3057 } 3058 3059 function jsonParse( str ) { 3060 try { 3061 var obj = JSON.parse( str ); 3062 return obj; 3063 } catch ( e ) { 3064 return false; 3065 } 3066 } 3067 3068 function clearInputsInFieldOnPage( containerId ) { 3069 var inputs = getInputsInFieldOnPage( containerId );
3070 clearValueForInputs( inputs, 'required' ); 3071 } 3072 3073 function clearInputsInFieldAcrossPage( depFieldArgs ) { 3074 var inputs = getInputsInFieldAcrossPage( depFieldArgs ); 3075 clearValueForInputs( inputs ); 3076 } 3077 3078 /** 3079 * Get all the child inputs in a hidden section (across a page) 3080 * 3081 * @param {Object} depFieldArgs 3082 * @param {string} depFieldArgs.fieldType 3083 * @param {string} depFieldArgs.fieldId 3084 */ 3085 function getInputsInHiddenSection( depFieldArgs ) { 3086 // If a section, get all inputs with data attribute 3087 var inputs = []; 3088 if ( depFieldArgs.fieldType === 'divider' ) { 3089 inputs = document.querySelectorAll( '[data-sectionid="' + depFieldArgs.fieldId + '"]' ); 3090 } 3091 3092 return inputs; 3093 } 3094 3095 // Get all the child inputs in a hidden embedded form (across a page) 3096 function getInputsInHiddenEmbeddedForm( depFieldArgs ) { 3097 // TODO: maybe remove form and [0]? 3098 // TODO: what if someone is using embeddedformfieldkey-moretext as their field key? Add data attribute for this 3099 return document.querySelectorAll( '[id^="field_' + depFieldArgs.fieldKey + '-"]' ); 3100 } 3101 3102 /** 3103 * Changes dropdown placeholder option color. 3104 * 3105 * @since 6.16.1 3106 * 3107 * @param {HTMLElement} select 3108 * 3109 * @returns {Void} 3110 */ 3111 function changeSelectColor( select ) { 3112 if ( select.options[select.selectedIndex]?.classList.contains( 'frm-select-placeholder' ) ) { 3113 const styleElement = select.closest( '.with_frm_style' ); 3114 const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : ''; 3115 select.style.setProperty( 'color', textColorDisabled, 'important' ); 3116 } 3117 } 3118 3119 /** 3120 * Clears value of field inputs. 3121 * 3122 * @since 5.4 Added the third param. 3123 * 3124 * @param {Array} inputs Array of inputs. 3125 * @param {String} required Required string. 3126 * @param {Boolean} resetToDefault Is `true` if reset to default value. Otherwise, reset to empty value. 3127 */ 3128 function clearValueForInputs( inputs, required, resetToDefault ) { 3129 var prevInput, blankSelect, valueChanged, l, i, input, defaultVal, reset, linkedRadioInput; 3130 3131 if ( inputs.length < 1 ) { 3132 return; 3133 } 3134 3135 valueChanged = true; 3136 3137 l = inputs.length; 3138 for ( i = 0; i < l; i++ ) { 3139 input = inputs[ i ]; 3140 3141 defaultVal = input.getAttribute( 'data-frmval' ); 3142 reset = resetToDefault && defaultVal; 3143 3144 // Don't remove values from some fields. 3145 if ( input.className.includes('frm_dnc') || input.name.includes('[row_ids]') ) { 3146 prevInput = input; 3147 continue; 3148 } 3149 3150 if ( i > 0 && prevInput.name != input.name && valueChanged === true ) { 3151 // Only trigger a change after all inputs in a field are cleared 3152 triggerChange( jQuery( prevInput ) ); 3153 } 3154 3155 valueChanged = true; 3156 3157 if ( input.type === 'radio' || input.type === 'checkbox' ) { 3158 if ( ! reset ) { 3159 input.checked = false; 3160 } else if ( 'radio' === input.type ) { 3161 input.checked = defaultVal === input.value; 3162 } else { 3163 resetCheckboxInputToValue( input, defaultVal ); 3164 } 3165 3166 maybeClearStarRatingInput( input ); 3167 } else if ( input.tagName === 'SELECT' ) { 3168 if ( isSlimSelect( input ) ) { 3169 setSlimValue( input, reset ? defaultVal : '' ); 3170 } else { 3171 if ( ! reset && resetToDefault && input.getAttribute( 'data-placeholder' ) ) { 3172 // When a dropdown has no default value, a placeholder option is added. 3173 // We want to reset to that, as it works the same as an empty default value. 3174 reset = true; 3175 defaultVal = ''; 3176 } 3177 3178 if ( ! reset ) { 3179 blankSelect = input.selectedIndex === 0 && input.options[ 0 ].text.trim() === ''; 3180 if ( blankSelect || ( input.selectedIndex === -1 ) ) { 3181 valueChanged = false; 3182 } else { 3183 input.selectedIndex = -1; 3184 } 3185 } else { 3186 valueChanged = resetSelectInputToValue( input, defaultVal ); 3187 } 3188 3189 const chosenId = input.id.replace( /[^\w]/g, '_' ); // Match what the script is doing. 3190 const autocomplete = document.getElementById( chosenId + '_chosen' ); 3191 if ( autocomplete !== null ) { 3192 jQuery( input ).trigger( 'chosen:updated' ); 3193 } 3194 } 3195 } else if ( input.type === 'range' ) { 3196 let sliderDefault = defaultVal; 3197 if ( ! reset ) { 3198 // Reset to mid value 3199 const min = parseFloat( input.getAttribute( 'min' ) ); 3200 const max = parseFloat( input.getAttribute( 'max' ) ); 3201 const mid = ( ( max - min ) / 2 ) + min; 3202 const step = parseFloat( input.getAttribute( 'step' ), 10 ); 3203 const midSteps = Math.round( mid / step ); 3204 sliderDefault = midSteps * step; 3205 input.parentElement.querySelector( '.frm_range_value' ).textContent = sliderDefault; 3206 } 3207 input.value = sliderDefault; 3208 initRangeInput( [ input ] ); 3209 } else if ( input.getAttribute( 'data-frmprice' ) !== null ) { 3210 setHiddenProduct( input ); 3211 } else { 3212 if ( ! reset ) { 3213 input.value = ''; 3214 } else { 3215 input.value = defaultVal; 3216 } 3217 3218 linkedRadioInput = input.id.includes('-otext') ? document.getElementById( input.id.replace( '-otext', '' ) ) : null; 3219 if ( linkedRadioInput && linkedRadioInput.checked === false ) { 3220 input.classList.add( 'frm_pos_none' ); 3221 } 3222 3223 if ( null !== input.getAttribute( 'data-frmfile' ) ) { 3224 clearDropzoneFiles( input ); 3225 } 3226 if ( input.type === 'hidden' && input.closest( '.frm_range_container ' ) ) { 3227 resetRangeInput( input ); 3228 } 3229 } 3230 3231 if ( required === 'required' ) { 3232 input.required = false;
3233 input.setAttribute( 'aria-required', false ); 3234 } 3235 3236 prevInput = inputs[ i ]; 3237 } 3238 3239 // trigger a change for the final input in the loop 3240 if ( valueChanged === true ) { 3241 triggerChange( jQuery( prevInput ) ); 3242 } 3243 } 3244 3245 function resetRangeInput( input ) { 3246 const rangeContainer = input.parentElement; 3247 const rangeSliderWrapperClone = rangeContainer.querySelector( '.frm-slider-wrapper' ).cloneNode( true ); 3248 // Replace old range slider elements with cloned ones to prevent jerky behavior after reset when initializeRangeSlider is called. 3249 rangeContainer.querySelector( 'div:last-of-type' ).replaceChild( rangeSliderWrapperClone, rangeContainer.querySelector( '.frm-slider-wrapper' ) ); 3250 initializeRangeSlider( input, rangeContainer ); 3251 } 3252 3253 function setSlimValue( input, value ) { 3254 if ( value.length && '[' === value[0] && -1 !== value.indexOf( ',' ) && ']' === value[ value.length - 1 ] && 'multiple' === input.getAttribute( 'multiple' ) ) { 3255 // Array values are stored as a JSON string. 3256 value = JSON.parse( value ); 3257 } 3258 input.slim.setSelected( value ); 3259 } 3260 3261 /** 3262 * Maybe clear star rating. 3263 * 3264 * @since 5.4 3265 * 3266 * @param {HTMLElement} input This should be the last input in star rating. 3267 */ 3268 function maybeClearStarRatingInput( input ) { 3269 var starGroup, checkedInput; 3270 if ( 'radio' !== input.type || ! input.matches( '.frm-star-group input:last-of-type' ) ) { 3271 return; 3272 } 3273 3274 starGroup = input.closest( '.frm-star-group' ); 3275 checkedInput = starGroup.querySelector( 'input:checked' ); 3276 if ( checkedInput ) { 3277 updateStars( checkedInput ); 3278 } else { 3279 clearStars( starGroup, true ); 3280 } 3281 } 3282 3283 /** 3284 * Resets checkbox input to a value. 3285 * 3286 * @since 5.4 3287 * 3288 * @param {HTMLElement} input Checkbox element. 3289 * @param {Object} val The value. 3290 */ 3291 function resetCheckboxInputToValue( input, val ) { 3292 var i; 3293 3294 val = jsonParse( val ); 3295 3296 if ( ! val ) { 3297 return; 3298 } 3299 3300 for ( i in val ) { 3301 if ( val[ i ] === input.value ) { 3302 input.checked = true; 3303 return; 3304 } 3305 } 3306 3307 input.checked = false; 3308 } 3309 3310 /** 3311 * Resets select input to a value. 3312 * 3313 * @since 5.4 3314 * 3315 * @param {HTMLElement} input Select element. 3316 * @param {String|Object} val The value. Is object if multiselect. 3317 * @return {Boolean} Return `true` if value is changed. 3318 */ 3319 function resetSelectInputToValue( input, val ) { 3320 if ( input.multiple ) { 3321 return resetMultiSelectInputToValue( input, val ); 3322 } 3323 3324 var i, 3325 valueChanged = false, 3326 options = input.querySelectorAll( 'option' ); 3327 3328 for ( i = 0; i < options.length; i++ ) { 3329 if ( val === options[ i ].value && ! options[ i ].selected ) { 3330 options[ i ].selected = true; 3331 valueChanged = true; 3332 continue; 3333 } 3334 3335 if ( val !== options[ i ].value && options[ i ].selected ) { 3336 options[ i ].selected = false; 3337 valueChanged = true; 3338 } 3339 } 3340 3341 return valueChanged; 3342 } 3343 3344 /** 3345 * Resets multiselect input to a value. 3346 * 3347 * @since 5.4 3348 * 3349 * @param {HTMLElement} input Select element. 3350 * @param {Object} val The value. 3351 * @return {Boolean} Return `true` if value is changed. 3352 */ 3353 function resetMultiSelectInputToValue( input, val ) { 3354 val = jsonParse( val ); 3355 3356 if ( ! val ) { 3357 return false; 3358 } 3359 3360 var i, contained, 3361 valueChanged = false, 3362 options = input.querySelectorAll( 'option' ); 3363 3364 for ( i = 0; i < options.length; i++ ) { 3365 contained = objectContainValue( val, options[ i ].value ); 3366 if ( contained && ! options[ i ].selected ) { 3367 options[ i ].selected = true; 3368 valueChanged = true; 3369 continue; 3370 } 3371 3372 if ( ! contained && options[ i ].selected ) { 3373 options[ i ].selected = false; 3374 valueChanged = true; 3375 } 3376 } 3377 3378 return valueChanged; 3379 } 3380 3381 /** 3382 * Checks if object contains a value. 3383 * 3384 * @since 5.4 3385 * 3386 * @param {Object} obj Object. 3387 * @param {Mixed} val Value. 3388 * @return {Boolean} 3389 */ 3390 function objectContainValue( obj, val ) { 3391 var x; 3392 3393 for ( x in obj ) { 3394 if ( obj[ x ] === val ) { 3395 return true; 3396 } 3397 } 3398 3399 return false; 3400 } 3401 3402 function clearDropzoneFiles( hiddenFileIdField ) { 3403 var dropzoneElement = hiddenFileIdField.nextElementSibling; 3404 if ( dropzoneElement && -1 !== dropzoneElement.className.indexOf( 'frm_dropzone' ) && 'object' === typeof dropzoneElement.dropzone && 'function' === typeof dropzoneElement.dropzone.removeAllFiles ) { 3405 dropzoneElement.dropzone.removeAllFiles( true ); 3406 } 3407 } 3408 3409 function isFieldCurrentlyShown( containerId, formId ) { 3410 return isFieldConditionallyHidden( containerId, formId ) === false; 3411 } 3412 3413 function isFieldConditionallyHidden( containerId, formId ) { 3414 var hidden = false, 3415 hiddenFields = getHiddenFields( formId ); 3416 3417 if ( hiddenFields.includes(containerId) ) { 3418 hidden = true; 3419 } 3420 3421 return hidden; 3422 } 3423 3424 function clearHideFields() { 3425 var hideFieldInputs = document.querySelectorAll( '[id^="frm_hide_fields_"]' ); 3426 clearValueForInputs( hideFieldInputs ); 3427 } 3428 3429 function addToHideFields( htmlFieldId, formId ) { //TODO: why is this run on submit? 3430 // Get all currently hidden fields 3431 var hiddenFields = getHiddenFields( formId ); 3432 3433 if ( hiddenFields.includes(htmlFieldId) ) { 3434 // If field id is already in the array, move on 3435 } else { 3436 // Add new conditionally hidden field to array 3437 hiddenFields.push( htmlFieldId ); 3438 3439 // Set the hiddenFields value in the frm_hide_field_formID input 3440 hiddenFields = JSON.stringify( hiddenFields ); 3441 var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); 3442 if ( frmHideFieldsInput !== null ) { 3443 frmHideFieldsInput.value = hiddenFields; 3444 } 3445 } 3446 } 3447 3448 function getAllHiddenFields() { 3449 var formId, i, 3450 hiddenFields = [], 3451 hideFieldInputs = document.querySelectorAll( '*[id^="frm_hide_fields_"]' ), 3452 formTotal = hideFieldInputs.length; 3453 3454 for ( i = 0; i < formTotal; i++ ) { 3455 formId = hideFieldInputs[ i ].id.replace( 'frm_hide_fields_', '' );
3456 hiddenFields = hiddenFields.concat( getHiddenFields( formId ) ); 3457 } 3458 3459 return hiddenFields; 3460 } 3461 3462 function getHiddenFields( formId ) { 3463 var hiddenFields = []; 3464 3465 // Fetch the hidden fields from the frm_hide_fields_formId input 3466 var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); 3467 if ( frmHideFieldsInput === null ) { 3468 return hiddenFields; 3469 } 3470 3471 hiddenFields = frmHideFieldsInput.value; 3472 if ( hiddenFields ) { 3473 hiddenFields = JSON.parse( hiddenFields ); 3474 } else { 3475 hiddenFields = []; 3476 } 3477 3478 return hiddenFields; 3479 } 3480 3481 function setDefaultValue( input, inContainer ) { 3482 var placeholder, isMultipleSelect, 3483 $input = jQuery( input ), 3484 defaultValue = $input.data( 'frmval' ); 3485 3486 if ( defaultValue === undefined && input.classList.contains( 'wp-editor-area' ) ) { 3487 // set value in rich text 3488 var defaultField = document.getElementById( input.id + '-frmval' ); 3489 if ( defaultField !== null ) { 3490 defaultValue = defaultField.value; 3491 var targetTinyMceEditor = tinymce.get( input.id ); 3492 if ( null !== targetTinyMceEditor ) { 3493 targetTinyMceEditor.setContent( defaultValue ); 3494 } 3495 } 3496 } else if ( defaultValue === undefined && input.type === 'hidden' ) { 3497 //with read only select input, value is in sibling select 3498 var $select = $input.next( 'select[disabled]' ); 3499 if ( $select.length > 0 ) { 3500 defaultValue = $select.data( 'frmval' ); 3501 } 3502 } 3503 3504 placeholder = defaultValue; 3505 defaultValue = setDropdownPlaceholder( defaultValue, input ); 3506 if ( placeholder !== defaultValue ) { 3507 placeholder = true; 3508 } 3509 3510 if ( defaultValue !== undefined ) { 3511 var numericKey = new RegExp( /\[\d*\]$/i ); 3512 3513 if ( input.type === 'checkbox' || input.type === 'radio' ) { 3514 setCheckboxOrRadioDefaultValue( input.name, defaultValue ); 3515 3516 } else if ( input.type === 'hidden' && input.name.includes('[]') ) { 3517 setHiddenCheckboxDefaultValue( input.name, defaultValue ); 3518 3519 // Set for hidden checkbox fields that aren't on the current page. Skip hidden fields in a repeater. 3520 } else if ( ! inContainer && input.type === 'hidden' && input.name.includes('][') && numericKey.test( input.name ) ) { 3521 setHiddenCheckboxDefaultValue( input.name.replace( numericKey, '' ), defaultValue ); 3522 3523 } else { 3524 isMultipleSelect = false; 3525 3526 if ( placeholder && 'boolean' !== typeof placeholder && input.tagName === 'SELECT' && -1 !== input.className.indexOf( 'frm_chzn' ) ) { 3527 placeholder = false; 3528 } 3529 3530 if ( 'SELECT' === input.tagName && 'multiple' === input.getAttribute( 'multiple' ) ) { 3531 isMultipleSelect = true; 3532 } 3533 3534 if ( defaultValue.constructor === Object ) { 3535 if ( ! isMultipleSelect ) { 3536 var addressType = input.getAttribute( 'name' ).split( '[' ).slice( -1 )[ 0 ]; 3537 if ( addressType !== null ) { 3538 addressType = addressType.replace( ']', '' ); 3539 defaultValue = defaultValue[ addressType ]; 3540 if ( defaultValue === undefined ) { 3541 defaultValue = ''; 3542 } 3543 } 3544 } 3545 } 3546 3547 if ( isMultipleSelect ) { 3548 selectMultiselectOptions( input, defaultValue ); 3549 } else { 3550 if ( typeof defaultValue === 'object' ) { 3551 // Don't replace a field shortcode with the id. 3552 defaultValue = '[' + defaultValue + ']'; 3553 } 3554 3555 if ( isSlimSelect( input ) ) { 3556 // This is true even when no placeholder or default is set when autocomplete is enabled. 3557 // So we need to check if the default value is an empty string and remove the hidden placeholder. 3558 if ( '' === defaultValue ) { 3559 maybeRemoveHiddenPlaceholder( input ); 3560 } 3561 3562 // Convert numbers to strings so SlimSelect matches properly. 3563 input.slim.setSelected( `${ defaultValue }` ); 3564 } else if ( input.classList.contains( 'frm_chzn' ) ) { 3565 if ( '' === defaultValue ) { 3566 maybeRemoveHiddenPlaceholder( input ); 3567 } 3568 3569 jQuery( input ).val( defaultValue ).trigger( 'chosen:updated' ); 3570 } else { 3571 input.value = defaultValue; 3572 3573 // Trigger change event for range slider input so it knows to sync the handles. 3574 if ( 'hidden' === input.type && input.dataset.isRangeSliderInitialized ) { 3575 input.dispatchEvent( new Event( 'change' ) ); 3576 } 3577 3578 if ( 'range' === input.type ) { 3579 // Trigger input event 3580 input.dispatchEvent( new Event( 'input' ) ); 3581 } 3582 } 3583 } 3584 if ( 'SELECT' === input.tagName ) { 3585 changeSelectColor( input ); 3586 } 3587 } 3588 3589 if ( ! placeholder && input.tagName === 'SELECT' ) { 3590 maybeUpdateChosenOptions( input ); 3591 if ( input.value === '' ) { 3592 setOtherSelectValue( input, defaultValue ); 3593 } 3594 } 3595 3596 triggerChange( $input ); 3597 } else if ( 'SELECT' === input.tagName ) { 3598 maybeRemoveHiddenPlaceholder( input ); 3599 } 3600 } 3601
3602 /** 3603 * @since 6.16.2 3604 * 3605 * @param {HTMLElement} input A select element. 3606 * @return {boolean} True if the hidden placeholder is detected and removed. 3607 */ 3608 function maybeRemoveHiddenPlaceholder( input ) { 3609 const hiddenPlaceholder = input.querySelector( 'option[value=""].frm_hidden.frm_hidden_placeholder' ); 3610 if ( ! hiddenPlaceholder ) { 3611 return false; 3612 } 3613 3614 hiddenPlaceholder.remove(); 3615 return true; 3616 } 3617 3618 function isSlimSelect( input ) { 3619 return input.classList.contains( 'frm_slimselect' ) && 'object' === typeof input.slim; 3620 } 3621 3622 function selectMultiselectOptions( select, values ) { 3623 if ( isSlimSelect( select ) && 'function' === typeof Object.values ) { 3624 select.slim.setSelected( Object.values( values ) ); 3625 return; 3626 } 3627 3628 var valueKey, option; 3629 for ( valueKey in values ) { 3630 option = select.querySelector( 'option[value="' + values[ valueKey ] + '"]' ); 3631 if ( option ) { 3632 option.selected = true; 3633 } 3634 } 3635 } 3636 3637 /** 3638 * Select the option with placeholder if applicable. 3639 */ 3640 function setDropdownPlaceholder( defaultValue, input ) { 3641 var placeholder; 3642 if ( defaultValue === undefined && input.tagName === 'SELECT' ) { 3643 placeholder = input.getAttribute( 'data-placeholder' ); 3644 if ( placeholder !== null ) { 3645 defaultValue = ''; 3646 } 3647 } 3648 return defaultValue; 3649 } 3650 3651 function setCheckboxOrRadioDefaultValue( inputName, defaultValue ) { 3652 // Get all checkbox/radio inputs for this field 3653 var radioInputs = document.getElementsByName( inputName ), 3654 isSet = false, 3655 firstInput = false; 3656 3657 if ( typeof defaultValue === 'object' ) { 3658 // Convert the object to an array. 3659 defaultValue = Object.keys( defaultValue ).map( 3660 ( key ) => { 3661 return defaultValue[ key ]; 3662 } 3663 ); 3664 } 3665 3666 let hiddenFieldIndex = 0; 3667 3668 // Loop through options and set the default value 3669 for ( var i = 0, l = radioInputs.length; i < l; i++ ) { 3670 if ( firstInput === false ) { 3671 firstInput = radioInputs[ i ]; 3672 } 3673 3674 if ( radioInputs[ i ].type === 'hidden' ) { 3675 // If field is read-only and there is a hidden input 3676 if ( Array.isArray( defaultValue ) && defaultValue[ hiddenFieldIndex ] !== null && 'undefined' !== typeof defaultValue[ hiddenFieldIndex ] ) { 3677 radioInputs[ i ].value = defaultValue[ hiddenFieldIndex ]; 3678 } else { 3679 radioInputs[ i ].value = defaultValue; 3680 } 3681 hiddenFieldIndex++; 3682 isSet = true; 3683 } else if ( radioInputs[ i ].value == defaultValue || 3684 ( Array.isArray( defaultValue ) && defaultValue.includes(radioInputs[ i ].value) ) ) { 3685 // If input's value matches the default value, set checked to true 3686 3687 radioInputs[ i ].checked = true; 3688 isSet = true; 3689 if ( radioInputs[ i ].type === 'radio' ) { 3690 break; 3691 } 3692 } 3693 } 3694 3695 if ( ! isSet && firstInput !== false ) { 3696 setOtherValueLimited( firstInput, defaultValue ); 3697 } 3698 } 3699 3700 // Set the default value for hidden checkbox or multi-select dropdown fields 3701 function setHiddenCheckboxDefaultValue( inputName, defaultValue ) { 3702 // Get all the hidden inputs with the same name 3703 var hiddenInputs = jQuery( 'input[name^="' + inputName + '"]' ).get(); 3704 3705 if ( typeof defaultValue === 'object' ) { 3706 // Convert the object to an array. 3707 defaultValue = Object.keys( defaultValue ).map( 3708 ( key ) => { 3709 return defaultValue[ key ]; 3710 } 3711 ); 3712 } 3713 3714 if ( Array.isArray( defaultValue ) ) { 3715 for ( var i = 0, l = defaultValue.length; i < l; i++ ) { 3716 if ( i in hiddenInputs ) { 3717 hiddenInputs[ i ].value = defaultValue[ i ]; 3718 } else { 3719 // TODO: accommodate for when there are multiple default values but the user has removed some 3720 } 3721 } 3722 } else if ( hiddenInputs[ 0 ] !== null && hiddenInputs[ 0 ] !== undefined ) { 3723 hiddenInputs[ 0 ].value = defaultValue; 3724 } 3725 } 3726 3727 function removeFromHideFields( htmlFieldId, formId ) { 3728 // Get all currently hidden fields 3729 var hiddenFields = getHiddenFields( formId ); 3730 3731 // If field id is in the array, delete it 3732 var itemIndex = hiddenFields.indexOf( htmlFieldId ); 3733 if ( itemIndex > -1 ) { 3734 // Remove field from the hiddenFields array 3735 hiddenFields.splice( itemIndex, 1 ); 3736 3737 // Update frm_hide_fields_formId input 3738 hiddenFields = JSON.stringify( hiddenFields ); 3739 var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); 3740 frmHideFieldsInput.value = hiddenFields; 3741 } 3742 } 3743
3744 /***************************************************** 3745 * Lookup Field Functions 3746 ******************************************************/ 3747 3748 /** 3749 * Check all fields that are "watching" a lookup field that changed 3750 */ 3751 function checkFieldsWatchingLookup( fieldId, changedInput, originalEvent ) { 3752 if ( typeof __FRMLOOKUP === 'undefined' || 3753 __FRMLOOKUP[ fieldId ] === undefined || 3754 __FRMLOOKUP[ fieldId ].dependents.length < 1 || 3755 changedInput === null || 3756 changedInput === undefined 3757 ) { 3758 return; 3759 } 3760 3761 var triggerFieldArgs = __FRMLOOKUP[ fieldId ]; 3762 3763 var parentRepeatArgs = getRepeatArgsFromFieldName( changedInput[ 0 ].name ); 3764 3765 for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) { 3766 updateWatchingFieldById( triggerFieldArgs.dependents[ i ], parentRepeatArgs, originalEvent ); 3767 } 3768 } 3769 3770 /** 3771 * Update all instances of a "watching" field 3772 * 3773 * @since 2.01.0 3774 * 3775 * @param {string} fieldId 3776 * @param {Object} parentRepeatArgs 3777 * @param {string} originalEvent 3778 * @return {void} 3779 */ 3780 function updateWatchingFieldById( fieldId, parentRepeatArgs, originalEvent ) { 3781 var childFieldArgs = getLookupArgsForSingleField( fieldId ); 3782 3783 // If lookup field has no parents, no need to update this field 3784 if ( childFieldArgs === false || childFieldArgs.parents.length < 1 ) { 3785 return; 3786 } 3787 3788 if ( childFieldArgs.fieldType === 'lookup' ) { 3789 updateLookupFieldOptions( childFieldArgs, parentRepeatArgs ); 3790 } else if ( originalEvent === 'value changed' ) { 3791 // If the original event was NOT triggered from a direct value change to the Lookup field, 3792 // do not update the text field value. 3793 updateWatchingFieldValue( childFieldArgs, parentRepeatArgs ); 3794 } 3795 } 3796 3797 /** 3798 * Update a Lookup field's options 3799 * 3800 * @param {Object} childFieldArgs 3801 * @param {Object} parentRepeatArgs 3802 * @param {String} parentRepeatArgs.repeatRow 3803 */ 3804 function updateLookupFieldOptions( childFieldArgs, parentRepeatArgs ) { 3805 var childFieldElements = []; 3806 if ( parentRepeatArgs.repeatRow !== '' ) { 3807 childFieldElements = getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs ); 3808 } else { 3809 childFieldElements = getAllFieldDivsOnCurrentPage( childFieldArgs ); 3810 } 3811 3812 for ( var i = 0, l = childFieldElements.length; i < l; i++ ) { 3813 addRepeatRow( childFieldArgs, childFieldElements[ i ].id ); 3814 updateSingleLookupField( childFieldArgs, childFieldElements[ i ]); 3815 } 3816 3817 processPendingLookups(); 3818 } 3819 3820 /** 3821 * Go through the list of pending look ups and process them all. 3822 * These are done in batches of 20 at a time. 3823 * 3824 * @since 6.15 3825 * 3826 * @return {void} 3827 */ 3828 function processPendingLookups() { 3829 const unprocessedPendingLookups = getUnprocessedPendingLookups(); 3830 3831 if ( ! unprocessedPendingLookups.length ) { 3832 return; 3833 } 3834 3835 const formId = unprocessedPendingLookups[0].childFieldArgs.formId; 3836 3837 // We want to allow duplicate form ids (inside repeaters) here because the number of calls 3838 // to enableFormAfterLookup matters for handling the processesRunning variable. 3839 const allFormIds = []; 3840 3841 const batchSize = 20; 3842 const batches = Math.ceil( unprocessedPendingLookups.length / batchSize ); 3843 for ( let batchNumber = 0; batchNumber < batches; ++batchNumber ) { 3844 const start = batchNumber * batchSize; 3845 const end = start + batchSize; 3846 3847 const currentBatch = unprocessedPendingLookups.slice( start, end ); 3848 3849 currentBatch.forEach( 3850 ( pendingLookup ) => { 3851 pendingLookup.pending = false; 3852 allFormIds.push( pendingLookup.childFieldArgs.formId ); 3853 } 3854 ); 3855 3856 postToAjaxUrl( 3857 getFormById( formId ), 3858 { 3859 action: 'frm_replace_lookup_field_options_arr', 3860 postData: currentBatch.map( pendingLookup => pendingLookup.childFieldArgs ), 3861 nonce: frm_js.nonce 3862 }, 3863 ( newOptionsByFieldId ) => { 3864 allFormIds.forEach( enableFormAfterLookup ); 3865 3866 Object.entries( newOptionsByFieldId ).forEach( entry => { 3867 const key = entry[0]; 3868 const newOptions = entry[1]; 3869 const optionLabels = 'undefined' !== typeof newOptionsByFieldId[ key + '_label' ] ? newOptionsByFieldId[ key + '_label' ] : []; 3870
3871 currentBatch.forEach( 3872 function( pendingLookup ) { 3873 if ( ! isNaN( key ) && pendingLookup.childFieldArgs.unique === parseInt( key ) ) { 3874 pendingLookup.callback( newOptions, optionLabels ); 3875 } 3876 } 3877 ); 3878 }); 3879 }, 3880 false, 3881 { 3882 dataType: 'json' 3883 } 3884 ); 3885 } 3886 } 3887 3888 /** 3889 * @return {Array} 3890 */ 3891 function getUnprocessedPendingLookups() { 3892 return pendingLookupFieldAjax.filter( pendingLookup => pendingLookup.pending ); 3893 } 3894 3895 /** 3896 * Get the div for a repeating field on the current page 3897 * @param {Object} childFieldArgs 3898 * @param {string} childFieldArgs.fieldId 3899 * @param {Object} parentRepeatArgs 3900 * @param {string} parentRepeatArgs.repeatingSection 3901 * @param {string} parentRepeatArgs.repeatRow 3902 * @returns {Array} 3903 */ 3904 function getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs ) { 3905 var childFieldDivs = [], 3906 selector = 'frm_field_' + childFieldArgs.fieldId + '-'; 3907 3908 selector += parentRepeatArgs.repeatingSection + '-' + parentRepeatArgs.repeatRow + '_container'; 3909 var container = document.getElementById( selector ); 3910 if ( container !== null ) { 3911 childFieldDivs.push( container ); 3912 } 3913 3914 return childFieldDivs; 3915 } 3916 3917 function updateWatchingFieldValue( childFieldArgs, parentRepeatArgs ) { 3918 var childFieldElements = getAllTextFieldInputs( childFieldArgs, parentRepeatArgs ); 3919 3920 for ( var i = 0, l = childFieldElements.length; i < l; i++ ) { 3921 addRepeatRowForInput( childFieldElements[ i ].name, childFieldArgs ); 3922 updateSingleWatchingField( childFieldArgs, childFieldElements[ i ]); 3923 } 3924 } 3925 3926 /** 3927 * Get the Lookup Args for a field ID 3928 * 3929 * @param {string} fieldId 3930 * @return {boolean|Object} 3931 */ 3932 function getLookupArgsForSingleField( fieldId ) { 3933 if ( typeof __FRMLOOKUP === 'undefined' || __FRMLOOKUP[ fieldId ] === undefined ) { 3934 return false; 3935 } 3936 3937 return __FRMLOOKUP[ fieldId ]; 3938 } 3939 3940 /** 3941 * Update a single Lookup field 3942 * 3943 * @since 2.01.0 3944 * @param {Object} childFieldArgs 3945 * @param {string} childFieldArgs.inputType 3946 * @param {object} childElement 3947 */ 3948 function updateSingleLookupField( childFieldArgs, childElement ) { 3949 childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); 3950 3951 if ( childFieldArgs.inputType === 'select' ) { 3952 maybeReplaceSelectLookupFieldOptions( childFieldArgs, childElement ); 3953 } else if ( childFieldArgs.inputType === 'radio' || childFieldArgs.inputType === 'checkbox' ) { 3954 maybeReplaceCbRadioLookupOptions( childFieldArgs, childElement ); 3955 } else if ( childFieldArgs.inputType === 'data' ) { 3956 maybeReplaceLookupList( childFieldArgs, childElement ); 3957 } 3958 } 3959 3960 /** 3961 * Update a standard field that is "watching" a Lookup 3962 * 3963 * @since 2.01.0 3964 * 3965 * @param {Object} childFieldArgs 3966 * @param {object} childElement 3967 * @return {void} 3968 */ 3969 function updateSingleWatchingField( childFieldArgs, childElement ) { 3970 childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); 3971 3972 if ( currentLookupHasQueue( childElement.id ) ) { 3973 addLookupToQueueOfTwo( childFieldArgs, childElement ); 3974 return; 3975 } 3976 3977 addLookupToQueueOfTwo( childFieldArgs, childElement ); 3978 3979 maybeInsertValueInFieldWatchingLookup( childFieldArgs, childElement ); 3980 } 3981 3982 /** 3983 * Get all the occurrences of a specific Text field 3984 * 3985 * @since 2.01.0 3986 * @param {Object} childFieldArgs 3987 * @param {boolean} childFieldArgs.isRepeating 3988 * @param {string} childFieldArgs.fieldKey 3989 * @param {Object} parentRepeatArgs 3990 * @param {string} parentRepeatArgs.repeatingSection 3991 * @param {string} parentRepeatArgs.repeatRow 3992 * @return {NodeList} 3993 */ 3994 function getAllTextFieldInputs( childFieldArgs, parentRepeatArgs ) { 3995 var selector = 'field_' + childFieldArgs.fieldKey; 3996 if ( childFieldArgs.isRepeating ) { 3997 if ( parentRepeatArgs.repeatingSection !== '' ) { 3998 // If trigger field is repeating/embedded, use its section row in selector 3999 selector = '[id="' + selector + '-' + parentRepeatArgs.repeatRow + '"]'; 4000 } else { 4001 // If trigger field is not repeating/embedded, get all repeating field inputs 4002 selector = '[id^="' + selector + '-"]'; 4003 } 4004 } else { 4005 selector = '[id="' + selector + '"]'; 4006 } 4007 4008 return document.querySelectorAll( selector ); 4009 } 4010 4011 // Set the value in a regular field that is watching a lookup field when it is conditionally shown 4012 function maybeSetWatchingFieldValue( input ) { 4013 var fieldId = frmFrontForm.getFieldId( input, false ), 4014 childFieldArgs = getLookupArgsForSingleField( fieldId ); 4015 4016 // If lookup field has no parents, no need to update this field 4017 if ( childFieldArgs === false || childFieldArgs.fieldType === 'lookup' ) { 4018 return; 4019 } 4020 4021 updateSingleWatchingField( childFieldArgs, input ); 4022 } 4023 4024 /** 4025 * Get all divs on the current page for a given field 4026 * 4027 * @since 2.01.0 4028 * @param {Object} childFieldArgs 4029 * @param {boolean} childFieldArgs.isRepeating 4030 * @param {string} childFieldArgs.fieldId 4031 * @returns {Array} 4032 */ 4033 function getAllFieldDivsOnCurrentPage( childFieldArgs ) { 4034 var childFieldDivs = []; 4035 4036 if ( childFieldArgs.isRepeating ) { 4037 childFieldDivs = document.querySelectorAll( '.frm_field_' + childFieldArgs.fieldId + '_container' ); 4038 } else { 4039 var container = document.getElementById( 'frm_field_' + childFieldArgs.fieldId + '_container' ); 4040 if ( container !== null ) { 4041 childFieldDivs.push( container ); 4042 } 4043 } 4044 4045 return childFieldDivs; 4046 } 4047 4048 // Get the field values from all parents 4049 function getParentLookupFieldVals( childFieldArgs ) { 4050 var parentFieldArgs, 4051 parentVals = [], 4052 parentIds = childFieldArgs.parents, 4053 parentValue = false;
4054 4055 for ( var i = 0, l = parentIds.length; i < l; i++ ) { 4056 parentFieldArgs = getLookupArgsForSingleField( parentIds[ i ]); 4057 parentValue = getFieldValue( parentFieldArgs, childFieldArgs ); 4058 4059 // If any parents have blank values, don't waste time looking for values 4060 if ( parentValue === '' || parentValue === false ) { 4061 parentVals = false; 4062 break; 4063 } 4064 4065 parentVals[ i ] = parentValue; 4066 } 4067 4068 return parentVals; 4069 } 4070 4071 // Get the value from array of radio inputs (could be type="hidden" or type="radio") 4072 function getValueFromRadioInputs( radioInputs ) { 4073 var radioValue = false, 4074 l = radioInputs.length; 4075 4076 for ( var i = 0; i < l; i++ ) { 4077 if ( radioInputs[ i ].type === 'hidden' || radioInputs[ i ].checked ) { 4078 radioValue = radioInputs[ i ].value; 4079 break; 4080 } 4081 } 4082 4083 return radioValue; 4084 } 4085 4086 /** 4087 * Maybe replace the options in a Select Lookup field 4088 * 4089 * @since 2.01.0 4090 * @param {Object} childFieldArgs 4091 * @param {Array} childFieldArgs.parents 4092 * @param {Array} childFieldArgs.parentVals 4093 * @param {string} childFieldArgs.fieldId 4094 * @param {string} childFieldArgs.fieldKey 4095 * @param {string} childFieldArgs.formId 4096 * @param {object} childDiv 4097 */ 4098 function maybeReplaceSelectLookupFieldOptions( childFieldArgs, childDiv ) { 4099 // Get select within childDiv 4100 var childSelect = childDiv.getElementsByTagName( 'SELECT' )[ 0 ]; 4101 if ( childSelect === null ) { 4102 return; 4103 } 4104 4105 var currentValue = childSelect.value; 4106 4107 if ( childFieldArgs.parentVals === false ) { 4108 // If any parents have blank values, don't waste time looking for values 4109 childSelect.options.length = 1; 4110 childSelect.value = ''; 4111 maybeUpdateChosenOptions( childSelect ); 4112 4113 if ( currentValue !== '' ) { 4114 triggerChange( jQuery( childSelect ), childFieldArgs.fieldKey ); 4115 } 4116 4117 } else { 4118 disableLookup( childSelect ); 4119 disableFormPreLookup( childFieldArgs.formId ); 4120 4121 getLookupValues( 4122 childFieldArgs, 4123 ( newOptions, optionLabels ) => { 4124 replaceSelectLookupFieldOptions( childFieldArgs, childSelect, newOptions, optionLabels ); 4125 triggerLookupOptionsLoaded( jQuery( childDiv ) ); 4126 enableFormAfterLookup( childFieldArgs.formId ); 4127 } 4128 ); 4129 } 4130 } 4131 4132 // Update chosen options if autocomplete is enabled 4133 function maybeUpdateChosenOptions( childSelect ) { 4134 if ( childSelect.className.includes('frm_chzn') && jQuery().chosen ) { 4135 jQuery( childSelect ).trigger( 'chosen:updated' ); 4136 } 4137 } 4138 4139 /** 4140 * Disable a Select Lookup field and add loading image 4141 * 4142 * @since 2.02.11 4143 * @param {object} childSelect 4144 */ 4145 function disableLookup( childSelect ) { 4146 childSelect.className = childSelect.className + ' frm_loading_lookup'; 4147 childSelect.disabled = true; 4148 maybeUpdateChosenOptions( childSelect ); 4149 } 4150 4151 /** 4152 * Disable a form prior to a Lookup field's Ajax request 4153 * 4154 * @since 2.03.02 4155 * @param {String} formId 4156 */ 4157 function disableFormPreLookup( formId ) { 4158 processesRunning++; 4159 4160 if ( processesRunning === 1 ) { 4161 4162 var form = getFormById( formId ); 4163 if ( form !== null ) { 4164 frmFrontForm.showSubmitLoading( jQuery( form ) ); 4165 } 4166 } 4167 } 4168 4169 /** 4170 * Enable a form if all Lookup field requests are completed 4171 * 4172 * @since 2.03.02 4173 * @param {String} formId 4174 */ 4175 function enableFormAfterLookup( formId ) { 4176 var form; 4177 4178 processesRunning--; 4179 if ( processesRunning <= 0 ) { 4180 form = getFormById( formId ); 4181 if ( form !== null ) { 4182 removeSubmitLoading( form, formId, processesRunning ); 4183 } 4184 } 4185 } 4186 4187 /** 4188 * Get a form element by the ID number 4189 * 4190 * @since 2.03.02 4191 * @param {string} formId 4192 * @returns {Element} 4193 */ 4194 function getFormById( formId ) { 4195 var form = document.querySelector( '#frm_form_' + formId + '_container form' ); 4196 if ( form === null ) { 4197 // The order is switched on the back end. 4198 form = document.getElementById( 'frm_form_' + formId + '_container' ); 4199 if ( form !== null ) { 4200 form = form.closest( 'form' ); 4201 } 4202 } 4203 return form; 4204 } 4205 4206 /** 4207 * Enable a Select Lookup field and remove loading image 4208 * 4209 * @since 2.02.11 4210 * @param {object} childSelect 4211 * @param {boolean} isReadOnly 4212 */ 4213 function enableLookup( childSelect, isReadOnly ) { 4214 if ( isReadOnly === false ) { 4215 childSelect.disabled = false;
4216 } 4217 childSelect.className = childSelect.className.replace( ' frm_loading_lookup', '' ); 4218 } 4219 4220 /** 4221 * @param {HTMLElement} element 4222 * @returns {bool} 4223 */ 4224 function isMultiSelect( element ) { 4225 return element.tagName.toLowerCase() === 'select' && element.multiple; 4226 } 4227 4228 /** 4229 * Gets selected options for a multiselect element. 4230 * 4231 * @param {HTMLElement} element 4232 * @returns {Array} 4233 */ 4234 function getSelectedOptions( element ) { 4235 if ( isSlimSelect( element ) ) { 4236 return element.slim.getSelected(); 4237 } 4238 4239 if ( ! isMultiSelect( element ) ) { 4240 return element.value; 4241 } 4242 4243 var i, option, 4244 selectedOptions = []; 4245 for ( i = 0; i < element.options.length; i++ ) { 4246 option = element.options[i]; 4247 if ( option.selected ) { 4248 selectedOptions.push( option.value ); 4249 } 4250 } 4251 4252 return selectedOptions; 4253 } 4254 4255 /** 4256 * Sets selected options for a dropdown element. 4257 * 4258 * @param {HTMLElement} element 4259 * @param {Array} values 4260 */ 4261 function setSelectedOptions( element, values ) { 4262 var option; 4263 4264 if ( ! isMultiSelect( element ) ) { 4265 element.value = values; 4266 return; 4267 } 4268 4269 // Reset all options to unselected 4270 Array.from( element.options ).forEach( ( option ) => { 4271 option.selected = false; 4272 }); 4273 4274 // Set selected attribute for the specified values 4275 values.forEach( ( value ) => { 4276 option = element.querySelector( 'option[value="' + value + '"]' ); 4277 if ( option ) { 4278 option.selected = true; 4279 } 4280 }); 4281 } 4282 4283 /** 4284 * Replace the options in a Select Lookup field 4285 * 4286 * @since 2.01.0 4287 * @param {Object} fieldArgs 4288 * @param {string} fieldArgs.fieldKey 4289 * @param {boolean} fieldArgs.isReadOnly 4290 * @param {object} childSelect 4291 * @param {Array} newOptions 4292 */ 4293 function replaceSelectLookupFieldOptions( fieldArgs, childSelect, newOptions, optionLabels ) { 4294 var origVal, i, optsLength, newOption, optionData; 4295 4296 // Get selected options 4297 origVal = getSelectedOptions( childSelect ); 4298 4299 // Remove old options 4300 for ( i = childSelect.options.length; i > 0; i-- ) { 4301 childSelect.remove( i ); 4302 } 4303 4304 // Add new options 4305 optsLength = newOptions.length; 4306 optionData = []; 4307 4308 if ( childSelect.options.length > 0 && '' === childSelect.options[0].value ) { 4309 optionData.push({ 4310 text: childSelect.options[0].textContent, 4311 value: childSelect.options[0].value, 4312 placeholder: true 4313 }); 4314 } 4315 4316 for ( i = 0; i < optsLength; i++ ) { 4317 const optionLabel = optionLabels[ i ] || newOptions[ i ]; 4318 newOption = new Option( optionLabel, newOptions[ i ], false, false ); 4319 childSelect.options[ i + 1 ] = newOption; 4320 optionData.push( newOption ); 4321 } 4322 4323 if ( childSelect.slim ) { 4324 childSelect.slim.setData( optionData ); 4325 } 4326 4327 setSelectLookupVal( childSelect, origVal ); 4328 4329 enableLookup( childSelect, fieldArgs.isReadOnly ); 4330 4331 maybeUpdateChosenOptions( childSelect ); 4332 4333 // Trigger a change if the new value is different from the old value 4334 if ( getSelectedOptions( childSelect ).toString() !== origVal.toString() ) { 4335 triggerChange( jQuery( childSelect ), fieldArgs.fieldKey ); 4336 } 4337 } 4338 4339 /** 4340 * Set the value in a refreshed Lookup Field. 4341 * 4342 * @param {HTMLElement} childSelect 4343 * @param {Array|string} origVal 4344 * @returns {void} 4345 */ 4346 function setSelectLookupVal( childSelect, origVal ) { 4347 if ( isSlimSelect( childSelect ) ) { 4348 if ( ! valueIsSingleItemArrayWithEmptyString( origVal ) ) { 4349 setSlimValue( childSelect, origVal ); 4350 } 4351 return; 4352 } 4353 4354 // Try setting the dropdown to the original value 4355 setSelectedOptions( childSelect, origVal ); 4356 4357 if ( childSelect.value === '' ) { 4358 // If the original value is no longer present, try setting to default value 4359 var defaultValue = childSelect.getAttribute( 'data-frmval' ); 4360 if ( defaultValue !== null ) { 4361 childSelect.value = defaultValue; 4362 } 4363 } 4364 4365 if ( childSelect.value === '' ) { 4366 // Set the value to empty string again to fix the issue with blank option show. 4367 childSelect.value = ''; 4368 } 4369 } 4370 4371 /** 4372 * @since 6.8.3 4373 * 4374 * @param {Array|string} value 4375 * @returns {boolean} 4376 */ 4377 function valueIsSingleItemArrayWithEmptyString( value ) { 4378 return Array.isArray( value ) && 1 === value.length && '' === value[0]; 4379 } 4380 4381 /** 4382 * Either hide checkbox/radio Lookup field or update its options 4383 * 4384 * @since 2.01.01 4385 * @param {Object} childFieldArgs 4386 * @param {Array} childFieldArgs.parentVals 4387 * @param {object} childDiv 4388 */ 4389 function maybeReplaceCbRadioLookupOptions( childFieldArgs, childDiv ) { 4390 if ( childFieldArgs.parentVals === false ) { 4391 // If any parents have blank values, don't waste time looking for values 4392 4393 var inputs = childDiv.getElementsByTagName( 'input' ); 4394 maybeHideRadioLookup( childFieldArgs, childDiv ); 4395 clearValueForInputs( inputs ); 4396 4397 } else { 4398 replaceCbRadioLookupOptions( childFieldArgs, childDiv ); 4399 } 4400 } 4401
4402 /** 4403 * Update the options in a checkbox/radio lookup field 4404 * 4405 * @since 2.01.01 4406 * @param {Object} childFieldArgs 4407 * @param {string} childFieldArgs.inputType 4408 * @param {Array} childFieldArgs.parents 4409 * @param {Array} childFieldArgs.parentVals 4410 * @param {string} childFieldArgs.fieldId 4411 * @param {string} childFieldArgs.repeatRow 4412 * @param {string} childFieldArgs.fieldKey 4413 * @param {string} childFieldArgs.formId 4414 * @param {object} childDiv 4415 */ 4416 function replaceCbRadioLookupOptions( childFieldArgs, childDiv ) { 4417 var optContainer, inputs, currentValue, defaultValue, form, data, success; 4418 4419 optContainer = childDiv.getElementsByClassName( 'frm_opt_container' )[ 0 ]; 4420 inputs = optContainer.getElementsByTagName( 'input' ); 4421 currentValue = ''; 4422 4423 addLoadingIconJS( childDiv, optContainer ); 4424 4425 if ( childFieldArgs.inputType == 'radio' ) { 4426 currentValue = getValueFromRadioInputs( inputs ); 4427 } else { 4428 currentValue = getValuesFromCheckboxInputs( inputs ); 4429 } 4430 4431 defaultValue = jQuery( inputs[ 0 ]).data( 'frmval' ); 4432 disableFormPreLookup( childFieldArgs.formId ); 4433 4434 form = getFormById( childFieldArgs.formId ); 4435 4436 data = { 4437 action: 'frm_replace_cb_radio_lookup_options', 4438 parent_fields: childFieldArgs.parents, 4439 parent_vals: childFieldArgs.parentVals, 4440 field_id: childFieldArgs.fieldId, 4441 container_field_id: getContainerFieldId( childFieldArgs ), 4442 row_index: childFieldArgs.repeatRow, 4443 current_value: currentValue, 4444 default_value: defaultValue, 4445 nonce: frm_js.nonce 4446 }; 4447 4448 success = function( newHtml ) { 4449 var input; 4450 optContainer.innerHTML = newHtml; 4451 4452 removeLoadingIconJS( childDiv, optContainer ); 4453 4454 if ( inputs.length == 1 && inputs[ 0 ].value === '' ) { 4455 maybeHideRadioLookup( childFieldArgs, childDiv ); 4456 } else { 4457 maybeShowRadioLookup( childFieldArgs, childDiv ); 4458 maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue ); 4459 } 4460 4461 input = inputs[0]; 4462 triggerChange( jQuery( input ), childFieldArgs.fieldKey ); 4463 triggerLookupOptionsLoaded( jQuery( childDiv ) ); 4464 4465 enableFormAfterLookup( childFieldArgs.formId ); 4466 }; 4467 4468 postToAjaxUrl( form, data, success ); 4469 } 4470 4471 /** 4472 * @since 4.0.03 4473 */ 4474 function maybeReplaceLookupList( childFieldArgs, childDiv ) { 4475 var inputs = childDiv.getElementsByTagName( 'input' ), 4476 content = inputs[0].previousElementSibling; 4477 4478 if ( childFieldArgs.parentVals === false ) { 4479 // If any parents have blank values, don't waste time looking for values 4480 maybeHideRadioLookup( childFieldArgs, childDiv ); 4481 if ( content !== undefined ) { 4482 content.innerHTML = ''; 4483 } 4484 } else { 4485 getLookupValues( childFieldArgs, ( response, optionLabels ) => { 4486 content.innerHTML = optionLabels.length ? optionLabels.join( ', ' ) : response.join( ', ' ); 4487 inputs[0].value = response; 4488 4489 maybeShowRadioLookup( childFieldArgs, childDiv ); 4490 triggerLookupOptionsLoaded( jQuery( childDiv ) ); 4491 }); 4492 } 4493 } 4494 4495 /** 4496 * Get the lookup options in an array. 4497 * @since 4.0.03 4498 */ 4499 function getLookupValues( childFieldArgs, callback ) { 4500 disableFormPreLookup( childFieldArgs.formId ); 4501 4502 const pendingLookupArgs = { 4503 formId: childFieldArgs.formId, 4504 fieldId: childFieldArgs.fieldId, 4505 parents: childFieldArgs.parents, 4506 parentVals: childFieldArgs.parentVals, 4507 unique: getAutoId() 4508 }; 4509 4510 pendingLookupFieldAjax.push({ childFieldArgs: pendingLookupArgs, callback, pending: true }); 4511 } 4512 4513 function getAutoId() { 4514 return autoId++; 4515 } 4516 4517 /** 4518 * Trigger the frm_lookup_options_loaded event on the field div 4519 * 4520 * @since 2.03.05 4521 * 4522 * @param {Object} $fieldDiv 4523 */ 4524 function triggerLookupOptionsLoaded( $fieldDiv ) { 4525 $fieldDiv.trigger( 'frmLookupOptionsLoaded' ); 4526 } 4527 4528 /** 4529 * Select the defatul value in a radio/checkbox field if no value is selected 4530 * 4531 * @since 2.02.11 4532 * 4533 * @param {Object} inputs 4534 * @param {Object} childFieldArgs 4535 * @param {string} childFieldArgs.inputType 4536 * @param {(string|Array)} defaultValue 4537 */ 4538 function maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue ) { 4539 if ( defaultValue === undefined ) { 4540 return; 4541 } 4542 4543 var currentValue = false;
4544 if ( childFieldArgs.inputType === 'radio' ) { 4545 currentValue = getValueFromRadioInputs( inputs ); 4546 } else { 4547 currentValue = getValuesFromCheckboxInputs( inputs ); 4548 } 4549 4550 if ( currentValue !== false || inputs.length < 1 ) { 4551 return; 4552 } 4553 4554 var inputName = inputs[ 0 ].name; 4555 setCheckboxOrRadioDefaultValue( inputName, defaultValue ); 4556 } 4557 4558 /** 4559 * Hide a Radio Lookup field if it doesn't have any options 4560 * 4561 * @since 2.01.01 4562 * @param {Object} childFieldArgs 4563 * @param {string} childFieldArgs.formId 4564 * @param {object} childDiv 4565 */ 4566 function maybeHideRadioLookup( childFieldArgs, childDiv ) { 4567 if ( isFieldConditionallyHidden( childDiv.id, childFieldArgs.formId ) ) { 4568 return; 4569 } 4570 4571 hideFieldContainer( childDiv.id ); 4572 addToHideFields( childDiv.id, childFieldArgs.formId ); 4573 } 4574 4575 /** 4576 * Show a radio Lookup field if it has options and conditional logic says it should be shown 4577 * 4578 * @since 2.01.01 4579 * @param {Object} childFieldArgs 4580 * @param {string} childFieldArgs.formId 4581 * @param {string} childFieldArgs.fieldId 4582 * @param {string} childFieldArgs.repeatRow 4583 * @param {object} childDiv 4584 * @return {void} 4585 */ 4586 function maybeShowRadioLookup( childFieldArgs, childDiv ) { 4587 if ( isFieldCurrentlyShown( childDiv.id, childFieldArgs.formId ) ) { 4588 return; 4589 } 4590 4591 var logicArgs = getRulesForSingleField( childFieldArgs.fieldId ); 4592 if ( logicArgs === false || logicArgs.conditions.length < 1 ) { 4593 removeFromHideFields( childDiv.id, childFieldArgs.formId ); 4594 showFieldContainer( childDiv.id ); 4595 } else { 4596 logicArgs.containerId = childDiv.id; 4597 logicArgs.repeatRow = childFieldArgs.repeatRow; 4598 hideOrShowSingleField( logicArgs ); 4599 } 4600 } 4601 4602 /** 4603 * Get new value for a text field if all Lookup Field parents have a value 4604 * 4605 * @since 2.01.0 4606 * @param {Object} childFieldArgs 4607 * @param {string} childFieldArgs.formId 4608 * @param {Array} childFieldArgs.parents 4609 * @param {Array} childFieldArgs.parentVals 4610 * @param {string} childFieldArgs.fieldKey 4611 * @param {string} childFieldArgs.fieldId 4612 * @param {object} childInput 4613 */ 4614 function maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput ) { 4615 if ( isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) ) { 4616 // TODO: What if field is in conditionally hidden section? 4617 checkQueueAfterLookupCompleted( childInput.id ); 4618 return; 4619 } 4620 4621 if ( childFieldArgs.parentVals === false ) { 4622 // If any parents have blank values, set the field value to the default value 4623 var newValue = childInput.getAttribute( 'data-frmval' ); 4624 if ( newValue === null ) { 4625 newValue = ''; 4626 } 4627 insertValueInFieldWatchingLookup( childFieldArgs, childInput, newValue ); 4628 checkQueueAfterLookupCompleted( childInput.id ); 4629 } else { 4630 // If all parents have values, check for a new value 4631 4632 disableFormPreLookup( childFieldArgs.formId ); 4633 4634 postToAjaxUrl( 4635 getFormById( childFieldArgs.formId ), 4636 { 4637 action: 'frm_get_lookup_text_value', 4638 parent_fields: childFieldArgs.parents, 4639 parent_vals: childFieldArgs.parentVals, 4640 field_id: childFieldArgs.fieldId, 4641 nonce: frm_js.nonce 4642 }, 4643 ( newValue ) => { 4644 if ( ! isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) && childInput.value != newValue ) { 4645 insertValueInFieldWatchingLookup( childFieldArgs.fieldKey, childInput, newValue ); 4646 } 4647 4648 enableFormAfterLookup( childFieldArgs.formId ); 4649 checkQueueAfterLookupCompleted( childInput.id ); 4650 } 4651 ); 4652 } 4653 } 4654 4655 /** 4656 * Check if the current Lookup watcher field has a queue 4657 * 4658 * @since 2.03.05 4659 * 4660 * @param {string} elementId 4661 * @returns {boolean} 4662 */ 4663 function currentLookupHasQueue( elementId ) { 4664 return ( elementId in lookupQueues && lookupQueues[ elementId ].length > 0 ); 4665 } 4666 4667 /** 4668 * Add the current Lookup watcher to a queue of size two 4669 * 4670 * @since 2.03.05 4671 * 4672 * @param {Object} childFieldArgs 4673 * @param {Object} childInput 4674 */ 4675 function addLookupToQueueOfTwo( childFieldArgs, childInput ) { 4676 var elementId = childInput.id; 4677 4678 if ( elementId in lookupQueues ) { 4679 if ( lookupQueues[ elementId ].length >= 2 ) { 4680 lookupQueues[ elementId ] = lookupQueues[ elementId ].slice( 0, 1 ); 4681 } 4682 } else { 4683 lookupQueues[ elementId ] = []; 4684 } 4685 4686 lookupQueues[ elementId ].push({ childFieldArgs, childInput }); 4687 } 4688 4689 /** 4690 * Check the lookupQueue after a value lookup is completed 4691 * 4692 * @since 2.03.05 4693 * 4694 * @param {string} elementId 4695 */ 4696 function checkQueueAfterLookupCompleted( elementId ) { 4697 removeLookupFromQueue( elementId ); 4698 doNextItemInLookupQueue( elementId ); 4699 } 4700 4701 /** 4702 * Remove a Lookup from the queue 4703 * 4704 * @since 2.03.05 4705 * 4706 * @param {string} elementId 4707 */ 4708 function removeLookupFromQueue( elementId ) { 4709 lookupQueues[ elementId ].shift(); 4710 } 4711 4712 /** 4713 * Check the current Lookup queue 4714 * 4715 * @since 2.03.05 4716 * 4717 * @param {string} elementId 4718 * @return {void} 4719 */ 4720 function doNextItemInLookupQueue( elementId ) { 4721 if ( ! currentLookupHasQueue( elementId ) ) { 4722 return; 4723 } 4724 4725 const childFieldArgs = lookupQueues[ elementId ][ 0 ].childFieldArgs; 4726 const childInput = lookupQueues[ elementId ][ 0 ].childInput; 4727 4728 updateParentValsForLookup( childInput.name, childFieldArgs ); 4729 maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput ); 4730 } 4731 4732 /** 4733 * Ensures Lookup field parentVals are updated for the current field. 4734 * Fixes issue #5611 4735 * 4736 * @since 6.19 4737 * 4738 * @param {string} childInputName 4739 * @param {object} childFieldArgs 4740 * 4741 * @returns {void} 4742 */ 4743 function updateParentValsForLookup( childInputName, childFieldArgs ) { 4744 if ( ! childInputName ) { 4745 return; 4746 } 4747 childFieldArgs.repeatRow = getRepeatArgsFromFieldName( childInputName ).repeatRow; 4748 childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); 4749 } 4750 4751 /** 4752 * Convert html entities back into their character values 4753 * 4754 * @param {string} string 4755 * @returns {string} 4756 */ 4757 function decodeEntities( string ) { 4758 var decoded = string.replace( /&/g, '&' ) 4759 .replace( /</g, '<' ) 4760 .replace( />/g, '>' ) 4761 .replace( /"/g, '"' ) 4762 .replace( /'/g, '\'' ); 4763 return decoded; 4764 } 4765 4766 /** 4767 * Insert a new text field Lookup value 4768 * 4769 * @since 2.01.0 4770 * @param {string} fieldKey 4771 * @param {object} childInput 4772 * @param {string} newValue 4773 */ 4774 function insertValueInFieldWatchingLookup( fieldKey, childInput, newValue ) { 4775 childInput.value = decodeEntities( newValue ); 4776 triggerChange( jQuery( childInput ), fieldKey ); 4777 } 4778 4779 /** 4780 * Add the repeat Row to the child field args 4781 * 4782 * @since 2.01.0 4783 * @param {string} fieldName 4784 * @param {Object} childFieldArgs 4785 */ 4786 function addRepeatRowForInput( fieldName, childFieldArgs ) {
4787 var repeatArgs = getRepeatArgsFromFieldName( fieldName ); 4788 4789 if ( repeatArgs.repeatRow !== '' ) { 4790 childFieldArgs.repeatRow = repeatArgs.repeatRow; 4791 } else { 4792 childFieldArgs.repeatRow = ''; 4793 } 4794 } 4795 4796 /******************************************************* 4797 Dynamic Field Functions 4798 *******************************************************/ 4799 4800 // Update a Dynamic field's data or options 4801 function updateDynamicField( depFieldArgs, onCurrentPage ) { 4802 var depFieldArgsCopy = cloneObjectForDynamicFields( depFieldArgs ); 4803 4804 if ( depFieldArgsCopy.inputType === 'data' ) { 4805 updateDynamicListData( depFieldArgsCopy, onCurrentPage ); 4806 } else if ( onCurrentPage ) { 4807 // Only update the options if field is on the current page 4808 updateDynamicFieldOptions( depFieldArgsCopy ); 4809 } 4810 } 4811 4812 /** 4813 * Clone the depFieldArgs object for use in ajax requests 4814 * 4815 * @since 2.01.0 4816 * @param {Object} depFieldArgs 4817 * @param {string|Array} depFieldArgs.dataLogic.actualValue 4818 * @param {string} depFieldArgs.fieldId 4819 * @param {string} depFieldArgs.fieldKey 4820 * @param {string} depFieldArgs.formId 4821 * @param {string} depFieldArgs.containerId 4822 * @param {string} depFieldArgs.repeatRow 4823 * @param {string} depFieldArgs.inputType 4824 * @return {Object} dynamicFieldArgs 4825 */ 4826 function cloneObjectForDynamicFields( depFieldArgs ) { 4827 var dataLogic = { 4828 actualValue: depFieldArgs.dataLogic.actualValue, 4829 fieldId: depFieldArgs.dataLogic.fieldId 4830 }; 4831 4832 var dynamicFieldArgs = { 4833 fieldId: depFieldArgs.fieldId, 4834 fieldKey: depFieldArgs.fieldKey, 4835 formId: depFieldArgs.formId, 4836 containerId: depFieldArgs.containerId, 4837 repeatRow: depFieldArgs.repeatRow, 4838 dataLogic, 4839 children: '', 4840 inputType: depFieldArgs.inputType 4841 }; 4842 4843 return dynamicFieldArgs; 4844 } 4845 4846 pendingDynamicFieldAjax = []; 4847 4848 /** 4849 * Update a Dynamic List field 4850 * 4851 * @since 2.01 4852 * @param {Object} depFieldArgs 4853 * @param {string} depFieldArgs.containerId 4854 * @param {string|Array} depFieldArgs.dataLogic.actualValue 4855 * @param {string} depFieldArgs.fieldId 4856 */ 4857 function updateDynamicListData( depFieldArgs, onCurrentPage ) { 4858 var $fieldDiv; 4859 4860 if ( onCurrentPage ) { 4861 $fieldDiv = jQuery( '#' + depFieldArgs.containerId ); 4862 addLoadingIcon( $fieldDiv ); 4863 } 4864 4865 pendingDynamicFieldAjax.push({ 4866 args: { 4867 depFieldArgs, 4868 onCurrentPage 4869 }, 4870 data: { 4871 entry_id: depFieldArgs.dataLogic.actualValue, 4872 current_field: depFieldArgs.fieldId, 4873 hide_id: depFieldArgs.containerId, 4874 on_current_page: onCurrentPage, 4875 nonce: frm_js.nonce 4876 } 4877 }); 4878 } 4879 4880 /** 4881 * Update a Dynamic dropdown, radio, or checkbox options 4882 * 4883 * @since 2.01 4884 * @param {Object} depFieldArgs 4885 * @param {string} depFieldArgs.containerId 4886 * @param {string} depFieldArgs.dataLogic.fieldId 4887 * @param {string|Array} depFieldArgs.dataLogic.actualValue 4888 * @param {string} depFieldArgs.fieldId 4889 */ 4890 function updateDynamicFieldOptions( depFieldArgs ) { 4891 var $fieldDiv = jQuery( '#' + depFieldArgs.containerId ), 4892 $fieldInputs = $fieldDiv.find( 'select[name^="item_meta"], input[name^="item_meta"]' ), 4893 prevValue = getFieldValueFromInputs( $fieldInputs ), 4894 defaultVal = $fieldInputs.data( 'frmval' ), 4895 editingEntry = $fieldDiv.closest( 'form' ).find( 'input[name="id"]' ).val(); 4896 4897 addLoadingIcon( $fieldDiv ); 4898 4899 postToAjaxUrl( 4900 getFormById( depFieldArgs.formId ), 4901 { 4902 action: 'frm_fields_ajax_data_options', 4903 trigger_field_id: depFieldArgs.dataLogic.fieldId, 4904 entry_id: depFieldArgs.dataLogic.actualValue, 4905 field_id: depFieldArgs.fieldId, 4906 default_value: defaultVal, 4907 container_id: depFieldArgs.containerId, 4908 editing_entry: editingEntry, 4909 prev_val: prevValue, 4910 nonce: frm_js.nonce 4911 }, 4912 ( html ) => { 4913 var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); 4914 $optContainer.html( html ); 4915 var $dynamicFieldInputs = $optContainer.find( 'select, input[type="checkbox"], input[type="radio"]' ); 4916 4917 removeLoadingIcon( $optContainer ); 4918 4919 if ( html === '' || $dynamicFieldInputs.length < 1 ) { 4920 hideDynamicField( depFieldArgs ); 4921 } else { 4922 var valueChanged = dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue ); 4923 showDynamicField( depFieldArgs, $fieldDiv, $dynamicFieldInputs, valueChanged ); 4924 } 4925 } 4926 ); 4927 } 4928 4929 function dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue ) { 4930 var newValue = getFieldValueFromInputs( $dynamicFieldInputs ); 4931 return ( prevValue !== newValue ); 4932 } 4933 4934 /** 4935 * Update the value in a hidden Dynamic List field 4936 * 4937 * @since 2.01.01 4938 * @param {Object} depFieldArgs 4939 * @param {string} depFieldArgs.fieldKey 4940 * @param {string} depFieldArgs.repeatRow 4941 * @param {string} depFieldArgs.containerId 4942 * @param {string} depFieldArgs.formId 4943 */ 4944 function updateHiddenDynamicListField( depFieldArgs, newValue ) { 4945 // Get the Dynamic List input
4946 var inputId = 'field_' + depFieldArgs.fieldKey; 4947 if ( depFieldArgs.repeatRow !== '' ) { 4948 inputId += '-' + depFieldArgs.repeatRow; 4949 } 4950 var listInput = document.getElementById( inputId ); 4951 4952 if ( listInput === null ) { 4953 return; 4954 } 4955 4956 // Set the new value 4957 listInput.value = newValue; 4958 4959 // Remove field from hidden field list 4960 if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { 4961 removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); 4962 } 4963 4964 triggerChange( jQuery( listInput ) ); 4965 } 4966 4967 // Add the loading icon with jQuery 4968 function addLoadingIcon( $fieldDiv ) { 4969 var currentHTML = $fieldDiv.html(); 4970 4971 if ( currentHTML.includes('frm-loading-img') ) { 4972 // Loading image already present 4973 } else { 4974 var loadingIcon = '<span class="frm-loading-img"></span>'; 4975 $fieldDiv.html( currentHTML + loadingIcon ); 4976 4977 var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); 4978 $optContainer.hide(); 4979 } 4980 } 4981 4982 // Add the loading icon with JavaScript 4983 function addLoadingIconJS( fieldDiv, optContainer ) {
4984 var currentHTML = fieldDiv.innerHTML; 4985 4986 if ( currentHTML.includes('frm-loading-img') ) { 4987 // Loading image already present 4988 } else { 4989 optContainer.classList.add( 'frm_hidden' ); 4990 4991 var loadingIcon = document.createElement( 'span' ); 4992 loadingIcon.setAttribute( 'class', 'frm-loading-img' ); 4993 fieldDiv.insertBefore( loadingIcon, optContainer.nextSibling ); 4994 } 4995 } 4996 4997 // Remove the loading icon with jQuery 4998 function removeLoadingIcon( $optContainer ) { 4999 $optContainer.parent().children( '.frm-loading-img' ).remove(); 5000 $optContainer.show(); 5001 } 5002 5003 // Remove the loading icon with JavaScript 5004 function removeLoadingIconJS( fieldDiv, optContainer ) { 5005 var loadingIcon = fieldDiv.getElementsByClassName( 'frm-loading-img' )[ 0 ]; 5006 if ( loadingIcon !== null && loadingIcon !== undefined ) { 5007 loadingIcon.parentNode.removeChild( loadingIcon ); 5008 } 5009 5010 optContainer.classList.remove( 'frm_hidden' ); 5011 } 5012 5013 // Get the field value from all the inputs 5014 function getFieldValueFromInputs( $inputs ) { 5015 var fieldValue = [], 5016 currentValue = ''; 5017 5018 $inputs.each( 5019 function() { 5020 currentValue = this.value; 5021 if ( this.type === 'radio' || this.type === 'checkbox' ) { 5022 if ( this.checked === true ) { 5023 fieldValue.push( currentValue ); 5024 } 5025 } else if ( currentValue !== '' ) { 5026 fieldValue.push( currentValue ); 5027 } 5028 } 5029 ); 5030 5031 if ( fieldValue.length === 0 ) { 5032 fieldValue = ''; 5033 } 5034 5035 return fieldValue; 5036 } 5037 5038 // Hide and clear a Dynamic Field 5039 function hideDynamicField( depFieldArgs ) { 5040 hideFieldAndClearValue( depFieldArgs, true ); 5041 } 5042 5043 // Show Dynamic field 5044 function showDynamicField( depFieldArgs, $fieldDiv, $fieldInputs, valueChanged ) { 5045 if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { 5046 removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); 5047 $fieldDiv.show(); 5048 } 5049 5050 if ( $fieldInputs.hasClass( 'frm_chzn' ) || $fieldInputs.hasClass( 'frm_slimselect' ) ) { 5051 loadAutocomplete(); 5052 } 5053 5054 if ( valueChanged === true ) { 5055 triggerChange( $fieldInputs ); 5056 } 5057 } 5058 5059 /************************************************* 5060 Calculations 5061 ************************************************/ 5062 5063 function triggerCalc() { 5064 if ( typeof __FRMCALC === 'undefined' ) { 5065 // there are no calculations on this page 5066 return; 5067 } 5068 5069 var triggers = __FRMCALC.triggers; 5070 if ( triggers ) { 5071 jQuery( triggers.join() ).trigger({ type: 'change', selfTriggered: true }); 5072 } 5073 5074 triggerCalcWithoutFields(); 5075 } 5076 5077 function triggerCalcWithoutFields() { 5078 var calcs = __FRMCALC.calc, 5079 vals = []; 5080 5081 for ( var fieldKey in calcs ) { 5082 if ( calcs[ fieldKey ].fields.length < 1 && calcs[ fieldKey ].calc && '0' !== calcs[ fieldKey ].calc ) { 5083 var totalField = document.getElementById( 'field_' + fieldKey ); 5084 if ( totalField !== null && ! isChildInputConditionallyHidden( totalField, calcs[ fieldKey ].form_id ) ) { 5085 // if field is not hidden, do calculation 5086 doSingleCalculation( __FRMCALC, fieldKey, vals ); 5087 } 5088 } 5089 } 5090 } 5091 5092 function doCalculation( fieldId, triggerField ) { 5093 if ( typeof __FRMCALC === 'undefined' ) { 5094 // there are no calculations on this page 5095 return; 5096 } 5097 5098 var allCalcs = __FRMCALC, 5099 calc = allCalcs.fields[ fieldId ], 5100 vals = []; 5101 5102 if ( calc === undefined ) { 5103 // this field is not used in a calculation 5104 return; 5105 } 5106 5107 var keys = calc.total; 5108 var len = keys.length; 5109 var pages = getStartEndPage( allCalcs.calc[ keys[ 0 ] ]); 5110 5111 // loop through each calculation this field is used in 5112 for ( var i = 0, l = len; i < l; i++ ) { 5113 var totalOnPage = isTotalFieldOnPage( allCalcs.calc[ keys[ i ] ], pages ); 5114 5115 // Proceed with calculation if total field is not conditionally hidden 5116 if ( totalOnPage && isTotalFieldConditionallyHidden( allCalcs.calc[ keys[ i ] ], triggerField.attr( 'name' ) ) === false ) { 5117 doSingleCalculation( allCalcs, keys[ i ], vals, triggerField ); 5118 } 5119 } 5120 } 5121 5122 /** 5123 * @param thisField 5124 * @since 2.05.06 5125 */ 5126 function getStartEndPage( thisField ) { 5127 var formId = thisField.form_id, 5128 formContainer = document.getElementById( 'frm_form_' + formId + '_container' ); 5129 5130 if ( formContainer === null && thisField.in_section ) { 5131 var fieldContainer = document.getElementById( 'frm_field_' + thisField.in_section + '_container' ); 5132 5133 if ( fieldContainer === null ) { 5134 // bail now and run the calculation if we can't determine if it's on the page 5135 return []; 5136 } 5137 5138 formContainer = closest( fieldContainer, ( el ) => { 5139 return el.tagName === 'FORM'; 5140 }); 5141 5142 formId = formContainer.elements.namedItem( 'form_id' ).value; 5143 } 5144 5145 var hasPreviousPage = formContainer.getElementsByClassName( 'frm_next_page' );
5146 var hasAnotherPage = document.getElementById( 'frm_page_order_' + formId ); 5147 5148 var pages = []; 5149 if ( hasPreviousPage.length > 0 ) { 5150 pages.start = hasPreviousPage[ 0 ]; 5151 } 5152 if ( hasAnotherPage !== null ) { 5153 pages.end = hasAnotherPage; 5154 } 5155 5156 return pages; 5157 } 5158 5159 function closest( el, fn ) { 5160 return el && ( fn( el ) ? el : closest( el.parentNode, fn ) ); 5161 } 5162 5163 /** 5164 * If the total field is not on the current page, don't trigger the calculation 5165 * 5166 * @param calcDetails 5167 * @param pages 5168 * @since 2.05.06 5169 */ 5170 function isTotalFieldOnPage( calcDetails, pages ) { 5171 if ( pages.start !== undefined || pages.end !== undefined ) { 5172 // the form has pages 5173 var hiddenTotalField = jQuery( 'input[type=hidden][name*="[' + calcDetails.field_id + ']"]' ); 5174 if ( hiddenTotalField.length ) { 5175 // the total field is a hidden field 5176 return isHiddenTotalOnPage( hiddenTotalField, pages ); 5177 } 5178 } 5179 5180 return true; 5181 } 5182 5183 function isHiddenTotalOnPage( hiddenTotalField, pages ) { 5184 var onPage, 5185 hiddenParent = hiddenTotalField.closest( '.frm_form_field' ); 5186 5187 if ( hiddenParent.length ) { 5188 // the field is in a section 5189 return true; 5190 } 5191 5192 var totalPos = hiddenTotalField.index(); 5193 var isAfterStart = true; 5194 var isBeforeEnd = true; 5195 if ( pages.start !== undefined ) { 5196 isAfterStart = jQuery( pages.start ).index() < totalPos; 5197 } 5198 if ( pages.end !== undefined ) { 5199 isBeforeEnd = jQuery( pages.end ).index() > totalPos; 5200 } 5201 5202 onPage = ( isAfterStart && isBeforeEnd ); 5203 if ( ! onPage ) { 5204 onPage = hiddenTotalField.closest( '.do-calculation' ).length > 0; 5205 } 5206 5207 return onPage; 5208 } 5209 5210 /** 5211 * Check if a total field is conditionally hidden 5212 * @param {Object} calcDetails 5213 * @param {string} calcDetails.field_id 5214 * @param {string} calcDetails.form_id 5215 * @param {string} calcDetails.inSection 5216 * @param {string} calcDetails.inEmbedForm 5217 * @param {string} triggerFieldName 5218 * @returns {boolean} 5219 */ 5220 function isTotalFieldConditionallyHidden( calcDetails, triggerFieldName ) { 5221 var hidden = false, 5222 fieldId = calcDetails.field_id, 5223 formId = calcDetails.form_id, 5224 hiddenFields = getHiddenFields( formId ); 5225 5226 // Check if there are any conditionally hidden fields 5227 if ( hiddenFields.length < 1 ) { 5228 return hidden; 5229 } 5230 5231 if ( calcDetails.inSection === '0' && calcDetails.inEmbedForm === '0' ) { 5232 // Field is not in a section or embedded form 5233 hidden = isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ); 5234 } else { 5235 // Field is in a section or embedded form 5236 var repeatArgs = getRepeatArgsFromFieldName( triggerFieldName ); 5237 5238 if ( isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) ) { 5239 // Check standard field 5240 hidden = true; 5241 } else if ( isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) ) { 5242 // Check repeating field 5243 hidden = true; 5244 } else if ( calcDetails.inSection !== '0' && calcDetails.inEmbedForm !== '0' ) { 5245 // Check section in embedded form 5246 hidden = isRepeatingFieldConditionallyHidden( calcDetails.inSection, repeatArgs, hiddenFields ); 5247 } else if ( calcDetails.inSection !== '0' ) { 5248 // Check section 5249 hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inSection, hiddenFields ); 5250 } else if ( calcDetails.inEmbedForm !== '0' ) { 5251 // Check embedded form 5252 hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inEmbedForm, hiddenFields ); 5253 } 5254 } 5255 5256 return hidden; 5257 } 5258 5259 // Check if a non-repeating field is conditionally hidden 5260 function isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) { 5261 var htmlID = 'frm_field_' + fieldId + '_container'; 5262 return ( hiddenFields.includes(htmlID) ); 5263 } 5264 5265 // Check if a repeating field is conditionally hidden 5266 function isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) { 5267 var hidden = false; 5268 5269 if ( repeatArgs.repeatingSection ) { 5270 var fieldRepeatId = 'frm_field_' + fieldId + '-' + repeatArgs.repeatingSection; 5271 fieldRepeatId += '-' + repeatArgs.repeatRow + '_container'; 5272 hidden = ( hiddenFields.includes(fieldRepeatId) ); 5273 } 5274 5275 return hidden; 5276 } 5277 5278 function maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc ) { 5279 var alertMessage = ''; 5280 5281 if ( ( ! jQuery( 'form' ).hasClass( 'frm-admin-viewing' ) ) ) { 5282 return; 5283 } 5284 5285 alertMessage += frm_js.calc_error + ' ' + fieldKey + ':\n\n'; 5286 alertMessage += thisFullCalc + '\n\n'; 5287 5288 if ( err.message ) { 5289 alertMessage += err.message + '\n\n'; 5290 } 5291 alert( alertMessage ); 5292 } 5293 5294 /** 5295 * Adjust a Date for timezone offset 5296 * 5297 * @param {Date} date 5298 * @returns {Date} 5299 */ 5300 function treatAsUTC( date ) { 5301 var copy = new Date( date.valueOf() ); 5302 copy.setMinutes( copy.getMinutes() - copy.getTimezoneOffset() ); 5303 return copy; 5304 } 5305 5306 /** 5307 * Try to change date strings and ints to Date objects before calculations 5308 * 5309 * @param {mixed} date
5310 * @returns {mixed} 5311 */ 5312 function normalizeDate( date ) { 5313 switch ( typeof date ) { 5314 case 'number': return new Date( date * 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day) 5315 case 'string': return new Date( date ); 5316 default: return date; 5317 } 5318 } 5319 5320 /** 5321 * Calculate the difference between two dates. 5322 * This is triggered by the [age] and [date_calc] shortcodes. 5323 * 5324 * @param {mixed} a Field date. 5325 * @param {mixed} b Compare date. 5326 * @param {string} format Either 'days' or 'years'. 5327 * @param {number|undefined} fieldId This is defined by the datepicker add-on. 5328 * @param {number|undefined} compareId This is defined by the datepicker add-on. 5329 * @returns {int} 5330 */ 5331 function calculateDateDifference( a, b, format, fieldId, compareId ) { 5332 a = normalizeDate( a ); 5333 b = normalizeDate( b ); 5334 5335 switch ( format ) { 5336 case 'days': { 5337 if ( 'function' === typeof window.frmCalcDateDifferenceDays && fieldId && compareId ) { 5338 return window.frmCalcDateDifferenceDays( treatAsUTC( a ), treatAsUTC( b ), fieldId, compareId ); 5339 } 5340 5341 return Math.floor( ( treatAsUTC( b ) - treatAsUTC( a ) ) / 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day) 5342 } 5343 5344 case 'years': default: { 5345 var years = b.getFullYear() - a.getFullYear(); 5346 if ( b.getMonth() < a.getMonth() || b.getMonth() === a.getMonth() && b.getDate() < a.getDate() ) { 5347 years--; 5348 } 5349 5350 return years; 5351 } 5352 } 5353 } 5354 5355 /** 5356 * @param {object} triggerField 5357 * @param {string} fieldKey 5358 * 5359 * @returns {object} 5360 */ 5361 function getTotalOrCalcField( triggerField, fieldKey ) { 5362 const form = document.contains( triggerField[0] ) ? triggerField.closest( 'form' ) : jQuery( `#frm_field_${triggerField[0].dataset.sectionid}_container` ).closest( 'form' ); 5363 return form.find( fieldKey ); 5364 } 5365 5366 /** 5367 * Set value on a field, properly handling IMask instances. 5368 * IMask handles formatting (leading zeros, fixed literals, etc.) natively 5369 * when value is set through the mask instance. 5370 * 5371 * @since 6.23 5372 * 5373 * @param {jQuery} field The field element 5374 * @param {string|number} value The value to set 5375 * @returns {void} 5376 */ 5377 function setFieldValueWithMask( field, value ) { 5378 const fieldElement = field.get( 0 ); 5379 5380 if ( fieldElement && frmMaskInstances.has( fieldElement ) ) { 5381 const padded = padValueForMask( String( value ), fieldElement.dataset.frmmask || '' ); 5382 frmMaskInstances.get( fieldElement ).value = padded; 5383 } else { 5384 field.val( value ); 5385 } 5386 } 5387 5388 /** 5389 * Pad a numeric value so it fills all digit slots in a mask pattern. 5390 * IMask processes characters left-to-right and cannot pad on its own, 5391 * so e.g. value "5" with mask "00.00" needs to become "05.00". 5392 * 5393 * Non-numeric values or masks without digit slots are returned as-is. 5394 * 5395 * @since 6.23 5396 * 5397 * @param {string} value The string value to pad. 5398 * @param {string} mask The mask pattern (e.g. "00", "00.00", "P0"). 5399 * @returns {string} 5400 */ 5401 function padValueForMask( value, mask ) { 5402 if ( ! mask ) { 5403 return value; 5404 } 5405 5406 // Strip non-digit/non-decimal/non-minus characters from the value for counting. 5407 const numericValue = value.replace( /[^0-9.\-]/g, '' ); 5408 if ( '' === numericValue || isNaN( Number( numericValue ) ) ) { 5409 return value; 5410 } 5411 5412 // Split the mask into parts around the literal "." if present. 5413 const maskParts = mask.split( '.' ); 5414 const intMask = maskParts[0]; 5415 const decMask = maskParts.length > 1 ? maskParts.slice( 1 ).join( '.' ) : ''; 5416 5417 // Count only digit-slot characters (0 and 9) in each part. 5418 const intSlots = ( intMask.match( /[09]/g ) || [] ).length; 5419 const decSlots = ( decMask.match( /[09]/g ) || [] ).length; 5420 5421 if ( 0 === intSlots && 0 === decSlots ) { 5422 return value; 5423 } 5424 5425 // Split the numeric value around the decimal point. 5426 const valueParts = numericValue.split( '.' ); 5427 let intValue = ( valueParts[0] || '0' ).replace( /^-/, '' ); 5428 const isNegative = numericValue.startsWith( '-' ); 5429 5430 // Pad integer part with leading zeros. 5431 if ( intSlots > intValue.length ) { 5432 intValue = intValue.padStart( intSlots, '0' ); 5433 } 5434 5435 let result = ( isNegative ? '-' : '' ) + intValue; 5436 5437 // Pad decimal part with trailing zeros. 5438 if ( decSlots > 0 ) { 5439 let decValue = valueParts[1] || ''; 5440 decValue = decValue.padEnd( decSlots, '0' ).substring( 0, decSlots ); 5441 result += '.' + decValue; 5442 } 5443 5444 return result; 5445 } 5446 5447 function doSingleCalculation( allCalcs, fieldKey, vals, triggerField ) { 5448 var currency, total, dec, updatedTotal, totalField, 5449 thisCalc = allCalcs.calc[ fieldKey ], 5450 thisFullCalc = thisCalc.calc, 5451 fieldInfo = { 5452 triggerField, 5453 inSection: false, 5454 thisFieldCall: 'input[id^="field_' + fieldKey + '-"]' 5455 }; 5456 5457 if ( triggerField !== undefined && triggerField && triggerField.length ) { 5458 totalField = getTotalOrCalcField( triggerField, '#field_' + fieldKey ); 5459 } else { 5460 totalField = jQuery( document.getElementById( 'field_' + fieldKey ) ); 5461 } 5462 5463 if ( totalField.get( 0 )?.dataset.minGap ) { // Avoid running calculation for Range sliders. 5464 return; 5465 } 5466 5467 // exit early for sliders as there is no calculation for such types and avoid errors that might rise due to formatting the value 5468 if ( totalField.attr( 'type' ) === 'range' ) { 5469 return; 5470 } 5471 5472 // TODO: update this to work more like conditional logic 5473 if ( totalField.length < 1 && triggerField !== undefined ) { 5474 // check if the total field is inside of a repeating/embedded form 5475 fieldInfo.inSection = true; 5476 fieldInfo.thisFieldId = objectSearch( allCalcs.fieldsWithCalc, fieldKey ); 5477 totalField = getSiblingField( fieldInfo ); 5478 } 5479 5480 if ( totalField === null || totalField.length < 1 ) { 5481 return; 5482 } 5483 5484 // loop through the fields in this calculation 5485 thisFullCalc = getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo ); 5486 5487 total = ''; 5488 dec = ''; 5489 5490 if ( 'function' === typeof window[ 'frmProGetCalcTotal' + thisCalc.calc_type ]) { 5491 total = window[ 'frmProGetCalcTotal' + thisCalc.calc_type ].call( thisCalc, thisFullCalc, { totalField }); 5492 } else if ( thisCalc.calc_type === 'text' ) { 5493 total = thisFullCalc; 5494 } else { 5495 if ( 'date' === thisCalc.calc_type ) { 5496 // If the datepicker JS did not load, skip date calculations. 5497 return; 5498 } 5499 5500 // Set the number of decimal places 5501 dec = thisCalc.calc_dec; 5502 5503 // allow .toFixed for reverse compatibility 5504 if ( thisFullCalc.includes(').toFixed(') ) { 5505 var calcParts = thisFullCalc.split( ').toFixed(' ); 5506 if ( isNumeric( calcParts[ 1 ]) ) { 5507 dec = calcParts[ 1 ]; 5508 thisFullCalc = thisFullCalc.replace( ').toFixed(' + dec, '' ); 5509 } 5510 } 5511 5512 thisFullCalc = trimNumericCalculation( thisFullCalc ); 5513 5514 if ( thisFullCalc !== '' ) { 5515 try { 5516 total = parseFloat( eval( thisFullCalc ) ); 5517 } catch ( err ) { 5518 maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc ); 5519 } 5520 } 5521 5522 if ( total === undefined || isNaN( total ) ) { 5523 total = 0; 5524 } 5525 5526 // Set decimal points 5527 if ( isNumeric( dec ) && total !== '' ) { 5528 total = total.toFixed( dec ); 5529 } 5530 } 5531 5532 if ( thisCalc.is_currency === true && isNumeric( total ) ) { 5533 currency = getCurrencyFromCalcRule( thisCalc ); 5534 if ( currency.decimals > 0 ) { 5535 total = Math.round10( total, currency.decimals ); 5536 total = maybeAddTrailingZeroToPrice( total, currency ); 5537 dec = currency.decimals; 5538 } 5539 } 5540 5541 if ( totalField.val() === total ) { 5542 setDisplayedTotal( totalField, total, currency ); 5543 return; 5544 } 5545 5546 updatedTotal = false;
5547 if ( ( isNumeric( dec ) || thisCalc.is_currency ) && [ 'number', 'text' ].includes(totalField.attr( 'type' )) ) { 5548 if ( total.toString().slice( -1 ) == '0' && navigator.userAgent.toLowerCase().includes('firefox') ) { 5549 // Change the input to text in Firefox. Otherwise, trailing decimals will fail. 5550 totalField[0].setAttribute( 'type', 'text' ); 5551 } 5552 5553 // The wrapper is frm_with_box when only one of prepend/append is set, and frm_with_boxes when both are. 5554 // A number input cannot hold a comma decimal separator, so switch it back to a period before setting it. 5555 if ( totalField.parent().is( '.frm_with_box.frm_hidden, .frm_with_boxes.frm_hidden' ) && 'string' === typeof total ) { 5556 updatedTotal = true; 5557 setFieldValueWithMask( totalField, total.replace( ',', '.' ) ); 5558 } 5559 } 5560 5561 if ( ! updatedTotal ) { 5562 // Set value using proper mask handling 5563 setFieldValueWithMask( totalField, total ); 5564 } 5565 5566 triggerEvent( document, 'frmCalcUpdatedTotal', { 5567 totalField, 5568 total 5569 }); 5570 5571 if ( triggerField === null || triggerField === undefined || totalField.attr( 'name' ) != triggerField.attr( 'name' ) ) { 5572 totalField.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey }); 5573 } 5574 5575 setDisplayedTotal( totalField, total, currency ); 5576 } 5577 5578 /** 5579 * Show the total for frm_total fields. 5580 */ 5581 function setDisplayedTotal( totalField, total, currency ) { 5582 var prepend, append, 5583 showTotal = totalField.parent().prev(); 5584 5585 if ( ! showTotal.hasClass( 'frm_total_formatted' ) ) { 5586 return; 5587 } 5588 5589 prepend = showTotal.data( 'prepend' ); 5590 append = showTotal.data( 'append' ); 5591 if ( prepend === undefined ) { 5592 prepend = ''; 5593 } 5594 if ( append === undefined ) { 5595 append = ''; 5596 } 5597 5598 if ( typeof currency === 'object' ) { 5599 total = formatCurrency( total, currency ); 5600 if ( currency.symbol_left === prepend ) { 5601 prepend = ''; 5602 } 5603 if ( currency.symbol_right === append ) { 5604 append = ''; 5605 } 5606 } 5607 5608 if ( prepend !== '' ) { 5609 prepend = '<span class="frm_inline_pre">' + prepend + '</span>'; 5610 } 5611 5612 if ( append !== '' ) { 5613 append = '<span class="frm_inline_pre">' + append + '</span>'; 5614 } 5615 5616 showTotal.html( prepend + '<span class="frm_inline_total">' + total + '</span>' + append ); 5617 } 5618 5619 function getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo ) { 5620 var fCount, f, field, date, findVar; 5621 5622 fCount = thisCalc.fields.length; 5623 5624 for ( f = 0; f < fCount; f++ ) { 5625 field = { 5626 triggerField: fieldInfo.triggerField, 5627 thisFieldId: thisCalc.fields[ f ], 5628 inSection: fieldInfo.inSection, 5629 valKey: fieldInfo.inSection + '' + thisCalc.fields[ f ], 5630 thisField: allCalcs.fields[ thisCalc.fields[ f ] ], 5631 thisFieldCall: 'input' + allCalcs.fieldKeys[ thisCalc.fields[ f ] ], 5632 formID: thisCalc.form_id 5633 }; 5634 5635 field = getCallForField( field, allCalcs ); 5636 5637 if ( ! thisCalc.calc_type ) { 5638 // Math calc type. 5639 field.valKey = 'num' + field.valKey; 5640 vals = getCalcFieldId( field, allCalcs, vals ); 5641 5642 if ( vals[ field.valKey ] === undefined || isNaN( vals[ field.valKey ]) ) { 5643 vals[ field.valKey ] = 0; 5644 5645 if ( field.thisField.type === 'date' ) { 5646 date = tryToGetDateValue( field ); 5647 if ( null !== date ) { 5648 vals[ field.valKey ] = Math.floor( date.getTime() / 86400000 ); 5649 } else { 5650 thisFullCalc = ''; 5651 } 5652 } 5653 } else if ( 0 === vals[ field.valKey ] && field.thisField.type === 'date' && dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) ) { 5654 thisFullCalc = ''; 5655 } 5656 } else { 5657 // Text calc type. 5658 field.valKey = 'text' + field.valKey; 5659 vals = getTextCalcFieldId( field, vals ); 5660 if ( vals[ field.valKey ] === undefined ) { 5661 vals[ field.valKey ] = ''; 5662 } 5663 } 5664 5665 if ( thisCalc.calc_type === 'text' ) { 5666 thisFullCalc = replaceShortcodesWithShowOptions( thisFullCalc, vals, field ); 5667 } 5668 5669 findVar = '[' + field.thisFieldId + ']'; 5670 findVar = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' ); 5671 thisFullCalc = thisFullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey ]); 5672 } 5673 5674 return thisFullCalc; 5675 } 5676 5677 /** 5678 * Support show=label (for field with options) and a show= option for each part of a 5679 * multi-part field, like show=first on a name field or show=city on an address field, 5680 * in text calculations. 5681 * 5682 * @param {String} fullCalc 5683 * @param {Array} vals 5684 * @param {Object} field 5685 * @returns {String} 5686 */ 5687 function replaceShortcodesWithShowOptions( fullCalc, vals, field ) { 5688 fullCalc = replaceShowShortcode( 5689 fullCalc, vals, field, 'label', 5690 () => { 5691 return getOptionLabelsFromValues( vals[ field.valKey ], field ); 5692 } 5693 ); 5694 5695 Array.prototype.forEach.call( 5696 getComboSubFields(), 5697 ( subField ) => { 5698 fullCalc = replaceSubFieldShortcode( fullCalc, vals, field, subField ); 5699 } 5700 ); 5701 5702 return fullCalc; 5703 } 5704 5705 /** 5706 * The parts of a multi-part field that a text calculation can show on their own. 5707 * Kept in step with FrmProFormsHelper::get_combo_sub_fields, which decides 5708 * which show= options survive into the formula. 5709 * 5710 * @returns {Array} 5711 */ 5712 function getComboSubFields() { 5713 return [ 5714 // Name field parts. 5715 'first', 5716 'middle', 5717 'last', 5718 // Address field parts. 5719 'line1', 5720 'line2', 5721 'city', 5722 'state', 5723 'zip', 5724 'country' 5725 ]; 5726 } 5727 5728 function replaceSubFieldShortcode( fullCalc, vals, field, show ) { 5729 var valueCallback = function() { 5730 var match = false;
5731 document.querySelectorAll( field.thisFieldCall ).forEach( 5732 ( input ) => { 5733 if ( show === input.id.substr( -show.length ) ) { 5734 match = input; 5735 } 5736 } 5737 ); 5738 return match ? match.value : ''; 5739 }; 5740 return replaceShowShortcode( fullCalc, vals, field, show, valueCallback ); 5741 } 5742 5743 function replaceShowShortcode( fullCalc, vals, field, show, valueCallback ) { 5744 var findVar; 5745 5746 findVar = '[' + field.thisFieldId + ' show=' + show + ']'; 5747 if ( -1 === fullCalc.indexOf( findVar ) ) { 5748 return fullCalc; 5749 } 5750 5751 vals[ field.valKey + show ] = valueCallback(); 5752 findVar = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' ); 5753 fullCalc = fullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey + show ]); 5754 5755 return fullCalc; 5756 } 5757 5758 function tryToGetDateValue( field ) { 5759 var $element = jQuery( field.thisField.key ); 5760 return $element.hasClass( 'hasDatepicker' ) ? $element.datepicker( 'getDate' ) : null; 5761 } 5762 5763 /** 5764 * Check if a date value is ready for use in a calculation. 5765 */ 5766 function dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) { 5767 if ( fieldInfo.triggerField !== null ) { 5768 if ( fieldInfo.triggerField.is( 'input' ) ) { 5769 if ( datepickerFieldShouldBeClearedForDateCalculation( fieldInfo.triggerField ) ) { 5770 return fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key ); 5771 } 5772 return '' === fieldInfo.triggerField.val(); 5773 } 5774 return fieldInfo.triggerField.is( 'div' ) && fieldInfo.triggerField.end().is( 'input' ) && '' === fieldInfo.triggerField.end().val(); 5775 } else if ( fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key ) ) { 5776 return true; 5777 } 5778 return false; 5779 } 5780 5781 function datepickerFieldShouldBeClearedForDateCalculation( field ) { 5782 var dateValue = field.hasClass( 'hasDatepicker' ) ? field.datepicker( 'getDate' ) : null; 5783 return null !== dateValue && -72000000 !== dateValue.getTime(); 5784 } 5785 5786 function fieldShouldBeClearedForDateCalculation( fieldCall, fieldKey ) { 5787 return 0 === fieldCall.indexOf( 'input' ) && 0 === fieldKey.indexOf( '[id=' ) && '' === jQuery( fieldKey ).val(); 5788 } 5789 5790 function getOptionLabelsFromValues( value, field ) { 5791 var fieldId, options, split, labels, length, index; 5792 5793 fieldId = field.thisFieldId; 5794 if ( 'undefined' === typeof __FRMCALC.options || 'undefined' === typeof __FRMCALC.options[ fieldId ]) { 5795 return value; 5796 } 5797 5798 options = __FRMCALC.options[ fieldId ]; 5799 5800 if ( 'checkbox' === field.thisField.type ) { 5801 split = value.split( ', ' ); 5802 labels = []; 5803 length = split.length; 5804 for ( index = 0; index < length; ++index ) { 5805 if ( 'undefined' !== typeof options[ split[ index ] ]) { 5806 labels.push( options[ split[ index ] ]); 5807 } 5808 } 5809 return labels.join( ', ' ); 5810 } 5811 5812 return 'undefined' !== typeof options[ value ] ? options[ value ] : ''; 5813 } 5814 5815 /** 5816 * Trim non-numeric characters from the end of a numeric calculation 5817 * 5818 * @since 2.03.02 5819 * @param {String} numericCalc 5820 * @returns {String} 5821 */ 5822 function trimNumericCalculation( numericCalc ) { 5823 var lastChar = numericCalc.charAt( numericCalc.length - 1 ); 5824 if ( lastChar === '+' || lastChar === '-' ) { 5825 numericCalc = numericCalc.substr( 0, numericCalc.length - 1 ); 5826 } 5827 5828 return numericCalc; 5829 } 5830 5831 function getCallForField( field, allCalcs ) { 5832 if ( field.thisField.type === 'checkbox' || field.thisField.type === 'radio' || field.thisField.type === 'scale' || field.thisField.type === 'star' ) { 5833 field.thisFieldCall = field.thisFieldCall + ':checked,' + field.thisFieldCall + '[type=hidden]'; 5834 } else if ( field.thisField.type === 'select' || field.thisField.type === 'time' ) { 5835 field.thisFieldCall = 'select' + allCalcs.fieldKeys[ field.thisFieldId ] + ' option:selected,' + field.thisFieldCall + '[type=hidden]'; 5836 } else if ( field.thisField.type === 'textarea' ) { 5837 field.thisFieldCall = field.thisFieldCall + ',textarea' + allCalcs.fieldKeys[ field.thisFieldId ]; 5838 } 5839 return field; 5840 } 5841 5842 function maybeDoCalcForSingleField( fieldInput ) { 5843 if ( typeof __FRMCALC === 'undefined' ) { 5844 // there are no calculations on this page 5845 return; 5846 } 5847 5848 // Exit now if field is a type that can't do calculations 5849 if ( ! fieldCanDoCalc( fieldInput.type ) ) { 5850 return; 5851 } 5852 5853 var allCalcs = __FRMCALC, 5854 fieldKey = getFieldKey( fieldInput.id, fieldInput.name ), 5855 triggerField = maybeGetTriggerField( fieldInput ), 5856 vals = []; 5857 5858 if ( allCalcs.calc[ fieldKey ] === undefined ) { 5859 // This field doesn't have any calculations 5860 return; 5861 } 5862 5863 doSingleCalculation( allCalcs, fieldKey, vals, triggerField ); 5864 } 5865 5866 function fieldCanDoCalc( fieldType ) { 5867 return -1 !== [ 'text', 'hidden', 'number', 'textarea' ].indexOf( fieldType ); 5868 } 5869 5870 function getFieldKey( fieldHtmlId, fieldName ) { 5871 var fieldKey = fieldHtmlId.replace( 'field_', '' ), 5872 newFieldKey = ''; 5873 5874 if ( isRepeatingFieldByName( fieldName ) ) { 5875 var fieldKeyParts = fieldKey.split( '-' ); 5876 for ( var i = 0; i < fieldKeyParts.length - 1; i++ ) { 5877 if ( newFieldKey === '' ) { 5878 newFieldKey = fieldKeyParts[ i ]; 5879 } else { 5880 newFieldKey = newFieldKey + '-' + fieldKeyParts[ i ]; 5881 } 5882 } 5883 fieldKey = newFieldKey; 5884 } 5885 5886 return fieldKey; 5887 } 5888 5889 function maybeGetTriggerField( fieldInput ) { 5890 var triggerField = null; 5891 if ( isRepeatingFieldByName( fieldInput.name ) ) { 5892 if ( fieldInput.type !== 'hidden' ) { 5893 triggerField = jQuery( fieldInput ).closest( '.frm_form_field' ); 5894 } else { 5895 triggerField = jQuery( fieldInput ); 5896 } 5897 } 5898 5899 return triggerField; 5900 } 5901 5902 function isRepeatingFieldByName( fieldName ) { 5903 var fieldNameParts = fieldName.split( '][' ); 5904 return fieldNameParts.length >= 3; 5905 } 5906 5907 function getCalcFieldId( field, allCalcs, vals ) { 5908 if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== 0 ) { 5909 return vals; 5910 } 5911 5912 vals[ field.valKey ] = 0; 5913 5914 var currency, 5915 calcField = getCalcField( field ); 5916 5917 if ( calcField === false ) { 5918 return vals; 5919 } 5920 5921 calcField.each( function() { 5922 const thisVal = getOptionValue( field.thisField, this ); 5923 const hasCustomFormat = this.classList.contains( 'frm-has-number-format' ); 5924 5925 if ( field.thisField.type === 'date' ) { 5926 var d = getDateFieldValue( allCalcs.date, thisVal ); 5927 if ( d !== null ) { 5928 vals[ field.valKey ] = Math.ceil( d / ( 1000 * 60 * 60 * 24 ) ); 5929 } 5930 } else if ( 'data' === field.thisField.type ) { 5931 vals[ field.valKey ] = 0;
5932 if ( '' !== thisVal ) { 5933 if ( 'SELECT' === this.tagName ) { 5934 vals[ field.valKey ] = parseFloat( this.querySelector( 'option[value="' + thisVal + '"]' ).textContent ); 5935 } else if ( null !== this.closest( '.frm_checkbox' ) ) { 5936 vals[ field.valKey ] = 0; 5937 jQuery( this.closest( '.frm_opt_container' ).querySelectorAll( 'input:checked' ) ).each( 5938 function() { 5939 vals[ field.valKey ] += parseFloat( this.parentNode.textContent ); 5940 } 5941 ); 5942 } else { 5943 vals[ field.valKey ] = parseFloat( this.parentNode.textContent ); 5944 } 5945 } 5946 } else if ( this.hasAttribute( 'data-frmprice' ) || field.thisField.type === 'total' || hasCustomFormat ) { 5947 // data-frmprice means product field. 5948 currency = getCurrency( field.formID ); 5949 vals[ field.valKey ] += parseFloat( 5950 ! currency 5951 ? thisVal 5952 : ( hasCustomFormat && String( thisVal ).trim().startsWith( '-' ) ? '-' : '' ) + preparePrice( thisVal, currency ) 5953 ); 5954 } else { 5955 var n = thisVal; 5956 5957 if ( n !== '' && n !== 0 ) { 5958 n = n.trim(); 5959 // Include e to support scientific notation like 10e2. 5960 n = parseFloat( n.replace( /,/g, '' ).match( /-?[\d\.e]+$/ ) ); 5961 } 5962 5963 if ( n === undefined || isNaN( n ) || n === '' ) { 5964 n = 0; 5965 } 5966 vals[ field.valKey ] += n; 5967 } 5968 }); 5969 5970 return vals; 5971 } 5972 5973 function getTextCalcFieldId( field, vals ) { 5974 if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== '' ) { 5975 return vals; 5976 } 5977 5978 vals[ field.valKey ] = ''; 5979 5980 var calcField = getCalcField( field ); 5981 if ( calcField === false ) { 5982 return vals; 5983 } 5984 5985 var count = 0; 5986 var sep = ''; 5987 5988 calcField.each( function() { 5989 var thisVal = getOptionValue( field.thisField, this ); 5990 thisVal = thisVal.trim(); 5991 5992 sep = getCalcSep( field, count ); 5993 5994 if ( thisVal !== '' ) { 5995 vals[ field.valKey ] += sep + thisVal; 5996 count++; 5997 } 5998 }); 5999 6000 return vals; 6001 } 6002 6003 function getCalcSep( field, count ) { 6004 var sep = ''; 6005 if ( count > 0 ) { 6006 if ( field.thisField.type === 'time' ) { 6007 if ( count == 1 ) { 6008 sep = ':'; 6009 } else if ( count == 2 ) { 6010 sep = ' '; 6011 } 6012 } else { 6013 sep = ', '; 6014 } 6015 6016 var customSep = jQuery( document ).triggerHandler( 'frmCalSeparation', [ field.thisField, count ]); 6017 if ( customSep !== undefined ) { 6018 sep = customSep; 6019 } 6020 } 6021 return sep; 6022 } 6023 6024 function getCalcField( field ) { 6025 var calcField; 6026 if ( field.inSection === false ) { 6027 if ( 'name' === field.thisField.type ) { 6028 return getOffScreenFieldForName( field ); 6029 } 6030 6031 if ( 'undefined' !== typeof field.triggerField && field.triggerField && field.triggerField.length ) { 6032 calcField = getTotalOrCalcField( field.triggerField, field.thisFieldCall ); 6033 } else { 6034 calcField = jQuery( field.thisFieldCall ); 6035 } 6036 if ( ! calcField.length && -1 !== [ 'date', 'data' ].indexOf( field.thisField.type ) ) { 6037 calcField = jQuery( field.thisField.key ); 6038 if ( ! calcField.length && 'data' === field.thisField.type ) { 6039 // radio and checkbox fields have additional hyphenated values in their ids. 6040 calcField = jQuery( field.thisField.key.replace( '="', '^=' ).replace( '"]', '-' ) + ']:checked' ); 6041 } 6042 } 6043 } else { 6044 calcField = getSiblingField( field ); 6045 } 6046 6047 if ( calcField === null || calcField === undefined || calcField.length < 1 ) { 6048 calcField = false; 6049 } 6050 6051 if ( calcField.length > 1 ) { 6052 calcField = filterCalcField( calcField, field.thisFieldId ); 6053 } 6054 6055 return calcField; 6056 } 6057 6058 /** 6059 * Filters calculation fields (elements) to be involved in the current calculation. This is so that elements that are in calcField just 6060 * because their id starts with a similar string while they don't relate to the calculation are removed from the list. 6061 * 6062 * Fixes #1337. 6063 * 6064 * @param {Object} $calcField A jQuery object that contains a set of `elements to be involved in the current calculation. 6065 * @param {String} thisFieldCall The query selector used to select calculation elements. 6066 */ 6067 function filterCalcField( $calcField, thisFieldId ) { 6068 return $calcField.filter( function() { 6069 var target = 'OPTION' === this.nodeName ? this.closest( 'select' ) : this; 6070 return target && target.name && target.name.includes(thisFieldId); 6071 }); 6072 } 6073 6074 /** 6075 * Create an input with both first and last name. 6076 * This isn't actually added to the page, but is used for Name calculations. 6077 * 6078 * @param {Object} field 6079 * @returns {Object} 6080 */ 6081 function getOffScreenFieldForName( field ) { 6082 var nameParts, input; 6083 6084 nameParts = []; 6085 document.querySelectorAll( field.thisFieldCall ).forEach( 6086 ( input ) => { 6087 nameParts.push( input.value ); 6088 } 6089 ); 6090 6091 input = document.createElement( 'input' ); 6092 input.value = nameParts.join( ' ' ); 6093 return jQuery( input ); 6094 } 6095 6096 /** 6097 * Get the value from a date field regardless of whether datepicker is defined for it 6098 * Limitations: If using a format with a 2-digit date, '20' will be added to the front if the year is prior to 70 6099 */ 6100 function getDateFieldValue( dateFormat, thisVal ) { 6101 var d = 0; 6102 6103 if ( ! thisVal ) { 6104 // If no value was selected in date field, use 0 6105 } else if ( jQuery.datepicker === undefined ) { 6106 // If date field is not on the current page 6107 6108 const splitAt = [ '/', '.' ].find( char =>
6108 dateFormat.includes( char ) ) || '-'; 6109 var year = '', 6110 month = '', 6111 day = '', 6112 formatPieces = dateFormat.split( splitAt ), 6113 datePieces = thisVal.split( splitAt ); 6114 6115 for ( var i = 0; i < formatPieces.length; i++ ) { 6116 if ( formatPieces[ i ] === 'y' ) { 6117 var currentYear = new Date().getFullYear() + 15; 6118 var currentYearPlusFifteen = currentYear.toString().substr( 2, 2 ); 6119 6120 if ( datePieces[ i ] > currentYearPlusFifteen ) { 6121 year = '19' + datePieces[ i ]; 6122 } else { 6123 year = '20' + datePieces[ i ]; 6124 } 6125 } else if ( formatPieces[ i ] === 'yy' ) { 6126 year = datePieces[ i ]; 6127 } else if ( formatPieces[ i ] === 'm' || formatPieces[ i ] === 'mm' ) { 6128 month = datePieces[ i ]; 6129 if ( month.length < 2 ) { 6130 month = '0' + month; 6131 } 6132 } else if ( formatPieces[ i ] === 'd' || formatPieces[ i ] === 'dd' ) { 6133 day = datePieces[ i ]; 6134 if ( day.length < 2 ) { 6135 day = '0' + day; 6136 } 6137 } 6138 } 6139 6140 d = Date.parse( year + '-' + month + '-' + day ); 6141 } else { 6142 d = jQuery.datepicker.parseDate( dateFormat, thisVal ); 6143 } 6144 return d; 6145 } 6146 6147 function getSiblingField( field ) { 6148 if ( field.triggerField === undefined ) { 6149 return null; 6150 } 6151 6152 var fields = null, 6153 container = field.triggerField.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ), 6154 repeatArgs = getRepeatArgsFromFieldName( field.triggerField.attr( 'name' ) ), 6155 siblingFieldCall = field.thisFieldCall.replace( '[id=', '[id^=' ).replace( /-"]/g, '-' + repeatArgs.repeatRow + '"]' ); 6156 6157 if ( container.length || repeatArgs.repeatRow !== '' ) { 6158 if ( container.length ) { 6159 fields = container.find( siblingFieldCall + ',' + siblingFieldCall.replace( 'input[', 'select[' ) ); 6160 } else { 6161 fields = jQuery( siblingFieldCall ); 6162 } 6163 6164 if ( fields === null || fields === undefined || fields.length < 1 ) { 6165 fields = uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall ); 6166 } 6167 } else { 6168 // the trigger is not in the repeating section 6169 fields = getNonSiblingField( field ); 6170 } 6171 6172 return fields; 6173 } 6174 6175 function uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall ) { 6176 var fields = null; 6177 if ( siblingFieldCall.indexOf( ':checked' ) ) { 6178 // check if the field has nothing selected, or is not inside the section 6179 var inSection = container.find( siblingFieldCall.replace( ':checked', '' ) ); 6180 if ( inSection.length < 1 ) { 6181 fields = getNonSiblingField( field ); 6182 } 6183 } else { 6184 // the field holding the value is outside of the section 6185 fields = getNonSiblingField( field ); 6186 } 6187 return fields; 6188 } 6189 6190 function getNonSiblingField( field ) { 6191 var nonSiblingField = jQuery( field.thisFieldCall + ',' + field.thisFieldCall.replace( 'input[', 'select[' ) ); 6192 if ( ! nonSiblingField.length && 'input[' === field.thisFieldCall.substr( 0, 6 ) ) { 6193 if ( 'undefined' !== typeof field.triggerField && field.triggerField.is( 'div' ) && field.triggerField.hasClass( 'frm_form_field' ) ) { 6194 nonSiblingField = field.triggerField.find( field.thisFieldCall.replace( 'input[', 'textarea[' ) ); 6195 } else { 6196 nonSiblingField = jQuery( field.thisFieldCall.replace( 'input[', 'textarea[' ) ); 6197 } 6198 } 6199 return nonSiblingField; 6200 } 6201 6202 function getOptionValue( thisField, currentOpt ) { 6203 var thisVal; 6204 6205 // If current option is an other option, get other value 6206 if ( isOtherOption( thisField, currentOpt ) ) { 6207 thisVal = getOtherValueAnyField( thisField, currentOpt ); 6208 } else if ( currentOpt.type === 'checkbox' || currentOpt.type === 'radio' ) { 6209 6210 if ( currentOpt.checked ) { 6211 // hasAttribute( 'data-frmprice' ) is a product field. 6212 thisVal = currentOpt.hasAttribute( 'data-frmprice' ) ? currentOpt.dataset.frmprice : currentOpt.value; 6213 } else { 6214 thisVal = currentOpt.dataset.off; 6215 } 6216 } else if ( currentOpt.hasAttribute( 'data-frmprice' ) ) { 6217 thisVal = currentOpt.dataset.frmprice; 6218 } else if ( isCurrencyFormatField( thisField ) ) { 6219 const fieldKey = thisField.key.replace( '[id="field_', '' ).replace( '"]', '' ); 6220 const calcRule = window.__FRMCALC?.calc?.[ fieldKey ]; 6221 6222 thisVal = jQuery( currentOpt ).val(); 6223 6224 if ( ! calcRule ) { 6225 return thisVal; 6226 } 6227 6228 const resultFieldKey = thisField.total[0]; 6229 const totalField = resultFieldKey ? window.__FRMCALC?.calc?.[ resultFieldKey ] : false;
6230 6231 if ( ! totalField || totalField.calc_type !== 'text' ) { 6232 const isNegative = '-' === thisVal.charAt( 0 ); 6233 const currency = getCurrencyFromCalcRule( calcRule ); 6234 6235 thisVal = '' + preparePrice( thisVal, currency ); 6236 6237 // Calling preparePrice() will remove the negative sign, so add it back if needed 6238 if ( isNegative ) { 6239 thisVal = '-' + thisVal; 6240 } 6241 } 6242 } else { 6243 thisVal = jQuery( currentOpt ).val(); 6244 } 6245 6246 if ( thisVal === undefined ) { 6247 thisVal = ''; 6248 } 6249 return thisVal; 6250 } 6251 6252 function isCurrencyFormatField( field ) { 6253 const fieldKey = field.key.replace( '[id="field_', '' ).replace( '"]', '' ); 6254 const isCalcField = window.__FRMCALC?.calc?.[ fieldKey ]?.is_currency; 6255 return isCalcField ? true : false; 6256 } 6257 6258 /* Check if current option is an "Other" option (not an Other text field) */ 6259 function isOtherOption( thisField, currentOpt ) { 6260 var isOtherOpt = false; 6261 6262 // If hidden, check for a value 6263 if ( currentOpt.type === 'hidden' ) { 6264 if ( getOtherValueLimited( currentOpt ) !== '' ) { 6265 isOtherOpt = true; 6266 } 6267 } else if ( thisField.type === 'select' ) { 6268 // If a visible dropdown field 6269 var optClass = currentOpt.className; 6270 if ( optClass && optClass.includes('frm_other_trigger') ) { 6271 isOtherOpt = true; 6272 } 6273 } else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) { 6274 // If visible checkbox/radio field 6275 if ( currentOpt.id.includes('-other_') && !currentOpt.id.includes('-otext') ) { 6276 isOtherOpt = true; 6277 } 6278 } 6279 6280 return isOtherOpt; 6281 } 6282 6283 /* Get the value from an "Other" text field */ 6284 /* Does NOT work for visible select fields */ 6285 function getOtherValueLimited( currentOpt ) { 6286 var otherVal = '', 6287 otherText = document.getElementById( currentOpt.id + '-otext' ); 6288 6289 if ( otherText !== null && otherText.value !== '' ) { 6290 otherVal = otherText.value; 6291 } 6292 return otherVal; 6293 } 6294 6295 /* Get value from Other text field */ 6296 function getOtherValueAnyField( thisField, currentOpt ) { 6297 var otherVal = 0; 6298 6299 if ( thisField.type === 'select' ) { 6300 if ( currentOpt.type === 'hidden' ) { 6301 if ( isCurrentOptRepeating( currentOpt ) ) { 6302 // Do nothing because regular doCalculation code takes care of it 6303 } else { 6304 otherVal = getOtherValueLimited( currentOpt ); 6305 } 6306 } else { 6307 otherVal = getOtherSelectValue( currentOpt ); 6308 } 6309 } else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) { 6310 if ( currentOpt.type === 'hidden' ) { 6311 // Do nothing because regular doCalculation code takes care of it 6312 } else { 6313 otherVal = getOtherValueLimited( currentOpt ); 6314 } 6315 } 6316 6317 return otherVal; 6318 } 6319 6320 /* Check if current option is in a repeating section */ 6321 function isCurrentOptRepeating( currentOpt ) { 6322 var isRepeating = false, 6323 parts = currentOpt.name.split( '[' ); 6324 6325 if ( parts.length > 2 ) { 6326 isRepeating = true; 6327 } 6328 return isRepeating; 6329 } 6330 6331 /* Get value from Other text field in a visible dropdown field */ 6332 function getOtherSelectValue( currentOpt ) { 6333 var fields = getOtherSelects( currentOpt ); 6334 return fields.val(); 6335 } 6336 6337 /** 6338 * Fill in the other text, and select the other option from a dropdown 6339 */ 6340 function setOtherSelectValue( thisField, value ) { 6341 var i, 6342 fields = getOtherSelects( thisField ); 6343 6344 if ( fields.length < 1 ) { 6345 return; 6346 } 6347 6348 fields.val( value ); 6349 6350 for ( i = 0; i < thisField.options.length; i++ ) { 6351 if ( thisField.options[ i ].className.includes('frm_other_trigger') ) { 6352 thisField.options[ i ].selected = true; 6353 } 6354 } 6355 } 6356 6357 function getOtherSelects( currentOpt ) { 6358 return jQuery( currentOpt ).closest( '.frm_other_container' ).find( '.frm_other_input' ); 6359 } 6360 6361 function setOtherValueLimited( thisField, value ) { 6362 var otherText, baseId, parentInput, 6363 i = 0, 6364 idParts = thisField.id.split( '-' ); 6365 idParts.pop(); //remove the last id 6366 baseId = idParts.join( '-' ); 6367 otherText = document.querySelectorAll( '[id^=' + baseId + '-other][id$=otext]' ); 6368 6369 if ( otherText.length > 0 ) { 6370 for ( i = 0; i < otherText.length; i++ ) { 6371 if ( otherText[ i ].value === '' ) { 6372 otherText[ i ].value = value; 6373 parentInput = document.getElementById( otherText[ i ].id.replace( '-otext', '' ) ); 6374 if ( parentInput !== null ) { 6375 parentInput.checked = true; 6376 } 6377 } 6378 } 6379 } 6380 } 6381 6382 function savingDraftEntry( object ) { 6383 var isDraft = false, 6384 savingDraft = jQuery( object ).find( '.frm_saving_draft' ); 6385 6386 if ( savingDraft.length ) { 6387 isDraft = savingDraft.val(); 6388 } 6389 return isDraft; 6390 } 6391 6392 function goingToPrevPage( object ) { 6393 var goingBack = false, 6394 nextPage = jQuery( object ).find( '.frm_next_page' ); 6395 6396 if ( nextPage.length && nextPage.val() ) { 6397 var formID = jQuery( object ).find( 'input[name="form_id"]' ).val(); 6398 var prevPage = jQuery( object ).find( 'input[name="frm_page_order_' + formID + '"]' ); 6399 if ( prevPage.length ) { 6400 prevPage = parseInt( prevPage.val() ); 6401 } else { 6402 prevPage = 0; 6403 } 6404 6405 if ( ! prevPage || ( parseInt( nextPage.val() ) < prevPage ) ) { 6406 goingBack = true; 6407 } 6408 } 6409 6410 return goingBack; 6411 } 6412 6413 function afterFormSubmitted( event, form ) { 6414 // if the success message is showing, run the logic 6415 checkConditionalLogic( 'pageLoad' ); 6416 doEditInPlaceCleanUp( form ); 6417 checkFieldsOnPage(); 6418 maybeShowMoreStepsButton(); 6419 } 6420 6421 function afterPageChanged() { 6422 checkFieldsOnPage(); 6423 addTopAddRowBtnForRepeater(); 6424 maybeDisableCheckboxesWithLimit(); 6425 // make sure this comes last so that the total is calculated only after knowing the visible fields 6426 calcProductsTotal(); 6427 maybeShowMoreStepsButton(); 6428 // calculations in a repeater need to be done again after page is changed to fix issue #3414 6429 triggerChangeOnCalcTriggers(); 6430 maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) ); 6431 initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) ); 6432 initFormatFieldValueNumbers(); 6433 } 6434 6435 function triggerChangeOnCalcTriggers() { 6436 if ( 'undefined' === typeof __FRMCALC || 'undefined' === typeof __FRMCALC.fieldKeys ) { 6437 return; 6438 } 6439
6440 Object.values( __FRMCALC.fieldKeys ).forEach( ( key ) => { 6441 jQuery( key + ':not(label):not([type=hidden])' ).each( function() { 6442 jQuery( this ).trigger({ type: 'change', selfTriggered: true }); 6443 }); 6444 }); 6445 } 6446 6447 /** Google Tables **/ 6448 6449 function generateGoogleTables( graphs, graphType ) { 6450 for ( var num = 0; num < graphs.length; num++ ) { 6451 generateSingleGoogleTable( graphs[ num ], graphType ); 6452 } 6453 } 6454 6455 function generateSingleGoogleTable( opts, type ) { 6456 google.load( 'visualization', '1.0', { packages: [ type ], callback() { 6457 compileGoogleTable( opts ); 6458 } }); 6459 } 6460 6461 function compileGoogleTable( opts ) { 6462 var data = new google.visualization.DataTable(), 6463 showID = false; 6464 6465 const ownerDocument = opts.document || document; 6466 6467 if ( jQuery.inArray( 'id', opts.options.fields ) !== -1 ) { 6468 showID = true; 6469 data.addColumn( 'number', frm_js.id ); 6470 } 6471 6472 var colCount = opts.fields.length; 6473 var type = 'string'; 6474 for ( var i = 0, l = colCount; i < l; i++ ) { 6475 var thisCol = opts.fields[ i ]; 6476 type = getGraphType( thisCol ); 6477 6478 data.addColumn( type, thisCol.name ); 6479 } 6480 6481 var showEdit = false; 6482 if ( opts.options.edit_link ) { 6483 showEdit = true; 6484 data.addColumn( 'string', opts.options.edit_link ); 6485 } 6486 6487 var showDelete = false; 6488 if ( opts.options.delete_link ) { 6489 showDelete = true; 6490 data.addColumn( 'string', opts.options.delete_link ); 6491 } 6492 6493 var col = 0; 6494 if ( opts.entries !== null ) { 6495 var entryCount = opts.entries.length; 6496 data.addRows( entryCount ); 6497 6498 var row = 0; 6499 6500 for ( var e = 0, len = entryCount; e < len; e++ ) { 6501 col = 0; 6502 var entry = opts.entries[ e ]; 6503 if ( showID ) { 6504 data.setCell( row, col, entry.id ); 6505 col++; 6506 } 6507 6508 for ( var field = 0, fieldCount = colCount; field < fieldCount; field++ ) { 6509 var thisEntryCol = opts.fields[ field ]; 6510 type = getGraphType( thisEntryCol ); 6511 6512 var fieldVal = entry.metas[ thisEntryCol.id ]; 6513 if ( type === 'number' && ( fieldVal === null || fieldVal === '' ) ) { 6514 fieldVal = 0; 6515 } else if ( type === 'boolean' ) { 6516 if ( fieldVal === null || fieldVal == 'false' || fieldVal === false ) { 6517 fieldVal = false;
6518 } else { 6519 fieldVal = true; 6520 } 6521 } 6522 6523 data.setCell( row, col, fieldVal ); 6524 6525 col++; 6526 } 6527 6528 if ( showEdit ) { 6529 if ( entry.editLink !== undefined ) { 6530 data.setCell( row, col, '<a href="' + entry.editLink + '">' + opts.options.edit_link + '</a>' ); 6531 } else { 6532 data.setCell( row, col, '' ); 6533 } 6534 col++; 6535 } 6536 6537 if ( showDelete ) { 6538 if ( entry.deleteLink !== undefined ) { 6539 data.setCell( row, col, '<a href="' + entry.deleteLink + '" class="frm_delete_link" data-frmconfirm="' + opts.options.confirm + '">' + opts.options.delete_link + '</a>' ); 6540 } else { 6541 data.setCell( row, col, '' ); 6542 } 6543 } 6544 6545 row++; 6546 } 6547 } else { 6548 data.addRows( 1 ); 6549 col = 0; 6550 6551 for ( i = 0, l = colCount; i < l; i++ ) { 6552 if ( col > 0 ) { 6553 data.setCell( 0, col, '' ); 6554 } else { 6555 data.setCell( 0, col, opts.options.no_entries ); 6556 } 6557 col++; 6558 } 6559 } 6560 6561 var chart = new google.visualization.Table( ownerDocument.getElementById( 'frm_google_table_' + opts.options.form_id ) ); 6562 chart.draw( data, opts.graphOpts ); 6563 } 6564 6565 /** Google Graphs **/ 6566 6567 function generateGoogleGraphs( graphs ) { 6568 var l, i; 6569 l = graphs.length; 6570 for ( i = 0; i < l; i++ ) { 6571 generateSingleGoogleGraph( graphs[ i ]); 6572 6573 if ( 'string' === typeof graphs[ i ].options.width && '%' === graphs[ i ].options.width.substr( -1 ) ) { 6574 addResponsiveGraphListener( graphs[ i ]); 6575 } 6576 } 6577 } 6578 6579 function addResponsiveGraphListener( graphData ) { 6580 window.addEventListener( 6581 'resize', 6582 () => { 6583 generateSingleGoogleGraph( graphData ); 6584 } 6585 ); 6586 } 6587 6588 function generateSingleGoogleGraph( graphData ) { 6589 google.charts.load( 'current', { packages: [ graphData.package ] }); 6590 google.charts.setOnLoadCallback( () => { 6591 compileGoogleGraph( graphData ); 6592 }); 6593 } 6594 6595 function compileGoogleGraph( graphData ) { 6596 var data = new google.visualization.DataTable(); 6597 data = google.visualization.arrayToDataTable( graphData.data ); 6598 6599 const ownerDocument = graphData.document || document; 6600 6601 var chartDiv = ownerDocument.getElementById( 'chart_' + graphData.graph_id ); 6602 if ( chartDiv === null ) { 6603 return; 6604 } 6605 6606 var type = ( graphData.type.charAt( 0 ).toUpperCase() + graphData.type.slice( 1 ) ); 6607 if ( type !== 'Histogram' && type !== 'Table' ) { 6608 type += 'Chart'; 6609 } 6610 6611 var chart = new google.visualization[ type ]( chartDiv ); 6612 6613 chart.draw( data, graphData.options ); 6614 jQuery( ownerDocument ).trigger( 'frmDrawChart', [ chart, 'chart_' + graphData.graph_id, data ]); 6615 } 6616 6617 function getGraphType( field ) { 6618 var type = 'string'; 6619 if ( field.type === 'number' ) { 6620 type = 'number'; 6621 } else if ( field.type === 'checkbox' || field.type === 'select' ) { 6622 var optCount = field.options.length; 6623 if ( field.type === 'select' && field.options[ 0 ] === '' ) { 6624 if ( field.field_options.post_field === 'post_status' ) { 6625 optCount = 3; 6626 } else { 6627 optCount = optCount - 1; 6628 } 6629 } 6630 if ( optCount == 1 ) { 6631 type = 'boolean'; 6632 } 6633 } 6634 return type; 6635 } 6636 6637 /** Repeating Fields **/ 6638 6639 function removeRow() { 6640 if ( ! confirmRowRemoval() ) { 6641 return; 6642 } 6643 6644 /*jshint validthis:true */ 6645 var rowNum = jQuery( this ).data( 'key' ), 6646 sectionID = jQuery( this ).data( 'parent' ), 6647 id = 'frm_section_' + sectionID + '-' + rowNum, 6648 thisRow = jQuery( this ).parents( 'div[id^="frm_section_"]' ), 6649 fields = thisRow.find( 'input, select, textarea, .frm_html_container' ), 6650 formId = jQuery( this ).closest( 'form' ).find( 'input[name="form_id"]' ).val(); 6651 6652 thisRow.fadeOut( 'slow', () => { 6653 const repeaterRow = thisRow[0].closest( '.frm_section_heading'); 6654 if ( repeaterRow.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' )?.length === 1 ) { 6655 repeaterRow.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' ).style.display = 'inline-block'; 6656 } 6657 thisRow.remove(); 6658 6659 fields.each( function() { 6660 /* update calculations when a row is removed */ 6661 var fieldID; 6662 if ( this.matches( '.frm_html_container' ) ) { 6663 fieldID = getHtmlFieldID( this ); 6664 } else { 6665 fieldID = frmFrontForm.getFieldId( this, false ); 6666 } 6667 6668 if ( this.type != 'file' ) { 6669 doCalculation( fieldID, jQuery( this ) ); 6670 } 6671 6672 var container = 'frm_field_' + fieldID + '-' + sectionID + '-' + rowNum + '_container'; 6673 removeFromHideFields( container, formId ); 6674 6675 // If the field is an RTE, remove the editor associated with it 6676 // this is done so that if/when the field is Added again it get initialized 6677 if ( this.classList.contains( 'wp-editor-area' ) ) { 6678 removeRichText( this.id ); 6679 } 6680 }); 6681 6682 showAddButton( sectionID ); 6683 maybeHideRemoveButtons( sectionID ); 6684 6685 /* eslint-disable camelcase */ 6686 if ( typeof frmThemeOverride_frmRemoveRow === 'function' ) { 6687 frmThemeOverride_frmRemoveRow( id, thisRow ); 6688 } 6689 /* eslint-enable camelcase */ 6690 6691 jQuery( document ).trigger( 'frmAfterRemoveRow' ); 6692 }); 6693 6694 return false;
6695 } 6696 6697 /** 6698 * When a repeater row is removed, check for the minimum number of rows and maybe hide remove buttons. 6699 * 6700 * @param {string|number} sectionID 6701 * @returns {void} 6702 */ 6703 function maybeHideRemoveButtons( sectionID ) { 6704 var sectionContainer, repeatButtons, minRows, currentRows; 6705 6706 sectionContainer = document.querySelector( '#frm_field_' + sectionID + '_container' ); 6707 if ( ! sectionContainer ) { 6708 return; 6709 } 6710 6711 repeatButtons = sectionContainer.querySelector( '.frm_repeat_buttons[data-repeat-min]' ); 6712 if ( ! repeatButtons || ! repeatButtons.dataset.repeatMin ) { 6713 return; 6714 } 6715 6716 minRows = repeatButtons.dataset.repeatMin; 6717 currentRows = document.querySelectorAll( '.frm_repeat_' + sectionID ).length; 6718 if ( currentRows <= minRows ) { 6719 hideRemoveButtons( sectionID ); 6720 } 6721 } 6722 6723 /** 6724 * Gets the html field ID from the container in a repeater. Ex. frm_field_151-152-0_container (151 is the field ID in this case). 6725 * 6726 * @param {Array} field 6727 * @returns {Number} 6728 */ 6729 function getHtmlFieldID( field ) { 6730 var parentIDParts; 6731 6732 parentIDParts = field.id.split( '_' ); 6733 6734 if ( parentIDParts.length < 3 ) { 6735 return 0; 6736 } 6737 6738 parentIDParts = parentIDParts[2]; 6739 parentIDParts = parentIDParts.split( '-' ); 6740 6741 if ( ! parentIDParts.length ) { 6742 return 0; 6743 } 6744 6745 return parentIDParts[0]; 6746 } 6747 6748 function confirmRowRemoval() { 6749 if ( ! frm_js.repeaterRowDeleteConfirmation ) { 6750 return true; 6751 } 6752 return confirm( frm_js.repeaterRowDeleteConfirmation ); 6753 } 6754 6755 function hideAddButton( sectionID ) { 6756 getRepeaterAddButtons( sectionID ).forEach( 6757 ( button ) => { 6758 button.classList.add( 'frm_hide_add_button' ); 6759 } 6760 ); 6761 } 6762 6763 function showAddButton( sectionID ) { 6764 getRepeaterAddButtons( sectionID ).forEach( 6765 ( button ) => { 6766 button.classList.remove( 'frm_hide_add_button' ); 6767 } 6768 ); 6769 } 6770 6771 function getRepeaterAddButtons( sectionID ) { 6772 return getFieldContainerChildren( sectionID, '.frm_add_form_row' ); 6773 } 6774 6775 function hideRemoveButtons( sectionID ) { 6776 getRepeaterRemoveButtons( sectionID ).forEach( 6777 ( button ) => { 6778 button.classList.add( 'frm_hide_remove_button' ); 6779 } 6780 ); 6781 } 6782 6783 function showRemoveButtons( sectionID ) { 6784 getRepeaterRemoveButtons( sectionID ).forEach( 6785 ( button ) => { 6786 button.classList.remove( 'frm_hide_remove_button' ); 6787 } 6788 ); 6789 } 6790 6791 function getRepeaterRemoveButtons( sectionID ) { 6792 return getFieldContainerChildren( sectionID, '.frm_remove_form_row' ); 6793 } 6794 6795 /** 6796 * @param {string|number} fieldD 6797 * @returns {HTMLElement|null} 6798 */ 6799 function getFieldContainer( fieldD ) { 6800 return document.getElementById( 'frm_field_' + fieldD + '_container' ); 6801 } 6802 6803 function getFieldContainerChildren( fieldID, childSelector ) { 6804 var container = getFieldContainer( fieldID ); 6805 if ( ! container ) { 6806 return []; 6807 } 6808 return container.querySelectorAll( childSelector ); 6809 } 6810 6811 function addRow() { 6812 /*jshint validthis:true */ 6813 var thisBtn, id, i, numberOfSections, lastRowIndex, stateField, state, form, data, success, error, extraParams; 6814 6815 // If row is currently being added, leave now 6816 if ( currentlyAddingRow === true ) { 6817 return false; 6818 } 6819 6820 // Indicate that a row is being added (so double clicking Add button doesn't cause problems) 6821 currentlyAddingRow = true; 6822 6823 thisBtn = jQuery( this ); 6824 id = thisBtn.data( 'parent' ); 6825 i = 0; 6826 numberOfSections = jQuery( '.frm_repeat_' + id ).length; 6827 6828 if ( numberOfSections > 0 ) { 6829 lastRowIndex = false; 6830 document.querySelectorAll( '.frm_repeat_' + id ).forEach( 6831 ( element ) => { 6832 var strippedId = element.id.replace( 'frm_section_' + id + '-', '' ), 6833 parsedId; 6834 6835 if ( ! strippedId.length || 'i' === strippedId[0]) { 6836 return; 6837 } 6838 6839 parsedId = parseInt( strippedId ); 6840 // strippedId should be a unique number index. 6841 // Since there is no "i" prefix, this is not an entry ID. 6842 if ( parsedId && ( false === lastRowIndex || parsedId > lastRowIndex ) ) { 6843 lastRowIndex = parsedId; 6844 } 6845 } 6846 ); 6847 6848 if ( false === lastRowIndex ) { 6849 i = 1; 6850 } else { 6851 i = lastRowIndex + 1; 6852 } 6853 } 6854 6855 stateField = document.querySelector( 'input[name="frm_state"]' ); 6856 state = null !== stateField ? stateField.value : ''; 6857 6858 form = jQuery( this ).closest( 'form' ).get( 0 ); 6859 data = { 6860 action: 'frm_add_form_row', 6861 field_id: id, 6862 i, 6863 numberOfSections, 6864 nonce: frm_js.nonce, 6865 frm_state: state 6866 }; 6867 6868 /** 6869 * Allow other add-ons to add additional data when a new repeater row is added. 6870 * 6871 * @since 6.25 6872 * 6873 * @param {Object} data 6874 */ 6875 triggerEvent( form, 'frmBeforeNewRepeaterRow', data ); 6876 6877 success = function( r ) {
6878 var html, item, checked, fieldID, fieldObject, repeatArgs, j, inputRanges; 6879 6880 //only process row if row actually added 6881 if ( r.html ) { 6882 html = r.html; 6883 item = jQuery( html ).addClass( 'frm-fade-in' ); 6884 const repeaterSection = thisBtn[0].closest( '.frm_section_heading' ); 6885 const toggleContainer = repeaterSection.querySelector( '.frm_toggle_container.frm_grid_container' ); 6886 if ( toggleContainer ) { 6887 toggleContainer.append( item[0] ); 6888 } else { 6889 repeaterSection.append( item[0] ); 6890 } 6891 const repeatButtons = repeaterSection.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' ); 6892 if ( repeatButtons ) { 6893 repeatButtons.style.display = 'none'; 6894 } 6895 6896 inputRanges = item[0].querySelectorAll( 'input[type=range]' ); 6897 for ( j = 0; j < inputRanges.length; j++ ) { 6898 handleSliderEvent.call( inputRanges[j]); 6899 } 6900 6901 if ( r.is_repeat_limit_reached ) { 6902 hideAddButton( id ); 6903 } 6904 6905 if ( ! r.passes_repeat_min_check ) { 6906 hideRemoveButtons( id ); 6907 } else { 6908 showRemoveButtons( id ); 6909 } 6910 6911 checked = [ 'other' ]; 6912 repeatArgs = { 6913 repeatingSection: id.toString(), 6914 repeatRow: i.toString() 6915 }; 6916 6917 // hide fields with conditional logic 6918 jQuery( html ).find( 'input, select, textarea' ).each( function() { 6919 // Readonly dropdown fields won't have a name attribute 6920 if ( this.name === '' ) { 6921 return true; 6922 } 6923 if ( this.type === 'file' ) { 6924 fieldID = this.name.replace( 'file', '' ).split( '-' )[0]; 6925 } else { 6926 fieldID = this.name.replace( 'item_meta[', '' ).split( ']' )[ 2 ].replace( '[', '' ); 6927 } 6928 if ( jQuery.inArray( fieldID, checked ) == -1 ) { 6929 if ( this.id === false || this.id === '' ) { 6930 return; 6931 } 6932 6933 fieldObject = jQuery( '#' + this.id ); 6934 checked.push( fieldID ); 6935 hideOrShowFieldById( fieldID, repeatArgs ); 6936 updateWatchingFieldById( fieldID, repeatArgs, 'value changed' ); 6937 // TODO: maybe trigger a change instead of running these three functions 6938 checkFieldsWithConditionalLogicDependentOnThis( fieldID, fieldObject ); 6939 checkFieldsWatchingLookup( fieldID, fieldObject, 'value changed' ); 6940 doCalculation( fieldID, fieldObject ); 6941 maybeDoCalcForSingleField( fieldObject.get( 0 ) ); 6942 } 6943 }); 6944 6945 jQuery( html ).find( '.frm_html_container' ).each( 6946 function() { 6947 // check heading logic 6948 var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ]; 6949 checked.push( fieldID ); 6950 hideOrShowFieldById( fieldID, repeatArgs ); 6951 } 6952 ); 6953 6954 maybeAddIntlTelInput( item.find( '.frm-intl-tel-input' ).get() ); 6955 if ( item.closest( '.with_frm_style' ).length > 0 ) { 6956 initRangeInput( item.find( 'input[type=range]' ).get() ); 6957 } 6958 6959 initFormatFieldValueNumbers(); 6960 loadDropzones( repeatArgs.repeatRow ); 6961 loadSliders(); 6962 loadAutocomplete(); 6963 loadImask(); 6964 6965 // Load rich textboxes. 6966 jQuery( html ).find( '.frm_html_container' ).each( function() { 6967 // check heading logic 6968 var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ]; 6969 checked.push( fieldID ); 6970 hideOrShowFieldById( fieldID, repeatArgs ); 6971 }); 6972 6973 // Find any RTEs in the new row/repeat and initialize the editor for them 6974 // there is an assumption here that tinyMCEPreInit.mceInit[0] refers to 6975 // an RTE that was initially loaded on the page. It's a safe assumption as 6976 // adding a row/repeat wouldn't have any RTEs if there wasn't one on the 6977 // page in the first place. 6978 jQuery( html ).find( '.wp-editor-area' ).each( function() { 6979 initRichText( this.id ); 6980 }); 6981 } 6982 6983 /* eslint-disable camelcase */ 6984 if ( typeof frmThemeOverride_frmAddRow === 'function' ) { 6985 frmThemeOverride_frmAddRow( id, r ); 6986 } 6987 /* eslint-enable camelcase */ 6988 6989 jQuery( document ).trigger( 'frmAfterAddRow' ); 6990 6991 handleRangeSliders(); 6992 triggerEvent( document, 'frmAfterAddRepeaterRow', { repeater: item.get(0) }); 6993 6994 jQuery( '.frm_repeat_' + id ).each( function( i ) { 6995 this.style.zIndex = 999 - i; 6996 }); 6997 6998 currentlyAddingRow = false;
6999 }; 7000 7001 error = function() { 7002 currentlyAddingRow = false; 7003 }; 7004 7005 extraParams = { dataType: 'json' }; 7006 7007 postToAjaxUrl( form, data, success, error, extraParams ); 7008 7009 return false; 7010 } 7011 7012 function triggerToggleClickOnSpace( e ) { 7013 if ( 32 === e.which ) { 7014 this.click(); 7015 } 7016 } 7017 7018 function removeRichText( id ) { 7019 tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, id ); 7020 } 7021 7022 function initRichText( id ) { 7023 var key = Object.keys( tinyMCEPreInit.mceInit )[0], 7024 orgSettings = tinyMCEPreInit.mceInit[ key ], 7025 newValues = { 7026 selector: '#' + id, 7027 body_class: orgSettings.body_class.replace( key, id ) 7028 }, 7029 newSettings = Object.assign({}, orgSettings, newValues ); 7030 tinymce.init( newSettings ); 7031 } 7032 7033 /***************************************************** 7034 * In-place edit 7035 ******************************************************/ 7036 7037 function editEntry() { 7038 /*jshint validthis:true */ 7039 var $edit = jQuery( this ), 7040 entryId = $edit.data( 'entryid' ), 7041 prefix = $edit.data( 'prefix' ), 7042 postId = $edit.data( 'pageid' ), 7043 formId = $edit.data( 'formid' ), 7044 cancel = $edit.data( 'cancel' ), 7045 fields = $edit.data( 'fields' ), 7046 excludeFields = $edit.data( 'excludefields' ), 7047 startPage = $edit.data( 'startpage' ), 7048 $cont = jQuery( document.getElementById( prefix + entryId ) ), 7049 orig = $cont.html(); 7050 7051 $cont.html( '<span class="frm-loading-img" id="' + prefix + entryId + '"></span><div class="frm_orig_content" style="display:none">' + orig + '</div>' ); 7052 jQuery.ajax({ 7053 type: 'POST', 7054 url: getUrlForInPlaceEdit(), 7055 dataType: 'html', 7056 data: { 7057 action: 'frm_entries_edit_entry_ajax', 7058 post_id: postId, 7059 entry_id: entryId, 7060 id: formId, 7061 nonce: frm_js.nonce, 7062 fields, 7063 exclude_fields: excludeFields, 7064 start_page: startPage 7065 }, 7066 success( html ) { 7067 $cont.children( '.frm-loading-img' ).replaceWith( html ); 7068 $edit.removeClass( 'frm_inplace_edit' ).addClass( 'frm_cancel_edit' ); 7069 $edit.html( cancel ); 7070 checkConditionalLogic( 'editInPlace' ); 7071 7072 // Make sure fields just loaded are properly bound 7073 if ( typeof frmFrontForm.fieldValueChanged === 'function' ) { 7074 jQuery( document ).on( 'change', '.frm-show-form input[name^="item_meta"], .frm-show-form select[name^="item_meta"], .frm-show-form textarea[name^="item_meta"]', frmFrontForm.fieldValueChanged ); 7075 } 7076 checkFieldsOnPage( prefix + entryId ); 7077 triggerEvent( document, 'frmInPlaceEdit' ); 7078 } 7079 }); 7080 return false; 7081 } 7082 7083 function getUrlForInPlaceEdit() { 7084 var url = frm_js.ajax_url, 7085 queryParams = getQueryParamsForInPlaceEdit(); 7086 7087 Array.prototype.forEach.call( 7088 Object.keys( queryParams ), 7089 ( queryParamKey ) => { 7090 url += -1 === url.indexOf( '?' ) ? '?' : '&'; 7091 url += queryParamKey + '=' + queryParams[ queryParamKey ]; 7092 } 7093 ); 7094 7095 return url; 7096 } 7097 7098 function getQueryParamsForInPlaceEdit() { 7099 var queryParams, queryString, pairs, keysToSkip, length, i, pair, key, value; 7100 7101 queryParams = {}; 7102 queryString = window.location.search.substring( 1 ); 7103 pairs = queryString.split( '&' ); 7104 keysToSkip = [ 7105 'action', 7106 'start_page', 7107 'nonce', 7108 'post_id', 7109 'entry_id', 7110 'id', 7111 'fields', 7112 'doing_wp_cron' 7113 ]; 7114 7115 length = pairs.length; 7116 for ( i = 0; i < length; i++ ) { 7117 pair = pairs[ i ].split( '=' ); 7118 key = decodeURIComponent( pair[0]); 7119 7120 if ( -1 !== keysToSkip.indexOf( key ) ) { 7121 continue; 7122 } 7123 7124 value = decodeURIComponent( pair[1]); 7125 7126 // Check if the key already exists in the object. 7127 if ( queryParams.hasOwnProperty( key ) ) { 7128 // If it's an array, push the new value. 7129 if ( Array.isArray( queryParams[ key ]) ) { 7130 queryParams[ key ].push( value ); 7131 } else { 7132 // If it's not an array, convert it to an array and push the new value. 7133 queryParams[ key ] = [ queryParams[ key ], value ]; 7134 } 7135 } else { 7136 // If the key doesn't exist, create a new entry. 7137 queryParams[ key ] = value; 7138 } 7139 } 7140 7141 return queryParams; 7142 } 7143 7144 function cancelEdit( event ) { 7145 /*jshint validthis:true */ 7146 7147 event.preventDefault(); 7148 7149 const $cancelLink = jQuery( this ); 7150 const prefix = $cancelLink.data( 'prefix' ); 7151 const entryId = $cancelLink.data( 'entryid' ); 7152 const $cont = jQuery( document.getElementById( prefix + entryId ) ); 7153 7154 $cont.children( '.frm_forms' ).replaceWith( '' ); 7155 $cont.children( '.frm_orig_content' ).fadeIn( 'slow' ).removeClass( 'frm_orig_content' ); 7156 7157 switchCancelToEdit( $cancelLink ); 7158 } 7159 7160 /** 7161 * Switch a Cancel Link back to Edit 7162 * 7163 * @since 2.03.08 7164 * 7165 * @param {object} $link 7166 */ 7167 function switchCancelToEdit( $link ) { 7168 var label = $link.data( 'edit' ); 7169 $link.removeClass( 'frm_cancel_edit' ).addClass( 'frm_inplace_edit' ); 7170 $link.html( label ); 7171 } 7172 7173 function deleteEntry() { 7174 /*jshint validthis:true */ 7175 var entryId, prefix, 7176 $link = jQuery( this ),
7177 confirmText = $link.data( 'deleteconfirm' ); 7178 7179 if ( confirm( confirmText ) ) { 7180 entryId = $link.data( 'entryid' ); 7181 prefix = $link.data( 'prefix' ); 7182 7183 $link.replaceWith( '<span class="frm-loading-img" id="frm_delete_' + entryId + '"></span>' ); 7184 jQuery.ajax({ 7185 type: 'POST', 7186 url: frm_js.ajax_url, 7187 data: { action: 'frm_entries_destroy', entry: entryId, nonce: frm_js.nonce }, 7188 success( html ) { 7189 if ( html.replace( /^\s+|\s+$/g, '' ) === 'success' ) { 7190 var container = jQuery( document.getElementById( prefix + entryId ) ); 7191 container.fadeOut( 'slow', () => { 7192 container.remove(); 7193 }); 7194 jQuery( document.getElementById( 'frm_delete_' + entryId ) ).fadeOut( 'slow' ); 7195 jQuery( document ).trigger( 'frmEntryDeleted', [ entryId ]); 7196 } else { 7197 jQuery( document.getElementById( 'frm_delete_' + entryId ) ).replaceWith( html ); 7198 } 7199 } 7200 }); 7201 } 7202 return false; 7203 } 7204 7205 /** 7206 * Switch Cancel link back to Edit link after entry is updated in-place 7207 * 7208 * @since 2.03.08 7209 * 7210 * @param {object} form 7211 */ 7212 function doEditInPlaceCleanUp( form ) { 7213 var entryIdField = jQuery( form ).find( 'input[name="id"]' ); 7214 7215 if ( entryIdField.length ) { 7216 var link = document.getElementById( 'frm_edit_' + entryIdField.val() ); 7217 7218 if ( isCancelLink( link ) ) { 7219 switchCancelToEdit( jQuery( link ) ); 7220 } 7221 } 7222 } 7223 7224 /** 7225 * Check if a link is a cancel link 7226 * 7227 * @since 2.03.08 7228 * 7229 * @param {object} link 7230 * @returns {boolean} 7231 */ 7232 function isCancelLink( link ) { 7233 return ( link !== null && link.className.includes('frm_cancel_edit') ); 7234 } 7235 7236 /********************************************** 7237 * Ajax time field unique check 7238 *********************************************/ 7239 7240 function loadUniqueTimeFields() { 7241 var timeFields, i, dateField; 7242 7243 if ( typeof __frmUniqueTimes === 'undefined' ) { 7244 return; 7245 } 7246 7247 timeFields = __frmUniqueTimes; 7248 for ( i = 0; i < timeFields.length; i++ ) { 7249 dateField = document.getElementById( timeFields[ i ].dateID ); 7250 jQuery( dateField ).on( 'change', maybeTriggerUniqueTime ); 7251 if ( '' !== dateField.value ) { 7252 jQuery( dateField ).trigger( 'change' ); 7253 } 7254 } 7255 } 7256 7257 function maybeTriggerUniqueTime() { 7258 /*jshint validthis:true */ 7259 var timeFields = __frmUniqueTimes; 7260 for ( var i = 0; i < timeFields.length; i++ ) { 7261 if ( timeFields[ i ].dateID == this.id ) { 7262 frmProForm.removeUsedTimes( this, timeFields[ i ].timeID ); 7263 } 7264 } 7265 } 7266 7267 /********************************************** 7268 * General Helpers 7269 *********************************************/ 7270 7271 function checkFieldsOnPage( containerId, event ) { 7272 if ( 'undefined' === typeof event ) { 7273 event = ''; 7274 } 7275 checkPreviouslyHiddenFields(); 7276 loadDateFields(); 7277 loadCustomInputMasks(); 7278 loadSliders(); 7279 loadAutocomplete( containerId );
7280 checkDynamicFields( event ); 7281 checkLookupFields(); 7282 // Use a timeout to avoid conflicts with Datepicker Calculationns. 7283 // If this runs right away, the Datepicker Calculation function may not be available yet. 7284 setTimeout( triggerCalc, 0 ); 7285 loadDropzones(); 7286 loadImask( containerId ); 7287 checkPasswordFields(); 7288 triggerLookupWatchUpdates(); 7289 } 7290 7291 function triggerLookupWatchUpdates() { 7292 var i, fieldId, keys, value, $changedInput; 7293 7294 if ( typeof __FRMLOOKUP === 'undefined' || document.querySelector( 'form input[name="id"]' ) ) { 7295 return; 7296 } 7297 7298 keys = Object.keys( __FRMLOOKUP ); 7299 for ( i = 0; i < keys.length; i++ ) { 7300 fieldId = keys[ i ]; 7301 value = __FRMLOOKUP[ fieldId ]; 7302 if ( value.dependents.length <= 0 ) { 7303 continue; 7304 } 7305 7306 $changedInput = jQuery( '#field_' + value.fieldKey ); 7307 if ( $changedInput.length ) { 7308 checkFieldsWatchingLookup( fieldId, $changedInput, 'value changed' ); 7309 } 7310 } 7311 } 7312 7313 function checkPasswordFields() { 7314 var passwordFields = document.querySelectorAll( '.frm_strength_meter' ), 7315 event = document.createEvent( 'HTMLEvents' ); 7316 7317 event.initEvent( 'keyup', true, true ); 7318 7319 for ( var i = 0; i < passwordFields.length; i++ ) { 7320 passwordFields[ i ].dispatchEvent( event ); 7321 } 7322 } 7323 7324 function checkPreviouslyHiddenFields() { 7325 if ( typeof __frmHideFields !== 'undefined' ) { 7326 frmProForm.hidePreviouslyHiddenFields(); 7327 } 7328 } 7329 7330 /** 7331 * @param {string|undefined} containerId 7332 * @returns {void} 7333 */ 7334 function loadAutocomplete( containerId ) { 7335 loadChosen( containerId ); 7336 loadSlimSelect( containerId ); 7337 } 7338 7339 /** 7340 * @param {string|undefined} chosenContainer 7341 * @returns {void} 7342 */ 7343 function loadChosen( chosenContainer ) { 7344 var opts; 7345 7346 if ( ! jQuery().chosen ) { 7347 return; 7348 } 7349 7350 opts = { 7351 allow_single_deselect: true, 7352 no_results_text: frm_js.no_results, 7353 search_contains: true 7354 }; 7355 if ( typeof __frmChosen !== 'undefined' ) { 7356 opts = '{' + __frmChosen + '}'; 7357 } 7358 7359 if ( chosenContainer !== undefined ) { 7360 jQuery( '#' + chosenContainer ).find( '.frm_chzn' ).chosen( opts ); 7361 } else { 7362 jQuery( '.frm_chzn' ).chosen( opts ); 7363 } 7364 } 7365 7366 function loadSlimSelect( containerId ) { 7367 var container, dropdowns; 7368 7369 if ( 'undefined' === typeof SlimSelect ) { 7370 return; 7371 } 7372 7373 if ( 'undefined' !== typeof containerId ) { 7374 container = document.getElementById( containerId ); 7375 if ( ! container ) { 7376 return; 7377 } 7378 7379 dropdowns = container.querySelectorAll( 'select.frm_slimselect' ); 7380 } else { 7381 dropdowns = document.querySelectorAll( 'select.frm_slimselect' ); 7382 } 7383 7384 dropdowns.forEach( 7385 ( autocompleteInput ) => { 7386 var emptyOption, allowDeselect, isMultiSelect, tabindex; 7387 7388 if ( 'none' === autocompleteInput.style.display || autocompleteInput.classList.contains( 'ss-main' ) || autocompleteInput.classList.contains( 'ss-content' ) ) { 7389 // Skip anything that is already initialized. 7390 return; 7391 } 7392 7393 emptyOption = autocompleteInput.querySelector( 'option[value=""]' ); 7394 allowDeselect = false; 7395 isMultiSelect = 'multiple' === autocompleteInput.getAttribute( 'multiple' ); 7396 7397 if ( emptyOption && '' === emptyOption.textContent.trim() ) { 7398 allowDeselect = true; 7399 if ( isMultiSelect ) { 7400 emptyOption.parentElement.removeChild( emptyOption ); 7401 } else { 7402 emptyOption.setAttribute( 'data-placeholder', 'true' ); 7403 } 7404 } else if ( autocompleteInput.querySelector( 'option[data-placeholder="true"]' ) ) { 7405 allowDeselect = true; 7406 } 7407 7408 tabindex = autocompleteInput.getAttribute( 'tabindex' ); 7409 7410 autocompleteInput.style.color = ''; 7411 new SlimSelect({ 7412 select: autocompleteInput, 7413 settings: { 7414 placeholderText: '', 7415 searchText: frm_js.no_results, 7416 searchPlaceholder: ' ', // Avoid default "Search" placeholder. 7417 allowDeselect, 7418 closeOnSelect: ! isMultiSelect, 7419 keepOrder: true 7420 }, 7421 events: { 7422 afterOpen() { 7423 // Make some accessibility adjustments to the .ss-content pop up. 7424 var ssContent, ssContentSearchInput, ssList, label; 7425 7426 ssContent = document.querySelector( '.ss-content[data-id="' + autocompleteInput.dataset.id + '"]' ); 7427 if ( ! ssContent ) { 7428 return; 7429 } 7430 7431 // The listbox role should be on the .ss-list element. 7432 ssContent.removeAttribute( 'role' ); 7433 7434 ssContentSearchInput = ssContent.querySelector( '.ss-search input' ); 7435 if ( ! ssContentSearchInput ) { 7436 return; 7437 } 7438 7439 ssContentSearchInput.removeAttribute( 'aria-label' ); 7440 7441 ssList = ssContent.querySelector( '.ss-list' ); 7442 if ( ssList ) { 7443 // Set a height so the list is scrollable. 7444 ssList.style.height = getHeightForSlimSelectList( ssContent ) + 'px'; 7445 ssList.setAttribute( 'role', 'listbox' ); 7446 } 7447 7448 // The search input and list elements should have labels. 7449 label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); 7450 if ( label ) { 7451 ssContentSearchInput.setAttribute( 'aria-labelledby', label.id ); 7452 if ( ssList ) { 7453 ssList.setAttribute( 'aria-labelledby', label.id ); 7454 } 7455 } 7456 } 7457 } 7458 }); 7459 7460 if ( tabindex ) { 7461 copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex ); 7462 } 7463 7464 makeSlimSelectAccessibilityChanges( autocompleteInput ); 7465 maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect ); 7466 7467 autocompleteInput.addEventListener( 7468 'change', 7469 function() { 7470 // This listener is required to toggle the Other input, and for conditional logic based on this field. 7471 jQuery( autocompleteInput ).trigger( 'change' ); 7472 } 7473 ); 7474 } 7475 ); 7476 } 7477 7478 function copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex ) { 7479 var slimselectElement = getSlimSelectMainElement( autocompleteInput ); 7480 if ( slimselectElement ) { 7481 slimselectElement.setAttribute( 'tabindex', tabindex ); 7482 } 7483 } 7484 7485 function getSlimSelectMainElement( autocompleteInput ) { 7486 return document.querySelector( '.ss-main[data-id="' + autocompleteInput.getAttribute( 'data-id' ) + '"]' ); 7487 } 7488 7489 /** 7490 * @param {HTMLElement} ssContent 7491 * @returns {number} 7492 */ 7493 function getHeightForSlimSelectList( ssContent ) { 7494 var ssSearch, listHeight; 7495
7496 listHeight = jQuery( ssContent ).outerHeight(); 7497 ssSearch = ssContent.querySelector( '.ss-search' ); 7498 7499 if ( ssSearch ) { 7500 listHeight -= jQuery( ssSearch ).outerHeight(); 7501 } 7502 7503 return listHeight; 7504 } 7505 7506 /** 7507 * @since 6.8 7508 * 7509 * @param {HTMLElement} autocompleteInput 7510 * @returns {void} 7511 */ 7512 function makeSlimSelectAccessibilityChanges( autocompleteInput ) { 7513 addAriaLabelledByToSlimSelectMainElement( autocompleteInput ); 7514 openSlimSelectOnLabelClick( autocompleteInput ); 7515 } 7516 7517 /** 7518 * If the autocomplete input is set to "Automatic width", use the target dropdown width as reference. 7519 * 7520 * @since 6.8.4 7521 * 7522 * @param {HTMLElement} autocompleteInput 7523 * @param {boolean} allowDeselect 7524 * @returns {void} 7525 */ 7526 function maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect ) { 7527 var slimselectElement, extraRequiredWidth; 7528 7529 if ( ! autocompleteInput.classList.contains( 'auto_width' ) ) { 7530 return; 7531 } 7532 7533 slimselectElement = getSlimSelectMainElement( autocompleteInput ); 7534 if ( slimselectElement ) { 7535 // The Slim Select dropdown is a bit larger than a regular dropdown because of the "X" icon for deselecting. 7536 // If we are not showing the "X", we don't need as much extra space. 7537 extraRequiredWidth = allowDeselect ? 36 : 14; 7538 slimselectElement.style.minWidth = ( jQuery( autocompleteInput ).outerWidth() + extraRequiredWidth ) + 'px'; 7539 } 7540 } 7541 7542 /** 7543 * @since 6.8 7544 * 7545 * @param {HTMLElement} autocompleteInput 7546 * @returns {void} 7547 */ 7548 function addAriaLabelledByToSlimSelectMainElement( autocompleteInput ) { 7549 var label, slimselectElement; 7550 7551 label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); 7552 if ( label ) { 7553 slimselectElement = getSlimSelectMainElement( autocompleteInput ); 7554 if ( slimselectElement ) { 7555 slimselectElement.setAttribute( 'aria-labelledby', label.id ); 7556 } 7557 } 7558 } 7559 7560 /** 7561 * @since 6.8 7562 * 7563 * @param {HTMLElement} autocompleteInput 7564 * @returns {void} 7565 */ 7566 function openSlimSelectOnLabelClick( autocompleteInput ) { 7567 var label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); 7568 if ( label ) { 7569 label.addEventListener( 'click', labelListener ); 7570 function labelListener() { 7571 setTimeout( 7572 () => { 7573 autocompleteInput.slim.open(); 7574 }, 7575 0 7576 ); 7577 } 7578 } 7579 } 7580 7581 function isPreviousSelectedStar( input ) { 7582 return null !== input.getAttribute( 'data-frm-star-selected' ); 7583 } 7584 7585 function loadStars() { 7586 if ( isPreviousSelectedStar( this ) ) { 7587 clearStars( this.parentElement, false ); 7588 return; 7589 } 7590 /*jshint validthis:true */ 7591 updateStars( this, true ); 7592 } 7593 7594 function hoverStars() { 7595 /*jshint validthis:true */ 7596 var input = this.previousSibling; 7597 if ( input.checked || isPreviousSelectedStar( this ) ) { 7598 // Do not need to update stars when hovering on the current checked star. 7599 return; 7600 } 7601 updateStars( input, false ); 7602 } 7603 7604 /** 7605 * Updates star rating. 7606 * 7607 * @param {HTMLElement} hovered Hovered or clicked input. 7608 * @param {Boolean} onClick Is true if this update is on click, false if is on hover. 7609 */ 7610 function updateStars( hovered, onClick ) { 7611 var starGroup = hovered.parentElement, 7612 stars = starGroup.children, 7613 current = parseInt( hovered.value ), 7614 starClass = 'star-rating', 7615 selectLabel = false; 7616 7617 starGroup.className += ' frm-star-hovered'; 7618 7619 for ( var i = 0; i < stars.length; i++ ) { 7620 if ( stars[ i ].matches( '.' + starClass ) ) { 7621 if ( selectLabel ) { 7622 stars[ i ].className += ' star-rating-hover'; 7623 } else { 7624 stars[ i ].classList.remove( 'star-rating-hover', 'star-rating-on' ); 7625 } 7626 } else { 7627 selectLabel = ( parseInt( stars[ i ].value ) <= current ); 7628 7629 if ( onClick ) { 7630 // Remove selected attribute from all star inputs. 7631 stars[ i ].removeAttribute( 'data-frm-star-selected' ); 7632 } 7633 } 7634 } 7635 7636 if ( onClick ) { 7637 // Add selected class to clicked star input. 7638 hovered.setAttribute( 'data-frm-star-selected', '' ); 7639 } 7640 } 7641 7642 function unhoverStars() { 7643 /*jshint validthis:true */ 7644 var input = this.previousSibling, 7645 starGroup = input.parentElement; 7646 starGroup.classList.remove( 'frm-star-hovered' ); 7647 var stars = starGroup.children;
7648 var selected = jQuery( starGroup ).find( 'input:checked' ).attr( 'id' ); 7649 var isSelected = ''; 7650 var starClass = 'star-rating'; 7651 7652 for ( var i = stars.length - 1; i > 0; i-- ) { 7653 if ( stars[ i ].matches( '.' + starClass ) ) { 7654 stars[ i ].classList.remove( 'star-rating-hover' ); 7655 if ( isSelected === '' && selected !== undefined && stars[ i ].getAttribute( 'for' ) == selected ) { 7656 isSelected = ' star-rating-on'; 7657 } 7658 if ( isSelected !== '' ) { 7659 stars[ i ].className += isSelected; 7660 } 7661 } 7662 } 7663 } 7664 7665 /** 7666 * Clears star rating. 7667 * 7668 * @since 5.4 7669 * 7670 * @param {HTMLElement} starGroup Star group element. 7671 * @param {Boolean} noClearInput Whether to clear input value or not. Default is `false`. 7672 */ 7673 function clearStars( starGroup, noClearInput ) { 7674 var labels, input; 7675 7676 labels = starGroup.querySelectorAll( '.star-rating-on' ); 7677 if ( labels && labels.length ) { 7678 labels.forEach( ( el ) => { 7679 el.classList.remove( 'star-rating-on' ); 7680 }); 7681 } 7682 7683 if ( ! noClearInput ) { 7684 input = starGroup.querySelector( 'input[type="radio"]:checked' ); 7685 if ( input ) { 7686 input.checked = false; 7687 input.removeAttribute( 'data-frm-star-selected' ); 7688 } 7689 } 7690 } 7691 7692 /** 7693 * Formats a value as currency. 7694 */ 7695 function formatValueAsCurrency( value, fieldKey ) { 7696 const currency = getCurrencyFromCalcRule( __FRMCALC.calc[ fieldKey ]); 7697 return new DOMParser().parseFromString( 7698 formatCurrency( normalizeTotal( value, currency ), currency ), 7699 'text/html' 7700 ).documentElement.textContent; 7701 } 7702 7703 function handleSliderEvent() { 7704 if ( this.type !== 'range' ) { 7705 return; 7706 } 7707 const sliderValueEl = this.parentNode.querySelector( '.frm_range_value' ); 7708 const fieldKey = getFieldKey( this.id, this.name ); 7709 if ( window.__FRMCALC?.calc[ fieldKey ]) { 7710 sliderValueEl.textContent = formatValueAsCurrency( this.value, fieldKey ); 7711 } else { 7712 sliderValueEl.textContent = this.value; 7713 } 7714 } 7715 7716 function loadSliders() { 7717 handleRangeSliders(); 7718 } 7719 7720 function getCurrencyFromCalcRule( calcRule ) { 7721 return 'undefined' !== typeof calcRule.custom_currency ? calcRule.custom_currency : getCurrency( calcRule.form_id ); 7722 } 7723 7724 function setInlineFormWidth() { 7725 var children, f, inlineForm, 7726 inlineForms = jQuery( '.frm_inline_form .frm_fields_container' ); 7727 7728 if ( inlineForms.length ) { 7729 for ( f = 0; f < inlineForms.length; f++ ) { 7730 inlineForm = jQuery( inlineForms[ f ]); 7731 children = inlineForm.children( '.frm_form_field' ); 7732 7733 if ( children.length <= 12 && ! fieldHasLayoutClass( children.last() ) ) { 7734 addAutoInlineLayout( inlineForm, children ); 7735 } 7736 } 7737 } 7738 } 7739 7740 function fieldHasLayoutClass( field ) { 7741 var i, 7742 classList = field.attr( 'class' ), 7743 layoutClasses = [ 'frm_full', 'half', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth' ]; 7744 7745 if ( classList === undefined ) { 7746 return false; 7747 } 7748 for ( i = 1; i <= 12; i++ ) { 7749 if ( field.hasClass( 'frm' + i ) ) { 7750 return true; 7751 } 7752 7753 if ( i === 12 ) { 7754 for ( var c = 0; c < layoutClasses.length; c++ ) { 7755 if ( classList.includes(layoutClasses[ c ]) ) { 7756 return true; 7757 } 7758 7759 if ( c === layoutClasses.length - 1 ) { 7760 return false; 7761 } 7762 } 7763 } 7764 } 7765 } 7766 7767 function addAutoInlineLayout( inlineForm, children ) { 7768 var fieldCount, colCount, i; 7769 7770 fieldCount = children.length + 1; // include the submit button 7771 colCount = Math.max( 2, Math.ceil( 12 / fieldCount ) ); 7772 for ( i = 0; i < children.length; i++ ) { 7773 if ( ! fieldHasLayoutClass( jQuery( children[ i ]) ) ) { 7774 jQuery( children[ i ]).addClass( 'frm' + colCount ); 7775 } 7776 } 7777 7778 inlineForm.children( '.frm_submit' ).addClass( 'frm' + colCount ); 7779 } 7780 7781 function checkConditionalLogic( event ) { 7782 if ( typeof __frmHideOrShowFields !== 'undefined' ) { 7783 frmProForm.hideOrShowFields( __frmHideOrShowFields, event ); 7784 } else { 7785 showForm(); 7786 } 7787 } 7788 7789 function showForm() { 7790 document.querySelectorAll( '.frm_pro_form' ).forEach( 7791 form => { 7792 jQuery( form ).fadeIn( 'slow' ); 7793 triggerEvent( form, 'frmProAfterFormFadeIn' ); 7794 } 7795 ); 7796 } 7797 7798 function checkDynamicFields( event ) { 7799 if ( typeof __frmDepDynamicFields !== 'undefined' ) { 7800 if ( 'pageLoad' === event && typeof __frmHideOrShowFields === 'undefined' ) { 7801 clearHideFields(); 7802 } 7803 7804 frmProForm.checkDependentDynamicFields( __frmDepDynamicFields ); 7805 } 7806 } 7807 7808 function checkLookupFields() { 7809 if ( typeof __frmDepLookupFields !== 'undefined' ) { 7810 frmProForm.checkDependentLookupFields( __frmDepLookupFields ); 7811 } 7812 } 7813 7814 function triggerChange( input, fieldKey ) { 7815 if ( fieldKey === undefined ) { 7816 fieldKey = 'dependent'; 7817 } 7818 7819 if ( input.length > 1 ) { 7820 input = input.eq( 0 ); 7821 } 7822 7823 input.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey }); 7824 } 7825 7826 function loadCustomInputMasks() { 7827 if ( typeof __frmMasks === 'undefined' ) { 7828 return; 7829 } 7830 7831 var maskFields = __frmMasks; 7832 for ( var i = 0; i < maskFields.length; i++ ) { 7833 jQuery( maskFields[ i ].trigger ).attr( 'data-frmmask', maskFields[ i ].mask ); 7834 } 7835 } 7836 7837 // Get the section ID and repeat row from a field name 7838 function getRepeatArgsFromFieldName( fieldName ) {
7839 var repeatArgs = { repeatingSection: '', repeatRow: '' }; 7840 7841 if ( fieldName !== undefined && isRepeatingFieldByName( fieldName ) ) { 7842 var inputNameParts = fieldName.split( '][' ); 7843 repeatArgs.repeatingSection = inputNameParts[ 0 ].replace( 'item_meta[', '' ); 7844 repeatArgs.repeatRow = inputNameParts[ 1 ]; 7845 } 7846 7847 return repeatArgs; 7848 } 7849 7850 function fadeOut( $remove ) { 7851 $remove.fadeOut( 'slow', () => { 7852 $remove.remove(); 7853 }); 7854 } 7855 7856 function objectSearch( array, value ) { 7857 for ( var prop in array ) { 7858 if ( array.hasOwnProperty( prop ) ) { 7859 if ( array[ prop ] === value ) { 7860 return prop; 7861 } 7862 } 7863 } 7864 return null; 7865 } 7866 7867 function isNumeric( obj ) { 7868 return ! Array.isArray( obj ) && ( obj - parseFloat( obj ) + 1 ) >= 0; 7869 } 7870 7871 function checkPasswordField() { 7872 var fieldId, 7873 fieldIdSplit, 7874 checks, 7875 split, 7876 suffix, 7877 check, 7878 span; 7879 7880 /*jshint validthis:true */ 7881 if ( this.className.includes('frm_strength_meter') ) { 7882 fieldId = this.name.substr( this.name.indexOf( '[' ) + 1 ).replace( /\]\[\d\]\[/, '-' ); 7883 if ( fieldId[ fieldId.length - 1 ] === ']' ) { 7884 fieldId = fieldId.substr( 0, fieldId.length - 1 ); 7885 } 7886 fieldIdSplit = fieldId.split( '-' ); 7887 7888 if ( fieldIdSplit.length === 2 ) { 7889 fieldId = fieldIdSplit[1] + '-' + fieldIdSplit[0]; 7890 } 7891 7892 checks = passwordChecks(); 7893 split = this.id.split( '-' ); 7894 suffix = split.length > 1 && ! isNaN( split[ split.length - 1 ]) ? '-' + split[ split.length - 1 ] : ''; 7895 7896 for ( check in checks ) { 7897 span = document.getElementById( 'frm-pass-' + check + '-' + fieldId + suffix ); 7898 addOrRemoveVerifyPass( checks[ check ], this.value, span ); 7899 } 7900 } 7901 } 7902 7903 function passwordChecks() { 7904 return { 7905 'eight-char': /^.{8,}$/, 7906 number: /\d/, 7907 uppercase: /[A-Z]/, 7908 lowercase: /[a-z]/, 7909 'special-char': /(?=.*[^a-zA-Z0-9])/ 7910 }; 7911 } 7912 7913 function addOrRemoveVerifyPass( regEx, password, span ) { 7914 if ( span !== null ) { 7915 var remove = regEx.test( password ); 7916 if ( remove ) { 7917 maybeRemovePassReq( span ); 7918 } else { 7919 maybeRemovePassVerified( span ); 7920 } 7921 } 7922 } 7923 7924 function maybeRemovePassReq( span ) { 7925 if ( span.classList.contains( 'frm-pass-req' ) ) { 7926 span.classList.remove( 'frm-pass-req' ); 7927 span.classList.add( 'frm-pass-verified' ); 7928 } 7929 } 7930 7931 function maybeRemovePassVerified( span ) { 7932 if ( span.classList.contains( 'frm-pass-verified' ) ) { 7933 span.classList.remove( 'frm-pass-verified' ); 7934 span.classList.add( 'frm-pass-req' ); 7935 } 7936 } 7937 7938 function checkCheckboxSelectionLimit() { 7939 /*jshint validthis:true */ 7940 var limit = parseInt( this.getAttribute( 'data-frmlimit' ) ), 7941 checked = this.checked; 7942 7943 if ( ! limit ) { 7944 return; 7945 } 7946 7947 var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' ); 7948 if ( limit >= allBoxes.length ) { 7949 return; 7950 } 7951 7952 var checkedBoxes = allBoxes.filter( function() { 7953 return this.checked; 7954 }); 7955 7956 if ( checked ) { 7957 if ( checkedBoxes.length >= limit ) { 7958 allBoxes.filter( function() { 7959 return ! this.checked; 7960 }).attr( 'disabled', 'disabled' ); 7961 } 7962 } else { 7963 const checkboxesLimitNotReached = allBoxes.filter( function() { 7964 return ! this.getAttribute( 'data-max-reached' ); 7965 }); 7966 7967 checkboxesLimitNotReached.prop( 'disabled', false ); 7968 } 7969 } 7970 7971 /** 7972 * Set up 'Add Row' buttons that will show up when all repeated fields have been removed. 7973 * Do it this way while we await CSS4 selectors that can do sth like .frm_repeat_sec:first-of-class. 7974 * 7975 * @since 4.02.01 7976 * @return {void} 7977 */ 7978 function addTopAddRowBtnForRepeater() { 7979 jQuery( '.frm_section_heading:has(div[class*="frm_repeat_"])' ).each( function() { 7980 const firstRepeatedSection = jQuery( this ).find( 'div[class*="frm_repeat_"]' ).first()[0]; 7981 const addButtonWrapper = document.createElement( 'div' ); 7982 7983 addButtonWrapper.classList.add( 'frm_form_field', 'frm_hidden_container', 'frm_repeat_buttons', 'frm_hidden' ); 7984 7985 const addButton = firstRepeatedSection.querySelector( '.frm_add_form_row' ); 7986 if ( addButton ) { 7987 addButtonWrapper.append( addButton.cloneNode( true ) ); 7988 } 7989 7990 const buttonContainer = firstRepeatedSection.closest( '.frm_toggle_container.frm_grid_container' ); 7991 if ( buttonContainer ) { 7992 buttonContainer.prepend( addButtonWrapper ); 7993 } else { 7994 firstRepeatedSection.parentNode.insertBefore( addButtonWrapper, firstRepeatedSection ); 7995 } 7996 }); 7997 } 7998 7999 function maybeDisableCheckboxesWithLimit() { 8000 // Disable unchecked ones after form submit error if limit is already reached. 8001 jQuery( 'input[type="checkbox"][data-frmlimit]:not(:checked)' ).each( function() { 8002 var limit = parseInt( this.getAttribute( 'data-frmlimit' ) ); 8003 8004 if ( ! limit ) { 8005 return; 8006 } 8007 8008 var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' ); 8009 if ( limit >= allBoxes.length ) { 8010 // Unreasonable limit 8011 return; 8012 } 8013 8014 var checkedBoxes = allBoxes.filter( function() { 8015 return this.checked; 8016 }); 8017 8018 if ( limit > checkedBoxes.length ) { 8019 return; 8020 } 8021 8022 this.setAttribute( 'disabled', 'disabled' ); 8023 }); 8024 } 8025 8026 /** 8027 * Check to make sure sure the quantity field value is within the min and max values. 8028 * 8029 * @param {HTMLElement} input 8030 * @return {number} 8031 */ 8032 function checkQuantityFieldMinMax( input ) { 8033 if ( '' === input.value ) { 8034 // Leave the value if it is empty. 8035 return 0; 8036 } 8037 8038 const val = parseFloat( input.value ? input.value.trim() : 0 ); 8039 if ( isNaN( val ) ) { 8040 return 0; 8041 } 8042 8043 let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0; 8044 let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0; 8045 8046 max = isNaN( max ) ? 0 : max; 8047 min = isNaN( min ) ? 0 : ( min < 0 ? 0 : min ); 8048 8049 if ( val < min ) { 8050 input.value = min; 8051 return min; 8052 } 8053 8054 if ( 0 !== max && val > max ) { 8055 input.value = max; 8056 return max; 8057 } 8058 8059 return val; 8060 } 8061 8062 /** 8063 * @since 4.04.01 8064 */ 8065 function setHiddenProduct( input ) { 8066 input.setAttribute( 'data-frmhidden', '1' ); 8067 triggerChange( jQuery( input ) ); 8068 } 8069 8070 /** 8071 * @since 4.04.01 8072 */ 8073 function setHiddenProductContainer( container ) { 8074 if ( container.innerHTML.includes('data-frmprice') ) { 8075 jQuery( container ).find( 'input[data-frmprice], select:has([data-frmprice])' ).attr( 'data-frmhidden', '1' ); 8076 } 8077 } 8078
8079 /** 8080 * @since 4.04.01 8081 */ 8082 function setShownProduct( input ) { 8083 var wasHidden = input.getAttribute( 'data-frmhidden' ); 8084 8085 if ( wasHidden !== null ) { 8086 input.removeAttribute( 'data-frmhidden' ); 8087 triggerChange( jQuery( input ) ); 8088 } 8089 } 8090 8091 /** 8092 * @since 4.04 8093 */ 8094 function calcProductsTotal( e ) { 8095 var formTotals = [], 8096 totalFields; 8097 8098 if ( typeof __FRMCURR === 'undefined' ) { 8099 return; 8100 } 8101 8102 if ( undefined !== e && 'undefined' !== typeof e.target && ( 'keyup' === e.type || 'change' === e.type ) ) { 8103 // an event has been fired 8104 var el = e.target; 8105 if ( el.hasAttribute( 'data-frmprice' ) && el instanceof HTMLInputElement && 'text' === el.type ) { 8106 // user-defined product 8107 el.setAttribute( 'data-frmprice', el.value.trim() ); 8108 } 8109 } 8110 8111 totalFields = jQuery( '[data-frmtotal]' ); 8112 if ( ! totalFields.length ) { 8113 return; 8114 } 8115 8116 totalFields.each( function() { 8117 var currency, formId, formatted, 8118 total = 0, 8119 totalField = jQuery( this ), 8120 $form = totalField.closest( 'form' ), 8121 isRepeatingTotal = isRepeatingFieldByName( this.name ); 8122 8123 if ( ! $form.length ) { 8124 return; 8125 } 8126
8127 formId = $form.find( 'input[name="form_id"]' ).val(); 8128 currency = getCurrency( formId ); 8129 8130 if ( formTotals[ formId ] !== undefined && ! isRepeatingTotal ) { 8131 total = formTotals[ formId ]; 8132 } else { 8133 8134 $form.find( 'input[data-frmprice],select:has([data-frmprice])' ).each( function() { 8135 var quantity, $this, 8136 price = 0, 8137 isUserDef = false, 8138 isSingle = false; 8139 8140 // total fields inside repeaters are for their corresponding rows only. 8141 if ( isRepeatingTotal && ! isRepeatingWithTotal( this, totalField[0]) ) { 8142 return; 8143 } 8144 8145 if ( this.hasAttribute( 'data-frmhigherpg' ) || isProductFieldHidden( this ) ) { 8146 return; 8147 } 8148 8149 if ( this.tagName === 'SELECT' ) { 8150 if ( this.selectedIndex !== -1 ) { 8151 price = this.options[ this.selectedIndex ].getAttribute( 'data-frmprice' ); 8152 } 8153 } else { 8154 isUserDef = 'text' === this.type; 8155 isSingle = 'hidden' === this.type; 8156 $this = jQuery( this ); 8157 if ( ( ! isUserDef && ! isSingle ) && ! $this.is( ':checked' ) ) { 8158 return; 8159 } 8160 price = this.getAttribute( 'data-frmprice' ); 8161 } 8162 8163 if ( ! price ) { 8164 price = 0; 8165 } else { 8166 price = preparePrice( price, currency ); 8167 quantity = getQuantity( isUserDef, this ); 8168 price = parseFloat( quantity ) * parseFloat( price ); 8169 } 8170 8171 if ( 'true' === this.getAttribute( 'data-frmdiscount' ) ) { 8172 price = price * -1; 8173 } 8174 8175 total += price; 8176 }); 8177 8178 if ( ! isRepeatingTotal ) { 8179 formTotals[ formId ] = total; 8180 } 8181 } 8182 8183 total = isNaN( total ) ? 0 : total; 8184 8185 // Set a decimal separator for currency if no default for it 8186 currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s) 8187 if ( ! currency.decimal_separator.length ) { 8188 currency.decimal_separator = '.'; 8189 } 8190 8191 // Round to the currency's decimal precision so floating-point artifacts 8192 // like 189.89999999999998 aren't posted as the submitted total value. 8193 // Avoid the normalized total as it can use commas as decimal separators. 8194 totalField.val( roundTotal( total, currency ) ); 8195 8196 // Make sure '0' is always set as '0.00' to avoid issues where 8197 // the value is interpreted as empty instead of zero. 8198 if ( '0' === totalField.val() && 2 === currency.decimals ) { 8199 totalField.val( '0.00' ); 8200 } 8201 8202 total = normalizeTotal( total, currency ); 8203 8204 // because of e.g. fields that might be using this field for calculations 8205 triggerChange( totalField ); 8206 8207 total = formatCurrency( total, currency ); 8208 formatted = totalField.prev( '.frm_total_formatted' ); 8209 if ( formatted.length < 1 ) { 8210 // In case paragraphs have been added to the form. 8211 formatted = totalField.closest( '.frm_form_field' ).find( '.frm_total_formatted' ); 8212 } 8213 if ( formatted.length ) { 8214 formatted.html( total ); 8215 } 8216 }); 8217 } 8218 8219 /** 8220 * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with. 8221 * 8222 * @param {float|string} total The total amount to normalize. 8223 * @param {Object} currency The currency object containing decimal information. 8224 * @returns {string} 8225 */ 8226 function normalizeTotal( total, currency ) { 8227 const isLargeTotal = total > Number.MAX_SAFE_INTEGER; 8228 8229 total = roundTotal( total, currency ); 8230 8231 return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal ); 8232 } 8233 8234 function roundTotal( total, currency ) { 8235 const isLargeTotal = total > Number.MAX_SAFE_INTEGER; 8236 8237 if ( ! isLargeTotal ) { 8238 const { decimals } = currency; 8239 total = decimals > 0 ? Math.round10( total, decimals ) : Math.ceil( total ); 8240 } 8241 8242 return total; 8243 } 8244 8245 /** 8246 * Format a numeric value according to the specified currency format settings. 8247 * 8248 * @since 4.05.01 8249 * 8250 * @param {string} total The numeric value to format. 8251 * @param {Object} currency The currency formatting configuration. 8252 * @returns {string} The formatted currency string. 8253 */ 8254 function formatCurrency( total, currency ) { 8255 total = maybeAddTrailingZeroToPrice( total, currency ); 8256 if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) { 8257 total = total.substr( 0, total.length - 1 ); 8258 } 8259 8260 total = maybeRemoveTrailingZerosFromPrice( total, currency ); 8261 total = addThousands( total, currency ); 8262 8263 const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : ''; 8264 const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : ''; 8265 8266 return leftSymbol + total + rightSymbol; 8267 } 8268 8269 /** 8270 * @since 5.0.15 8271 */ 8272 function maybeRemoveTrailingZerosFromPrice( total, currency ) { 8273 var split = total.split( currency.decimal_separator ); 8274 if ( 2 !== split.length || split[1].length <= currency.decimals ) { 8275 return total; 8276 } 8277 if ( 0 === currency.decimals ) { 8278 return split[0]; 8279 } 8280 return split[0] + currency.decimal_separator + split[1].substr( 0, currency.decimals ); 8281 } 8282 8283 /** 8284 * @since 5.0.10 8285 */ 8286 function addRteRequiredMessages() { 8287 var keys, length, index, key, field; 8288 if ( 'undefined' === typeof __FRMRTEREQMESSAGES ) { 8289 return; 8290 } 8291 keys = Object.keys( __FRMRTEREQMESSAGES ); 8292 length = keys.length; 8293 for ( index = 0; index < length; ++index ) { 8294 key = keys[ index ]; 8295 field = document.getElementById( key ); 8296 if ( field ) { 8297 field.setAttribute( 'data-reqmsg', __FRMRTEREQMESSAGES[ key ]); 8298 } 8299 } 8300 } 8301
8302 /** 8303 * @since 4.04.01 8304 * 8305 * This function is most suited for (product) fields that are 8306 * on their own page i.e. not hidden (in a multi-paged form). 8307 * 8308 * As for fields that are conditionally hidden - themselves or 8309 * their parent - and are not on their own page, the PHP side 8310 * handles them well; either their HTML is not included on the 8311 * page at all (e.g. fields in a conditionally hidden section) 8312 * or their value is empty & price is thus 0, so no worries here. 8313 */ 8314 function isProductFieldHidden( input ) { 8315 return input.getAttribute( 'data-frmhidden' ) !== null; 8316 } 8317 8318 /** 8319 * @since 4.04 8320 */ 8321 function isRepeatingWithTotal( input, total ) { 8322 // .+ is safer than d+ because field keys might be used at times. 8323 var regex = /item_meta\[.+?\]\[.+?\]/; 8324 8325 return isRepeatingFieldByName( input.name ) && ( total.name.match( regex )[0] === input.name.match( regex )[0]); 8326 } 8327 8328 /** 8329 * @since 4.04 8330 */ 8331 function getCurrency( formId ) { 8332 if ( typeof __FRMCURR !== 'undefined' && __FRMCURR[ formId ] !== undefined ) { 8333 return __FRMCURR[ formId ]; 8334 } 8335 } 8336 8337 /** 8338 * @since 4.04 8339 */ 8340 function getQuantity( isUserDef, field ) { 8341 var quantity, quantityFields, isRepeating, fieldID, 8342 $this = jQuery( field ); 8343 8344 fieldID = frmFrontForm.getFieldId( field, false ); 8345 if ( ! fieldID ) { 8346 return 0; 8347 } 8348 8349 isRepeating = isRepeatingFieldByName( field.name ); 8350 8351 if ( isRepeating ) { 8352 // .+ is safer than d+ because field keys might be used at times. 8353 var match = field.name.match( /item_meta\[.+?\]\[.+?\]/ ); 8354 if ( null === match ) { 8355 // very unlikely though 8356 return 0; 8357 } 8358 // we may need this in getQuantityFields 8359 $this.nameMatch = match[0]; 8360 } 8361 8362 quantity = getQuantityField( $this, fieldID, isRepeating ); 8363 8364 if ( quantity ) { 8365 quantity = checkQuantityFieldMinMax( quantity ); 8366 } else { 8367 quantityFields = getQuantityFields( $this, isRepeating ); 8368 if ( 1 === quantityFields.length && '' === quantityFields[0].getAttribute( 'data-frmproduct' ).trim() ) { 8369 quantity = checkQuantityFieldMinMax( quantityFields[0]); 8370 } else { 8371 // If there is no quantity field, assume 1. 8372 quantity = 1; 8373 } 8374 } 8375 8376 if ( 0 === quantity && isUserDef ) { 8377 // only user-defined fields may not have attached quantity fields 8378 quantity = 1; 8379 } 8380 8381 return quantity; 8382 } 8383 8384 function getQuantityField( elementObj, fieldID, isRepeating ) { 8385 var quantity, 8386 quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]' ); 8387 8388 fieldID = fieldID.toString(); 8389 8390 quantityFields.each( function() { 8391 var ids; 8392 8393 if ( isRepeating && -1 === this.name.indexOf( elementObj.nameMatch ) ) { 8394 return true; 8395 } 8396 8397 ids = JSON.parse( this.getAttribute( 'data-frmproduct' ).trim() ); 8398 if ( '' === ids ) { 8399 return true; 8400 } 8401 8402 // convert to array if necessary cos of existing fields that are already using single product fields 8403 ids = 'string' === typeof ids ? [ ids.toString() ] : ids; 8404 if ( ids.includes(fieldID) ) { 8405 quantity = this; 8406 return false; 8407 } 8408 }); 8409 8410 return quantity; 8411 } 8412 8413 /** 8414 * @since 4.04 8415 */ 8416 function getQuantityFields( elementObj, isRepeating ) { 8417 var quantityFields; 8418 if ( isRepeating ) { 8419 quantityFields = elementObj.closest( 'form' ).find( '[name^="' + elementObj.nameMatch + '"]' + '[data-frmproduct]' ); 8420 } else { 8421 // make sure the search is form-based (i.e. per form) cos there could be more than 1 form on the page 8422 // not([id*="-"]) means : not inside a repeater 8423 quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]:not([id*="-"])' ); 8424 } 8425 8426 return quantityFields; 8427 } 8428 8429 /** 8430 * @since 4.04 8431 */ 8432 function preparePrice( price, currency ) { 8433 var matches; 8434 8435 if ( ! price ) { 8436 return 0; 8437 } 8438 price = price + ''; // convert to string just to be sure 8439 8440 const regex = getRegexForPrice( currency ); 8441 8442 matches = price.match( regex ); 8443 if ( null === matches ) { 8444 return 0; 8445 } 8446 8447 price = matches.length ? matches[ matches.length - 1 ] : 0; 8448 price = price.trim(); 8449 8450 // Fix issues with parsing Fr.15.00. The regex catches .15.00. 8451 // This checks for the leading decimal and removes it. 8452 if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[0] === '.' ) { 8453 price = price.substr( 1 ); 8454 } 8455 8456 if ( price ) { 8457 price = maybeUseDecimal( price, currency ); 8458 price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' ); 8459 } 8460 8461 return price; 8462 } 8463 8464 /** 8465 * @param {Object} currency The currency object. 8466 * @return {RegExp} 8467 */ 8468 function getRegexForPrice( currency ) { 8469 let regexString = '[0-9,.'; 8470 8471 if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) { 8472 regexString += currency.thousand_separator; 8473 } 8474 if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) { 8475 regexString += currency.decimal_separator; 8476 } 8477 8478 regexString += ']*\\.?\\,?[0-9]+'; 8479 8480 return new RegExp( regexString, 'g' ); 8481 } 8482 8483 /** 8484 * @since 4.04 8485 */ 8486 function maybeUseDecimal( amount, currency ) { 8487 var usedForDecimal, amountParts; 8488 if ( currency.thousand_separator == '.' ) { 8489 amountParts = amount.split( '.' ); 8490 usedForDecimal = ( 2 == amountParts.length && 2 == amountParts[1].length ); 8491 if ( usedForDecimal ) { 8492 amount = amount.replace( '.', currency.decimal_separator ); 8493 } 8494 } 8495 return amount; 8496 } 8497 8498 /** 8499 * Add trailing zeros to a price if necessary and replace the decimal separator. 8500 * 8501 * @since 4.04 8502 * 8503 * @param {number|string} price The price to format. 8504 * @param {Object}
8504 currency The currency object containing the decimal separator. 8505 * @param {boolean} [force=false] Whether to force processing even if the price is not a number. 8506 * @returns {string} 8507 */ 8508 function maybeAddTrailingZeroToPrice( price, currency, force = false ) { 8509 if ( 'number' !== typeof price && ! force ) { 8510 return price; 8511 } 8512 8513 price += ''; // first convert to string 8514 var pos = price.indexOf( '.' ); 8515 8516 if ( pos === -1 ) { 8517 price = price + '.'; 8518 8519 for ( let n = 0; n < currency.decimals; ++n ) { 8520 price += '0'; 8521 } 8522 } else { 8523 const decimalsString = price.substring( pos + 1 ); 8524 if ( decimalsString.length < currency.decimals ) { 8525 if ( decimalsString.length < 2 ) { 8526 price += '0'; 8527 } 8528 8529 for ( let n = 2; n < currency.decimals; ++n ) { 8530 price += '0'; 8531 } 8532 } 8533 } 8534 8535 return price.replace( '.', currency.decimal_separator ); 8536 } 8537 8538 /** 8539 * Format a numeric string by adding thousand separators. 8540 * 8541 * @since 4.04.04 8542 * 8543 * @param {string|number} total The numeric value to format. 8544 * @param {Object} options Formatting options. 8545 * @param {string} options.decimal_separator Character used as decimal separator. 8546 * @param {string} options.thousand_separator Character used as thousand separator. 8547 * 8548 * @returns {string} 8549 */ 8550 function addThousands( total, { decimal_separator, thousand_separator } ) { 8551 const split = decimal_separator === '' 8552 ? [ total.toString() ] 8553 : total.split( decimal_separator ); 8554 8555 if ( thousand_separator ) { 8556 split[0] = split[0].replace( /\B(?=(\d{3})+(?!\d))/g, thousand_separator ); 8557 } 8558 8559 return split.join( decimal_separator ); 8560 } 8561 8562 /** 8563 * @since 5.1 8564 */ 8565 function setAutoHeightForTextArea() { 8566 document.querySelectorAll( '.frm-show-form textarea' ).forEach( 8567 ( element ) => { 8568 var minHeight, callback; 8569 8570 if ( element.dataset.autoGrow === undefined || element.getAttribute( 'frm-autogrow' ) ) { 8571 return; 8572 } 8573 8574 minHeight = getElementHeight( element ); 8575 element.style.overflowY = 'hidden'; 8576 element.style.transition = 'none'; 8577 8578 callback = function() { 8579 adjustHeight( element, minHeight ); 8580 }; 8581 8582 callback(); 8583 element.addEventListener( 'input', callback ); 8584 8585 if ( shouldCallResizeCallback() ) { 8586 window.addEventListener( 'resize', callback ); 8587 } 8588 8589 document.addEventListener( 'frmShowField', callback ); 8590 element.setAttribute( 'frm-autogrow', 1 ); 8591 } 8592 ); 8593 } 8594 8595 /** 8596 * Returns true if window resize callback should be called. 8597 * 8598 * Avoid this resize callback on Android and iPhone devices because it causes shaking issues. See issue #5349 for more information. 8599 * 8600 * @since 6.16.1 8601 * 8602 * @return {Boolean} 8603 */ 8604 function shouldCallResizeCallback() { 8605 if ( ! ( 'userAgent' in navigator ) ) { 8606 return true; 8607 } 8608 const userAgent = navigator.userAgent.toLowerCase(); 8609 return !userAgent.includes('android') && !userAgent.includes('iphone'); 8610 } 8611 8612 function getElementHeight( element ) { 8613 var clone, container, height; 8614 8615 clone = element.cloneNode( true ); 8616 clone.style.position = 'absolute'; 8617 clone.style.left = '-10000px'; 8618 clone.style.top = '-10000px'; 8619 8620 container = jQuery( element ).closest( '.frm_forms' ).get( 0 ); 8621 container.append( clone ); 8622 8623 height = clone.clientHeight; 8624 8625 container.removeChild( clone ); 8626 8627 return height; 8628 } 8629 8630 /** 8631 * @since 5.1 8632 */ 8633 function adjustHeight( el, minHeight ) { 8634 if ( minHeight >= el.scrollHeight ) { 8635 return; 8636 } 8637 8638 el.style.height = 0; 8639 el.style.height = Math.max( minHeight, el.scrollHeight ) + 'px'; 8640 } 8641 8642 function updateContentLength() { 8643 function onChange( e ) { 8644 var length, max, type, 8645 messageEl = e.target.nextElementSibling, 8646 countEl = messageEl.querySelector( 'span' ); 8647 8648 if ( ! countEl ) { 8649 return; 8650 } 8651 8652 type = messageEl.getAttribute( 'data-max-type' ); 8653 max = parseInt( messageEl.getAttribute( 'data-max' ) ); 8654 if ( 'word' === type ) { 8655 length = e.target.value.split( /\s+/ ).filter( 8656 ( word ) => { 8657 return word; 8658 } 8659 ).length; 8660 } else { 8661 length = e.target.value.length; 8662 } 8663 8664 countEl.innerText = length; 8665 8666 messageEl.classList.toggle( 'frm_limit_error', length > max ); 8667 } 8668 8669 document.addEventListener( 'input', function( e ) { 8670 var target; 8671 // loop parent nodes from the target to the delegation node 8672 for ( target = e.target; target && target != this; target = target.parentNode ) { 8673 if ( target.matches( 'textarea' ) && target.nextElementSibling && target.nextElementSibling.matches( '.frm_pro_max_limit_desc' ) ) { 8674 onChange( e ); 8675 break; 8676 } 8677 } 8678 }, false ); 8679 } 8680 8681 /** 8682 * Triggers custom event. 8683 * 8684 * @since 5.1 8685 * @since 5.3.3 Added the third parameter. 8686 * 8687 * @param {HTMLElement} element Element. 8688 * @param {String} eventType Event name. 8689 * @param {Object} data Data to add to the event. 8690 */ 8691 function triggerEvent( element, eventType, data ) { 8692 if ( 'function' === typeof frmFrontForm.triggerCustomEvent ) { 8693 frmFrontForm.triggerCustomEvent( element, eventType, data ); 8694 } 8695 } 8696
8697 /** 8698 * Handles start over button. 8699 * 8700 * @since 5.4 8701 */ 8702 function startOverButton() { 8703 /** 8704 * Gets all field inputs. 8705 * 8706 * @param {HTMLElement} formEl Form element. 8707 * @return {Array} 8708 */ 8709 function getInputs( formEl ) { 8710 return getInputsInFieldOnPage( formEl ); 8711 } 8712 8713 /** 8714 * Resets field inputs. 8715 * 8716 * @param {HTMLElement} formEl Form element. 8717 */ 8718 function resetInputs( formEl ) { 8719 var inputs = getInputs( formEl ); 8720 8721 /** 8722 * Resets a single repeater. 8723 * 8724 * @param {HTMLElement} repeatBtns Repeater buttons. 8725 */ 8726 function resetRepeater( repeatBtns ) { 8727 var repeater, items; 8728 8729 repeater = repeatBtns.parentElement; 8730 items = repeater.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ); 8731 if ( ! items.length ) { 8732 // Add the first row if all rows are deleted. 8733 currentlyAddingRow = false; 8734 repeatBtns.querySelector( '.frm_add_form_row' ).click(); 8735 } else if ( items.length > 1 ) { 8736 // Remove all rows except the first one. 8737 items.forEach( ( item, index ) => { 8738 if ( index ) { 8739 item.parentElement.removeChild( item ); 8740 } else { 8741 item.querySelector( '.frm_add_form_row' ).classList.remove( 'frm_hide_add_button' ); 8742 } 8743 }); 8744 } 8745 } 8746 8747 // Remove repeater sections. 8748 formEl.querySelectorAll( '.frm_section_heading > .frm_repeat_buttons' ).forEach( resetRepeater ); 8749 8750 // Clear field value. 8751 clearValueForInputs( inputs, '', true ); 8752 8753 // Remove "disabled" attribute added by limit selections option. 8754 inputs.forEach( ( input ) => { 8755 if ( input.disabled && input.getAttribute( 'data-frmlimit' ) ) { 8756 input.removeAttribute( 'disabled' ); 8757 } 8758 }); 8759 } 8760 8761 /** 8762 * Checks if given form is a multiple pages form. 8763 * 8764 * @param {Number} formId Form Id. 8765 * @return {Boolean} 8766 */ 8767 function isMultiPagesForm( formId ) { 8768 return document.getElementById( 'frm_page_order_' + formId ) || document.querySelector( '#frm_form_' + formId + '_container input[name="frm_next_page"]' ); 8769 } 8770 8771 /** 8772 * Reloads form. 8773 * 8774 * @param {Number} formId Form ID. 8775 * @param {HTMLElement} formEl Form element. 8776 */ 8777 function reloadForm( formId, formEl ) { 8778 const stateField = document.querySelector( 'input[name="frm_state"]' ); 8779 const state = null !== stateField ? stateField.value : ''; 8780 8781 formEl.classList.add( 'frm_loading_form' ); 8782 postToAjaxUrl( 8783 formEl, 8784 { 8785 action: 'frm_load_form', 8786 form: formId, 8787 _ajax_nonce: frm_js.nonce, 8788 frm_state: state 8789 }, 8790 ( response ) => { 8791 var idValueMapping; 8792 if ( ! response.success ) { 8793 console.warn( response ); 8794 return; 8795 } 8796 8797 idValueMapping = getDefaultValuesFromForm( formEl ); 8798 8799 jQuery( formEl.closest( '.frm_forms' ) ).replaceWith( response.data ); 8800 8801 setDefaultValues( idValueMapping ); 8802 maybeShowMoreStepsButton(); 8803 8804 if ( 'undefined' !== typeof __frmAjaxDropzone ) { 8805 window.__frmDropzone = __frmAjaxDropzone; 8806 } 8807 8808 checkConditionalLogic( 'pageLoad' ); 8809 checkFieldsOnPage( 'frm_form_' + formId + '_container' ); 8810 8811 triggerCompletedEvent( formId ); 8812 }, 8813 ( response ) => { 8814 console.warn( response ); 8815 } 8816 ); 8817 } 8818 8819 /** 8820 * Gets the default values of given form. 8821 * 8822 * @param {HTMLElement} formEl Form element. 8823 * @return {Object} Object with keys are input IDs, values are the default values. 8824 */ 8825 function getDefaultValuesFromForm( formEl ) { 8826 var inputs, 8827 values = {}; 8828 8829 inputs = formEl.querySelectorAll( '[data-frmval]' ); 8830 inputs.forEach( ( input ) => { 8831 values[ input.id ] = input.getAttribute( 'data-frmval' ); 8832 }); 8833 8834 return values; 8835 } 8836 8837 /** 8838 * Sets default values of fields in given form from the id value mapping built before. 8839 * 8840 * @param {Object} idValueMapping Object with keys are input IDs, values are the default values. 8841 */ 8842 function setDefaultValues( idValueMapping ) { 8843 Object.keys( idValueMapping ).forEach( ( id ) => { 8844 var input = document.getElementById( id ); 8845 if ( ! input ) { 8846 return; 8847 } 8848 8849 input.setAttribute( 'data-frmval', idValueMapping[ id ]); 8850 8851 if ( 'checkbox' === input.type || 'radio' === input.type ) { 8852 // Every checkbox and radio input has data-frmval, the one has value === data-frmval is the default. 8853 if ( input.value === idValueMapping[ id ]) { 8854 input.checked = true; 8855 } 8856 return; 8857 } 8858 8859 input.value = idValueMapping[ id ]; 8860 }); 8861 } 8862 8863 /** 8864 * Checks if form has Save draft enabled. 8865 * 8866 * @param {HTMLElement} formEl Form element. 8867 * @return {Boolean} 8868 */ 8869 function hasSaveDraft( formEl ) { 8870 return ! ! formEl.querySelector( '.frm_save_draft' ); 8871 } 8872 8873 /** 8874 * Deletes draft version. 8875 * 8876 * @param {Number} formId Form ID. 8877 * @param {HTMLElement} formEl Form element. 8878 */ 8879 function deleteDraft( formId, formEl ) { 8880 postToAjaxUrl( 8881 formEl, 8882 { 8883 action: 'frm_delete_draft_entry', 8884 form: formId, 8885 _ajax_nonce: frm_js.nonce 8886 } 8887 ); 8888 } 8889 8890 function onClickStartOver( e ) { 8891 e.preventDefault(); 8892 8893 var formEl, formId, draftIdInput; 8894 formEl = e.target.closest( 'form' ); 8895 if ( ! formEl ) { 8896 return; 8897 } 8898 8899 formId = formEl.querySelector( 'input[name="form_id"]' ).value; 8900 8901 if ( hasSaveDraft( formEl ) ) { 8902 deleteDraft( formId, formEl ); 8903 8904 // Clear draft inputs. 8905 draftIdInput = formEl.querySelector( 'input[name="id"]' ); 8906 if ( draftIdInput ) { 8907 draftIdInput.remove(); 8908 } 8909 8910 formEl.querySelector( 'input[name="frm_action"]' ).value = 'create'; 8911 } 8912 8913 if ( isMultiPagesForm( formId ) ) { 8914 reloadForm( formId, formEl ); 8915 } else { 8916 resetInputs( formEl ); 8917 triggerCompletedEvent( formId ); 8918 } 8919 } 8920 8921 function triggerCompletedEvent( formId ) { 8922 triggerEvent( document, 'frm_after_start_over', { formId }); 8923 } 8924 8925 document.addEventListener( 'click', function( e ) { 8926 var target; 8927 8928 // loop parent nodes from the target to the delegation node. 8929 for ( target = e.target; target && target != this; target = target.parentNode ) { 8930 if ( target.matches( '.frm_start_over' ) ) { 8931 onClickStartOver.call( target, e ); 8932 break; 8933 } 8934 } 8935 }, false ); 8936 } 8937 8938 function maybeShowMoreStepsButton() { 8939 var i, listWrappers, listWrapper, rootlineSteps, 8940 wrappingElementsCount, startIndex, hiddenSteps, 8941 showMoreButtonLi, showMoreButton, hiddenStepsWrapper, 8942 oldIE; 8943 8944 listWrappers = document.getElementsByClassName( 'frm_rootline' ); 8945 copyRootlines( listWrappers ); 8946 oldIE = isOldIEVersion( 10 ); 8947 8948 for ( i = 0; i < listWrappers.length; i++ ) { 8949 listWrapper = listWrappers[ i ]; 8950 if ( oldIE ) { 8951 // Hide the rootlines in < IE 11. 8952 listWrapper.className += ' frm_hidden'; 8953 continue; 8954 } 8955 8956 rootlineSteps = listWrapper.children; 8957 8958 wrappingElementsCount = countOverflowPages( rootlineSteps ); 8959 8960 if ( ! wrappingElementsCount ) { 8961 continue; 8962 } 8963 8964 showMoreButton = listWrapper.querySelector( '.frm_rootline_show_more_btn' ); 8965 if ( ! showMoreButton ) { 8966 continue; 8967 } 8968 8969 showMoreButton.addEventListener( 'click', showMoreSteps ); 8970 8971 // Show the existing "..." button. 8972 showMoreButtonLi = showMoreButton.parentNode; 8973 showMoreButtonLi.className = showMoreButtonLi.className.replace( ' frm_hidden', '' ); 8974 8975 // Don't count the overflow step in the HTML. 8976 startIndex = rootlineSteps.length - wrappingElementsCount > 1 ? rootlineSteps.length - wrappingElementsCount - 3 : 0; 8977 hiddenSteps = [].slice.call( rootlineSteps, Math.max( startIndex, 1 ), rootlineSteps.length - 2 ); 8978 8979 hiddenStepsWrapper = showMoreButtonLi.querySelector( '.frm_rootline_hidden_steps' ); 8980 8981 hiddenSteps.forEach( ( hiddenStep ) => { 8982 hiddenStepsWrapper.append( hiddenStep ); 8983 }); 8984 8985 moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton ); 8986 8987 listWrapper.insertBefore( showMoreButtonLi, listWrapper.children[ listWrapper.children.length - 1 ]); 8988 8989 if ( listWrapper.children[ listWrapper.children.length - 1 ].className.includes('frm_current_page') ) { 8990 updateRootlineStyle( hiddenStepsWrapper ); 8991 } 8992 } 8993 } 8994
8995 /** 8996 * Check if the browser is IE. If it matches the max or is lower, return true. 8997 * 8998 * @param int max 8999 * @returns bool 9000 */ 9001 function isOldIEVersion( max ) { 9002 var version, 9003 myNav = navigator.userAgent.toLowerCase(); 9004 version = myNav.includes('msie') ? parseInt( myNav.split( 'msie' )[1]) : false; 9005 return version !== false && max >= version; 9006 } 9007 9008 function countOverflowPages( rootlineSteps ) { 9009 var j, 9010 wrappingElementsCount = 0; 9011 9012 for ( j = 0; j < rootlineSteps.length; j++ ) { 9013 if ( rootlineSteps[ j ].offsetTop !== rootlineSteps[0].offsetTop && !rootlineSteps[ j ].className.includes('frm_rootline_show_hidden_steps_btn') ) { 9014 wrappingElementsCount++; 9015 } 9016 } 9017 return wrappingElementsCount; 9018 } 9019 9020 function moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton ) { 9021 var currentPageTitle, currentStepTitle, rootlineGroup, 9022 activeHiddenStepLink = hiddenStepsWrapper.querySelector( 'input:not(.frm_page_back):not(.frm_page_skip)' ); 9023 9024 if ( activeHiddenStepLink ) { 9025 currentPageTitle = activeHiddenStepLink.parentElement.querySelector( '.frm_rootline_title' ); 9026 9027 maybeUpdateRootlineTitles( activeHiddenStepLink.parentElement.previousElementSibling, hiddenStepsWrapper ); 9028 9029 showMoreButton.parentElement.className += ' active'; 9030 showMoreButton.className += ' active'; 9031 } else { 9032 currentPageTitle = listWrapper.querySelector( '.frm_current_page' ).querySelector( '.frm_rootline_title' ); 9033 } 9034 9035 if ( ! currentPageTitle ) { 9036 return; 9037 } 9038 9039 currentStepTitle = currentPageTitle.textContent; 9040 if ( ! currentStepTitle ) { 9041 return; 9042 } 9043 9044 // Add current step on top of rootline bar. 9045 rootlineGroup = listWrapper.closest( '.frm_rootline_group' ); 9046 9047 showCurrentHiddenStepText( currentStepTitle ); 9048 } 9049 9050 function showCurrentHiddenStepText( currentStepTitle ) { 9051 var rootlineCurrentStep = document.createElement( 'span' ); 9052 rootlineCurrentStep.className = 'frm_rootline_title'; 9053 rootlineCurrentStep.textContent = currentStepTitle; 9054 document.querySelector( '.frm_rootline_show_hidden_steps_btn' ).append( rootlineCurrentStep ); 9055 } 9056 9057 function copyRootlines( listWrappers ) { 9058 var i, listWrapper, form, clone, rootlinesBackup; 9059 rootlinesBackup = {}; 9060 for ( i = 0; i < listWrappers.length; i++ ) { 9061 listWrapper = listWrappers[ i ]; 9062 form = listWrapper.closest( 'form' ); 9063 clone = listWrapper.cloneNode( true ); 9064 9065 // Don't bake a transient disabled state (eg. mid dependent-field load) into the 9066 // snapshot used to rebuild the rootline on resize, or the "Show More" button gets 9067 // stuck disabled forever after a resize lands during that window. 9068 if ( ! form.classList.contains( 'frm_loading_form' ) ) { 9069 clone.querySelectorAll( 'input[type="button"]' ).forEach( button => button.disabled = false ); 9070 } 9071 9072 rootlinesBackup[ form.getAttribute( 'id' ) ] = clone; 9073 } 9074 listWrappersOriginal = rootlinesBackup; 9075 } 9076 9077 function maybeUpdateRootlineTitles( previousPageLink, hiddenStepsWrapper ) { 9078 var i; 9079 if ( previousPageLink ) { 9080 i = 0; 9081 while ( previousPageLink ) { 9082 i++; 9083 previousPageLink = previousPageLink.previousElementSibling; 9084 } 9085 updateRootlineStyle( hiddenStepsWrapper, i ); 9086 } 9087 } 9088 9089 function updateRootlineStyle( hiddenStepsWrapper, uptoIndex ) { 9090 var rootlineTitles, rootlineTitle; 9091 9092 if ( ! uptoIndex ) { 9093 uptoIndex = hiddenStepsWrapper.children.length; 9094 } 9095 rootlineTitles = [].slice.call( hiddenStepsWrapper.children, 0, uptoIndex );
9096 rootlineTitles.forEach( ( el ) => { 9097 rootlineTitle = el.querySelector( '.frm_rootline_title' ); 9098 if ( rootlineTitle ) { 9099 rootlineTitle.className += ' frm_prev_page_title'; 9100 } 9101 }); 9102 } 9103 9104 function showMoreSteps( e ) { 9105 var hiddenStepsWrapper = e.target.parentElement.querySelector( 'ul' ); 9106 if ( hiddenStepsWrapper.className.includes('frm_hidden') ) { 9107 hiddenStepsWrapper.className = hiddenStepsWrapper.className.replace( ' frm_hidden', '' ); 9108 } else { 9109 hiddenStepsWrapper.className += ' frm_hidden'; 9110 } 9111 } 9112 9113 /** 9114 * Validates form values when submitting form. 9115 */ 9116 function validateForm() { 9117 document.addEventListener( 'frm_get_ajax_form_errors', ( event ) => { 9118 if ( ! event.frmData.formEl ) { 9119 return; 9120 } 9121 9122 validateCheckboxMinSelections( event.frmData.formEl, event.frmData.errors ); 9123 }); 9124 } 9125 9126 /** 9127 * @since 6.14 9128 * 9129 * @param {HTMLElement|Object} formEl Form object. This might be a jQuery object. 9130 * @param {Array} errors 9131 * @return {void} 9132 */ 9133 function validateCheckboxMinSelections( formEl, errors ) { 9134 var checkboxes, 9135 checkedField = {}, 9136 errorMsg = frmCheckboxI18n.errorMsg.min_selections; 9137 9138 if ( 'function' === typeof formEl.get ) { 9139 // Get the HTMLElement from a jQuery object. 9140 formEl = formEl.get( 0 ); 9141 } 9142 9143 checkboxes = formEl.querySelectorAll( 'input[type="checkbox"][data-frmmin]:checked' ); 9144 checkboxes.forEach( ( checkbox ) => { 9145 var min, fieldEl, checkedCheckboxes, key; 9146 9147 min = parseInt( checkbox.dataset.frmmin, 10 ); 9148 if ( ! min ) { 9149 return; 9150 } 9151 fieldEl = checkbox.closest( '.frm_form_field' ); 9152 key = fieldEl.id.replace( 'frm_field_', '' ).replace( '_container', '' ); 9153 9154 if ( checkedField[ key ]) { 9155 // This field is processed before. 9156 return; 9157 } 9158 9159 checkedCheckboxes = fieldEl.querySelectorAll( 'input[type="checkbox"]:checked' ); 9160 if ( ! checkedCheckboxes.length ) { 9161 // If no checkbox is checked, let required field validation do its job. 9162 return; 9163 } 9164 9165 if ( checkedCheckboxes.length < min ) { 9166 errors[ key ] = errorMsg.replace( '%1$d', min ).replace( '%2$d', checkedCheckboxes.length ); 9167 } 9168 9169 checkedField[ key ] = true; 9170 }); 9171 } 9172 9173 function validateFieldValue() { 9174 document.addEventListener( 'frm_validate_field_value', ( event ) => { 9175 if ( 'object' !== typeof event.frmData.field || 'object' !== typeof event.frmData.errors ) { 9176 return; 9177 } 9178 9179 if ( 'password' === event.frmData.field.type ) { 9180 validatePasswordStrength( event.frmData.field, event.frmData.errors ); 9181 } 9182 }); 9183 } 9184 9185 function validatePasswordStrength( field, errors ) { 9186 var check, regex, checks; 9187 9188 if ( 'object' !== typeof window.frm_password_checks ) { 9189 return; 9190 } 9191 9192 if ( -1 === field.className.indexOf( 'frm_strong_pass' ) || 0 === field.id.indexOf( 'field_conf_' ) ) { 9193 return; 9194 } 9195 9196 checks = window.frm_password_checks; 9197 9198 for ( check in checks ) { 9199 regex = checks[ check ].regex.slice( 1, checks[ check ].regex.length - 1 ); 9200 regex = new RegExp( regex ); 9201 if ( ! regex.test( field.value ) ) { 9202 errors[ frmFrontForm.getFieldId( field ) ] = checks[ check ].message; 9203 return; 9204 } 9205 } 9206 } 9207 9208 function maybeTriggerCalc( event ) { 9209 if ( event.persisted || ( window.performance && window.performance.getEntriesByType( 'navigation' )[0].type === 'back_forward' ) ) { 9210 triggerCalc(); 9211 } 9212 } 9213 9214 function showMoreStepsButtonEvents() { 9215 var timeout; 9216 9217 /** 9218 * @since 6.34 9219 */ 9220 function onViewportResize() { 9221 var i, listWrappers, listWrapper, form; 9222 listWrappers = document.getElementsByClassName( 'frm_rootline' ); 9223 for ( i = 0; i < listWrappers.length; i++ ) { 9224 listWrapper = listWrappers[ i ]; 9225 form = listWrapper.closest( 'form' ); 9226 form.querySelector( '.frm_rootline_group' ).replaceChild( listWrappersOriginal[ form.getAttribute( 'id' ) ], listWrapper ); 9227 } 9228 9229 clearTimeout( timeout ); 9230 timeout = setTimeout( maybeShowMoreStepsButton(), 100 ); 9231 } 9232 9233 window.addEventListener( 'resize', onViewportResize ); 9234 9235 // Some mobile browsers (eg. iOS Safari) only fire visualViewport's resize event, not 9236 // window's, when the address bar collapses/expands without changing the layout viewport.
9237 if ( window.visualViewport ) { 9238 window.visualViewport.addEventListener( 'resize', onViewportResize ); 9239 } 9240 } 9241 9242 function handleShowPasswordBtn() { 9243 documentOn( 'click', '.frm_show_password_btn', ( event ) => { 9244 var input = event.target.closest( '.frm_show_password_wrapper' ).querySelector( 'input' ), 9245 button = input.nextElementSibling; 9246 9247 if ( 'password' === input.type ) { 9248 input.type = 'text'; 9249 button.setAttribute( 'data-show-password-label', button.title ); 9250 button.title = button.getAttribute( 'data-hide-password-label' ); 9251 } else { 9252 input.type = 'password'; 9253 button.title = button.getAttribute( 'data-show-password-label' ); 9254 } 9255 9256 button.setAttribute( 'aria-label', button.title ); 9257 }); 9258 } 9259 9260 function handleRangeSliders() { 9261 document.querySelectorAll( '.frm_range_container' ).forEach( ( rangeContainer ) => { 9262 const rangeInput = rangeContainer.querySelector( 'input[type="hidden"]' ); 9263 if ( ! rangeInput || rangeInput.dataset.isRangeSliderInitialized ) { 9264 return; 9265 } 9266 9267 initializeRangeSlider( rangeInput, rangeContainer ); 9268 }); 9269 } 9270 9271 /** 9272 * Does the same as jQuery( document ).on( 'event', 'selector', handler ). 9273 * 9274 * @since 6.3.1 9275 * 9276 * @param {String} event Event name. 9277 * @param {String} selector Selector. 9278 * @param {Function} handler Handler. 9279 * @param {Boolean|Object} options Options to be added to `addEventListener()` method. Default is `false`. 9280 */ 9281 function documentOn( event, selector, handler, options ) { 9282 if ( 'undefined' === typeof options ) { 9283 options = false; 9284 } 9285 9286 document.addEventListener( event, function( e ) { 9287 var target; 9288 9289 // loop parent nodes from the target to the delegation node. 9290 for ( target = e.target; target && target != this; target = target.parentNode ) { 9291 if ( target && target.matches && target.matches( selector ) ) { 9292 handler.call( target, e ); 9293 break; 9294 } 9295 } 9296 }, options ); 9297 } 9298 9299 /** 9300 * @since 6.5.1 9301 */ 9302 function handleElementorPopupConflicts() { 9303 var elementorPopupWrapper = document.querySelector( '.elementor-popup-modal' ); 9304 9305 if ( null !== elementorPopupWrapper ) { 9306 // Make dropzone items clickable. 9307 elementorPopupWrapper.querySelectorAll( '.frm_dropzone' ).forEach( ( item ) => { 9308 item.classList.remove( 'dz-clickable' ); 9309 }); 9310 9311 // Remove field chosen containers. 9312 elementorPopupWrapper.querySelectorAll( '.frm_form_field .chosen-container' ).forEach( ( chosenContainer ) => { 9313 chosenContainer.remove(); 9314 }); 9315 } 9316 9317 loadDropzones(); 9318 loadAutocomplete(); 9319 } 9320 9321 function getAllFormClasses( input ) { 9322 var formContainer, formClasses; 9323 9324 formContainer = input.closest( '.with_frm_style' ); 9325 if ( ! formContainer ) { 9326 return []; 9327 } 9328 9329 formClasses = []; 9330 Array.prototype.forEach.call( 9331 formContainer.className.split( ' ' ), 9332 ( className ) => { 9333 var trimmedClassName = className.trim(); 9334 if ( '' !== trimmedClassName && 'frm_forms' !== trimmedClassName ) { 9335 formClasses.push( trimmedClassName ); 9336 } 9337 } 9338 ); 9339 9340 return formClasses; 9341 } 9342 9343 function maybeAddIntlTelInput( phoneInputs ) { 9344 if ( document.body.classList.contains( 'wp-admin' ) || typeof window.intlTelInput !== 'function' ) { 9345 return; 9346 } 9347 9348 phoneInputs.forEach( ( phoneInput ) => { 9349 const parentContainer = document.createElement( 'div' ); 9350 parentContainer.classList.add( 'frm_forms', 'with_frm_style' ); 9351 9352 document.body.append( parentContainer ); 9353 9354 const intlPhoneConfig = { 9355 initialCountry: 'auto', 9356 formatOnDisplay: false, 9357 dropdownContainer: parentContainer, 9358 geoIpLookup( callback ) { 9359 if ( 'function' === typeof window.fetch ) { 9360 fetch( 'https://ipapi.co/json' ) 9361 .then( function( res ) { return res.json(); } ) 9362 .then( function( data ) { callback( data.country_code ); } ) 9363 .catch( function() { callback( 'us' ); } ); 9364 } 9365 }, 9366 loadUtils: () => import( getProPluginUrl() + '/js/intl-tel-input-utils.min.js' ), 9367 }; 9368 9369 // Allow someone to specify which countries to show in the dropdown 9370 // by adding data-only-countries in a shortcode. 9371 if ( phoneInput.dataset.onlyCountries ) { 9372 intlPhoneConfig.onlyCountries = phoneInput.dataset.onlyCountries.split( ',' ); 9373 9374 if ( 1 === intlPhoneConfig.onlyCountries.length ) { 9375 intlPhoneConfig.initialCountry = intlPhoneConfig.onlyCountries[0]; 9376 intlPhoneConfig.allowDropdown = false;
9377 } 9378 } 9379 9380 if ( phoneInput.dataset.countryOrder ) { 9381 intlPhoneConfig.countryOrder = phoneInput.dataset.countryOrder.split( ',' ); 9382 } 9383 9384 const intlTelInputInstance = window.intlTelInput( phoneInput, intlPhoneConfig ); 9385 9386 phoneInput.addEventListener( 'countrychange', function() { 9387 if ( ! this.value ) { 9388 return; 9389 } 9390 this.dispatchEvent( new Event( 'change', { bubbles: true } ) ); 9391 }); 9392 9393 // Update the input value with the full number including the country code 9394 phoneInput.addEventListener( 'blur', function() { 9395 if ( phoneInput.dataset.numberFormat ) { 9396 phoneInput.value = intlTelInputInstance.getNumber( determineNumberFormat( phoneInput.dataset.numberFormat ) ); 9397 } else { 9398 phoneInput.value = intlTelInputInstance.getNumber(); 9399 } 9400 }); 9401 9402 intlPhoneInputs[ phoneInput.id ] = intlTelInputInstance; 9403 }); 9404 } 9405 9406 /** 9407 * Determines the number format based on the provided format string. 9408 * 9409 * @param {String} numberFormat The number format string used in the shortcode. 9410 * @returns {Number} The number format number. 9411 */ 9412 function determineNumberFormat( numberFormat ) { 9413 numberFormat = numberFormat.toLowerCase(); 9414 const map = { 9415 e164: 0, 9416 international: 1, 9417 national: 2, 9418 rfc3966: 3, 9419 }; 9420 return map[ numberFormat ] ?? 0; 9421 } 9422 9423 /** 9424 * We have set 'overflow-x = clip;' initially to fix horizontal animation issue but it should be reverted 9425 * after that to avoid another style break. 9426 * 9427 * See issue #4895. 9428 */ 9429 function maybeUpdateFormsOverflowX() { 9430 document.querySelectorAll('.frm-show-form').forEach( 9431 ( slideinForm ) => { 9432 slideinForm.style['overflow-x'] = 'visible'; 9433 } 9434 ); 9435 } 9436 9437 /** 9438 * Updates the background gradient of a range input field based on its current value. 9439 * 9440 * @since 6.12 9441 */ 9442 function initRangeInput( rangeInputs ) { 9443 rangeInputs.forEach( ( rangeInput ) => { 9444 updateRangeInputBackground( rangeInput ); 9445 rangeInput.addEventListener( 'input', function() { 9446 updateRangeInputBackground( rangeInput ); 9447 }); 9448 rangeInput.addEventListener( 'onchange', function() { 9449 updateRangeInputBackground( rangeInput ); 9450 }); 9451 }); 9452 9453 function updateRangeInputBackground( rangeInput ) { 9454 // Calculate the percentage of the current slider value 9455 const sliderValuePercent = ( rangeInput.value - rangeInput.min ) / ( rangeInput.max - rangeInput.min ) * 100; 9456 // Update the background gradient of the slider 9457 rangeInput.style.background = `linear-gradient(to right, var(--slider-color) 0%, var(--slider-color) ${sliderValuePercent}%, var(--slider-bar-color) ${sliderValuePercent}% 100%)`; 9458 } 9459 } 9460 9461 /** 9462 * Initialize number formatting and input restrictions for `.frm-has-number-format` fields. 9463 */ 9464 function initFormatFieldValueNumbers() { 9465 document.querySelectorAll( '.frm-has-number-format:not([data-has-number-format-events])' ).forEach( field => { 9466 field.dataset.hasNumberFormatEvents = true; 9467 9468 document.addEventListener( 'frmShowField', () => onShowField( field ) ); 9469 field.addEventListener( 'focus', handleFocus ); 9470 field.addEventListener( 'blur', handleBlur ); 9471 9472 if ( field.classList.contains( 'frm_field_number' ) ) { 9473 field.addEventListener( 'keydown', restrictToNumericInput ); 9474 } 9475 9476 triggerFormatting( field ); 9477 }); 9478 9479 /** 9480 * Trigger formatting manually. 9481 * 9482 * @param {HTMLElement} field Field element. 9483 */ 9484 function triggerFormatting( field ) { 9485 field.dispatchEvent( new Event( 'blur', { bubbles: true } ) ); 9486 } 9487 9488 /** 9489 * Handle the 'frmShowField' event. 9490 * 9491 * @param {HTMLElement} field Field element. 9492 */ 9493 function onShowField( field ) { 9494 if ( ! field.offsetHeight || field.value !== '' ) { 9495 return; 9496 } 9497 9498 // Restore raw value and apply formatting when field becomes visible. 9499 const { rawValue, frmval } = field.dataset; 9500 const valueToUse = rawValue || frmval; 9501 if ( valueToUse ?? false ) { 9502 field.value = valueToUse; 9503 triggerFormatting( field ); 9504 } 9505 } 9506 9507 /** 9508 * Restore the unformatted value when the field gains focus. 9509 * 9510 * @param {FocusEvent} event Focus event for the field. 9511 */ 9512 function handleFocus( { target: field } ) { 9513 const { rawValue } = field.dataset; 9514 9515 if ( rawValue !== null ) { 9516 field.value = rawValue; 9517 } 9518 } 9519 9520 /** 9521 * Save the raw value and apply formatting when the field loses focus. 9522 * 9523 * @param {FocusEvent} event Blur event for the field. 9524 */ 9525 function handleBlur( event ) { 9526 const field = event.target; 9527 const previousValue = field.value; 9528 const previousRawValue = field.getAttribute( 'data-raw-value' ) ?? '';
9529 const fieldKey = getFieldKey( field.id, field.name ); 9530 const userTyped = field.value; 9531 9532 field.setAttribute( 'data-raw-value', userTyped ); 9533 field.value = applyNumberFormatting( userTyped, fieldKey ); 9534 9535 if ( previousValue !== field.value || previousRawValue !== userTyped ) { 9536 if ( event.isTrusted ) { 9537 field.dispatchEvent( new Event( 'change', { bubbles: true } ) ); 9538 } else { 9539 jQuery( field ).trigger({ type: 'change', selfTriggered: true }); 9540 } 9541 } 9542 } 9543 9544 /** 9545 * Restrict input to valid numeric characters based on custom separators. 9546 * 9547 * @param {KeyboardEvent} event Keydown event for the field. 9548 */ 9549 function restrictToNumericInput( event ) { 9550 const { target: field, key, ctrlKey, metaKey } = event; 9551 const fieldKey = getFieldKey( field.id, field.name ); 9552 const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc?.[ fieldKey ] ) || {}; 9553 const decimalSeparator = formatConfig.decimal_separator ?? '.'; 9554 const thousandSeparator = formatConfig.thousand_separator ?? '';
9555 const decimals = parseInt(formatConfig.decimals ?? 2, 10); 9556 9557 const allowedKeys = [ 9558 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 9559 'Backspace', 'Delete', 'Tab', 'Home', 'End', 'Enter', 9560 ]; 9561 9562 if ( allowedKeys.includes( key ) || ctrlKey || metaKey ) { 9563 return; 9564 } 9565 9566 // Build regex based on allowed chars 9567 let regexPattern = '^[0-9\\+\\-'; 9568 9569 // Only add decimal separator if decimals > 0 9570 if ( decimals > 0 ) { 9571 regexPattern += escapeForRegex( decimalSeparator ); 9572 } 9573 9574 if ( thousandSeparator ) { 9575 regexPattern += escapeForRegex( thousandSeparator ); 9576 } 9577 9578 regexPattern += ']$'; 9579 9580 const oneCharRegex = new RegExp( regexPattern ); 9581 9582 if ( ! oneCharRegex.test( key ) ) { 9583 event.preventDefault(); 9584 return; 9585 } 9586 9587 // Prevent decimal input if decimals = 0 9588 if ( decimals === 0 && key === ( decimalSeparator !== '' ? decimalSeparator : '.' ) ) { 9589 event.preventDefault(); 9590 return; 9591 } 9592 9593 // Only allow one decimal point 9594 if ( key === decimalSeparator && field.value.includes( decimalSeparator ) ) { 9595 event.preventDefault(); 9596 return; 9597 } 9598 9599 // If user typed '+' or '-', allow only if it's the first character 9600 if ( ( key === '+' || key === '-' ) && field.selectionStart !== 0 ) { 9601 event.preventDefault(); 9602 } 9603 } 9604 9605 /** 9606 * Format numeric substrings in the provided text based on the custom format configuration. 9607 * 9608 * @param {string} text The text containing numbers to format. 9609 * @param {string} fieldKey The field key used to retrieve the format configuration. 9610 * @returns {string} The text with formatted numbers. 9611 */ 9612 function applyNumberFormatting( text, fieldKey ) { 9613 if ( ! __FRMCALC?.calc?.[ fieldKey ] ) { 9614 return text; 9615 } 9616 9617 const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc[fieldKey] ); 9618 const { 9619 decimal_separator: decimalSeparator = '.', 9620 thousand_separator: thousandSeparator = ',', 9621 symbol_left: symbolLeft = '', 9622 symbol_right: symbolRight = '', 9623 symbol_padding: symbolPadding = '' 9624 } = formatConfig; 9625 9626 const escapedDecimalSeparator = escapeForRegex( decimalSeparator ); 9627 const escapedThousandSeparator = escapeForRegex( thousandSeparator ); 9628 const escapedSymbolLeft = escapeForRegex( symbolLeft ); 9629 const escapedSymbolRight = escapeForRegex( symbolRight ); 9630 const escapedSymbolPadding = escapeForRegex( symbolPadding ); 9631 9632 // Build the regex pattern 9633 let pattern = '(?<!\\S)'; 9634 9635 // Add left symbol with padding if there is a left symbol 9636 if ( symbolLeft ) { 9637 pattern += `(?:${escapedSymbolLeft}${symbolPadding ? escapedSymbolPadding : ''})?`; 9638 } 9639 9640 // Add the core numeric pattern 9641 pattern += `([-+]?\\d+(?:${escapedThousandSeparator}\\d+)*(?:${escapedDecimalSeparator}\\d+)?)`; 9642 9643 // Add right symbol with padding if there is a right symbol 9644 if ( symbolRight ) { 9645 pattern += `(?:${symbolPadding ? escapedSymbolPadding : ''}${escapedSymbolRight})?`; 9646 } 9647 9648 pattern += '(?!\\S)'; 9649 9650 const numericRegex = new RegExp( pattern, 'g' ); 9651 9652 return text.replace( numericRegex, ( match ) => { 9653 const sign = match.startsWith( '-' ) ? '-' : ( match.startsWith( '+' ) ? '+' : '' ); 9654 const signRemoved = match.replace( /^[-+]/, '' ); 9655 9656 const number = stripSymbolsAndFormatNumber( signRemoved, formatConfig ); 9657 const formatted = formatCurrency( normalizeTotal( number, formatConfig ), formatConfig ); 9658 9659 return `${sign}${formatted}`; 9660 }); 9661 } 9662 9663 /** 9664 * Strip currency symbols and convert a numeric string to a standard decimal format by removing custom separators. 9665 * 9666 * @param {string} value The numeric string. 9667 * @param {Object} config The format configuration. 9668 * @param {string} [config.decimal_separator] The custom decimal separator. 9669 * @param {string} [config.thousand_separator] The custom thousand separator. 9670 * @param {string} [config.symbol_left] The left currency symbol. 9671 * @param {string} [config.symbol_right] The right currency symbol. 9672 * @returns {string} The numeric string in standard decimal format. 9673 */ 9674 function stripSymbolsAndFormatNumber( value, config = {} ) { 9675 const { 9676 symbol_left: symbolLeft = '', 9677 symbol_right: symbolRight = '', 9678 decimal_separator: decimalSeparator = '.', 9679 thousand_separator: thousandSeparator = ',' 9680 } = config; 9681 9682 let cleanedValue = value.trim(); 9683 9684 // Remove left and right symbols if present 9685 if ( symbolLeft ) { 9686 cleanedValue = cleanedValue.replace( symbolLeft, '' ); 9687 } 9688 9689 if ( symbolRight ) { 9690 cleanedValue = cleanedValue.replace( symbolRight, '' ); 9691 } 9692 9693 cleanedValue = cleanedValue.trim(); 9694 9695 // Remove thousand separators and replace custom decimal separator 9696 if ( thousandSeparator ) { 9697 const split = cleanedValue.split( thousandSeparator ); 9698 // Make sure that it isn't actually a decimal separator first. 9699 if ( split.length !== 2 || split[1].length > 2 ) { 9700 cleanedValue = split.join( '' ); 9701 } else { 9702 cleanedValue = split[0] + '.' + split[1]; 9703 } 9704 } 9705 9706 if ( decimalSeparator.trim() && decimalSeparator !== '.' ) { 9707 cleanedValue = cleanedValue.split( decimalSeparator ).join( '.' ); 9708 } 9709 9710 return cleanedValue.trim(); 9711 } 9712 9713 /** 9714 * Escape special characters in a string for use in regular expressions. 9715 * 9716 * @param {string} str The string to escape. 9717 * @returns {string} The escaped string. 9718 */ 9719 function escapeForRegex( str ) { 9720 return str.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); 9721 } 9722 } 9723
9724 /** 9725 * Initializes a range slider. 9726 * 9727 * @param {HTMLInputElement} rangeInput The input element of the range slider. 9728 * @param {HTMLElement} rangeContainer The container element of the range slider. 9729 * @return {void} 9730 */ 9731 function initializeRangeSlider( rangeInput, rangeContainer ) { 9732 const parseDataset = dataset => { 9733 const parsed = {}; 9734 for ( const [ key, value ] of Object.entries( dataset ) ) { 9735 parsed[ key ] = parseFloat( value, 10 ); 9736 } 9737 return parsed; 9738 }; 9739 const { minGap, maxGap } = parseDataset( rangeInput.dataset ); 9740 9741 const min = parseFloat( rangeInput.min, 10 ); 9742 const max = parseFloat( rangeInput.max, 10 ); 9743 const minRange = minGap || 0; 9744 const maxRange = maxGap || max - min; 9745 9746 // Validate inputs 9747 if ( min >= max ) { 9748 throw new Error( 'Min value must be less than max value' ); 9749 } 9750 9751 if ( minRange <= 0 || minRange > ( max - min ) ) { 9752 throw new Error( 'Invalid minimum range value' ); 9753 } 9754 9755 if ( maxRange < minRange || maxRange > ( max - min ) ) { 9756 throw new Error( 'Invalid maximum range value' ); 9757 } 9758 9759 const values = rangeInput.value.split( ',' ); 9760 values[0] ||= min; 9761 values[1] ||= max; 9762 const step = parseFloat( rangeInput.step, 10 ); 9763 const sliderRange = max - min; 9764 const stepInPercent = step / sliderRange * 100; 9765 const minRangePercent = ( minRange / sliderRange ) * 100; 9766 const maxRangePercent = ( maxRange / sliderRange ) * 100; 9767 const minHandle = rangeContainer.querySelector( '.min-handle' ); 9768 const maxHandle = rangeContainer.querySelector( '.max-handle' ); 9769 const rangeElement = rangeContainer.querySelector( '.frm-slider-range' ); 9770 const minValueDisplay = rangeContainer.querySelector( '.min-value' ); 9771 const maxValueDisplay = rangeContainer.querySelector( '.max-value' ); 9772 const track = rangeContainer.querySelector( '.frm-slider-track' ); 9773 9774 let decimalsToUse = 0; 9775 9776 [ step, min, max, minRange, maxRange ].forEach( value => { 9777 const decimal = countDecimals( value ); 9778 if ( decimal > decimalsToUse ) { 9779 decimalsToUse = decimal; 9780 } 9781 }); 9782 9783 const initialMinValue = Math.max( values[0], min ); 9784 // An end value below the start value is not a supported range. Collapse the handles rather 9785 // than letting the filled section render with a negative width. setHandlesBasedOnValue 9786 // already guards this the same way. 9787 const initialMaxValue = Math.max( Math.min( values[1], initialMinValue + maxRange, max ), initialMinValue ); 9788 9789 // Measured at drag start, when the slider is guaranteed to be visible. 9790 let trackWidth = 0; 9791 9792 // Initialize positions 9793 let minPos = ( initialMinValue - min ) / sliderRange * 100; 9794 let maxPos = ( initialMaxValue - min ) / sliderRange * 100; 9795 9796 updateHandlesPositionAndValue(); 9797 9798 // State variables for dragging 9799 let isDragging = false; 9800 let activeHandle = null; 9801 let startX = 0; 9802 let startMinPos = 0; 9803 let startMaxPos = 0; 9804 9805 // Setup both handles 9806 setupHandle( minHandle ); 9807 setupHandle( maxHandle ); 9808 9809 rangeInput.addEventListener( 'change', setHandlesBasedOnValue ); 9810 9811 function setupHandle( handle ) { 9812 handle.addEventListener( 'touchstart', handleTouchStart, { passive: false }); 9813 handle.addEventListener( 'mousedown', handleMouseDown ); 9814 } 9815 9816 function handleTouchStart( e ) { 9817 isDragging = true; 9818 trackWidth = track.getBoundingClientRect().width; 9819 activeHandle = e.target; 9820 const touch = e.touches[0]; 9821 startX = touch.clientX; 9822 startMinPos = minPos; 9823 startMaxPos = maxPos; 9824 document.addEventListener( 'touchmove', handleTouchMove, {passive: false}); 9825 document.addEventListener( 'touchend', handleTouchEnd ); 9826 e.preventDefault(); 9827 } 9828 9829 function handleMouseDown( e ) { 9830 isDragging = true; 9831 trackWidth = track.getBoundingClientRect().width; 9832 activeHandle = e.target; 9833 startX = e.clientX; 9834 startMinPos = minPos; 9835 startMaxPos = maxPos; 9836 document.addEventListener( 'mousemove', onMouseMove ); 9837 document.addEventListener( 'mouseup', onMouseUp ); 9838 e.preventDefault(); 9839 } 9840 9841 function handleTouchMove( e ) { 9842 if ( ! isDragging ) { 9843 return; 9844 } 9845 const touch = e.touches[0]; 9846 const mouseEvent = new MouseEvent( 'mousemove', { 9847 clientX: touch.clientX, 9848 clientY: touch.clientY 9849 }); 9850 onMouseMove( mouseEvent, e ); 9851 e.preventDefault(); 9852 } 9853 9854 function handleTouchEnd() { 9855 document.removeEventListener( 'touchmove', handleTouchMove ); 9856 document.removeEventListener( 'touchend', handleTouchEnd ); 9857 } 9858 9859 function onMouseMove( e ) { 9860 if ( ! isDragging ) { 9861 return; 9862 } 9863 9864 const deltaX = e.clientX - startX; 9865 const deltaPercent = ( deltaX / trackWidth ) * 100; 9866 const minHandleIsMoved = activeHandle === minHandle; 9867 let newMinPos, newMaxPos, hasMovedMinimumSteps; 9868 9869 if ( minHandleIsMoved ) { 9870 newMinPos = startMinPos + deltaPercent; // The new left handle's position in % -- at the current mouse position. 9871 hasMovedMinimumSteps = Math.abs( newMinPos - minPos ) >= stepInPercent; 9872 } else { 9873 newMaxPos = startMaxPos + deltaPercent; 9874 hasMovedMinimumSteps = Math.abs( newMaxPos - maxPos ) >= stepInPercent; 9875 } 9876 9877 if ( ! hasMovedMinimumSteps ) { 9878 return; 9879 } 9880 9881 const movingRight = minHandleIsMoved ? newMinPos > minPos : newMaxPos > maxPos; 9882 const numberOfNewSteps = movingRight ? Math.floor( deltaPercent / stepInPercent ) : Math.ceil( deltaPercent / stepInPercent ); 9883 const newRange = activeHandle === minHandle ? maxPos - newMinPos : newMaxPos - minPos; // The distance between the right and left handles in %. 9884 const stepsDelta = numberOfNewSteps * stepInPercent; 9885 9886 if ( activeHandle === minHandle ) { 9887 if ( newRange < minRangePercent ) { // Move the whole range to right. 9888 const tempMinPos = Math.max( startMinPos + stepsDelta, 0 ); 9889 if ( tempMinPos + minRangePercent <= 100 ) { 9890 minPos = tempMinPos; 9891 maxPos = Math.max( 0, minPos + minRangePercent ); 9892 } 9893 } else if ( newRange > maxRangePercent ) { // Move the whole range to left. 9894 minPos = Math.max( 0, startMinPos + stepsDelta ); 9895 maxPos = minPos + maxRangePercent; 9896 } else { 9897 // Normal movement within constraints 9898 minPos = Math.max( 0, startMinPos + stepsDelta ); 9899 } 9900 updateHandlesPositionAndValue(); 9901 return; 9902 } 9903 9904 // Max handle is moved 9905 if ( newRange < minRangePercent ) { // Move the whole range to left. 9906 const tempMaxPos = Math.min( startMaxPos + stepsDelta, 100 ); 9907 if ( tempMaxPos - minRangePercent >= 0 ) { 9908 maxPos = tempMaxPos; 9909 minPos = Math.max( 0, maxPos - minRangePercent ); 9910 } 9911 } else if ( newRange > maxRangePercent ) { // Move the whole range to right. 9912 maxPos = Math.min( 100, startMaxPos + stepsDelta ); 9913 minPos = maxPos - maxRangePercent; 9914 } else { 9915 // Normal movement within constraints 9916 maxPos = Math.min( 100, startMaxPos + stepsDelta ); 9917 } 9918 updateHandlesPositionAndValue(); 9919 } 9920 9921 function updateHandlesPositionAndValue() { 9922 minPos = parseFloat( minPos ); 9923 maxPos = parseFloat( maxPos ); 9924 9925 // Update min and max handles positions. 9926 minHandle.style.left = `${minPos}%`; 9927 maxHandle.style.right = `${100-maxPos}%`; 9928 9929 // Update range element. 9930 updateRangeElement(); 9931 9932 // Update displayed values. 9933 const { currentMin, currentMax } = prepareCurrentValues(); 9934 updateDisplayedValues( currentMin, currentMax ); 9935 9936 // Update input value. 9937 rangeContainer.querySelector( 'input' ).value = `${currentMin},${currentMax}`; 9938 } 9939 9940 function countDecimals( num ) { 9941 const str = num.toString(); 9942 const decimalIndex = str.indexOf( '.' ); 9943 return decimalIndex === -1 ? 0 : str.length - decimalIndex - 1; 9944 } 9945 9946 function formatSliderValue ( value ) { 9947 const fieldKey = getFieldKey( rangeInput.id, rangeInput.name ); 9948 if ( window.__FRMCALC?.calc[ fieldKey ]) { 9949 return formatValueAsCurrency( value, fieldKey ); 9950 } 9951 return value; 9952 } 9953 9954 function updateDisplayedValues( currentMin, currentMax ) { 9955 minValueDisplay.textContent = formatSliderValue( currentMin ); 9956 maxValueDisplay.textContent = formatSliderValue( currentMax ); 9957 } 9958 9959 function prepareCurrentValues() { 9960 let currentMin = ( min + ( minPos / stepInPercent ) * step ).toFixed( decimalsToUse ); 9961 let currentMax = ( min + ( maxPos / stepInPercent ) * step ).toFixed( decimalsToUse ); 9962 currentMin = Number( currentMin ).toPrecision().toString(); 9963 currentMax = Number( currentMax ).toPrecision().toString(); 9964 9965 return { currentMin, currentMax }; 9966 } 9967 9968 function onMouseUp() { 9969 isDragging = false;
9970 document.removeEventListener( 'mousemove', onMouseMove ); 9971 document.removeEventListener( 'mouseup', onMouseUp ); 9972 } 9973 9974 function updateRangeElement() { 9975 rangeElement.style.left = `${minPos}%`; 9976 rangeElement.style.width = `${maxPos - minPos}%`; 9977 } 9978 9979 /** 9980 * Change events are only triggered for the range slider when the default value is set 9981 * The default value is set when the field is conditionally shown. 9982 */ 9983 function setHandlesBasedOnValue() { 9984 const { minGap } = parseDataset( rangeInput.dataset ); 9985 const min = parseFloat( rangeInput.min, 10 ); 9986 const max = parseFloat( rangeInput.max, 10 ); 9987 const minRange = minGap || 0; 9988 const sliderRange = max - min; 9989 const values = rangeInput.value.split( ',' ); 9990 values[0] ||= min; 9991 values[1] ||= max; 9992 const initialMinValue = Math.max( values[0], min ); 9993 const initialMaxValue = Math.min( Math.max( values[1], initialMinValue + minRange ), max ); 9994 9995 minPos = ( initialMinValue - min ) / sliderRange * 100; 9996 maxPos = ( initialMaxValue - min ) / sliderRange * 100; 9997 updateHandlesPositionAndValue(); 9998 } 9999 } 10000 10001 return { 10002 init() { 10003 maybeUpdateFormsOverflowX(); 10004 10005 maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) ); 10006 initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) ); 10007 initFormatFieldValueNumbers(); 10008 10009 addEventListener( 'pageshow', maybeTriggerCalc ); 10010 10011 jQuery( document ).on( 'frmFormComplete', afterFormSubmitted ); 10012 jQuery( document ).on( 'frmPageChanged', afterPageChanged ); 10013 jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal ); 10014 10015 jQuery( document ).on( 'click', '.frm_trigger', toggleSection ); 10016 jQuery( document ).on( 'keydown', '.frm_trigger', toggleSection ); 10017 10018 var $blankField = jQuery( '.frm_blank_field' ); 10019 if ( $blankField.length ) { 10020 $blankField.closest( '.frm_toggle_container' ).prev( '.frm_trigger' ).trigger( 'click' ); 10021 } 10022 10023 jQuery( document ).on( 'click', '.frm_remove_link', removeFile ); 10024 10025 jQuery( document ).on( 'frmFieldChanged', maybeCheckDependent ); 10026 10027 jQuery( document ).on( 'keyup', 'input.frm_strength_meter', checkPasswordField ); 10028 jQuery( document ).on( 'keydown', '.frm_switch', triggerToggleClickOnSpace ); 10029 10030 jQuery( document ).on( 'mouseenter click', '.frm-star-group input', loadStars ); 10031 jQuery( document ).on( 'mouseenter', '.frm-star-group .star-rating:not(.star-rating-readonly)', hoverStars ); 10032 jQuery( document ).on( 'mouseleave', '.frm-star-group .star-rating:not(.star-rating-readonly)', unhoverStars ); 10033 10034 jQuery( document ).on( 10035 'click', 10036 '.frm-show-form input[type="submit"], .frm-show-form input[name="frm_prev_page"], .frm_page_back, .frm_page_skip, .frm-show-form .frm_save_draft, .frm_prev_page, .frm_button_submit, .frm_rootline_show_hidden_steps_btn .frm_rootline_single', 10037 setNextPage 10038 ); 10039 10040 jQuery( document ).on( 'change', '.frm_other_container input[type="checkbox"], .frm_other_container input[type="radio"], .frm_other_container select', showOtherText ); 10041 jQuery( document ).on( 'change', '.frm_switch_block input[type="checkbox"]', setToggleAriaChecked ); 10042 10043 jQuery( document ).on( 'click', '.frm_remove_form_row', removeRow ); 10044 jQuery( document ).on( 'click', '.frm_add_form_row', addRow ); 10045 10046 // In place edit. 10047 jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_inplace_edit', editEntry ); 10048 jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_cancel_edit', cancelEdit ); 10049 jQuery( document ).on( 'click', '.frm_ajax_delete', deleteEntry ); 10050 10051 // toggle collapsible entries shortcode 10052 jQuery( '.frm_month_heading, .frm_year_heading' ).on( 'click', function() { 10053 var content = jQuery( this ).children( '.ui-icon-triangle-1-e, .ui-icon-triangle-1-s' ); 10054 if ( content.hasClass( 'ui-icon-triangle-1-e' ) ) { 10055 content.addClass( 'ui-icon-triangle-1-s' ).removeClass( 'ui-icon-triangle-1-e' ); 10056 jQuery( this ).next( '.frm_toggle_container' ).fadeIn( 'slow' ); 10057 } else { 10058 content.addClass( 'ui-icon-triangle-1-e' ).removeClass( 'ui-icon-triangle-1-s' ); 10059 jQuery( this ).next( '.frm_toggle_container' ).hide(); 10060 } 10061 }); 10062 10063 // Elementor popup show event. Fix Elementor Popup && FF PRO fields( file upload | dynamic field ) conflicts 10064 jQuery( document ).on( 'elementor/popup/show', handleElementorPopupConflicts ); 10065 10066 addTopAddRowBtnForRepeater(); 10067
10068 // Checkbox Selection Limit 10069 jQuery( document ).on( 'click', 'input[type="checkbox"][data-frmlimit]', checkCheckboxSelectionLimit ); 10070 10071 // Total field calc 10072 jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal ); 10073 jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal ); 10074 10075 jQuery( document ).on( 'frmFormComplete frmPageChanged frmInPlaceEdit frmAfterAddRow', setAutoHeightForTextArea ); 10076 10077 maybeDisableCheckboxesWithLimit(); 10078 10079 setInlineFormWidth(); 10080 checkConditionalLogic( 'pageLoad' ); 10081 checkFieldsOnPage( undefined, 'pageLoad' ); 10082 processPendingAjax(); 10083 10084 addRteRequiredMessages(); 10085 setAutoHeightForTextArea(); 10086 updateContentLength(); 10087 10088 // make sure this comes last, particularly after checkConditionalLogic & checkFieldsOnPage 10089 calcProductsTotal(); 10090 10091 startOverButton(); 10092 10093 maybeShowMoreStepsButton(); 10094 10095 showMoreStepsButtonEvents(); 10096 10097 validateFieldValue(); 10098 validateForm(); 10099 10100 handleShowPasswordBtn(); 10101 10102 jQuery( document ).on( 'input change', 'input[data-frmrange]', handleSliderEvent ); 10103 }, 10104 10105 savingDraft( object ) { 10106 return savingDraftEntry( object ); 10107 }, 10108 10109 goingToPreviousPage( object ) { 10110 return goingToPrevPage( object ); 10111 }, 10112 10113 hideOrShowFields( ids, event ) { 10114 if ( 'pageLoad' === event ) { 10115 clearHideFields(); 10116 } 10117 var len = ids.length, 10118 repeatArgs = { repeatingSection: '', repeatRow: '' }; 10119 for ( var i = 0, l = len; i < l; i++ ) { 10120 hideOrShowFieldById( ids[ i ], repeatArgs ); 10121 if ( i == ( l - 1 ) ) { 10122 showForm(); 10123 } 10124 } 10125 }, 10126 10127 hidePreviouslyHiddenFields() { 10128 var hiddenFields = getAllHiddenFields(), 10129 len = hiddenFields.length; 10130 for ( var i = 0, l = len; i < l; i++ ) { 10131 var container = document.getElementById( hiddenFields[ i ]); 10132 10133 //check for submit button 10134 if ( container == null ) { 10135 container = document.querySelector( '#' + hiddenFields[ i ]); 10136 if ( container != null && hiddenFields[ i ].includes('frm_final_submit') ) { 10137 hidePreviouslyHiddenSubmitButton( hiddenFields[ i ]); 10138 continue; 10139 } 10140 } 10141 10142 if ( container !== null ) { 10143 container.style.display = 'none'; 10144 setHiddenProductContainer( container ); 10145 } 10146 } 10147 }, 10148 10149 submitAllowed( object ) { 10150 var formElementId = object.getAttribute( 'id' ); 10151 10152 if ( ! isSubmitButtonOnPage( formElementId + ' .frm_final_submit' ) || goingToPrevPage( object ) || savingDraftEntry( object ) ) { 10153 return true; 10154 } 10155 10156 var formKey = getFormKeyFromFormElementID( formElementId ); 10157 10158 return ! isOnPageSubmitButtonHidden( formKey ); 10159 }, 10160 10161 checkDependentDynamicFields( ids ) { 10162 var len = ids.length, 10163 repeatArgs = { repeatingSection: '', repeatRow: '' }; 10164 for ( var i = 0, l = len; i < l; i++ ) { 10165 hideOrShowFieldById( ids[ i ], repeatArgs ); 10166 } 10167 }, 10168 10169 checkDependentLookupFields( ids ) { 10170 var fieldId, 10171 repeatArgs = { repeatingSection: '', repeatRow: '' }; 10172 for ( var i = 0, l = ids.length; i < l; i++ ) { 10173 fieldId = ids[ i ]; 10174 updateWatchingFieldById( fieldId, repeatArgs, 'value changed' ); 10175 } 10176 }, 10177 10178 loadGoogle() { 10179 var graphs, packages, i; 10180 10181 if ( typeof google === 'undefined' || ! google || ! google.load ) { 10182 setTimeout( frmProForm.loadGoogle, 30 ); 10183 return; 10184 } 10185 10186 graphs = __FRMTABLES; 10187 packages = Object.keys( graphs ); 10188 10189 for ( i = 0; i < packages.length; i++ ) { 10190 if ( packages[ i ] === 'graphs' ) { 10191 generateGoogleGraphs( graphs[ packages[ i ] ]); 10192 } else { 10193 generateGoogleTables( graphs[ packages[ i ] ], packages[ i ]); 10194 } 10195 } 10196 }, 10197 10198 removeUsedTimes( obj, timeField ) { 10199 var $form, form, e, data, success, extraParams; 10200 10201 $form = jQuery( obj ).parents( 'form' ).first(); 10202 form = $form.get( 0 );
10203 e = $form.find( 'input[name="id"]' ); 10204 10205 data = { 10206 action: 'frm_fields_ajax_time_options', 10207 time_field: timeField, 10208 date_field: obj.id, 10209 entry_id: ( e ? e.val() : '' ), date: jQuery( obj ).val(), 10210 nonce: frm_js.nonce 10211 }; 10212 10213 success = function( opts ) { 10214 if ( null !== document.getElementById( timeField + '_H' ) ) { 10215 frmProForm.removeUsedTimesForMultipleDropdown( opts, timeField ); 10216 return; 10217 } 10218 10219 var $timeField = jQuery( document.getElementById( timeField ) ); 10220 $timeField.find( 'option' ).prop( 'disabled', false ); 10221 if ( opts.length > 0 ) { 10222 for ( var i = 0, l = opts.length; i < l; i++ ) { 10223 $timeField.get( 0 ).querySelectorAll( 'option[value="' + opts[ i ] + '"]' ).forEach( 10224 ( option ) => { 10225 option.disabled = true; 10226 if ( option.selected ) { 10227 option.selected = false; 10228 } 10229 } 10230 ); 10231 } 10232 } 10233 }; 10234 10235 extraParams = { dataType: 'json' }; 10236 10237 postToAjaxUrl( form, data, success, false, extraParams ); 10238 }, 10239 10240 removeUsedTimesForMultipleDropdown( times, timeField ) { 10241 const self = this; 10242 10243 self.hDropdown = null; 10244 self.mDropdown = null; 10245 self.ampmDropdown = null; 10246 10247 function getTimeDropdownsElements( timeField ) { 10248 return { 10249 h: getTimeDropdownsElements.prototype.h[ timeField ], 10250 m: getTimeDropdownsElements.prototype.m[ timeField ], 10251 ampm: getTimeDropdownsElements.prototype.ampm[ timeField ] 10252 }; 10253 } 10254 10255 this.init = function() { 10256 if ( 'undefined' === typeof getTimeDropdownsElements.prototype.h ) { 10257 getTimeDropdownsElements.prototype.h = []; 10258 getTimeDropdownsElements.prototype.m = []; 10259 getTimeDropdownsElements.prototype.ampm = []; 10260 } 10261 10262 if ( 'undefined' !== typeof getTimeDropdownsElements.prototype.h[ timeField ] ) { 10263 return; 10264 } 10265 10266 getTimeDropdownsElements.prototype.h[ timeField ] = document.getElementById( timeField + '_H' ); 10267 getTimeDropdownsElements.prototype.m[ timeField ] = document.getElementById( timeField + '_m' ); 10268 getTimeDropdownsElements.prototype.ampm[ timeField ] = document.getElementById( timeField + '_A' ); 10269 10270 if ( null === getTimeDropdownsElements( timeField ).h ) { 10271 return; 10272 } 10273 10274 getTimeDropdownsElements( timeField ).h.addEventListener( 'change', ( event ) =>{ 10275 self.disableMinutes( event.target.value ); 10276 }); 10277 getTimeDropdownsElements( timeField ).m.addEventListener( 'change', ( event ) =>{ 10278 }); 10279 getTimeDropdownsElements( timeField ).ampm.addEventListener( 'change', () =>{ 10280 self.disableHours(); 10281 }); 10282 10283 self.disableHours(); 10284 self.disableAmpm(); 10285 10286 }; 10287 10288 this.checkIfTimeIsDisabled = time => times.includes( time ); 10289 10290 this.getActiveTime = function() { 10291 var h = getTimeDropdownsElements( timeField ).h.value || '12', 10292 m = getTimeDropdownsElements( timeField ).m.value || '00', 10293 ampm = getTimeDropdownsElements( timeField ).ampm.value || 'AM'; 10294 10295 return h + ':' + m + ' ' + ampm; 10296 }; 10297 10298 this.getMeridiem = function() { 10299 return getTimeDropdownsElements( timeField ).ampm.value || 'AM'; 10300 }; 10301 10302 this.listPossibleHourTimes = function( hour ) { 10303 const minutes = []; 10304 const ampm = []; 10305 const minutesElement = getTimeDropdownsElements( timeField ).m; 10306 const ampmElement = getTimeDropdownsElements( timeField ).ampm; 10307 10308 for ( let i = 0; i < ampmElement.options.length; i++ ) { 10309 ampm.push( ampmElement.options[ i ].value ); 10310 } 10311 10312 for ( let i = 0; i < minutesElement.options.length; i++ ) { 10313 if ( '' !== minutesElement.options[ i ].value ) { 10314 minutes.push( minutesElement.options[ i ].value ); 10315 } 10316 } 10317 10318 return ampm.reduce( ( array, ampm ) => { 10319 array[ ampm ] = minutes.map( ( minute ) => { 10320 return hour + ':' + minute + ' ' + ampm; 10321 }); 10322 return array; 10323 }, {} ); 10324 }; 10325 10326 this.hourHasEmptySlots = function( hour ) { 10327 const possibleTimes = this.listPossibleHourTimes( hour ); 10328 const ampm = this.getMeridiem(); 10329 const allPossibleTimes = possibleTimes[ ampm ]; 10330 return ! allPossibleTimes.every( ( time ) => { 10331 return times.includes( time ); 10332 }); 10333 }; 10334 10335 this.disableHours = function() { 10336 if ( null === getTimeDropdownsElements( timeField ).h ) { 10337 return; 10338 } 10339 10340 getTimeDropdownsElements( timeField ).h.querySelectorAll( 'option' ).forEach( ( option ) => { 10341 option.disabled = ! self.hourHasEmptySlots( option.value ); 10342 }); 10343 }; 10344 10345 this.disableMinutes = function( hour ) { 10346 if ( null === getTimeDropdownsElements( timeField ).m ) { 10347 return; 10348 } 10349 10350 getTimeDropdownsElements( timeField ).m.querySelectorAll( 'option' ).forEach( ( option ) => { 10351 const timeString = hour + ':' + option.value + ' AM';
10352 option.disabled = times.includes( timeString ); 10353 }); 10354 }; 10355 10356 this.disableAmpm = function() { 10357 if ( null === getTimeDropdownsElements( timeField ).ampm ) { 10358 return; 10359 } 10360 getTimeDropdownsElements( timeField ).ampm.querySelectorAll( 'option' ).forEach( ( option ) => { 10361 if ( times.includes( option.value ) ) { 10362 option.disabled = true; 10363 } 10364 }); 10365 }; 10366 10367 this.init(); 10368 }, 10369 10370 changeRte( editor ) { 10371 editor.on( 'change', () => { 10372 var content = editor.getBody().innerHTML; 10373 jQuery( '#' + editor.id ).val( content ).trigger( 'change' ); 10374 }); 10375 }, 10376 10377 addFormidableClassToDatepicker( _, options ) { 10378 if ( options.dpDiv ) { 10379 Array.prototype.forEach.call( 10380 getAllFormClasses( options.input.get( 0 ) ), 10381 ( formClass ) => { 10382 options.dpDiv.get( 0 ).classList.add( formClass ); 10383 } 10384 ); 10385 10386 options.dpDiv.addClass( 'frm-datepicker' ); 10387 options.dpDiv.get( 0 ).setAttribute( 'is-formidable-datepicker', 1 ); 10388 options.input.get( 0 )?.classList.add( 'active' ); 10389 } 10390 return options; 10391 }, 10392 10393 removeFormidableClassFromDatepicker( _, options ) { 10394 var dpDiv; 10395 if ( options.dpDiv ) { 10396 dpDiv = options.dpDiv.get( 0 ); 10397 dpDiv.removeAttribute( 'is-formidable-datepicker' ); 10398 options.input.get( 0 )?.classList.remove( 'active' ); 10399 10400 // Delay it slightly so it doesn't use styling while it's fading out. 10401 setTimeout( 10402 () => { 10403 if ( dpDiv.hasAttribute( 'is-formidable-datepicker' ) ) { 10404 // Avoid removing it the class if the datepicker was immediately opened again. 10405 return; 10406 } 10407 10408 Array.prototype.forEach.call( 10409 getAllFormClasses( options.input.get( 0 ) ), 10410 function( formClass ) { 10411 options.dpDiv.get( 0 ).classList.remove( formClass ); 10412 } 10413 ); 10414 10415 jQuery( dpDiv ).removeClass( 'frm-datepicker' ); 10416 }, 10417 400 10418 ); 10419 } 10420 }, 10421 10422 isIntlPhoneInput( field ) { 10423 var pattern = field.getAttribute( 'pattern' ); 10424 if ( '\\+?\\d{1,4}[\\s\\-]?(?:\\(\\d{1,3}\\)[\\s\\-]?)?\\d{1,4}[\\s\\-]?\\d{1,4}[\\s\\-]?\\d{1,4}$' !== pattern ) { 10425 return false; 10426 } 10427 return 'undefined' !== typeof intlPhoneInputs && 'undefined' !== typeof intlPhoneInputs[ field.id ]; 10428 }, 10429 10430 validateIntlPhoneInput( field ) { 10431 return intlPhoneInputs[ field.id ].isValidNumber(); 10432 }, 10433 10434 frmDatepicker: frmDatepickerPro, 10435 }; 10436 10437 /** 10438 * Keep label floating if datepicker is open (prevents jumping when clicking date) 10439 * 10440 * @param {Event} event Focusout event 10441 * @return {void} 10442 */ 10443 function keepFloatingLabelOnDateBlur( event ) { 10444 const input = event.target.closest( '.frm_date' ); 10445 if ( input?.classList.contains( 'active' ) ) { 10446 input.closest( '.frm_inside_container' )?.classList.add( 'frm_label_float_top' ); 10447 } 10448 } 10449} 10450var frmProForm = frmProFormJS(); 10451 10452document.addEventListener( 10453 'frmMaybeDelayFocus', 10454 ( event ) => { 10455 if ( 'object' !== typeof event.frmData || 'undefined' === typeof event.frmData.input ) { 10456 return; 10457 } 10458 10459 const input = event.frmData.input; 10460 10461 // Possibly delay focus. 10462 const form = input.closest( 'form' ); 10463 if ( form ) { 10464 const focusHandler = () => { 10465 input.focus(); 10466 form.removeEventListener( 'frmProAfterFormFadeIn', focusHandler ); 10467 }; 10468 form.addEventListener( 'frmProAfterFormFadeIn', focusHandler ); 10469 } 10470 } 10471); 10472 10473jQuery( document ).ready( () => { 10474 frmProForm.init(); 10475}); 10476 10477if ( ! Math.round10 ) { 10478 // https://www.jacklmoore.com/notes/rounding-in-javascript/ 10479 Math.round10 = function( value, decimals ) { 10480 return Number( Math.round( value + 'e' + decimals ) + 'e-' + decimals ); 10481 }; 10482} 10483 10484window.frmUpdateField = function( entryId, fieldId, value, message, num ) { 10485 jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).html( '<span class="frm-loading-img"></span>' ); 10486 jQuery.ajax({ 10487 type: 'POST', 10488 url: frm_js.ajax_url, // eslint-disable-line camelcase 10489 data: { 10490 action: 'frm_entries_update_field_ajax', 10491 entry_id: entryId, 10492 field_id: fieldId, 10493 value, 10494 nonce: frm_js.nonce // eslint-disable-line camelcase 10495 }, 10496 success() { 10497 if ( message.replace( /^\s+|\s+$/g, '' ) === '' ) { 10498 jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).fadeOut( 'slow' ); 10499 } else { 10500 jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).replaceWith( message ); 10501 } 10502 } 10503 }); 10504};
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.