1/* eslint-env jquery */ 2 3var gform = window.gform || {}; 4 5// "prop" method fix for previous versions of jQuery (1.5 and below) 6if( typeof jQuery.fn.prop === 'undefined' ) { 7 jQuery.fn.prop = jQuery.fn.attr; 8} 9 10//Formatting free form currency fields to currency 11jQuery( document ).on( 'gform_post_render', gformBindFormatPricingFields ); 12 13function gformBindFormatPricingFields(){ 14 // Namespace the event and remove before adding to prevent double binding. 15 jQuery(".ginput_amount, .ginput_donation_amount").off('change.gform').on("change.gform", function(){ 16 gformFormatPricingField(this); 17 }); 18 19 jQuery(".ginput_amount, .ginput_donation_amount").each(function(){ 20 gformFormatPricingField(this); 21 }); 22} 23 24//---------------------------------------- 25//------ INSTANCES ----------------------- 26//---------------------------------------- 27 28/** 29 * Namespace to store our JavaScript class instances 30 */ 31 32gform.instances = gform.instances || {}; 33 34//---------------------------------------- 35//------ CONSOLE FUNCTIONS --------------- 36//---------------------------------------- 37 38/** 39 * Console namespace for our safe to use and extendable console functions. 40 */ 41 42gform.console = { 43 error: function( message ) { 44 if( window.console ) { 45 console.error( message ); 46 } 47 }, 48 info: function( message ) { 49 if( window.console ) { 50 console.info( message ); 51 } 52 }, 53 log: function( message ) { 54 if( window.console ) { 55 console.log( message ); 56 } 57 }, 58}; 59 60//---------------------------------------- 61//------ ADMIN UTIL FUNCTIONS ------------ 62//---------------------------------------- 63 64/** 65 * Namespace for our admin utlity functions 66 */ 67 68gform.adminUtils = { 69 70 /** 71 * Handle any unsaved changes to the current settings page. 72 * 73 * @since 2.4 74 * 75 * @param {string} elemId The ID of the current element to check for changes. 76 */ 77 handleUnsavedChanges: function( elemId ) { 78 var hasUnsavedChanges = null; 79 80 jQuery( elemId ).find( 'input, select, textarea' ).on( 'change keyup', function() { 81 82 if ( jQuery( this ).attr( 'onChange' ) === undefined && jQuery( this ).attr( 'onClick' ) === undefined ) { 83 hasUnsavedChanges = true; 84 } 85 86 // Don't trigger unsaved changes on the enable api access button. 87 if ( ( jQuery( this ).next().data("jsButton") || jQuery( this ).data("jsButton") ) === 'enable-api' ) { 88 hasUnsavedChanges = null; 89 } 90 91 } ); 92 93 // Standalone logic for the web api settings page. Trigger unsaved changes if the setting doesn't match the checkbox state. 94 if ( this.getUrlParameter( 'subview' ) === 'gravityformswebapi' ) { 95 if ( gf_webapi_vars.api_enabled !== gf_webapi_vars.enable_api_checkbox_checked ) { 96 hasUnsavedChanges = true; 97 } 98 } 99 100 jQuery( elemId ).on( 'submit', function() { 101 hasUnsavedChanges = null; 102 } ); 103 104 window.onbeforeunload = function() { 105 return hasUnsavedChanges; 106 }; 107 }, 108 109 getUrlParameter: function( param ) { 110 var url = window.location.search.substring( 1 ); 111 var urlVariables = url.split( '&' ); 112 for ( var i = 0; i < urlVariables.length; i++ ) { 113 var parameterName = urlVariables[i].split( '=' ); 114 if ( parameterName[0] == param ) 115 { 116 return parameterName[1]; 117 } 118 } 119 }, 120} 121 122window.HandleUnsavedChanges = gform.adminUtils.handleUnsavedChanges; 123 124//---------------------------------------- 125//------ TOOL FUNCTIONS ------------------ 126//---------------------------------------- 127 128/** 129 * Tool namespace to house our common dom/function tools. 130 */ 131 132gform.tools = { 133 /** 134 * Wrapper to add debouncing to any given callback. 135 * 136 * @since 2.5.2 137 * 138 * @param {Function} fn The callback to execute. 139 * @param {integer} debounceLength The amount of time for which to debounce (in milliseconds) 140 * @param {bool} isImmediate Whether to fire this immediately, or at the tail end of the timeout. 141 * 142 * @returns {function} 143 */ 144 debounce: function( fn, debounceLength, isImmediate ) { 145 // Initialize var to hold our window timeout 146 var timeout; 147 var lastArgs; 148 var lastFn; 149 150 return function() { 151 // Initialize local versions of our context and arguments to pass to apply() 152 var callbackContext = this; 153 var args = arguments; 154 155 // Create a deferred callback to fire if this shouldn't be immediate. 156 var deferredCallback = function() { 157 timeout = null; 158 159 if ( ! isImmediate ) { 160 fn.apply( callbackContext, args ); 161 } 162 }; 163 164 // Begin processing the actual callback. 165 var callNow = isImmediate && ! timeout; 166 167 // Reset timeout if it is the same method with the same args. 168 if ( args === lastArgs && ( ''+lastFn == ''+fn ) ) { 169 clearTimeout( timeout ); 170 } 171 172 // Set the value of the last function call and arguments to help determine whether the next call is unique. 173 var cachePreviousCall = function( fn, args ) { 174 lastFn = fn; 175 lastArgs = args; 176 } 177 178 timeout = setTimeout( deferredCallback, debounceLength ); 179 cachePreviousCall( fn, args ); 180 181 // Method should be executed on the trailing edge of the timeout. Bail for now. 182 if ( ! callNow ) { 183 return; 184 } 185 186 // Callback should be called immediately, and isn't currently debounced; execute it. 187 fn.apply( callbackContext, args ); 188 }; 189 }, 190 191 /** 192 * @function gform.tools.defaultFor
193 * @description Returns a default if first arg is undefined. Once we start migrating to es6 or use babel can 194 * easily swap to default args 195 * 196 * @since 2.5 197 * 198 * @param {*} arg 199 * @param {*} val 200 * @returns {*} 201 */ 202 203 defaultFor: function( arg, val ) { 204 return typeof arg !== 'undefined' ? arg : val; 205 }, 206 207 /** 208 * @function gform.tools.getFocusable 209 * @description Get focusable elements inside a container and return as an array. 210 * 211 * @since 2.5 212 * 213 * @param container the parent to search for focusable elements inside of 214 * @returns {*[]} 215 */ 216 217 getFocusable: function( container ) { 218 container = this.defaultFor( container, document ); 219 var focusable = this.convertElements( 220 container.querySelectorAll( 221 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 222 ) 223 ); 224 return focusable.filter( function( item ) { 225 return this.visible( item ); 226 }.bind( this ) ); 227 }, 228 229 /** 230 * @function gform.tools.htmlToElement 231 * 232 * Allows you to convert an HTML string to a DOM Object. 233 * 234 * @param {string} html 235 * 236 * @returns {ChildNode} 237 */ 238 htmlToElement: function( html ) { 239 var template = document.createElement( 'template' ); 240 html = html.trim(); 241 template.innerHTML = html; 242 243 return template.content.firstChild; 244 }, 245 246 /** 247 * @function gform.tools.elementToHTML 248 * 249 * Converts a DOM Element to an HTML string. 250 * 251 * @param {object} el 252 * 253 * @returns {string} 254 */ 255 elementToHTML: function( el ) { 256 return el.outerHTML; 257 }, 258 259 /** 260 * @function gform.tools.convertElements 261 * @description Efficient function to convert a nodelist into a standard array. 262 * Allows you to run Array.forEach in ie11/saf on result of querySelector functions. 263 * Used by getNodes below. 264 * 265 * @since 2.5 266 * 267 * @param {Element|NodeList} elements Elements to convert 268 * 269 * @returns {Array} Of converted elements 270 */ 271 272 convertElements: function( elements ) { 273 var converted = []; 274 var i = elements.length; 275 for ( i; i--; converted.unshift( elements[ i ] ) ) ; 276 277 return converted; 278 }, 279 280 /** 281 * @function gform.tools.delegate 282 * @description Simple jQuery on replacement. When migrating to ES6 bundle replace with npm delegate. 283 * 284 * @since 2.5 285 * 286 * @param {String} selector 287 * @param {String} event 288 * @param {String} childSelector 289 * @param {Function} handler 290 */ 291 292 delegate: function( selector, event, childSelector, handler ) { 293 var is = function( el, selector ) { 294 return ( el.matches || el.msMatchesSelector ).call( el, selector ); 295 }; 296 297 var elements = document.querySelectorAll( selector ); 298 [].forEach.call( elements, function( el, i ) { 299 el.addEventListener( event, function( e ) { 300 if ( is( e.target, childSelector ) ) { 301 handler( e ); 302 } 303 } ); 304 } ); 305 }, 306 307 /** 308 * @function gform.tools.getClosest 309 * @description Get a parent node based on selector plus passed in child element. 310 * 311 * @since 2.5 312 * 313 * @param {Element|EventTarget} el 314 * @param {String} selector 315 * 316 * @returns {null|*} 317 */ 318 319 getClosest: function( el, selector ) { 320 var matchesFn; 321 var parent; 322 323 [ 'matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector' ] 324 .some( function( fn ) { 325 if ( typeof document.body[ fn ] === 'function' ) { 326 matchesFn = fn; 327 return true; 328 } 329 return false; 330 } ); 331 332 while ( el ) { 333 parent = el.parentElement; 334 if ( parent && parent[ matchesFn ]( selector ) ) { 335 return parent; 336 } 337 338 el = parent; 339 } 340 341 return null; 342 }, 343 344 /** 345 * @function gform.tools.getNodes 346 * @description Used for getting nodes. Please use the data-js attribute whenever possible. 347 * 348 * @since 2.5 349 * 350 * @param {String} selector The selector string to search for. If arg 4 is false (default) then we search for [data-js="selector"] 351 * @param {Boolean} [convert] Convert the NodeList to an array? Then we can Array.forEach directly. Uses convertElements from above. 352 * @param {Element|EventTarget|Document} [node] Parent node to search from. Defaults to document. 353 * @param {Boolean} [custom] Is this a custom selector were we don't want to use the data-js attribute? 354 * 355 * @returns {NodeList|Array} 356 */ 357 358 getNodes: function( selector, convert, node, custom ) { 359 if ( ! selector ) { 360 gform.console.error( 'Please pass a selector to gform.tools.getNodes' ); 361 return []; 362 } 363 node = this.defaultFor( node, document );
364 var selectorString = custom ? selector : '[data-js="' + selector + '"]'; 365 var nodes = node.querySelectorAll( selectorString ); 366 if ( convert ) { 367 nodes = this.convertElements( nodes ); 368 } 369 return nodes; 370 }, 371 372 /** 373 * @function gform.tools.mergeObjects 374 * @description ES5 Object.assign. Usage: gforms.tools.mergeObjects( obj1, obj2, obj3 ); 375 * 376 * @since 2.5 377 * 378 * @returns {{}} 379 */ 380 381 mergeObjects: function() { 382 var resObj = {}; 383 for ( var i = 0; i < arguments.length; i += 1 ) { 384 var obj = arguments[ i ] 385 var keys = Object.keys( obj ); 386 for ( var j = 0; j < keys.length; j += 1 ) { 387 resObj[ keys[ j ] ] = obj[ keys[ j ] ]; 388 } 389 } 390 return resObj; 391 }, 392 393 /** 394 * @function gform.tools.setAttr 395 * @description Sets attributes for a group of nodes based on a passed selector. 396 * Can apply to document or subset, and has optional delay. 397 * 398 * @since 2.5 399 * 400 * @param {String} selector A selector string, and valid js selector string for a dom element. 401 * @param {String} attr The attribute name. 402 * @param {String} value The attribute value. 403 * @param {Element|EventTarget|Document} [container] Node to search from, default is document. 404 * @param {Number} [delay] The delay to apply. 405 */ 406 407 setAttr: function( selector, attr, value, container, delay ) { 408 if ( ! selector || ! attr || ! value ) { 409 gform.console.error( 'Please pass a selector, attribute and value to gform.tools.setAttr' ); 410 return []; 411 } 412 container = this.defaultFor( container, document ); 413 delay = this.defaultFor( delay, 0 ); 414 415 setTimeout( function() { 416 gform.tools.getNodes( selector, true, container, true ) 417 .forEach( function( node ) { 418 node.setAttribute( attr, value ); 419 } ); 420 }, delay ); 421 }, 422 423 /** 424 * @function gform.tools.isRtl 425 * @description Determine if the page is in RTL. 426 * 427 * @since 2.5 428 * 429 */ 430 431 isRtl: function() { 432 if ( jQuery( 'html' ).attr( 'dir' ) === 'rtl' ) { 433 return true; 434 } 435 }, 436 437 /** 438 * @function gform.tools.trigger 439 * @description Trigger custom or native events on any element in a cross browser way, and pass along optional data. 440 * 441 * @since 2.5.1.1 442 * 443 * @param {String} eventName The event name. 444 * @param {Element|EventTarget|Document} el Default document. The element to trigger the event on. 445 * @param {Boolean} native Default fasle. Is this a custom event or native? 446 * @param {Object} data Custom data to send along, available in event.detail on listener. 447 */ 448 449 trigger: function( eventName, el, native, data ) { 450 var event; 451 eventName = this.defaultFor( eventName, '' ); 452 el = this.defaultFor( el, document ); 453 native = this.defaultFor( native, false ); 454 data = this.defaultFor( data, {} ); 455 if ( native ) { 456 event = document.createEvent( 'HTMLEvents' ); 457 event.initEvent( eventName, true, false ); 458 } else { 459 try { 460 event = new CustomEvent( eventName, { detail: data } ); 461 } catch ( e ) { 462 event = document.createEvent( 'CustomEvent' ); 463 event.initCustomEvent( eventName, true, true, data ); 464 } 465 } 466 467 el.dispatchEvent( event ); 468 }, 469 470 /** 471 * @function gform.tools.uniqueId 472 * @description Generate a unique id 473 * 474 * @since 2.5.5.2 475 * 476 * @param {String} prefix 477 * @returns {string} 478 */ 479 480 uniqueId: function( prefix ) { 481 prefix = this.defaultFor( prefix, 'id' ); 482 return prefix + '-' + Math.random().toString( 36 ).substr( 2, 9 ); 483 }, 484 485 /** 486 * @function gform.tools.visible 487 * @description Determine if an element is visible in the dom. 488 * 489 * @since 2.5 490 * 491 * @param elem The element to check 492 * @returns {boolean} 493 */ 494 495 visible: function( elem ) { 496 return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); 497 }, 498 499 stripSlashes: function( str ) { 500 return (str + '').replace(/\\(.?)/g, function (s, n1) { 501 switch (n1) { 502 case '\\': 503 return '\\'; 504 case '0': 505 return '\u0000'; 506 case '': 507 return ''; 508 default: 509 return n1; 510 } 511 }); 512 }, 513 514 /** 515 * @function gform.tools.getCookie 516 * @description Gets a specific cookie. 517 * 518 * @since 2.5.8 519 * 520 * @param name The cookie to get 521 * @returns {boolean|string} 522 */ 523 524 getCookie: function( name ) { 525 var cookieArr = document.cookie.split( ";" ); 526 527 for(var i = 0; i < cookieArr.length; i++) { 528 var cookiePair = cookieArr[i].split( "=" ); 529 530 if( name == cookiePair[0].trim() ) { 531 return decodeURIComponent( cookiePair[1] ); 532 } 533 } 534 535 return null; 536 }, 537 538 /**
539 * @function gform.tools.setCookie 540 * @description Creates and sets a cookie. 541 * 542 * @since 2.5.8 543 * 544 * @param name The cookie name 545 * @param value The cookie value 546 * @param daysToExpire The number of days until cookie should expire. If not set, 547 * will expire at the end of the user sessions. 548 * @param updateExistingValue Whether or not to update the existing cookie value to include the new value. 549 * Can be helpful for keeping cookie count lower for the browser. 550 */ 551 552 setCookie: function( name, value, daysToExpire, updateExistingValue ) { 553 var expirationDate = ''; 554 var cookieValue = value; 555 556 if ( daysToExpire ) { 557 var date = new Date(); 558 date.setTime( date.getTime() + ( daysToExpire * 24 * 60 * 60 * 1000 ) ); 559 expirationDate = ' expires=' + date.toUTCString(); 560 } 561 562 if ( updateExistingValue ) { 563 var currentValue = gform.tools.getCookie( name ); 564 cookieValue = currentValue !== '' && currentValue !== null ? currentValue + ',' + value : value; 565 } 566 567 // Set cookie 568 document.cookie = encodeURIComponent( name ) + '=' + encodeURIComponent( cookieValue ) + ';' + expirationDate; 569 }, 570 571 /** 572 * @function gform.tools.removeCookie 573 * @description Removes a cookie. 574 * 575 * @since 2.5.8 576 * 577 * @param name The cookie name to check 578 */ 579 580 removeCookie: function( name ) { 581 gform.tools.setCookie( name, '', -1 ); 582 } 583}; 584 585//------------------------------------------------ 586//---------- A11Y FUNCTIONS ---------------------- 587//------------------------------------------------ 588 589/** 590 * A11y namespace to house our accessibility functions. 591 */ 592 593gform.a11y = {}; 594 595//------------------------------------------------ 596//---------- OPTIONS ----------------------------- 597//------------------------------------------------ 598 599/** 600 * Options namespace to house common plugin and custom options objects for reuse across our JavaScript. 601 */ 602 603gform.options = { 604 605 /** 606 * Accordions in the editor sidebar use these options. Should be applied to any accordions that want to emulate 607 * that look and feel, and patches an a11y issue with jq accordion and our custom usage. 608 */ 609 610 jqEditorAccordions: { 611 header: 'button.panel-block-tabs__toggle', 612 heightStyle: 'content', 613 collapsible: true, 614 animate: false, 615 create: function( event ) { 616 gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 ); 617 }, 618 activate: function( event ) { 619 gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 ); 620 }, 621 beforeActivate: function( event ) { 622 // handle advanced tab operations as needed before the tab is revealed in a fields settings 623 if ( event.currentTarget.id === 'advanced_tab_toggle' ) { 624 // handle address field 625 if ( window.field && window.field.type && window.field.type === 'address' ) { 626 // regen the Autocomplete UI on every tab open to handle changes to input visibility from interactions 627 CreateAutocompleteUI( window.field ); 628 } 629 } 630 } 631 }, 632 633 jqAddFieldAccordions: { 634 heightStyle: 'content', 635 collapsible: true, 636 animate: false, 637 create: function( event ) { 638 gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 ); 639 }, 640 activate: function( event ) { 641 gform.tools.setAttr( '.ui-accordion-header', 'tabindex', '0', event.target, 100 ); 642 }, 643 }, 644}; 645 646//------------------------------------------------ 647//---------- CURRENCY ---------------------------- 648//------------------------------------------------ 649 650function Currency(currency){ 651 console.warn( 'Currency has been deprecated since Gravity Forms 2.9. Use gform.Currency instead.' ); 652 return new gform.Currency( currency ); 653} 654 655/** 656 * Gets a formatted number and returns a clean "decimal dot" number. 657 * 658 * Note: Input must be formatted according to the specified parameters (symbol_right, symbol_left, decimal_separator). 659 * @example input -> $1.20, output -> 1.2 660 * 661 * @since 2.1.1.16 Modified to support additional param in Currency.toMoney. 662 * 663 * @param text string The currency-formatted number.
664 * @param symbol_right string The symbol used on the right. 665 * @param symbol_left string The symbol used on the left. 666 * @param decimal_separator string The decimal separator being used. 667 * 668 * @return float The unformatted numerical value. 669 */ 670function gformCleanNumber(text, symbol_right, symbol_left, decimal_separator){ 671 console.warn( 'gformCleanNumber() has been deprecated since Gravity Forms 2.9. Use gform.Currency.cleanNumber() instead.' ); 672 return gform.Currency.cleanNumber( text, symbol_right, symbol_left, decimal_separator ); 673} 674 675function gformGetDecimalSeparator(numberFormat){ 676 console.warn( 'gformGetDecimalSeparator() has been deprecated since Gravity Forms 2.9. Use gform.Currency.getDecimalSeparator() instead.' ); 677 return gform.Currency.getDecimalSeparator( numberFormat ); 678} 679 680function gformIsNumber(n) { 681 console.warn( 'gformIsNumber() has been deprecated since Gravity Forms 2.9. Use gform.utils.isNumber() instead.' ); 682 return gform.utils.isNumber( n ); 683} 684 685function gformIsNumeric(value, number_format){ 686 687 switch(number_format){ 688 case "decimal_dot" : 689 var r = new RegExp("^(-?[0-9]{1,3}(?:,?[0-9]{3})*(?:\.[0-9]+)?)$"); 690 return r.test(value); 691 break; 692 693 case "decimal_comma" : 694 var r = new RegExp("^(-?[0-9]{1,3}(?:\.?[0-9]{3})*(?:,[0-9]+)?)$"); 695 return r.test(value); 696 break; 697 } 698 return false; 699} 700 701//------------------------------------------------ 702//---------- MULTI-PAGE -------------------------- 703//------------------------------------------------ 704function gformDeleteUploadedFile(formId, fieldId, deleteButton){ 705 var parent = jQuery("#field_" + formId + "_" + fieldId); 706 707 var fileIndex = jQuery(deleteButton).parent().index(); 708 709 var filePreview = jQuery( deleteButton ).closest( '.ginput_preview' )[0]; 710 var fileId = filePreview.id; 711 filePreview.remove(); 712 713 //removing the gform_hidden class 714 parent.find('.validation_message,#extensions_message_' + formId + '_' + fieldId).removeClass("gform_hidden"); 715 716 //displaying post image label 717 parent.find(".ginput_post_image_file").show(); 718 719 //clearing post image meta fields 720 parent.find("input[type=\"text\"]").val(''); 721 722 //removing file from uploaded meta 723 var filesJson = jQuery('#gform_uploaded_files_' + formId).val(); 724 725 if(filesJson){ 726 var files = jQuery.secureEvalJSON(filesJson); 727 if(files) { 728 var inputName = "input_" + fieldId; 729 var $multfile = parent.find("#gform_multifile_upload_" + formId + "_" + fieldId ); 730 if( $multfile.length > 0 ) { 731 files[inputName].splice(fileIndex, 1); 732 var settings = $multfile.data('settings'); 733 var count = files[ inputName ].length; 734 if ( count === 0 ) { 735 jQuery( '#' + settings.gf_vars.message_id ).html(''); 736 gfMultiFileUploader.toggleDisabled( settings, false ); 737 } else { 738 jQuery( '#error_' + fileId ).remove(); // Removing the file-specific validation message. 739 var max = settings.gf_vars.max_files; 740 if ( count < max ) 741 gfMultiFileUploader.toggleDisabled( settings, false ); 742 } 743 744 } else { 745 files[inputName] = null; 746 } 747 748 jQuery('#gform_uploaded_files_' + formId).val(jQuery.toJSON(files)); 749 } 750 } 751} 752 753 754//------------------------------------------------ 755//---------- PRICE ------------------------------- 756//------------------------------------------------ 757var _gformPriceFields = new Array(); 758var _anyProductSelected; 759 760function gformIsHidden(element){ 761 isHidden = element.parents('.gfield').not(".gfield_hidden_product").css("display") == "none"; 762 763 /** 764 * Allows user to filter the logic for determining if a field is hidden by conditional logic.. 765 * 766 * @since 2.8.10 767 * 768 * @param bool Whether or not the field is hidden. 769 * @param object $element jQuery object for field input. 770 */ 771 return gform.applyFilters('gform_is_hidden', isHidden, element); 772 773} 774 775/** 776 * Calculate total price when input is updated. 777 * 778 * @since 2.5.2 - This method is run through debounce() to avoid recursions. 779 * 780 */ 781var gformCalculateTotalPrice = gform.tools.debounce(function(formId){ 782 if(!_gformPriceFields[formId]) { 783 return; 784 } 785 var price = 0; 786 787 _anyProductSelected = false; //Will be used by gformCalculateProductPrice(). 788 for(var i=0; i<_gformPriceFields[formId].length; i++){ 789 price += gformCalculateProductPrice(formId, _gformPriceFields[formId][i]); 790 } 791 792 //add shipping price if a product has been selected 793 if(_anyProductSelected){ 794 //shipping price 795 var shipping = gformGetShippingPrice(formId) 796 price += shipping; 797 } 798 799 //gform_product_total filter. Allows users to perform custom price calculation 800 if(window["gform_product_total"]) 801 price = window["gform_product_total"](formId, price); 802 803 price = gform.applyFilters('gform_product_total', price, formId); 804 805 gformUpdateTotalFieldPrice( formId, price ); 806}, 50, false ); 807 808/** 809 * Updates the value of the total field with a new price if it has changed. 810 * 811 * @since 2.5.5 812 * 813 * @param {string|number} formId The ID of the form with the total field. 814 * @param {int} price The new price to apply. 815 * 816 * @return {void} 817 */ 818function gformUpdateTotalFieldPrice( formId, price ) { 819 var $totalElement = jQuery( '.ginput_total_' + formId ); 820 if ( ! $totalElement.length > 0 ) { 821 return; 822 } 823 824 /** 825 * @function priceHasChanged 826 * @description For legacy, compare numeric values, otherwise compare currency as that's what 827 * the input stores as value. 828 * 829 * @param {Object} priceData 830 * @returns {boolean} 831 */ 832 var priceHasChanged = function( priceData ) { 833 return isLegacy 834 ? priceData.current !== priceData.new 835 : priceData.current !== priceData.newFormatted; 836 } 837 838 // Check whether this form is in legacy mode. 839 var isLegacy = document.querySelector( '#gform_wrapper_' + formId + '.gform_legacy_markup_wrapper' ); 840 // Input is hidden in legacy mode and comes after span that displays value, currently only the input is present and visible. 841 var $totalInput = isLegacy ? $totalElement.next() : $totalElement; 842 // Contains current value (numeric or currency formatted), new numeric value and newFormatted value 843 var priceData = { 844 current: String( $totalInput.val() ), 845 new: String( price ), 846 newFormatted: gformFormatMoney( String( price ), true ), 847 } 848 849 // New value is the same as the current value, bail before updating. 850 if ( ! priceHasChanged( priceData ) ) { 851 return; 852 } 853 854 // Legacy field 855 if ( isLegacy ) { 856 // Set input value to numeric value and trigger a change event for any js listeners in conditional logic 857 // or third party integrations. 858 $totalInput.val( priceData.new ).trigger( 'change' ); 859 // Inject span with currency value for display. 860 $totalElement.html( priceData.newFormatted ); 861 return; 862 } 863 864 // First set the input to the numeric value and trigger the change event so that js listeners get the value in expected format. 865 $totalInput.val( priceData.new ).trigger( 'change' ); 866 // Then set the input to the currency value for display. If you have a script that wants to get the value 867 // of this input without listening to the change event you will have to also handle removing the currency formatting 868 // if expecting number in your code. 869 $totalInput.val( priceData.newFormatted ); 870} 871 872function gformGetShippingPrice(formId){ 873 var shippingField = jQuery(".gfield_shipping_" + formId + " input[readonly], .gfield_shipping_" + formId + " select, .gfield_shipping_" + formId + " input:checked"); 874 var shipping = 0; 875 if(shippingField.length == 1 && !gformIsHidden(shippingField)){ 876 if(shippingField.attr("readonly")) 877 shipping = shippingField.val(); 878 else 879 shipping = gformGetPrice(shippingField.val()); 880 } 881 882 return gformToNumber(shipping); 883} 884 885function gformGetFieldId(element){ 886 var id = jQuery(element).attr("id"); 887 var pieces = id.split("_"); 888 if(pieces.length <=0) 889 return 0; 890 891 var fieldId = pieces[pieces.length-1]; 892 return fieldId; 893 894} 895 896function gformCalculateProductPrice(form_id, productFieldId){ 897 898 var suffix = '_' + form_id + '_' + productFieldId; 899 900 901 //Drop down auto-calculating labels 902 jQuery('.gfield_option' + suffix + ', .gfield_shipping_' + form_id).find('select').each(function(){ 903 904 var dropdown_field = jQuery(this); 905 var selected_price = gformGetPrice(dropdown_field.val()); 906 var field_id = dropdown_field.attr('id').split('_')[2]; 907 dropdown_field.children('option').each(function(){ 908 var choice_element = jQuery(this);
909 var label = gformGetOptionLabel(choice_element, choice_element.val(), selected_price, form_id, field_id); 910 choice_element.html(label); 911 }); 912 }); 913 914 915 //Checkboxes labels with prices 916 jQuery('.gfield_option' + suffix).find('.gfield_checkbox').find('input:checkbox').each(function(){ 917 var checkbox_item = jQuery(this); 918 var id = checkbox_item.attr('id'); 919 var field_id = id.split('_')[2]; 920 var label_id = id.replace('choice_', '#label_'); 921 var label_element = jQuery(label_id); 922 var label = gformGetOptionLabel(label_element, checkbox_item.val(), 0, form_id, field_id); 923 label_element.html(label); 924 }); 925 926 927 //Radio button auto-calculating lables 928 jQuery('.gfield_option' + suffix + ', .gfield_shipping_' + form_id).find('.gfield_radio').each(function(){ 929 var selected_price = 0; 930 var radio_field = jQuery(this); 931 var id = radio_field.attr('id'); 932 var fieldId = id.split('_')[2]; 933 var selected_value = radio_field.find('input:radio:checked').val(); 934 935 if(selected_value) 936 selected_price = gformGetPrice(selected_value); 937 938 radio_field.find('input:radio').each(function(){ 939 var radio_item = jQuery(this); 940 var label_id = radio_item.attr('id').replace('choice_', '#label_'); 941 var label_element = jQuery(label_id); 942 if ( label_element ) { 943 var label = gformGetOptionLabel(label_element, radio_item.val(), selected_price, form_id, fieldId); 944 label_element.html(label); 945 } 946 }); 947 }); 948 949 var price = gformGetBasePrice(form_id, productFieldId); 950 var quantity = gformGetProductQuantity( form_id, productFieldId ); 951 952 //calculating options if quantity is more than 0 (a product was selected). 953 if( quantity > 0 ) { 954 955 jQuery('.gfield_option' + suffix).find('input:checked, select').each(function(){ 956 if(!gformIsHidden(jQuery(this))) 957 price += gformGetPrice(jQuery(this).val()); 958 }); 959 960 //setting global variable if quantity is more than 0 (a product was selected). Will be used when calculating total 961 _anyProductSelected = true; 962 } 963 964 price = price * quantity; 965 966 price = gformRoundPrice(price) ; 967 968 969 return price; 970} 971 972 973function gformGetProductQuantity(formId, productFieldId) { 974 //If product is not selected 975 if (!gformIsProductSelected(formId, productFieldId)) { 976 return 0; 977 } 978 979 var quantity, 980 quantityInput = jQuery( '#ginput_quantity_' + formId + '_' + productFieldId ), 981 numberFormat; 982 983 // New input ID starts from 2.5, for the single product and calculation fields. 984 if ( ! quantityInput.length ) { 985 quantityInput = jQuery( '#input_' + formId + '_' + productFieldId + '_1' ); 986 } 987 988 if (gformIsHidden(quantityInput)) { 989 return 0; 990 } 991 992 if (quantityInput.length > 0) { 993 994 quantity = quantityInput.val(); 995 996 } else { 997 998 quantityInput = jQuery('.gfield_quantity_' + formId + '_' + productFieldId + ' :input'); 999 quantity = 1; 1000 1001 if (quantityInput.length > 0) { 1002 quantity = quantityInput.val(); 1003 1004 var htmlId = quantityInput.attr('id'), 1005 fieldId = gf_get_input_id_by_html_id(htmlId); 1006 1007 numberFormat = gf_get_field_number_format( fieldId, formId, 'value' ); 1008 } 1009 1010 } 1011 1012 if (!numberFormat) 1013 numberFormat = 'currency'; 1014 1015 var decimalSeparator = gform.Currency.getDecimalSeparator(numberFormat); 1016 1017 quantity = gform.Currency.cleanNumber(quantity, '', '', decimalSeparator); 1018 if (!quantity) 1019 quantity = 0; 1020 1021 return quantity; 1022} 1023 1024 1025function gformIsProductSelected( formId, productFieldId ) { 1026 1027 var suffix = "_" + formId + "_" + productFieldId; 1028 1029 var productField = jQuery("#ginput_base_price" + suffix + ", .gfield_donation" + suffix + " input[type=\"text\"], .gfield_product" + suffix + " .ginput_amount"); 1030 if( productField.val() && ! gformIsHidden(productField) ){ 1031 return true; 1032 } 1033 else 1034 { 1035 productField = jQuery(".gfield_product" + suffix + " select, .gfield_product" + suffix + " input:checked, .gfield_donation" + suffix + " select, .gfield_donation" + suffix + " input:checked"); 1036 if( productField.val() && ! gformIsHidden(productField) ){ 1037 return true; 1038 } 1039 } 1040 return false;
1041} 1042 1043function gformGetBasePrice(formId, productFieldId){ 1044 1045 var suffix = "_" + formId + "_" + productFieldId; 1046 var price = 0; 1047 var productField = jQuery("#ginput_base_price" + suffix+ ", .gfield_donation" + suffix + " input[type=\"text\"], .gfield_product" + suffix + " .ginput_amount"); 1048 if(productField.length > 0){ 1049 price = productField.val(); 1050 1051 //If field is hidden by conditional logic, don't count it for the total 1052 if(gformIsHidden(productField)){ 1053 price = 0; 1054 } 1055 } 1056 else 1057 { 1058 productField = jQuery(".gfield_product" + suffix + " select, .gfield_product" + suffix + " input:checked, .gfield_donation" + suffix + " select, .gfield_donation" + suffix + " input:checked"); 1059 var val = productField.val(); 1060 if(val){ 1061 const value = gformParseChoiceValue( val ); 1062 val = value.name; 1063 price = value.price || 0; 1064 } 1065 1066 //If field is hidden by conditional logic, don't count it for the total 1067 if(gformIsHidden(productField)) 1068 price = 0; 1069 1070 } 1071 1072 var c = new gform.Currency(gf_global.gf_currency_config); 1073 price = c.toNumber(price); 1074 return price === false ? 0 : price; 1075} 1076 1077/** 1078 * @function gformParseChoiceValue 1079 * @description Parse a choice value into its name and price components. 1080 * 1081 * @since 2.9.30 1082 * 1083 * @param {string} value The choice value string in the format "name|price". 1084 * 1085 * @return {object} Returns an object in the format: { price: PRODUCT_PRICE, name: PRODUCT_NAME } 1086 */ 1087function gformParseChoiceValue( value ) { 1088 if ( window.gform?.products?.parser?.parseChoiceValue ) { 1089 return window.gform.products.parser.parseChoiceValue( value ); 1090 } 1091 1092 if ( ! value ) { 1093 return { name: null, price: null }; 1094 } 1095 1096 const idx = value.lastIndexOf( '|' ); 1097 if ( idx === -1 ) { 1098 return { name: value, price: null }; 1099 } 1100 1101 const name = value.slice( 0, idx ); 1102 const price = gformToNumber( value.slice( idx + 1 ) ); 1103 1104 return { name, price }; 1105} 1106 1107function gformFormatMoney(text, isNumeric){ 1108 if(!gf_global.gf_currency_config) 1109 return text; 1110 1111 var currency = new gform.Currency(gf_global.gf_currency_config); 1112 return currency.toMoney(text, isNumeric); 1113} 1114 1115function gformFormatPricingField(element){ 1116 if(gf_global.gf_currency_config){ 1117 var currency = new gform.Currency(gf_global.gf_currency_config); 1118 var price = currency.toMoney(jQuery(element).val()); 1119 jQuery(element).val(price); 1120 } 1121} 1122 1123function gformToNumber(text){ 1124 var currency = new gform.Currency(gf_global.gf_currency_config); 1125 return currency.toNumber(text); 1126} 1127 1128function gformGetPriceDifference(currentPrice, newPrice){ 1129 1130 //getting price difference 1131 var diff = parseFloat(newPrice) - parseFloat(currentPrice); 1132 price = gformFormatMoney(diff, true); 1133 if(diff > 0) 1134 price = "+" + price; 1135 1136 return price; 1137} 1138 1139function gformGetOptionLabel(element, selected_value, current_price, form_id, field_id){ 1140 element = jQuery(element); 1141 var price = gformGetPrice(selected_value); 1142 var current_diff = element.attr('price'); 1143 var original_label = element.html().replace(/<span(.*)<\/span>/i, "").replace(current_diff, ""); 1144 1145 var diff = gformGetPriceDifference(current_price, price); 1146 diff = gformToNumber(diff) == 0 ? "" : " " + diff; 1147 element.attr('price', diff); 1148 1149 //don't add <span> for drop down items (not supported) 1150 var price_label = element[0].tagName.toLowerCase() == "option" ? diff : "<span class='ginput_price'>" + diff + "</span>"; 1151 var label = original_label + price_label; 1152 1153 //calling hook to allow for custom option formatting 1154 if(window["gform_format_option_label"]) 1155 label = gform_format_option_label(label, original_label, price_label, current_price, price, form_id, field_id); 1156 1157 return label; 1158} 1159 1160function gformGetProductIds(parent_class, element){ 1161 var classes = jQuery(element).hasClass(parent_class) ? jQuery(element).attr("class").split(" ") : jQuery(element).parents("." + parent_class).attr("class").split(" "); 1162 for(var i=0; i<classes.length; i++){ 1163 if(classes[i].substr(0, parent_class.length) == parent_class && classes[i] != parent_class) 1164 return {formId: classes[i].split("_")[2], productFieldId: classes[i].split("_")[3]}; 1165 } 1166 return {formId:0, fieldId:0}; 1167} 1168 1169function gformGetPrice(text){ 1170 var val = gformParseChoiceValue( text ); 1171 1172 if(val.price) 1173 return val.price; 1174 1175 return 0; 1176} 1177 1178function gformRoundPrice(price){ 1179 1180 var currency = new gform.Currency(gf_global.gf_currency_config); 1181 var roundedPrice = currency.numberFormat( price, currency.currency['decimals'], '.', '' ); 1182 1183 return parseFloat( roundedPrice ); 1184} 1185 1186function gformRegisterPriceField(item){ 1187 1188 if( ! item.formId ) { 1189 return; 1190 } 1191 1192 if(!_gformPriceFields[item.formId]) { 1193 _gformPriceFields[item.formId] = new Array(); 1194 } 1195 1196 //ignore price fields that have already been registered 1197 for(var i=0; i<_gformPriceFields[item.formId].length; i++) 1198 if(_gformPriceFields[item.formId][i] == item.productFieldId) 1199 return; 1200 1201 //registering new price field 1202 _gformPriceFields[item.formId].push(item.productFieldId); 1203} 1204 1205function gformInitPriceFields(){ 1206 1207 // Getting all product fields and registering them. 1208 const priceFields = gform.tools.getNodes('.gfield_price', true, document, true ); 1209 priceFields.forEach( ( field ) => { 1210 const productIds = gformGetProductIds( 'gfield_price', field ); 1211 gformRegisterPriceField( productIds ); 1212 }); 1213 1214 1215 // Getting all forms that have product fields. 1216 const formIds = Object.keys( _gformPriceFields ); 1217 formIds.forEach( ( formId ) => { 1218 1219 gformCalculateTotalPrice( formId ); 1220 1221 gform.state.watch( formId, ['products', 'feeds'], gformHandleProductChange ); 1222 bindProductChangeEvent(); 1223 } ); 1224} 1225 1226function bindProductChangeEvent() { 1227 // For backwards compatibility, fire jQuery gform_price_change event. 1228 document.addEventListener( 'gform/products/product_field_changed', function( event ) { 1229 const productIds = { formId : event.detail.formId, productFieldId : event.detail.productFieldId } 1230 1231 jQuery( document ).trigger( 'gform_price_change', [ productIds, event.detail.htmlInput, this ] ); 1232 } ); 1233} 1234 1235 1236function gformHandleProductChange( formId, key, data ) { 1237 gformCalculateTotalPrice( formId ); 1238} 1239 1240//------------------------------------------- 1241//---------- PASSWORD ----------------------- 1242//------------------------------------------- 1243function gformShowPasswordStrength(fieldId){ 1244 var password = document.getElementById( fieldId ).value, 1245 confirm = document.getElementById( fieldId + '_2' ) ? document.getElementById( fieldId + '_2' ).value : ''; 1246 1247 var result = gformPasswordStrength( password, confirm ), 1248 text = window[ 'gf_text' ][ "password_" + result ], 1249 resultClass = result === 'unknown' ? 'blank' : result; 1250 1251 jQuery("#" + fieldId + "_strength").val(result);
1252 jQuery("#" + fieldId + "_strength_indicator").removeClass("blank mismatch short good bad strong").addClass(resultClass).html(text); 1253} 1254 1255// Password strength meter 1256function gformPasswordStrength( password1, password2 ) { 1257 1258 if ( password1.length <= 0 ) { 1259 return 'blank'; 1260 } 1261 1262 var disallowedList = wp.passwordStrength.hasOwnProperty( 'userInputDisallowedList' ) ? wp.passwordStrength.userInputDisallowedList() : wp.passwordStrength.userInputBlacklist(), 1263 strength = wp.passwordStrength.meter( password1, disallowedList, password2 ); 1264 1265 switch ( strength ) { 1266 1267 case -1: 1268 return 'unknown'; 1269 1270 case 2: 1271 return 'bad'; 1272 1273 case 3: 1274 return 'good'; 1275 1276 case 4: 1277 return 'strong'; 1278 1279 case 5: 1280 return 'mismatch'; 1281 1282 default: 1283 return 'short'; 1284 1285 } 1286 1287} 1288 1289function gformToggleShowPassword( fieldId ) { 1290 var $password = jQuery( '#' + fieldId ), 1291 $button = $password.parent().find( 'button' ), 1292 $icon = $button.find( 'span' ), 1293 currentType = $password.attr( 'type' ); 1294 1295 switch ( currentType ) { 1296 case 'password': 1297 $password.attr( 'type', 'text' ); 1298 $button.attr( 'aria-label', $button.attr( 'data-label-hide' ) ); 1299 $icon.removeClass( 'dashicons-hidden' ).addClass( 'dashicons-visibility' ); 1300 break; 1301 case 'text': 1302 $password.attr( 'type', 'password' ); 1303 $button.attr( 'aria-label', $button.attr( 'data-label-show' ) ); 1304 $icon.removeClass( 'dashicons-visibility' ).addClass( 'dashicons-hidden' ); 1305 break; 1306 } 1307} 1308 1309//---------------------------- 1310//------ CHECKBOX FIELD ------ 1311//---------------------------- 1312 1313function gformToggleCheckboxes( toggleElement ) { 1314 1315 var checked, 1316 $toggleElement = jQuery( toggleElement ), 1317 toggleElementCheckbox = $toggleElement.is( 'input[type="checkbox"]' ), 1318 $toggle = $toggleElement.parent(), 1319 $toggleLabel = $toggle.find( 'label' ), 1320 $checkboxes = $toggle.parent().find( '.gchoice:not( .gchoice_select_all )' ), 1321 formId = gf_get_form_id_by_html_id( $toggle.parents( '.gfield' ).attr( 'id' ) ), 1322 calcObj = rgars( window, 'gf_global/gfcalc/' + formId ); 1323 1324 // Determine checked state. 1325 if ( toggleElementCheckbox ) { 1326 1327 checked = toggleElement.checked; 1328 1329 } else { 1330 1331 // Get checked data. 1332 var checkedData = $toggleElement.data( 'checked' ); 1333 1334 if ( typeof checkedData === 'boolean' ) { 1335 checked = !checkedData; 1336 } else { 1337 checked = !( parseInt( checkedData ) === 1 ) 1338 } 1339 1340 } 1341 1342 // Set checkboxes state. 1343 $checkboxes.each( function() { 1344 1345 // Set checkbox checked state. 1346 jQuery( 'input[type="checkbox"]', this ).prop( 'checked', checked ).trigger( 'change' ); 1347 1348 // Execute onclick event. 1349 if ( typeof jQuery( 'input[type="checkbox"]', this )[0].onclick === 'function' ) { 1350 jQuery( 'input[type="checkbox"]', this )[0].onclick(); 1351 } 1352 1353 } ); 1354 1355 // Change toggle label, checked state. 1356 gformToggleSelectAll( toggleElement, checked ? 'deselect' : 'select' ); 1357 1358 // Announce change. 1359 wp.a11y.speak( checked ? gf_field_checkbox.strings.selected : gf_field_checkbox.strings.deselected ); 1360 1361 if ( calcObj ) { 1362 calcObj.runCalcs( formId, calcObj.formulaFields ); 1363 } 1364 1365} 1366 1367function gformToggleSelectAll( selectAllElement, action ) { 1368 var $selectAllElement = jQuery( selectAllElement ), 1369 toggleElementCheckbox = $selectAllElement.is( 'input[type="checkbox"]' ), 1370 $toggle = toggleElementCheckbox ? $selectAllElement.parent() : $selectAllElement.prev(), 1371 $toggleLabel = $toggle.find( 'label' ); 1372 1373 if ( ! toggleElementCheckbox ) { 1374 $selectAllElement.html( action === 'deselect' ? $selectAllElement.data( 'label-deselect' ) : $selectAllElement.data( 'label-select' ) ); 1375 $selectAllElement.data( 'checked', action === 'deselect' ? 1 : 0 ); 1376 } 1377} 1378 1379jQuery(document).on('click', '.gfield_choice--select_all_enabled *', function() { 1380 var $select_all = jQuery( this ).closest( '.gfield_choice--select_all_enabled' ).find( '.gfield_choice_all_toggle' ); 1381 1382 // if any of the checkboxes are unchecked, turn the "deselect all" button/checkbox into a "select all" button/checkbox 1383 if ( jQuery( this ).is( '.gchoice input[type="checkbox"]' ) ) { 1384 if( $select_all.is( 'input[type="checkbox"]' ) ) { 1385 if ( !jQuery( this ).prop( 'checked' ) ) { 1386 $select_all.prop( 'checked', false ); 1387 } 1388 } else { 1389 gformToggleSelectAll( $select_all, 'select' ); 1390 } 1391 } 1392 1393 // if all checkboxes that are not the "select all" checkbox are checked, turn the "select all" button/checkbox into a "deselect all" button/checkbox 1394 if ( jQuery( this ).is( '.gchoice input[type="checkbox"]' ) ) { 1395 var $checkboxes = jQuery( this ).closest( '.gfield_choice--select_all_enabled' ).find( '.gchoice input[type="checkbox"]:not(".gfield_choice_all_toggle")' ); 1396 if ( $checkboxes.length === $checkboxes.filter( ':checked' ).length ) { 1397 if( $select_all.is( 'input[type="checkbox"]' ) ) { 1398 $select_all.prop( 'checked', true ); 1399 gformToggleSelectAll( $select_all, 'deselect' ); 1400 } else { 1401 gformToggleSelectAll( $select_all, 'deselect' ); 1402 } 1403 } 1404 } 1405 1406}); 1407 1408//---------------------------- 1409//------ RADIO FIELD ------ 1410//---------------------------- 1411 1412function gformToggleRadioOther( radioElement ) { 1413 1414 // Get Other input element. 1415 var $other = gform.tools.getClosest( radioElement, '.ginput_container_radio' ).querySelector( 'input.gchoice_other_control' ); 1416 1417 if ( $other ) { 1418 $other.disabled = radioElement.value !== 'gf_other_choice'; 1419 } 1420 1421} 1422 1423//---------------------------- 1424//------ LIST FIELD ---------- 1425//---------------------------- 1426 1427function gformAddListItem( addButton, max ) { 1428 1429 var $addButton = jQuery( addButton ); 1430 1431 if( $addButton.hasClass( 'gfield_icon_disabled' ) ) { 1432 return; 1433 } 1434 1435 var $group = $addButton.parents( '.gfield_list_group' ), 1436 $clone = $group.clone(), 1437 $container = $group.parents( '.gfield_list_container' ), 1438 tabindex = $clone.find( ':input:last' ).attr( 'tabindex' ); 1439 1440 // reset all inputs to empty state 1441 $clone
1442 .find( 'input, select, textarea' ).attr( 'tabindex', tabindex ) 1443 .not( ':checkbox, :radio' ).val( '' ).attr( 'value', '' ); 1444 $clone.find( ':checkbox, :radio' ).prop( 'checked', false ); 1445 1446 $clone = gform.applyFilters( 'gform_list_item_pre_add', $clone, $group ); 1447 1448 $group.after( $clone ); 1449 1450 gformToggleIcons( $container, max ); 1451 gformAdjustClasses( $container ); 1452 gformAdjustRowAttributes( $container ); 1453 1454 gform.doAction( 'gform_list_post_item_add', $clone, $container ); 1455 1456 wp.a11y.speak( window.gf_global.strings.newRowAdded ); 1457 1458} 1459 1460function gformDeleteListItem( deleteButton, max ) { 1461 1462 var $deleteButton = jQuery( deleteButton ); 1463 if ( $deleteButton.prop( 'disabled' ) ) { 1464 return; 1465 } else { 1466 $deleteButton.prop( 'disabled', true ); 1467 } 1468 1469 var $group = $deleteButton.parents( '.gfield_list_group' ), 1470 $container = $group.parents( '.gfield_list_container' ); 1471 1472 $group.remove(); 1473 1474 gformToggleIcons( $container, max ); 1475 gformAdjustClasses( $container ); 1476 gformAdjustRowAttributes( $container ); 1477 1478 gform.doAction( 'gform_list_post_item_delete', $container ); 1479 1480 wp.a11y.speak( window.gf_global.strings.rowRemoved ); 1481 1482} 1483 1484function gformAdjustClasses( $container ) { 1485 1486 var $groups = $container.find( '.gfield_list_group' ); 1487 1488 $groups.each( function( i ) { 1489 1490 var $group = jQuery( this ), 1491 oddEvenClass = ( i + 1 ) % 2 == 0 ? 'gfield_list_row_even' : 'gfield_list_row_odd'; 1492 1493 $group.removeClass( 'gfield_list_row_odd gfield_list_row_even' ).addClass( oddEvenClass ); 1494 1495 } ); 1496 1497} 1498 1499function gformAdjustRowAttributes( $container ) { 1500 1501 if( $container.parents( '.gform_wrapper' ).hasClass( 'gform_legacy_markup_wrapper' ) ) { 1502 return; 1503 } 1504 1505 $container.find( '.gfield_list_group' ).each( function( i ) { 1506 1507 var $input = jQuery( this ).find( 'input, select, textarea' ); 1508 $input.each( function( index, input ) { 1509 var $this = jQuery( input ); 1510 $this.attr( 'aria-label', $this.data( 'aria-label-template' ).gformFormat( i + 1 ) ); 1511 } ); 1512 1513 var $remove = jQuery( this ).find( '.delete_list_item' ); 1514 $remove.attr( 'aria-label', $remove.data( 'aria-label-template' ).gformFormat( i + 1 ) ); 1515 1516 } ); 1517 1518} 1519 1520function gformToggleIcons( $container, max ) { 1521 1522 var groupCount = $container.find( '.gfield_list_group' ).length, 1523 $addButtons = $container.find( '.add_list_item' ), 1524 isLegacy = typeof gf_legacy !== 'undefined' && gf_legacy.is_legacy; 1525 1526 if ( groupCount === 1 ) { 1527 $container.find( '.delete_list_item' ).prop( 'disabled', true ).css( 'visibility', 'hidden' ); 1528 } else { 1529 $container.find( '.delete_list_item' ).prop( 'disabled', false ).css( 'visibility', 'visible' ); 1530 } 1531 1532 if ( max > 0 && groupCount >= max ) { 1533 1534 // store original title in the add button 1535 $addButtons.data( 'title', $container.find( '.add_list_item' ).attr( 'title' ) ); 1536 $addButtons.addClass( 'gfield_icon_disabled' ).attr( 'title', '' ); 1537 1538 if ( ! isLegacy ) { 1539 $addButtons.prop( 'disabled', true ); 1540 } 1541 1542 } else if( max > 0 ) { 1543 1544 $addButtons.removeClass( 'gfield_icon_disabled' ); 1545 1546 if ( ! isLegacy ) { 1547 $addButtons.prop( 'disabled', false ); 1548 } 1549 1550 if( $addButtons.data( 'title' ) ) { 1551 $addButtons.attr( 'title', $addButtons.data( 'title' ) ); 1552 } 1553 1554 } 1555} 1556 1557//----------------------------------- 1558//--------- REPEATER FIELD ---------- 1559//----------------------------------- 1560 1561function gformAddRepeaterItem( addButton, max ) { 1562 1563 var $addButton = jQuery( addButton ); 1564 1565 if( $addButton.hasClass( 'gfield_icon_disabled' ) ) { 1566 return; 1567 } 1568 1569 var $item = $addButton.closest( '.gfield_repeater_item' ), 1570 $clone = $item.clone(), 1571 $container = $item.closest( '.gfield_repeater_container' ), 1572 tabindex = $clone.find( ':input:last' ).attr( 'tabindex' ); 1573 1574 // reset all inputs to empty state 1575 $clone 1576 .find( 'input[type!="hidden"], select, textarea' ).attr( 'tabindex', tabindex ) 1577 .not( ':checkbox, :radio' ).each( function( index ){ 1578 // if the field has a value pre-populated, use that value in the cloned field 1579 if( jQuery( this ).attr( 'value' ) ) { 1580 jQuery( this ).val( jQuery( this ).attr( 'value' ) ); 1581 } else if ( jQuery( this ).is( 'textarea' ) ) { 1582 jQuery( this ).val( this.innerHTML ); 1583 } else { 1584 jQuery( this ).val( '' ); 1585 } 1586 } ); 1587 $clone.find( ':checkbox, :radio' ).prop( 'checked', false ); 1588 $clone.find('.validation_message').remove(); 1589 $clone.find('.gform-datepicker.initialized').removeClass('initialized'); 1590 1591 $clone = gform.applyFilters( 'gform_repeater_item_pre_add', $clone, $item ); 1592 1593 $item.after( $clone ); 1594 1595 var $cells = $clone.children('.gfield_repeater_cell'); 1596 $cells.each(function () { 1597 var $subContainer = jQuery(this).find('.gfield_repeater_container').first(); 1598 if ($subContainer.length > 0) { 1599 resetContainerItems = function ($c) { 1600 $c.children('.gfield_repeater_items').children('.gfield_repeater_item').each(function (i) { 1601 var $children = jQuery(this).children('.gfield_repeater_cell'); 1602 $children.each(function () { 1603 var $subSubContainer = jQuery(this).find('.gfield_repeater_container').first(); 1604 if ($subSubContainer.length > 0) { 1605 resetContainerItems($subSubContainer); 1606 } 1607 }) 1608 }) 1609 $c.children('.gfield_repeater_items').children('.gfield_repeater_item').not(':first').remove(); 1610 } 1611 resetContainerItems($subContainer); 1612 } 1613 }) 1614 1615 gformResetRepeaterAttributes($container); 1616 1617 if ( typeof gformInitDatepicker == 'function' ) { 1618 $container.find('.ui-datepicker-trigger').remove(); 1619 $container.find('.hasDatepicker').removeClass('hasDatepicker'); 1620 gformInitDatepicker(); 1621 } 1622 1623 gformBindFormatPricingFields(); 1624 1625 gformToggleRepeaterButtons( $container, max ); 1626 1627 gform.doAction('gform_repeater_post_item_add', $clone, $container); 1628 1629} 1630 1631function gformDeleteRepeaterItem(deleteButton, max) { 1632 1633 var $deleteButton = jQuery(deleteButton), 1634 $group = $deleteButton.closest('.gfield_repeater_item'), 1635 $container = $group.closest('.gfield_repeater_container'); 1636 1637 $group.remove(); 1638 1639 gformResetRepeaterAttributes($container); 1640 gformToggleRepeaterButtons($container, max); 1641 1642 gform.doAction('gform_repeater_post_item_delete', $container); 1643 1644} 1645 1646function gformResetRepeaterAttributes($container, depth, row) { 1647 1648 var cachedRadioSelection = null; 1649 1650 if (typeof depth === 'undefined') { 1651 depth = 0; 1652 } 1653 1654 if (typeof row === 'undefined') { 1655 row = 0; 1656 } 1657 1658 $container.children('.gfield_repeater_items').children('.gfield_repeater_item').each(function () { 1659 var $children = jQuery(this).children('.gfield_repeater_cell'); 1660 $children.each(function () { 1661 var $cell = jQuery(this); 1662 var $subContainer = jQuery(this).find('.gfield_repeater_container').first(); 1663 1664 if ($subContainer.length > 0) { 1665 var newDepth = depth + 1; 1666 gformResetRepeaterAttributes($subContainer, newDepth, row); 1667 return; 1668 } 1669 1670 jQuery(this).find('input, select, textarea, :checkbox, :radio').each(function () { 1671 var $this = jQuery(this); 1672 var name = $this.attr('name'); 1673 1674 if ( typeof name == 'undefined' ) { 1675 return; 1676 } 1677 1678 var regEx = /^(input_[^\[]*)((\[[0-9]+\])+)/, 1679 parts = regEx.exec(name); 1680 1681 if (!parts) { 1682 return; 1683 } 1684 var inputName = parts[1], 1685 arayParts = parts[2], 1686 regExIndex = /\[([0-9]+)\]/g, 1687 indexes = [], 1688 match = regExIndex.exec(arayParts); 1689 1690 while (match != null) { 1691 indexes.push(match[1]); 1692 match = regExIndex.exec(arayParts); 1693 } 1694 var newNameIndex = parts[1]; 1695 indexes = indexes.reverse(); 1696 var newId = ''; 1697 for (var n = indexes.length - 1; n >= 0; n--) { 1698 if (n == depth) { 1699 newNameIndex += '[' + row + ']'; 1700 newId += '-' + row; 1701 } else { 1702 newNameIndex += '[' + indexes[n] + ']'; 1703 newId += '-' + indexes[n]; 1704 } 1705 } 1706 1707 var currentId = $this.attr('id'); 1708 var $label = $cell.find("label[for='" + currentId + "']"); 1709 1710 if ( currentId ) { 1711 var matches = currentId.match(/((choice|input)_[0-9|_]*)-/); 1712 if ( matches && matches[2] ) { 1713 newId = matches[1] + newId; 1714 $label.attr('for', newId); 1715 $this.attr('id', newId); 1716 } 1717 } 1718 var newName = name.replace(parts[0], newNameIndex), 1719 newNameIsChecked = jQuery('input[name="'+ newName +'"]').is(':checked'); 1720 1721 if ( $this.is(':radio') && $this.is(':checked') && name !== newName && newNameIsChecked ) { 1722 if ( cachedRadioSelection !== null ) { 1723 cachedRadioSelection.prop('checked', true); 1724 } 1725 1726 $this.prop('checked', false); 1727 cachedRadioSelection = $this; 1728 } 1729 1730 $this.attr('name', newName); 1731 }); 1732 }); 1733 if (depth === 0) { 1734 row++; 1735 } 1736 }); 1737 1738 if ( cachedRadioSelection !== null ) { 1739 cachedRadioSelection.prop('checked', true); 1740 cachedRadioSelection = null; 1741 } 1742 1743} 1744 1745function gformToggleRepeaterButtons($container) { 1746 1747 var max = $container.closest('.gfield_repeater_wrapper').data('max_items'), 1748 groupCount = $container.children('.gfield_repeater_items').children('.gfield_repeater_item').length, 1749 $buttonsContainer = $container.children('.gfield_repeater_items').children('.gfield_repeater_item').children('.gfield_repeater_buttons'),
1750 $addButtons = $buttonsContainer.children('.add_repeater_item'); 1751 1752 $buttonsContainer.children('.remove_repeater_item').css('visibility', groupCount == 1 ? 'hidden' : 'visible'); 1753 1754 if (max > 0 && groupCount >= max) { 1755 1756 // store original title in the add button 1757 $addButtons.data('title', $buttonsContainer.children('.add_repeater_item').attr('title')); 1758 $addButtons.addClass('gfield_icon_disabled').attr('title', ''); 1759 1760 } else if (max > 0) { 1761 1762 $addButtons.removeClass('gfield_icon_disabled'); 1763 1764 if ($addButtons.data('title')) { 1765 $addButtons.attr('title', $addButtons.data('title')); 1766 } 1767 } 1768 1769 $container 1770 .children('.gfield_repeater_items') 1771 .children('.gfield_repeater_item') 1772 .children( '.gfield_repeater_cell').each(function (i) { 1773 var $subContainer = jQuery(this).find('.gfield_repeater_container').first(); 1774 if ($subContainer.length > 0) { 1775 gformToggleRepeaterButtons($subContainer); 1776 } 1777 }); 1778} 1779 1780 1781//----------------------------------- 1782//------ CREDIT CARD FIELD ---------- 1783//----------------------------------- 1784function gformMatchCard(id) { 1785 1786 var cardType = gformFindCardType(jQuery('#' + id).val()); 1787 var cardContainer = jQuery('#' + id).parents('.gfield').find('.gform_card_icon_container'); 1788 1789 if(!cardType) { 1790 1791 jQuery(cardContainer).find('.gform_card_icon').removeClass('gform_card_icon_selected gform_card_icon_inactive'); 1792 1793 } else { 1794 1795 jQuery(cardContainer).find('.gform_card_icon').removeClass('gform_card_icon_selected').addClass('gform_card_icon_inactive'); 1796 jQuery(cardContainer).find('.gform_card_icon_' + cardType).removeClass('gform_card_icon_inactive').addClass('gform_card_icon_selected'); 1797 } 1798} 1799 1800function gformFindCardType(value) { 1801 1802 if(value.length < 4) 1803 return false; 1804 1805 var rules = window['gf_cc_rules']; 1806 var validCardTypes = new Array(); 1807 1808 for(type in rules) { 1809 1810 //needed when implementing for in loops 1811 if(!rules.hasOwnProperty(type)) 1812 continue; 1813 1814 1815 for(i in rules[type]) { 1816 1817 if(!rules[type].hasOwnProperty(i)) 1818 continue; 1819 1820 if(rules[type][i].indexOf(value.substring(0, rules[type][i].length)) === 0) { 1821 validCardTypes[validCardTypes.length] = type; 1822 break; 1823 } 1824 1825 } 1826 } 1827 1828 return validCardTypes.length == 1 ? validCardTypes[0].toLowerCase() : false; 1829} 1830 1831function gformToggleCreditCard(){ 1832 if(jQuery("#gform_payment_method_creditcard").is(":checked")) 1833 jQuery(".gform_card_fields_container").slideDown(); 1834 else 1835 jQuery(".gform_card_fields_container").slideUp(); 1836} 1837 1838 1839//---------------------------------------- 1840//------ CHOSEN DROP DOWN FIELD ---------- 1841//---------------------------------------- 1842 1843function gformInitChosenFields( fieldList, noResultsText ) { 1844 return jQuery( fieldList ).each( function(){ 1845 var element = jQuery( this ); 1846 var isConvoForm = typeof gfcf_theme_config !== 'undefined' ? ( gfcf_theme_config !== null && typeof gfcf_theme_config.data !== 'undefined' ? gfcf_theme_config.data.is_conversational_form : undefined ) : false; 1847 1848 // RTL support 1849 if( jQuery( 'html' ).attr( 'dir' ) == 'rtl' ) { 1850 element.addClass( 'chosen-rtl chzn-rtl' ); 1851 } 1852 1853 // only initialize once 1854 if( ( element.is( ':visible' ) || isConvoForm ) && element.siblings( '.chosen-container' ).length == 0 ) { 1855 var chosenOptions = { no_results_text: noResultsText }; 1856 if ( isConvoForm ) { 1857 chosenOptions.width = element.css( 'inline-size' ); 1858 } 1859 var options = gform.applyFilters( 'gform_chosen_options', chosenOptions, element ); 1860 element.chosen( options ); 1861 } 1862 }); 1863} 1864 1865//---------------------------------------- 1866//--- CURRENCY FORMAT NUMBER FIELD ------- 1867//---------------------------------------- 1868 1869function gformInitCurrencyFormatFields(fieldList){ 1870 jQuery(fieldList).each(function(){ 1871 var $this = jQuery(this); 1872 $this.val( gformFormatMoney( jQuery(this).val() ) ); 1873 }).change( function( event ) { 1874 jQuery(this).val( gformFormatMoney( jQuery(this).val() ) ); 1875 }); 1876} 1877 1878 1879 1880//---------------------------------------- 1881//------ JS MERGE TAGS ------------------- 1882//---------------------------------------- 1883 1884/** 1885 * @var {Object} GFMergeTag Handles MergeTag Operations. 1886 * @remove-in 4.0 1887 * @deprecated Use gform.mergeTags instead. 1888 */ 1889var GFMergeTag = function() { 1890 /** 1891 * Gets the merge tag value for the specified input Id 1892 * @param formId The current form Id 1893 * @param inputId The input Id to get the merge tag from. This could be a field id (i.e. 1) or a specific input Id for multi-input fields (i.e. 1.2) 1894 * @param modifier The merge tag modifier to be used. i.e. value, currency, price, etc... 1895 * @returns Returns a string containing the merge tag value for the specified input Id 1896 * @remove-in 4.0 1897 * @deprecated Use gform.mergeTags.getFieldValue() instead. 1898 */ 1899 GFMergeTag.getMergeTagValue = function( formId, inputId, modifier ) { 1900 1901 const mergeTagInfo = gform.mergeTags.getMergeTagInfo( formId, inputId, modifier ); 1902 1903 if ( ! mergeTagInfo.isVisible ) { 1904 return ''; 1905 } 1906 1907 const inputForFilter = jQuery( mergeTagInfo.input ); 1908 let value = window.gform.applyFilters( 'gform_value_merge_tag_' + formId + '_' + mergeTagInfo.fieldId, false, inputForFilter, mergeTagInfo.modifier ); 1909 if ( value !== false ) { 1910 return value; 1911 } 1912 1913 return gform.mergeTags.getFieldValue( formId, inputId, modifier, mergeTagInfo ); 1914 } 1915 1916 /** 1917 * Parses the specified text for merge tags, and replaces all of them with the appropriate merge tag values. Returns the resulting string 1918 * @param formId The current form Id 1919 * @param text The text containing merge tags 1920 * @returns Retuns the original "text" strings with all merge tags replaced with the appropriate merge tag values 1921 * @remove-in 4.0 1922 * @deprecated Use gform.mergeTags.replaceMergeTags() instead. 1923 */ 1924 GFMergeTag.replaceMergeTags = function( formId, text ) { 1925 return gform.mergeTags.replaceMergeTags( formId, text ); 1926 } 1927 1928 /** 1929 * @deprecated Use gform.mergeTags.formatValue() instead. 1930 * @remove-in 4.0 1931 */ 1932 GFMergeTag.formatValue = function( value, modifier ) { 1933 return gform.mergeTags.formatValue( value, modifier ); 1934 } 1935 1936 /** 1937 * Parses the merge tags in the specified text and returns an array of all the matched merge tags 1938 * 1939 * @param text The text with merge tags to be parsed 1940 * @param regEx The regular expression to be used to parse for merge tags. 1941 * 1942 * @returns Returns an array with all the merge tags that were matched in the original text 1943 * @deprecated Use gform.mergeTags.parseMergeTags() instead. 1944 * @remove-in 4.0 1945 */ 1946 GFMergeTag.parseMergeTags = function( text, regEx ) { 1947 return gform.mergeTags.parseMergeTags( text, regEx ); 1948 } 1949} 1950 1951new GFMergeTag(); 1952 1953 1954//---------------------------------------- 1955//------ CALCULATION FUNCTIONS ----------- 1956//---------------------------------------- 1957 1958var GFCalc = function(formId, formulaFields){ 1959 1960 this.formId = formId; 1961 this.formulaFields = formulaFields; 1962 1963 this.exprPatt = /^[0-9 -/*\(\)]+$/i; 1964 this.isCalculating = {}; 1965 1966 this.init = function(formId, formulaFields) { 1967 1968 var calc = this; 1969 1970 // @since 2.5.10 - namespace event to avoid multiple bindings. 1971 jQuery(document) 1972 .off("gform_post_conditional_logic.gfCalc_{0}".gformFormat(formId)) 1973 .on("gform_post_conditional_logic.gfCalc_{0}".gformFormat(formId), function(){ 1974 calc.runCalcs( formId, formulaFields ); 1975 } ); 1976 1977 for(var i=0; i<formulaFields.length; i++) { 1978 var formulaField = jQuery.extend({}, formulaFields[i]); 1979 this.runCalc(formulaField, formId); 1980 this.bindCalcEvents(formulaField, formId); 1981 } 1982 1983 } 1984 1985 this.runCalc = function(formulaField, formId) { 1986 var calcObj = this, 1987 field = jQuery('#field_' + formId + '_' + formulaField.field_id), 1988 formulaInput = field.hasClass( 'gfield_price' ) ? jQuery( '#ginput_base_price_' + formId + '_' + formulaField.field_id ) : jQuery( '#input_' + formId + '_' + formulaField.field_id ), 1989 previous_val = formulaInput.val(), 1990 formula = gform.applyFilters( 'gform_calculation_formula', formulaField.formula, formulaField, formId, calcObj ), 1991 expr = calcObj.replaceFieldTags( formId, formula, formulaField ).replace(/(\r\n|\n|\r)/gm,""), 1992 result = ''; 1993 1994 if(calcObj.exprPatt.test(expr)) { 1995 try { 1996 1997 //run calculation 1998 result = eval(expr); 1999 2000 } catch( e ) { } 2001 } else { 2002 return; 2003 } 2004 2005 // if result is positive infinity, negative infinity or a NaN, defaults to 0 2006 if( ! isFinite( result ) ) 2007 result = 0; 2008 2009 // allow users to modify result with their own function 2010 if( window["gform_calculation_result"] ) { 2011 result = window["gform_calculation_result"](result, formulaField, formId, calcObj); 2012 if( window.console ) 2013 console.log( '"gform_calculation_result" function is deprecated since version 1.8! Use "gform_calculation_result" JS hook instead.' ); 2014 } 2015 2016 // allow users to modify result with their own function 2017 result = gform.applyFilters( 'gform_calculation_result', result, formulaField, formId, calcObj ); 2018 2019 // allow result to be custom formatted 2020 var formattedResult = gform.applyFilters( 'gform_calculation_format_result', false, result, formulaField, formId, calcObj ); 2021 2022 var numberFormat = gf_get_field_number_format(formulaField.field_id, formId); 2023 2024 //formatting number 2025 if( formattedResult !== false) { 2026 result = formattedResult; 2027 } 2028 else if( field.hasClass( 'gfield_price' ) || numberFormat == "currency") { 2029 2030 result = gformFormatMoney(result ? result : 0, true); 2031 } 2032 else { 2033 2034 var decimalSeparator = "."; 2035 var thousandSeparator = ","; 2036 2037 if(numberFormat == "decimal_comma"){ 2038 decimalSeparator = ","; 2039 thousandSeparator = "."; 2040 } 2041 2042 result = gformFormatNumber(result, !gform.utils.isNumber(formulaField.rounding) ? -1 : formulaField.rounding, decimalSeparator, thousandSeparator); 2043 } 2044 2045 //If value doesn't change, abort. 2046 //This is needed to prevent an infinite loop condition with conditional logic 2047 if( result == previous_val ) 2048 return; 2049 2050 // if this is a calculation product, handle differently 2051 if(field.hasClass('gfield_price')) { 2052 jQuery('#input_' + formId + '_' + formulaField.field_id).text(result); 2053 2054 // Firing jQuery change event for backwards compatibility with legacy code. 2055 formulaInput.val(result).trigger('change'); 2056 2057 // Firing native change event for compatibility with new code in JS bundle. 2058 if ( formulaInput && formulaInput.length > 0 ) { 2059 window.gform.utils.trigger( { event: 'change', el: formulaInput[0], native: true } ); 2060 } 2061 2062 // Announce the price change of the product only if there's no Total field. 2063 if ( jQuery( '.gfield_label_product' ).length && ! jQuery( '.ginput_total' ).length ) { 2064 result = jQuery( 'label[ for=input_' + formId + '_' + formulaField.field_id + '_1 ]' ).find( '.gfield_label_product' ).text() + ' ' + result; 2065 wp.a11y.speak( result ); 2066 } 2067 } else { 2068 formulaInput.val(result).trigger('change'); 2069 } 2070 2071 }; 2072 2073 this.runCalcs = function( formId, formulaFields ) {
2074 for(var i=0; i<formulaFields.length; i++) { 2075 var formulaField = jQuery.extend({}, formulaFields[i]); 2076 this.runCalc( formulaField, formId ); 2077 } 2078 } 2079 2080 this.bindCalcEvents = function(formulaField, formId) { 2081 2082 var calcObj = this; 2083 var formulaFieldId = formulaField.field_id; 2084 var matches = GFMergeTag.parseMergeTags( formulaField.formula ); 2085 2086 calcObj.isCalculating[formulaFieldId] = false; 2087 2088 for(var i in matches) { 2089 2090 if(! matches.hasOwnProperty(i)) 2091 continue; 2092 2093 var inputId = matches[i][1]; 2094 var fieldId = parseInt(inputId,10); 2095 var input = jQuery('#field_' + formId + '_' + fieldId).find('input[name="input_' + inputId + '"], select[name="input_' + inputId + '"]'); 2096 2097 if(input.prop('type') == 'checkbox' || input.prop('type') == 'radio') { 2098 jQuery(input).click(function(){ 2099 calcObj.bindCalcEvent(inputId, formulaField, formId, 0); 2100 }); 2101 // Bind calc event to the image in an image choice field. 2102 var imageChoice = input.closest('.gfield--type-image_choice .gchoice'); 2103 if ( imageChoice.length > 0 ) { 2104 jQuery(imageChoice).click(function(){ 2105 calcObj.bindCalcEvent(inputId, formulaField, formId, 0); 2106 }); 2107 } 2108 } else if(input.is('select') || input.prop('type') == 'hidden') { 2109 jQuery(input).change(function(){ 2110 calcObj.bindCalcEvent(inputId, formulaField, formId, 0); 2111 }); 2112 } else { 2113 jQuery(input).keydown(function(){ 2114 calcObj.bindCalcEvent(inputId, formulaField, formId); 2115 }).change(function(){ 2116 calcObj.bindCalcEvent(inputId, formulaField, formId, 0); 2117 }); 2118 } 2119 2120 // allow users to add custom methods for triggering calculations 2121 gform.doAction( 'gform_post_calculation_events', matches[i], formulaField, formId, calcObj ); 2122 2123 } 2124 2125 } 2126 2127 this.bindCalcEvent = function(inputId, formulaField, formId, delay) { 2128 2129 var calcObj = this; 2130 var formulaFieldId = formulaField.field_id; 2131 2132 delay = delay == undefined ? 345 : delay; 2133 2134 if(calcObj.isCalculating[formulaFieldId][inputId]) 2135 clearTimeout(calcObj.isCalculating[formulaFieldId][inputId]); 2136 2137 calcObj.isCalculating[formulaFieldId][inputId] = window.setTimeout(function() { 2138 calcObj.runCalc(formulaField, formId); 2139 }, delay); 2140 2141 } 2142 2143 this.replaceFieldTags = function( formId, expr, formulaField ) { 2144 2145 var matches = GFMergeTag.parseMergeTags( expr ); 2146 2147 for(i in matches) { 2148 2149 if(! matches.hasOwnProperty(i)) 2150 continue; 2151 2152 var inputId = matches[i][1]; 2153 var fieldId = parseInt(inputId,10); 2154 2155 if ( fieldId == formulaField.field_id && fieldId == inputId ) { 2156 continue; 2157 } 2158 2159 var modifier = 'value'; 2160 if( matches[i][3] ){ 2161 modifier = matches[i][3]; 2162 } 2163 else { 2164 var is_product_radio = jQuery('.gfield_price input[name=input_' + fieldId + ']').is('input[type=radio]'); 2165 var is_product_dropdown = jQuery('.gfield_price select[name=input_' + fieldId + ']').length > 0; 2166 var is_option_checkbox = jQuery('.gfield_price input[name="input_' + inputId + '"]').is('input[type=checkbox]'); 2167 2168 if( is_product_dropdown || is_product_radio || is_option_checkbox ) { 2169 modifier = 'price'; 2170 } 2171 } 2172 2173 var isVisible = window['gf_check_field_rule'] ? gf_check_field_rule( formId, fieldId, true, '' ) == 'show' : true; 2174 2175 var value = isVisible ? GFMergeTag.getMergeTagValue( formId, inputId, modifier ) : 0; 2176 2177 // allow users to modify value with their own function 2178 value = gform.applyFilters( 'gform_merge_tag_value_pre_calculation', value, matches[i], isVisible, formulaField, formId ); 2179 2180 value = this.cleanNumber( value, formId, fieldId, formulaField ); 2181 2182 expr = expr.replace( matches[i][0], value ); 2183 } 2184 2185 return expr; 2186 } 2187 2188 this.cleanNumber = function ( value, formId, fieldId, formulaField ) { 2189 2190 var numberFormat = gf_get_field_number_format( fieldId, formId ); 2191 2192 if( ! numberFormat ) { 2193 numberFormat = gf_get_field_number_format(formulaField.field_id, formId); 2194 } 2195 2196 var decimalSeparator = gform.Currency.getDecimalSeparator(numberFormat); 2197 2198 value = gform.Currency.cleanNumber( value, '', '', decimalSeparator ); 2199 if( ! value ) 2200 value = 0; 2201 2202 return value; 2203 } 2204 2205 this.init(formId, formulaFields); 2206 2207 2208} 2209 2210function gformFormatNumber(number, rounding, decimalSeparator, thousandSeparator){ 2211 2212 if(typeof decimalSeparator == "undefined"){ 2213 if(window['gf_global']){ 2214 var currency = new gform.Currency(gf_global.gf_currency_config); 2215 decimalSeparator = currency.currency["decimal_separator"]; 2216 } 2217 else{ 2218 decimalSeparator = "."; 2219 } 2220 } 2221 2222 if(typeof thousandSeparator == "undefined"){ 2223 if(window['gf_global']){ 2224 var currency = new gform.Currency(gf_global.gf_currency_config); 2225 thousandSeparator = currency.currency["thousand_separator"]; 2226 } 2227 else{ 2228 thousandSeparator = ","; 2229 } 2230 } 2231 2232 var currency = new gform.Currency(); 2233 return currency.numberFormat(number, rounding, decimalSeparator, thousandSeparator, false) 2234} 2235 2236/** 2237 * @deprecated. Use GFMergeTags.parseMergeTag() instead 2238 * @remove-in 3.0 2239 */ 2240function getMatchGroups(expr, patt) { 2241 2242 console.log('getMatchGroups() has been deprecated and will be removed in version 3.0. Use GFMergeTags.parseMergeTag() instead.'); 2243 2244 var matches = new Array(); 2245 2246 while(patt.test(expr)) { 2247 2248 var i = matches.length; 2249 matches[i] = patt.exec(expr) 2250 expr = expr.replace('' + matches[i][0], ''); 2251 2252 } 2253 2254 return matches; 2255} 2256 2257function gf_get_field_number_format(fieldId, formId, context) { 2258 2259 var fieldNumberFormats = rgars(window, 'gf_global/number_formats/{0}/{1}'.gformFormat(formId, fieldId)), 2260 format = false;
2261 2262 if (fieldNumberFormats === '') { 2263 return format; 2264 } 2265 2266 if (typeof context == 'undefined') { 2267 format = fieldNumberFormats.price !== false ? fieldNumberFormats.price : fieldNumberFormats.value; 2268 } else { 2269 format = fieldNumberFormats[context]; 2270 } 2271 2272 return format; 2273} 2274 2275//---------------------------------------- 2276//------ reCAPTCHA FUNCTIONS ------------- 2277//---------------------------------------- 2278 2279gform.recaptcha = { 2280 /** 2281 * Callback function on the reCAPTCAH API script. 2282 * 2283 * @see GF_Field_CAPTCHA::get_field_input() in /includes/fields/class-gf-field-catpcha.php 2284 */ 2285 renderRecaptcha: function() { 2286 jQuery( '.ginput_recaptcha:not(.gform-initialized)' ).each( function() { 2287 let $elem = jQuery( this ), 2288 parameters = { 2289 'sitekey': $elem.data( 'sitekey' ), 2290 'theme': $elem.data( 'theme' ), 2291 'tabindex': $elem.data( 'tabindex' ), 2292 'error-callback': () => { 2293 console.error( 'Gravity Forms: There was an error initializing reCAPTCHA v2. Please ensure your reCAPTCHA API keys are valid.' ); 2294 $elem.attr( 'data-recaptcha-error', '1' ); 2295 } 2296 }; 2297 2298 if ( $elem.data( 'stoken' ) ) { 2299 parameters.stoken = $elem.data( 'stoken' ); 2300 } 2301 2302 /** 2303 * Allows a custom callback function to be executed when the user successfully submits the captcha. 2304 * 2305 * @since 2.4.x The callback will be a function if reCAPTCHA v2 Invisible is used. 2306 * @since 2.2.5.20 2307 * 2308 * @param string|false|object The name of the callback function or the function object itself to be executed when the user successfully submits the captcha. 2309 * @param object $elem The jQuery object containing the div element with the ginput_recaptcha class for the current reCaptcha field. 2310 */ 2311 const callback = gform.applyFilters( 'gform_recaptcha_callback', false, $elem ); 2312 if ( callback ) { 2313 parameters.callback = callback; 2314 } 2315 2316 // Rendering recaptcha and saving the widget id as an attribute. 2317 const widgetId = grecaptcha.render( this.id, parameters ); 2318 $elem[0].setAttribute( 'data-widget-id', widgetId ); 2319 2320 if ( parameters.tabindex ) { 2321 $elem.find( 'iframe' ).attr( 'tabindex', parameters.tabindex ); 2322 } 2323 2324 $elem.addClass( 'gform-initialized' ); 2325 2326 gform.doAction( 'gform_post_recaptcha_render', $elem ); 2327 } ); 2328 2329 gform.recaptcha.bindRecaptchaSubmissionEvents(); 2330 }, 2331 2332 isSubmissionEventsInitialized: false, 2333 bindRecaptchaSubmissionEvents: function() { 2334 // If already initialized, abort. 2335 if ( gform.recaptcha.isSubmissionEventsInitialized ) { 2336 return; 2337 } 2338 // Setting initialized flag. 2339 gform.recaptcha.isSubmissionEventsInitialized = true; 2340 2341 // Subscribe to the pre_submission filter to execute invisible recaptcha when form is submitted. 2342 window.gform.utils.addAsyncFilter( 'gform/submission/pre_submission', async ( data ) => { 2343 2344 const requiresRecaptcha = data.submissionType === gform.submission.SUBMISSION_TYPE_SUBMIT || data.submissionType === gform.submission.SUBMISSION_TYPE_NEXT; 2345 2346 // Execute recaptcha if this is the right submission type and the submission hasn't been flagged to be aborted. 2347 if ( requiresRecaptcha && ! data.abort ) { 2348 await gform.recaptcha.maybeExecuteInvisibleRecaptcha( data ); 2349 } 2350 return data; 2351 }); 2352 2353 // Subscribe to the pre_ajax_validation filter to execute invisible recaptcha when form is validated via AJAX. 2354 window.gform.utils.addAsyncFilter( 'gform/ajax/pre_ajax_validation', gform.recaptcha.maybeExecuteInvisibleRecaptcha ); 2355 2356 // Subscribe to the AJAX submission and validation events to save the recaptcha result. 2357 window.gform.utils.addFilter( 'gform/ajax/post_ajax_submission', gform.recaptcha.handleAjaxPostSubmission ); 2358 window.gform.utils.addFilter( 'gform/ajax/post_ajax_validation', gform.recaptcha.handleAjaxPostValidation ); 2359 }, 2360 2361 /** 2362 * @function maybeExecuteInvisibleRecaptcha 2363 * @description Executes the invisible recaptcha and waits for the response. 2364 2365 * @since 2.9.0 2366 * 2367 * @param {object} data Data passed by the pre submission filter. 2368 * @returns {Promise<*>} Returns the pre submission data object unchanged. 2369 */ 2370 maybeExecuteInvisibleRecaptcha: async function( data ) { 2371 2372 if ( gform.recaptcha.gformIsRecaptchaPending( jQuery( data.form ) ) ) { 2373 const recaptcha = gform.utils.getNode( '.ginput_recaptcha', data.form, true ); 2374 2375 await gform.recaptcha.executeRecaptcha( recaptcha.getAttribute( 'data-widget-id' ), data.form ); 2376 } 2377 return data; 2378 }, 2379 2380 /** 2381 * @function executeRecaptcha
2382 * @description Executes recaptcha and waits for the response by polling the .g-recaptcha-response field. 2383 2384 * @since 2.9.0 2385 * 2386 * @param {string} widgetId The recaptcha widgetId. 2387 * @param {HTMLFormElement} form The form being submitted 2388 * @returns {Promise<string>} Returns the recaptcha response when it becomes available in the .g-recaptcha-response 2389 */ 2390 executeRecaptcha: async function( widgetId, form ) { 2391 2392 // If there was an error loading recaptcha, just abort and let the submission fail validation. 2393 const recaptcha = gform.utils.getNode( '.ginput_recaptcha', form, true ); 2394 if ( recaptcha.getAttribute( 'data-recaptcha-error' ) === '1' ) { 2395 return; 2396 } 2397 2398 // Executes recaptcha. 2399 window.grecaptcha.execute( widgetId ); 2400 2401 // Resolve promise when response is available. 2402 return new Promise(( resolve, reject ) => { 2403 const intervalId = setInterval(() => { 2404 const response = gform.utils.getNode( '.g-recaptcha-response', form, true ); 2405 2406 if ( response && response.value ) { 2407 clearInterval( intervalId ); 2408 resolve( response.value ); 2409 } 2410 }, 100 ); 2411 }); 2412 }, 2413 2414 /** 2415 * @function handleAjaxPostValidation 2416 * @description Saves the recaptcha response after an AJAX validation request. 2417 2418 * @since 2.9.0 2419 * 2420 * @param {object} data Data passed by the ajax post validation filter. 2421 * @returns {object} Returns the data object unchanged. 2422 */ 2423 handleAjaxPostValidation: function( data ) { 2424 gform.recaptcha.saveRecaptchaResponse( data.validationResult.data.recaptcha_response, data.form ); 2425 return data; 2426 }, 2427 2428 /** 2429 * @function handleAjaxPostSubmission 2430 * @description Saves the recaptcha response after an AJAX submission request. 2431 2432 * @since 2.9.0 2433 * 2434 * @param {object} data Data passed by the ajax post submission filter. 2435 * @returns {object} Returns the data object unchanged. 2436 */ 2437 handleAjaxPostSubmission: function( data ) { 2438 gform.recaptcha.saveRecaptchaResponse( data.submissionResult.data.recaptcha_response, data.form ); 2439 return data; 2440 }, 2441 2442 /** 2443 * @function saveRecaptchaResponse 2444 * @description Saves the specified recaptcha response in a hidden field. 2445 2446 * @since 2.9.0 2447 * 2448 * @param {string} recaptchaResponse The recaptcha response to be saved. 2449 * @param {form} form The form being submitted. 2450 * 2451 * @returns {void} 2452 */ 2453 saveRecaptchaResponse: function( recaptchaResponse, form ) { 2454 2455 if ( ! recaptchaResponse ) { 2456 return; 2457 } 2458 2459 let recaptchaInput = gform.tools.getNodes( 'input[name=g-recaptcha-response]', true, form, true ); 2460 if ( recaptchaInput.length === 0 ) { 2461 recaptchaInput = document.createElement( 'input' ); 2462 recaptchaInput.type = 'hidden'; 2463 recaptchaInput.name = 'g-recaptcha-response'; 2464 form.appendChild( recaptchaInput ); 2465 } else { 2466 recaptchaInput = recaptchaInput[0]; 2467 } 2468 recaptchaInput.value = recaptchaResponse; 2469 }, 2470 2471 /** 2472 * Helper function to determine whether a recaptcha is pending. 2473 * 2474 * @since 2.4.23 2475 * 2476 * @param {Object} form jQuery form object. 2477 * @returns {boolean} 2478 */ 2479 gformIsRecaptchaPending: function( form ) { 2480 const recaptcha = form.find( '.ginput_recaptcha' ); 2481 2482 if ( ! recaptcha.length || recaptcha.data( 'size' ) !== 'invisible' ) { 2483 return false; 2484 } 2485 2486 const recaptchaResponse = recaptcha.find( '.g-recaptcha-response' ); 2487 2488 return !( recaptchaResponse.length && recaptchaResponse.val() ); 2489 }, 2490 2491 /** 2492 * @function gform.recaptcha.needsRender 2493 * @description Is there a non-rendered Recaptcha field on the page? 2494 * 2495 * @since 2.5.6 2496 */ 2497 needsRender: function() { 2498 return document.querySelectorAll( '.ginput_recaptcha:not(.gform-initialized)' )[ 0 ]; 2499 }, 2500 2501 /** 2502 * @function gform.recaptcha.renderOnRecaptchaLoaded 2503 * @description Render recaptcha fields once the library is available, only if non rendered elements are present. 2504 * 2505 * @since 2.5.6 2506 */ 2507 renderOnRecaptchaLoaded: function() { 2508 // if nothing to render, exit 2509 if ( ! gform.recaptcha.needsRender() ) { 2510 return; 2511 } 2512 var gfRecaptchaPoller = setInterval( function() { 2513 if ( ! window.grecaptcha || ! window.grecaptcha.render ) { 2514 return; 2515 } 2516 this.renderRecaptcha(); 2517 clearInterval( gfRecaptchaPoller ); 2518 }, 100 ); 2519 } 2520}; 2521 2522jQuery( document ).on( 'gform_post_render', gform.recaptcha.renderOnRecaptchaLoaded ); 2523 2524window.renderRecaptcha = gform.recaptcha.renderRecaptcha; 2525window.gformIsRecaptchaPending = gform.recaptcha.gformIsRecaptchaPending; 2526 2527 2528//---------------------------------------- 2529//----- SINGLE FILE UPLOAD FUNCTIONS ----- 2530//---------------------------------------- 2531 2532function gformValidateFileSize( field, max_file_size ) { 2533 var validation_element; 2534 2535 // Get validation message element. 2536 if ( jQuery( field ).closest( 'div' ).siblings( '.validation_message' ).length > 0 ) { 2537 validation_element = jQuery( field ).closest( 'div' ).siblings( '.validation_message' ); 2538 } else { 2539 validation_element = jQuery( field ).siblings( '.validation_message' ); 2540 } 2541 2542 // If file API is not supported within browser, return. 2543 if ( ! window.FileReader || ! window.File || ! window.FileList || ! window.Blob ) { 2544 return; 2545 } 2546 2547 // Get selected file. 2548 var file = field.files[0]; 2549 2550 // If selected file is larger than maximum file size, set validation message and unset file selection. 2551 if ( file && file.size > max_file_size ) { 2552 2553 // Set validation message. 2554 validation_element.text(file.name + " - " + gform_gravityforms.strings.file_exceeds_limit); 2555 // Announce error. 2556 wp.a11y.speak( file.name + " - " + gform_gravityforms.strings.file_exceeds_limit ); 2557 2558 } else { 2559 2560 // Reset validation message. 2561 validation_element.remove(); 2562 2563 } 2564 2565} 2566 2567//---------------------------------------- 2568//------ MULTIFILE UPLOAD FUNCTIONS ------ 2569//---------------------------------------- 2570 2571(function (gfMultiFileUploader, $) { 2572 gfMultiFileUploader.uploaders = {}; 2573 var strings = typeof gform_gravityforms != 'undefined' ? gform_gravityforms.strings : {}; 2574 var imagesUrl = typeof gform_gravityforms != 'undefined' ? gform_gravityforms.vars.images_url : ""; 2575 2576 $(document).on('gform_post_render', function(e, formID){ 2577 $( "form#gform_" + formID + " .gform_fileupload_multifile" ).each( function(){ 2578 setup( this ); 2579 } ); 2580 2581 bindFileUploadSubmissionEvents(); 2582 }); 2583 2584 $(document).on("gform_post_conditional_logic", function(e,formID, fields, isInit){ 2585 if(!isInit){ 2586 $.each(gfMultiFileUploader.uploaders, function(i, uploader){ 2587 uploader.refresh(); 2588 }); 2589 } 2590 }); 2591 2592 $(document).ready(function () { 2593 if((typeof adminpage !== 'undefined' && adminpage === 'toplevel_page_gf_edit_forms')|| typeof plupload == 'undefined'){ 2594 $(".gform_button_select_files").prop("disabled", true); 2595 } else if (typeof adminpage !== 'undefined' && adminpage.indexOf('_page_gf_entries') > -1) { 2596 $(".gform_fileupload_multifile").each(function(){ 2597 setup(this); 2598 }); 2599 } 2600 }); 2601 2602 gfMultiFileUploader.setup = function (uploadElement){ 2603 setup( uploadElement ); 2604 }; 2605 2606 let isInitialized = false;
2607 2608 /** 2609 * Binds the file upload to the pre_submission event so that it can abort submission if there are pending files being uploaded. 2610 * 2611 * @since 2.9.0 2612 */ 2613 function bindFileUploadSubmissionEvents() { 2614 2615 // If already initialized, abort. 2616 if ( isInitialized ) { 2617 return; 2618 } 2619 isInitialized = true; 2620 2621 // Making sure there aren't any pending file uploads. 2622 window.gform.utils.addFilter( 'gform/submission/pre_submission', ( data ) => { 2623 if ( hasPendingUploads() ) { 2624 alert( strings.currently_uploading ); 2625 data.abort = true; 2626 } 2627 2628 return data; 2629 }, 8); 2630 } 2631 2632 /** 2633 * Check if there are any files currently in the process of being uploaded. 2634 * 2635 * @since 2.9.0 2636 * 2637 * @return {boolean} Returns true if there are files that haven't finished being uploaded yet. Returns false otherwise. 2638 */ 2639 function hasPendingUploads() { 2640 let pendingUploads = false; 2641 $.each( gfMultiFileUploader.uploaders, function( i, uploader ) { 2642 if( uploader.total.queued > 0 ) { 2643 pendingUploads = true; 2644 return false; 2645 } 2646 }); 2647 return pendingUploads; 2648 } 2649 2650 function setup(uploadElement){ 2651 var settings = $(uploadElement).data('settings'); 2652 2653 var uploader = new plupload.Uploader(settings); 2654 formID = uploader.settings.multipart_params.form_id; 2655 gfMultiFileUploader.uploaders[settings.container] = uploader; 2656 var formID; 2657 var uniqueID; 2658 2659 uploader.bind( 'Init', function( up, params ) { 2660 if ( ! up.features.dragdrop ) { 2661 $( ".gform_drop_instructions" ).hide(); 2662 } 2663 2664 setFieldAccessibility( up.settings.container ); 2665 toggleLimitReached( up.settings ); 2666 } ); 2667 2668 gfMultiFileUploader.toggleDisabled = function (settings, disabled){ 2669 2670 var button = typeof settings.browse_button == "string" ? $("#" + settings.browse_button) : $(settings.browse_button); 2671 button.prop("disabled", disabled); 2672 }; 2673 2674 /** 2675 * @function setFieldAccessibility 2676 * @description Patches accessibility issues with the plupload multi file container. 2677 * 2678 * @since 2.5.1 2679 * 2680 * @param {Node} container The generated plupload container. 2681 */ 2682 2683 function setFieldAccessibility( container ) { 2684 var input = container.querySelectorAll( 'input[type="file"]' )[ 0 ]; 2685 var button = container.querySelectorAll( '.gform_button_select_files' )[ 0 ]; 2686 var label = $( uploadElement ).closest( '.gfield' ).find( '.gfield_label' )[ 0 ]; 2687 if ( ! input || ! label || ! button ) { 2688 return; 2689 } 2690 2691 label.setAttribute( 'for', input.id ); 2692 button.setAttribute( 'aria-label', button.innerText.toLowerCase() + ', ' + label.innerText.toLowerCase() ); 2693 input.setAttribute( 'tabindex', '-1' ); 2694 input.setAttribute( 'aria-hidden', 'true' ); 2695 } 2696 2697 function addMessage( messagesID, message) { 2698 $( "#" + messagesID ).prepend( "<li class='gfield_description gfield_validation_message'>" + htmlEncode( message ) + "</li>" ); 2699 // Announce errors. 2700 setTimeout(function () { 2701 wp.a11y.speak( $( "#" + messagesID ).text() ); 2702 }, 1000 ); 2703 } 2704 2705 function removeMessage(messagesID, message) { 2706 $("#" + messagesID + " li:contains('" + message + "')").remove(); 2707 } 2708 2709 function toggleLimitReached(settings) { 2710 var limit = parseInt(settings.gf_vars.max_files, 10); 2711 if (limit > 0) { 2712 var totalCount = countFiles(settings.multipart_params.field_id), 2713 limitReached = totalCount >= limit; 2714 2715 gfMultiFileUploader.toggleDisabled(settings, limitReached); 2716 if (!limitReached) { 2717 removeMessage(settings.gf_vars.message_id, strings.max_reached); 2718 } 2719 } 2720 } 2721 2722 uploader.init(); 2723 2724 uploader.bind('BeforeUpload', function(up, file){ 2725 up.settings.multipart_params.original_filename = file.name; 2726 }); 2727 2728 uploader.bind('FilesAdded', function(up, files) { 2729 var max = parseInt(up.settings.gf_vars.max_files,10), 2730 fieldID = up.settings.multipart_params.field_id, 2731 totalCount = countFiles(fieldID), 2732 disallowed = up.settings.gf_vars.disallowed_extensions, 2733 extension; 2734 2735 if( max > 0 && totalCount >= max){ 2736 $.each(files, function(i, file) { 2737 up.removeFile(file); 2738 return; 2739 }); 2740 return; 2741 } 2742 $.each(files, function(i, file) { 2743 2744 extension = file.name.split('.').pop(); 2745 2746 if($.inArray(extension, disallowed) > -1){ 2747 addMessage(up.settings.gf_vars.message_id, file.name + " - " + strings.illegal_extension); 2748 up.removeFile(file); 2749 return; 2750 } 2751 2752 if ((file.status == plupload.FAILED) || (max > 0 && totalCount >= max)){ 2753 up.removeFile(file); 2754 return; 2755 } 2756 2757 var size = typeof file.size !== 'undefined' ? plupload.formatSize(file.size) : strings.in_progress, 2758 removeFileJs = '$this=jQuery(this); var uploader = gfMultiFileUploader.uploaders.' + up.settings.container.id + ';uploader.stop();uploader.removeFile(uploader.getFile(\'' + file.id +'\'));$this.after(\'' + strings.cancelled + '\'); uploader.start();$this.remove();', 2759 statusMarkup = '<div id="{0}" class="ginput_preview"><span class="gfield_fileupload_filename">{1}</span><span class="gfield_fileupload_filesize">{2}</span><span class="gfield_fileupload_progress"><span class="gfield_fileupload_progressbar"><span class="gfield_fileupload_progressbar_progress"></span></span><span class="gfield_fileupload_percent"></span></span><a class="gfield_fileupload_cancel gform-theme-button gform-theme-button--simple" href="javascript:void(0)" title="{3}" onclick="{4}" onkeypress="{4}">{5}</a>'; 2760 2761 /** 2762 * Filer the file upload markup as it is being uploaded.
2763 * 2764 * @param {string} statusMarkup Markup template used to render the status of the file being uploaded. 2765 * @param {plupload.File} file Instance of File being uploaded. See: https://www.plupload.com/docs/v2/File. 2766 * @param {int|string} size File size. 2767 * @param {object} strings Array of localized strings relating to the file upload UI. 2768 * @param {string} removeFileJs JS used to remove the file when the "Cancel" link is click/pressed. 2769 * @param {plupload.Uploader} up Instance of Uploader responsible for uploading current file. See: https://www.plupload.com/docs/v2/Uploader. 2770 */ 2771 statusMarkup = gform.applyFilters( 'gform_file_upload_status_markup', statusMarkup, file, size, strings, removeFileJs, up ) 2772 .gformFormat( file.id, htmlEncode( file.name ), size, strings.cancel_upload, removeFileJs, strings.cancel ); 2773 2774 $( '#' + up.settings.filelist ).prepend( statusMarkup ); 2775 2776 totalCount++; 2777 2778 }); 2779 2780 up.refresh(); // Reposition Flash 2781 2782 var formElementID = "form#gform_" + formID; 2783 var uidElementID = "input:hidden[name='gform_unique_id']"; 2784 var uidSelector = formElementID + " " + uidElementID; 2785 var $uid = $(uidSelector); 2786 if($uid.length==0){ 2787 $uid = $(uidElementID); 2788 } 2789 2790 uniqueID = $uid.val(); 2791 if('' === uniqueID){ 2792 uniqueID = generateUniqueID(); 2793 $uid.val(uniqueID); 2794 } 2795 2796 2797 if(max > 0 && totalCount >= max){ 2798 gfMultiFileUploader.toggleDisabled(up.settings, true); 2799 addMessage(up.settings.gf_vars.message_id, strings.max_reached) 2800 } 2801 2802 2803 up.settings.multipart_params.gform_unique_id = uniqueID; 2804 up.start(); 2805 2806 }); 2807 2808 uploader.bind('UploadProgress', function(up, file) { 2809 var html = file.percent + "%"; 2810 $('#' + file.id + ' span.gfield_fileupload_percent').html(html); 2811 $('#' + file.id + ' span.gfield_fileupload_progressbar_progress').css('width', file.percent + '%'); 2812 }); 2813 2814 uploader.bind('Error', function(up, err) { 2815 if(err.code === plupload.FILE_EXTENSION_ERROR){ 2816 var extensions = typeof up.settings.filters.mime_types != 'undefined' ? up.settings.filters.mime_types[0].extensions /* plupoad 2 */ : up.settings.filters[0].extensions; 2817 addMessage(up.settings.gf_vars.message_id, err.file.name + " - " + strings.invalid_file_extension + " " + extensions); 2818 } else if (err.code === plupload.FILE_SIZE_ERROR) { 2819 addMessage(up.settings.gf_vars.message_id, err.file.name + " - " + strings.file_exceeds_limit); 2820 } else { 2821 const errorResponse = JSON.parse( err.response ); 2822 const errorCode = errorResponse?.error?.code || err.code; 2823 const errorMessage = errorResponse?.error?.message || err.message; 2824 const filePart = err.file?.name ? `${ err.file.name } - ` : ''; 2825 const m = `${ filePart }${ strings.error }: ${ errorCode }, ${ strings.message }: ${ errorMessage }`; 2826 2827 addMessage(up.settings.gf_vars.message_id, m); 2828 } 2829 $('#' + err.file.id ).html(''); 2830 up.removeFile( err.file ); 2831 up.refresh(); // Reposition Flash 2832 }); 2833 2834 uploader.bind('ChunkUploaded', function(up, file, result) { 2835 var response = $.secureEvalJSON(result.response); 2836 if(response.status == "error"){ 2837 up.removeFile(file); 2838 addMessage(up.settings.gf_vars.message_id, file.name + " - " + response.error.message); 2839 $('#' + file.id ).html(''); 2840 } else { 2841 up.settings.multipart_params[file.target_name] = response.data; 2842 } 2843 }); 2844 2845 uploader.bind('FileUploaded', function(up, file, result) { 2846 if (!up.getFile(file.id)) { 2847 // The file has been removed from the queue. 2848 return; 2849 } 2850 2851 var response = $.secureEvalJSON(result.response); 2852 if (response.status == "error") { 2853 addMessage(up.settings.gf_vars.message_id, file.name + " - " + response.error.message); 2854 $('#' + file.id).html(''); 2855 toggleLimitReached(up.settings); 2856 return; 2857 } 2858 2859 var uploadedName = rgars(response, 'data/uploaded_filename'); 2860 var html = '<span class="gfield_fileupload_filename">' + htmlEncode(uploadedName) + '</span><span class="gfield_fileupload_filesize">' + plupload.formatSize(file.size) + '</span>'; 2861 html += '<span class="gfield_fileupload_progress gfield_fileupload_progress_complete"><span class="gfield_fileupload_progressbar"><span class="gfield_fileupload_progressbar_progress"></span></span><span class="gfield_fileupload_percent">' + file.percent + '%</span></span>'; 2862 var formId = up.settings.multipart_params.form_id; 2863 var fieldId = up.settings.multipart_params.field_id; 2864 2865 if (typeof gf_legacy !== 'undefined' && gf_legacy.is_legacy) { 2866 html = "<img " 2867 + "class='gform_delete' " 2868 + "src='" + imagesUrl + "/delete.png' " 2869 + "onclick='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);' " 2870 + "onkeypress='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);' " 2871 + "alt='" + strings.delete_file + "' " 2872 + "title='" + strings.delete_file 2873 + "' /> " 2874 + html; 2875 } else { 2876 html = html + "<button class='gform_delete_file gform-theme-button gform-theme-button--simple' onclick='gformDeleteUploadedFile(" + formId + "," + fieldId + ", this);'><span class='dashicons dashicons-trash' aria-hidden='true'></span><span class='screen-reader-text'>" + strings.delete_file + ': ' + htmlEncode(uploadedName) + "</span></button>"; 2877 } 2878 2879 /** 2880 * Allows the markup for the file to be overridden. 2881 * 2882 * @since 1.9 2883 * @since 2.4.23 Added the response param. 2884 * 2885 * @param {string} html The HTML for the file name and delete button. 2886 * @param {object} file The file upload properties. See: https://www.plupload.com/docs/v2/File. 2887 * @param {object} up The uploader properties. See: https://www.plupload.com/docs/v2/Uploader. 2888 * @param {object} strings Localized strings relating to file uploads. 2889 * @param {string} imagesURL The base URL to the Gravity Forms images directory. 2890 * @param {object} response The response from GFAsyncUpload. 2891 */ 2892 html = gform.applyFilters('gform_file_upload_markup', html, file, up, strings, imagesUrl, response); 2893 2894 $('#' + file.id).html(html); 2895 $('#' + file.id + ' span.gfield_fileupload_progressbar_progress').css('width', file.percent + '%'); 2896 2897 if (file.percent == 100) { 2898 if (response.status && response.status == 'ok') { 2899 response.data.id = file.id; 2900 addFile(fieldId, response.data); 2901 } else { 2902 addMessage(up.settings.gf_vars.message_id, strings.unknown_error + ': ' + file.name); 2903 } 2904 } 2905 2906 }); 2907 2908 uploader.bind('FilesRemoved', function (up, files) { 2909 toggleLimitReached(up.settings); 2910 }); 2911 2912 function getAllFiles(){ 2913 var selector = '#gform_uploaded_files_' + formID, 2914 $uploadedFiles = $(selector), files; 2915 2916 files = $uploadedFiles.val(); 2917 files = (typeof files === "undefined") || files === '' ? {} : $.parseJSON(files); 2918 2919 return files; 2920 } 2921 2922 function getFiles(fieldID){ 2923 var allFiles = getAllFiles(); 2924 var inputName = getInputName(fieldID); 2925 2926 if(typeof allFiles[inputName] == 'undefined') 2927 allFiles[inputName] = []; 2928 return allFiles[inputName]; 2929 } 2930 2931 function countFiles(fieldID){ 2932 var files = getFiles(fieldID); 2933 return files.length; 2934 } 2935 2936 function addFile(fieldID, fileInfo){ 2937 2938 var files = getFiles(fieldID); 2939 2940 files.unshift(fileInfo); 2941 setUploadedFiles(fieldID, files); 2942 } 2943 2944 function setUploadedFiles(fieldID, files){ 2945 var allFiles = getAllFiles(); 2946 var $uploadedFiles = $('#gform_uploaded_files_' + formID); 2947 var inputName = getInputName(fieldID); 2948 allFiles[inputName] = files; 2949 $uploadedFiles.val($.toJSON(allFiles)); 2950 } 2951 2952 function getInputName(fieldID){ 2953 return "input_" + fieldID; 2954 } 2955 2956 // fixes drag and drop in IE10 2957 $("#" + settings.drop_element).on({ 2958 "dragenter": ignoreDrag, 2959 "dragover": ignoreDrag 2960 }); 2961 2962 function ignoreDrag( e ) { 2963 e.preventDefault(); 2964 } 2965 } 2966 2967 2968 function generateUniqueID() { 2969 return 'xxxxxxxx'.replace(/[xy]/g, function (c) { 2970 var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 0x3 | 0x8; 2971 return v.toString(16); 2972 }); 2973 } 2974 2975 function htmlEncode(value){ 2976 return $('<div/>').text(value).html(); 2977 } 2978 2979}(window.gfMultiFileUploader = window.gfMultiFileUploader || {}, jQuery)); 2980 2981 2982//---------------------------------------- 2983//------ GENERAL FUNCTIONS ------- 2984//---------------------------------------- 2985let gformIsSpinnerInitialized = false;
2986function gformInitSpinner(formId, spinnerUrl, isLegacy = true) { 2987 2988 // If already initialized, abort. 2989 if ( gformIsSpinnerInitialized ) { 2990 return; 2991 } 2992 gformIsSpinnerInitialized = true; 2993 2994 // Adding spinner on pre_submission. 2995 window.gform.utils.addFilter( 'gform/submission/pre_submission', ( data ) => { 2996 2997 gformShowSpinner( data.form.dataset.formid, spinnerUrl ); 2998 2999 return data; 3000 }, 3 ); 3001 3002 // Removing spinner if submission is aborted. 3003 document.addEventListener( 'gform/submission/submission_aborted', function( event ) { 3004 3005 // Removing new theme framework spinner. 3006 gformRemoveSpinner(); 3007 3008 // Removing legacy spinner. 3009 jQuery( '#gform_ajax_spinner_' + event.detail.form.dataset.formid ).remove(); 3010 } ); 3011} 3012 3013/** 3014 * Shows the spinner. 3015 * 3016 * @since 2.9.0 3017 * 3018 * @param {int} formId The form id that is being submitted. 3019 * @param {string} spinnerUrl The image to use for the spinner. 3020 * @return {void} 3021 */ 3022function gformShowSpinner( formId, spinnerUrl ) { 3023 3024 let filteredSpinner = gform.applyFilters('gform_spinner_url', spinnerUrl, formId); 3025 let defaultSpinner = gform.applyFilters('gform_spinner_url', gf_global.spinnerUrl, formId); 3026 3027 // Legacy spinner: this is not referring to Legacy Markup, but to the pre-2.7 spinner implementation. 3028 const isLegacy = filteredSpinner !== defaultSpinner; 3029 if ( isLegacy ) { 3030 gformAddSpinner( formId, filteredSpinner ); 3031 return; 3032 } 3033 3034 let $spinnerTarget = gform.applyFilters('gform_spinner_target_elem', jQuery('#gform_submit_button_' + formId + ', #gform_wrapper_' + formId + ' .gform_next_button, #gform_send_resume_link_button_' + formId), formId); 3035 3036 gformInitializeSpinner( formId, $spinnerTarget ); 3037} 3038/** 3039 * @description Initializes the theme-framework-based spinner after the provided target. 3040 * 3041 * @since 2.7 3042 * 3043 * @param {int} formId The ID of the form within which to initialize the spinner. 3044 * @param {object} target The target element after which to inject the spinner. 3045 * @param {string} uniqId A unique ID to use for the spinner - used when removing the spinner. 3046 * 3047 * @return void 3048 */ 3049function gformInitializeSpinner( formId, target, uniqId = 'gform-ajax-spinner' ) { 3050 if (jQuery('#gform_ajax_spinner_' + formId).length == 0) { 3051 var loaderHTML = '<span data-js-spinner-id="' + uniqId + '" id="gform_ajax_spinner_' + formId + '" class="gform-loader"></span>'; 3052 var $spinnerTarget = target instanceof jQuery ? target : jQuery( target ); 3053 $spinnerTarget.after( loaderHTML ); 3054 } 3055} 3056 3057/** 3058 * @description Removes an existing theme-framework-based spinner. 3059 * 3060 * @since 2.7 3061 * 3062 * @param {string} uniqId A unique ID to use for the spinner - used when removing the spinner. 3063 * 3064 * @return void 3065 */ 3066function gformRemoveSpinner( uniqId = 'gform-ajax-spinner' ) { 3067 var spinners = document.querySelectorAll( '[data-js-spinner-id="' + uniqId + '"]' ); 3068 3069 if ( ! spinners ) { 3070 return; 3071 } 3072 3073 // Remove all instances of the spinner. 3074 spinners.forEach( function( spinner ) { 3075 spinner.remove(); 3076 } ); 3077} 3078 3079function gformAddSpinner(formId, spinnerUrl) { 3080 3081 if (typeof spinnerUrl == 'undefined' || !spinnerUrl) { 3082 spinnerUrl = gform.applyFilters('gform_spinner_url', gf_global.spinnerUrl, formId); 3083 } 3084 3085 if (jQuery('#gform_ajax_spinner_' + formId).length == 0) { 3086 /** 3087 * Filter the element after which the AJAX spinner will be inserted. 3088 * 3089 * @since 2.0 3090 * 3091 * @param object $targetElem jQuery object containing all of the elements after which the AJAX spinner will be inserted. 3092 * @param int formId ID of the current form. 3093 */ 3094 var $spinnerTarget = gform.applyFilters('gform_spinner_target_elem', jQuery('#gform_submit_button_' + formId + ', #gform_wrapper_' + formId + ' .gform_next_button, #gform_send_resume_link_button_' + formId), formId); 3095 $spinnerTarget.after('<img id="gform_ajax_spinner_' + formId + '" class="gform_ajax_spinner" src="' + spinnerUrl + '" alt="" />'); 3096 } 3097 3098} 3099 3100//---------------------------------------- 3101//------ TINYMCE FUNCTIONS --------------- 3102//---------------------------------------- 3103 3104/** 3105 * @function gformReInitTinymceInstance 3106 * @description Reinitializes a tinymce instance bound to a gform field if found. 3107 * 3108 * @since 2.5 3109 * 3110 * @param formId {int} Required. The form id. 3111 * @param fieldId {int} Required. The field id. 3112 */ 3113 3114function gformReInitTinymceInstance( formId, fieldId ) { 3115 // check for required arguments 3116 if ( ! formId || ! fieldId ) { 3117 gform.console.error( 'gformReInitTinymceInstance requires a form and field id.' ); 3118 return; 3119 } 3120 // make sure we have tinymce 3121 var tinymce = window.tinymce; 3122 if ( ! tinymce ) { 3123 gform.console.error( 'gformReInitTinymceInstance requires tinymce to be available.' ); 3124 return; 3125 } 3126 // get the editor instance by form and field id and bail if not found 3127 var editor = tinymce.get( 'input_' + formId + '_' + fieldId ); 3128 if ( ! editor ) { 3129 gform.console.error( 'gformReInitTinymceInstance did not find an instance for input_' + formId + '_' + fieldId + '.' ); 3130 return; 3131 } 3132 // get the settings, destroy the instance and reinitialize 3133 var settings = jQuery.extend( {}, editor.settings ); 3134 editor.remove(); 3135 tinymce.init( settings ); 3136 gform.console.log( 'gformReInitTinymceInstance reinitialized TinyMCE on input_' + formId + '_' + fieldId + '.' ); 3137} 3138 3139//---------------------------------------- 3140//------ EVENT FUNCTIONS ----------------- 3141//---------------------------------------- 3142 3143var __gf_keyup_timeout; 3144 3145jQuery( document ).on( 'change keyup', '.gfield input, .gfield select, .gfield textarea', function( event ) { 3146 gf_raw_input_change( event, this ); 3147} ); 3148 3149function gf_raw_input_change( event, elem ) { 3150 3151 // clear regardless of event type for maximum efficiency ;) 3152 clearTimeout( __gf_keyup_timeout ); 3153 3154 var $input = jQuery( elem ), 3155 htmlId = $input.attr( 'id' ),
3156 fieldId = gf_get_input_id_by_html_id( htmlId ), 3157 formId = gf_get_form_id_by_html_id( htmlId ), 3158 /** 3159 * Filter the field meta generated by a raw input change. 3160 * 3161 * @since 2.4.1 3162 * 3163 * @param object fieldMeta An object containing the field ID and form ID of the triggering Gravity Forms field. 3164 * @param object $input The jQuery object for the triggering field element. 3165 * @param object event The raw JS event. 3166 */ 3167 fieldMeta = gform.applyFilters( 'gform_field_meta_raw_input_change', { fieldId: fieldId, formId: formId }, $input, event ); 3168 3169 fieldId = fieldMeta.fieldId; 3170 formId = fieldMeta.formId; 3171 3172 if( ! fieldId ) { 3173 return; 3174 } 3175 3176 var isChangeElem = $input.is( ':checkbox' ) || $input.is( ':radio' ) || $input.is( 'select' ), 3177 isKeyupElem = ! isChangeElem || $input.is( 'textarea' ); 3178 3179 if( event.type == 'keyup' && ! isKeyupElem ) { 3180 return; 3181 } else if( event.type == 'change' && ! isChangeElem && ! isKeyupElem ) { 3182 return; 3183 } 3184 3185 if( event.type == 'keyup' ) { 3186 __gf_keyup_timeout = setTimeout( function() { 3187 gf_input_change( elem, formId, fieldId ); 3188 }, 300 ); 3189 } else { 3190 gf_input_change( elem, formId, fieldId ); 3191 } 3192 3193} 3194 3195/** 3196 * Get the input id from a form element's HTML id. 3197 * 3198 * @param {string} htmlId The HTML id of a form element. 3199 * 3200 * @returns {string} inputId The input id. 3201 */ 3202function gf_get_input_id_by_html_id( htmlId ) { 3203 3204 var ids = gf_get_ids_by_html_id( htmlId ), 3205 id = ids[ ids.length - 1 ]; 3206 3207 if ( ids.length == 3 ) { 3208 ids.shift(); 3209 id = ids.join( '.' ); 3210 } 3211 3212 return id; 3213} 3214 3215/** 3216 * Get the form id from a form element's HTML id. 3217 * 3218 * @param {string} htmlId The HTML id of a form element. 3219 * 3220 * @returns {string} formId The form id. 3221 */ 3222function gf_get_form_id_by_html_id( htmlId ) { 3223 var ids = gf_get_ids_by_html_id( htmlId ); 3224 return ids[0]; 3225} 3226 3227/** 3228 * Get the form, field, and input id by a form elements HTML id. 3229 * 3230 * Note: Only multi-input fields will be return an input ID. 3231 * 3232 * @param {string} htmlId The HTML id of a form element. 3233 * 3234 * @returns {array} ids An array contain the form, field and input id. 3235 */ 3236function gf_get_ids_by_html_id( htmlId ) { 3237 var ids = htmlId ? htmlId.split( '_' ) : []; 3238 for( var i = ids.length - 1; i >= 0; i-- ) { 3239 if ( ! gform.utils.isNumber( ids[ i ] ) ) { 3240 ids.splice( i, 1 ); 3241 } 3242 } 3243 return ids; 3244} 3245 3246function gf_input_change( elem, formId, fieldId ) { 3247 gform.doAction( 'gform_input_change', elem, formId, fieldId ); 3248} 3249 3250function gformExtractFieldId( inputId ) { 3251 var fieldId = parseInt( inputId.toString().split( '.' )[0],10 ); 3252 return ! fieldId ? inputId : fieldId; 3253} 3254 3255function gformExtractInputIndex( inputId ) { 3256 var inputIndex = parseInt( inputId.toString().split( '.' )[1],10 ); 3257 return ! inputIndex ? false : inputIndex; 3258} 3259 3260 3261 3262//---------------------------------------- 3263//------ HELPER FUNCTIONS ---------------- 3264//---------------------------------------- 3265 3266if( ! window['rgars'] ) { 3267 function rgars( array, prop ) { 3268 3269 var props = prop.split( '/' ), 3270 value = array; 3271 3272 for( var i = 0; i < props.length; i++ ) { 3273 value = rgar( value, props[ i ] ); 3274 } 3275 3276 return value; 3277 } 3278} 3279 3280if( ! window['rgar'] ) { 3281 function rgar( array, prop ) { 3282 if ( typeof array[ prop ] != 'undefined' ) { 3283 return array[ prop ]; 3284 } 3285 return ''; 3286 } 3287} 3288 3289if ( ! String.prototype.gformFormat ) { 3290 String.prototype.gformFormat = function() { 3291 var args = arguments; 3292 return this.replace( /{(\d+)}/g, function( match, number ) { 3293 return typeof args[ number ] != 'undefined' ? args[ number ] : match; 3294 } ); 3295 }; 3296} 3297 3298 3299/** 3300 * Toggle the dropdown submenus in the form editor menu bar. 3301 * 3302 * @since 2.5 3303 */ 3304jQuery( document ).ready( function() { 3305 jQuery( '#gform-form-toolbar__menu' ) 3306 .on( 'mouseenter focus', '> li',function() { 3307 jQuery( this ).find( '.gform-form-toolbar__submenu' ).toggleClass( 'open' ); 3308 jQuery( this ).find( '.has_submenu' ).toggleClass( 'submenu-open' ); 3309 } ); 3310 jQuery( '#gform-form-toolbar__menu' ) 3311 .on( 'mouseleave blur', '> li',function() { 3312 jQuery( '.gform-form-toolbar__submenu.open' ).removeClass( 'open' ); 3313 jQuery( '.has_submenu.submenu-open' ).removeClass( 'submenu-open' ); 3314 } ); 3315 jQuery( '#gform-form-toolbar__menu .has_submenu' ) 3316 .on( 'click', function( e ) { 3317 e.preventDefault(); 3318 } ); 3319} ); 3320 3321/** 3322 * Add a containing class to fields with multiple inputs that we want to display inline. 3323 * 3324 * @since 2.5 3325 */ 3326jQuery( document ).ready( function() { 3327 var settingsFields = jQuery( '.gform-settings-field' ); 3328 settingsFields.each( function() { 3329 if ( jQuery( this ).find( '> .gform-settings-input__container' ).length > 1 ) { 3330 jQuery( this ).addClass( 'gform-settings-field--multiple-inputs' ); 3331 } 3332 } ); 3333} ); 3334 3335jQuery( function() { 3336 gform.tools.trigger( 'gform_main_scripts_loaded' ); 3337} );
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.