PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | js/formidable.js +504 -40 6.29 → trunk View file →
@@ -281,11 +281,14 @@
281 281 if ( field.type === 'url' ) {
282 282 maybeAddHttpsToUrl( field );
283 283 }
284 284 const form = field.closest( 'form' );
285 - if ( form && hasClass( form, 'frm_js_validate' ) ) {
286 - validateField( field );
285 + if ( ! form ) {
286 + return;
287 287 }
288 +
289 + // Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
290 + validateField( field, hasClass( form, 'frm_js_validate' ) );
288 291 }
289 292
290 293 /**
291 294 * @param {HTMLElement} field
@@ -300,13 +303,18 @@
300 303
301 304 /**
302 305 * Validate a field with JS.
303 306 *
307 + * Removing stale errors is universal. Adding errors only happens when JS validation is enabled.
308 + *
309 + * @since 6.32 Added the `addErrors` parameter.
310 + *
304 311 * @param {HTMLElement} field
312 + * @param {boolean} addErrors Whether to add new errors. Defaults to `true`.
305 313 *
306 314 * @return {void}
307 315 */
308 - function validateField( field ) {
316 + function validateField( field, addErrors = true ) {
309 317 let errors;
310 318 let key;
311 319
312 320 errors = [];
@@ -324,13 +332,20 @@
324 332 if ( errors.length < 1 ) {
325 333 validateFieldValue( field, errors, false );
326 334 }
327 335
328 - removeFieldError( fieldContainer );
329 - if ( Object.keys( errors ).length > 0 ) {
330 - for ( key in errors ) {
331 - addFieldError( fieldContainer, key, errors );
336 + const hasErrors = Object.keys( errors ).length > 0;
337 +
338 + if ( addErrors ) {
339 + removeFieldError( fieldContainer );
340 + if ( hasErrors ) {
341 + for ( key in errors ) {
342 + addFieldError( fieldContainer, key, errors );
343 + }
332 344 }
345 + } else if ( ! hasErrors ) {
346 + // JS validation is off, so only remove existing errors once the field passes validation.
347 + removeFieldError( fieldContainer );
333 348 }
334 349 }
335 350
336 351 /**
@@ -605,9 +620,9 @@
605 620 const strippedId = field.id.replace( 'conf_', '' );
606 621 const strippedFieldID = fieldID.replace( 'conf_', '' );
607 622 const confirmField = document.getElementById( strippedId.replace( 'field_', 'field_conf_' ) );
608 623
609 - if ( confirmField === null || errors[ `conf_${ strippedFieldID }` ] !== undefined ) {
624 + if ( ! confirmField || errors[ `conf_${ strippedFieldID }` ] !== undefined ) {
610 625 return;
611 626 }
612 627
613 628 if ( fieldID !== strippedFieldID ) {
@@ -763,9 +778,9 @@
763 778 }
764 779
765 780 if ( response.length === 0 ) {
766 781 const fieldContainer = recaptcha.closest( '.frm_form_field' );
767 - if ( fieldContainer && fieldContainer.id ) {
782 + if ( fieldContainer?.id ) {
768 783 const fieldID = fieldContainer.id.replace( 'frm_field_', '' ).replace( '_container', '' );
769 784 errors[ fieldID ] = '';
770 785 }
771 786 }
@@ -882,8 +897,10 @@
882 897 response = defaultResponse;
883 898 }
884 899 }
885 900
901 + let willRedirect = false;
902 +
886 903 if ( response.redirect !== undefined ) {
887 904 if ( shouldTriggerEvent ) {
888 905 triggerCustomEvent( object, 'frmSubmitEvent' );
889 906 return;
@@ -895,8 +912,10 @@
895 912 }, 1000 * response.delay );
896 913 } else {
897 914 doRedirect( response );
898 915 }
916 +
917 + willRedirect = true;
899 918 }
900 919
901 920 if ( 'string' === typeof response.content && response.content !== '' ) {
902 921 // the form or success message was returned
@@ -939,9 +958,9 @@
939 958 afterFormSubmitted( object, response );
940 959 },
941 960 delay
942 961 );
943 - } else if ( Object.keys( response.errors ).length ) {
962 + } else if ( response.errors !== undefined && Object.keys( response.errors ).length ) {
944 963 // errors were returned
945 964 removeSubmitLoading( jQuery( object ), 'enable' );
946 965
947 966 //show errors
@@ -955,12 +974,12 @@
955 974 $fieldCont = fieldContEl ? jQuery( fieldContEl ) : jQuery();
956 975
957 976 if ( $fieldCont.length ) {
958 977 if ( ! $fieldCont.is( ':visible' ) ) { // eslint-disable-line no-jquery/no-is
959 - const inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' ); // eslint-disable-line no-jquery/no-closest
978 + const inCollapsedSection = $fieldCont.closest( '.frm_toggle_container' ); // eslint-disable-line no-jquery/no-closest, formidable/no-jquery-variable-methods
960 979 if ( inCollapsedSection.length ) {
961 980 let frmTrigger = inCollapsedSection.prev();
962 - if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) {
981 + if ( ! frmTrigger.hasClass( 'frm_trigger' ) ) { // eslint-disable-line formidable/no-jquery-variable-methods
963 982 // If the frmTrigger object is the section description, check to see if the previous element is the trigger
964 983 frmTrigger = frmTrigger.prev( '.frm_trigger' );
965 984 }
966 985 frmTrigger.trigger( 'click' );
@@ -1006,10 +1025,10 @@
1006 1025 } else {
1007 1026 object.insertAdjacentHTML( 'afterbegin', response.error_message );
1008 1027 checkForErrorsAndMaybeSetFocus();
1009 1028 }
1010 - } else {
1011 - // there may have been a plugin conflict, or the form is not set to submit with ajax
1029 + } else if ( ! willRedirect ) { // Avoid double submission if redirecting to a page.
1030 + // There may have been a plugin conflict, or the form is not set to submit with ajax.
1012 1031
1013 1032 showFileLoading( object );
1014 1033
1015 1034 object.submit();
@@ -1139,12 +1158,12 @@
1139 1158 return;
1140 1159 }
1141 1160
1142 1161 container.classList.add( 'frm_blank_field' );
1143 - const input = container.querySelector( 'input, select, textarea' );
1144 - const id = getErrorElementId( key, input );
1162 + const inputs = container.querySelectorAll( 'input, select, textarea' );
1163 + const id = getErrorElementId( key, inputs[ 0 ] );
1145 1164
1146 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1165 + let describedBy;
1147 1166
1148 1167 if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase
1149 1168 frmThemeOverride_frmPlaceError( key, jsErrors );
1150 1169 } else {
@@ -1155,10 +1174,10 @@
1155 1174 const roleString = frm_js.include_alert_role ? 'role="alert"' : '';
1156 1175 errorHtml = `<div class="frm_error" ${ roleString } id="${ id }">${ jsErrors[ key ] }</div>`;
1157 1176 }
1158 1177 container.insertAdjacentHTML( 'beforeend', errorHtml );
1159 -
1160 - if ( input ) {
1178 + inputs.forEach( input => {
1179 + describedBy = input.getAttribute( 'aria-describedby' );
1161 1180 if ( ! describedBy ) {
1162 1181 describedBy = id;
1163 1182 } else if ( ! describedBy.includes( id ) && ! describedBy.includes( 'frm_error_field_' ) ) {
1164 1183 const { errorFirst } = input.dataset;
@@ -1168,12 +1187,12 @@
1168 1187 describedBy = `${ id } ${ describedBy }`;
1169 1188 }
1170 1189 }
1171 1190 input.setAttribute( 'aria-describedby', describedBy );
1172 - }
1191 + } );
1173 1192 }
1174 1193
1175 - if ( input ) {
1194 + inputs.forEach( input => {
1176 1195 if ( [ 'radio', 'checkbox' ].includes( input.type ) ) {
1177 1196 const group = input.closest( '[role="radiogroup"], [role="group"]' );
1178 1197 if ( group ) {
1179 1198 group.setAttribute( 'aria-invalid', 'true' );
@@ -1180,9 +1199,9 @@
1180 1199 }
1181 1200 } else {
1182 1201 input.setAttribute( 'aria-invalid', 'true' );
1183 1202 }
1184 - }
1203 + } );
1185 1204
1186 1205 jQuery( document ).trigger( 'frmAddFieldError', [ jQuery( container ), key, jsErrors ] );
1187 1206 }
1188 1207
@@ -1214,11 +1233,9 @@
1214 1233 return;
1215 1234 }
1216 1235
1217 1236 const errorMessage = container.querySelector( '.frm_error' );
1218 - const errorId = errorMessage ? errorMessage.id : '';
1219 1237 const input = container.querySelector( 'input, select, textarea' );
1220 - let describedBy = input ? input.getAttribute( 'aria-describedby' ) : null;
1221 1238
1222 1239 container.classList.remove( 'frm_blank_field', 'has-error' );
1223 1240
1224 1241 if ( input ) {
@@ -1232,20 +1249,35 @@
1232 1249 }
1233 1250 }
1234 1251
1235 1252 if ( errorMessage ) {
1253 + removeElementFromInputDescribedBy( errorMessage );
1236 1254 errorMessage.remove();
1237 1255 }
1256 + }
1238 1257
1239 - if ( input ) {
1258 + /**
1259 + * Updates the aria-describedby attribute, removing the target element ID.
1260 + *
1261 + * @since 6.32
1262 + *
1263 + * @param {HTMLElement} el The target element that is removed from the aria-describedby data.
1264 + * @return {void}
1265 + */
1266 + function removeElementFromInputDescribedBy( el ) {
1267 + document.querySelectorAll( `[aria-describedby*="${ el.id }"]` ).forEach( input => {
1268 + let ariaDescribedBy = input.getAttribute( 'aria-describedby' ).split( ' ' );
1269 + ariaDescribedBy = ariaDescribedBy.filter( value => {
1270 + const trimmedValue = value.trim();
1271 + return trimmedValue && trimmedValue !== el.id;
1272 + } );
1273 +
1274 + if ( ariaDescribedBy.length ) {
1275 + input.setAttribute( 'aria-describedby', ariaDescribedBy.join( ' ' ) );
1276 + return;
1277 + }
1240 1278 input.removeAttribute( 'aria-describedby' );
1241 - if ( describedBy ) {
1242 - describedBy = describedBy.replace( errorId, '' ).trim();
1243 - if ( describedBy ) {
1244 - input.setAttribute( 'aria-describedby', describedBy );
1245 - }
1246 - }
1247 - }
1279 + } );
1248 1280 }
1249 1281
1250 1282 function removeAllErrors() {
1251 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
@@ -1250,9 +1282,12 @@
1250 1282 function removeAllErrors() {
1251 1283 document.querySelectorAll( '.form-field' ).forEach( field => {
1252 1284 field.classList.remove( 'frm_blank_field', 'has-error' );
1253 1285 } );
1254 - document.querySelectorAll( '.form-field .frm_error' ).forEach( error => error.remove() );
1286 + document.querySelectorAll( '.form-field .frm_error' ).forEach( el => {
1287 + removeElementFromInputDescribedBy( el );
1288 + el.remove();
1289 + } );
1255 1290 document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
1256 1291 }
1257 1292
1258 1293 /**
@@ -1276,9 +1311,9 @@
1276 1311 disableSaveDraft( $object );
1277 1312 }
1278 1313
1279 1314 function showLoadingIndicator( $object ) {
1280 - if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) { // eslint-disable-line no-jquery/no-class
1315 + if ( ! $object.hasClass( 'frm_loading_form' ) && ! $object.hasClass( 'frm_loading_prev' ) ) { // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1281 1316 addLoadingClass( $object );
1282 1317 $object.trigger( 'frmStartFormLoading' );
1283 1318 }
1284 1319 }
@@ -1285,13 +1320,13 @@
1285 1320
1286 1321 function addLoadingClass( $object ) {
1287 1322 const loadingClass = isGoingToPrevPage( $object ) ? 'frm_loading_prev' : 'frm_loading_form';
1288 1323
1289 - $object.addClass( loadingClass ); // eslint-disable-line no-jquery/no-class
1324 + $object.addClass( loadingClass ); // eslint-disable-line no-jquery/no-class, formidable/no-jquery-variable-methods
1290 1325 }
1291 1326
1292 1327 function isGoingToPrevPage( $object ) {
1293 - return ( typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object ) );
1328 + return typeof frmProForm !== 'undefined' && frmProForm.goingToPreviousPage( $object );
1294 1329 }
1295 1330
1296 1331 function removeSubmitLoading( _, enable, processesRunning ) {
1297 1332 if ( processesRunning > 0 ) {
@@ -1310,9 +1345,9 @@
1310 1345 }
1311 1346
1312 1347 function showFileLoading( object ) {
1313 1348 const loading = document.getElementById( 'frm_loading' );
1314 - if ( loading === null ) {
1349 + if ( ! loading ) {
1315 1350 return;
1316 1351 }
1317 1352
1318 1353 const fileInput = object.querySelector( 'input[type=file]' );
@@ -1341,9 +1376,9 @@
1341 1376 */
1342 1377 function onHoneypotFieldChange() {
1343 1378 /*jshint validthis:true */
1344 1379 const css = window.getComputedStyle( this ).boxShadow;
1345 - if ( css && css.match( /inset/ ) ) {
1380 + if ( css?.match( /inset/ ) ) {
1346 1381 this.remove();
1347 1382 }
1348 1383 }
1349 1384
@@ -1555,8 +1590,9 @@
1555 1590 );
1556 1591 }
1557 1592
1558 1593 checkDropdownLabel();
1594 + calcProductsTotal();
1559 1595 };
1560 1596
1561 1597 runOnLoad( true );
1562 1598
@@ -1730,8 +1766,431 @@
1730 1766 label.htmlFor = captchaResponse.id;
1731 1767 }
1732 1768 }
1733 1769
1770 + /**
1771 + * Check to make sure the quantity field value is within the min and max values.
1772 + *
1773 + * @param {HTMLElement} input
1774 + * @return {number} The quantity value.
1775 + */
1776 + function checkQuantityFieldMinMax( input ) {
1777 + if ( '' === input.value ) {
1778 + // Leave the value if it is empty.
1779 + return 0;
1780 + }
1781 +
1782 + const val = parseFloat( input.value ? input.value.trim() : 0 );
1783 + if ( isNaN( val ) ) {
1784 + return 0;
1785 + }
1786 +
1787 + let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0;
1788 + let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0;
1789 +
1790 + max = isNaN( max ) ? 0 : max;
1791 + min = isNaN( min ) ? 0 : Math.max( 0, min );
1792 +
1793 + if ( val < min ) {
1794 + input.value = min;
1795 + return min;
1796 + }
1797 +
1798 + if ( 0 !== max && val > max ) {
1799 + input.value = max;
1800 + return max;
1801 + }
1802 +
1803 + return val;
1804 + }
1805 +
1806 + function triggerChange( input, fieldKey ) {
1807 + if ( fieldKey === undefined ) {
1808 + fieldKey = 'dependent';
1809 + }
1810 +
1811 + jQuery( input ).trigger( { type: 'change', selfTriggered: true, frmTriggered: fieldKey } );
1812 + }
1813 +
1814 + /**
1815 + * Calculates the total price.
1816 + *
1817 + * @param {Event|undefined} e The event object.
1818 + * @return {void}
1819 + */
1820 + function calcProductsTotal( e ) {
1821 + if ( 'object' === typeof frmProForm ) {
1822 + // Pro is installed, use the Pro JS.
1823 + return;
1824 + }
1825 +
1826 + if ( typeof __FRMCURR === 'undefined' ) {
1827 + return;
1828 + }
1829 +
1830 + const totalFields = document.querySelectorAll( '[data-frmtotal]' );
1831 + if ( ! totalFields.length ) {
1832 + return;
1833 + }
1834 +
1835 + const formTotals = [];
1836 +
1837 + totalFields.forEach( totalField => {
1838 + let total = 0;
1839 + const form = totalField.closest( 'form' );
1840 +
1841 + if ( ! form ) {
1842 + return;
1843 + }
1844 +
1845 + const formId = form.querySelector( 'input[name="form_id"]' ).value;
1846 + const currency = getCurrency( formId );
1847 +
1848 + if ( undefined !== formTotals[ formId ] ) {
1849 + total = formTotals[ formId ];
1850 + } else {
1851 + form.querySelectorAll( 'input[data-frmprice],select:has([data-frmprice])' ).forEach( function( input ) {
1852 + let quantity = 0;
1853 + let price = 0;
1854 + const isSingle = 'hidden' === input.type;
1855 +
1856 + if ( input.tagName === 'SELECT' ) {
1857 + if ( input.selectedIndex !== -1 ) {
1858 + price = input.options[ input.selectedIndex ].getAttribute( 'data-frmprice' );
1859 + }
1860 + } else {
1861 + if ( ! isSingle && ! input.matches( ':checked' ) ) {
1862 + return;
1863 + }
1864 + price = input.getAttribute( 'data-frmprice' );
1865 + }
1866 +
1867 + if ( ! price ) {
1868 + price = 0;
1869 + } else {
1870 + price = preparePrice( price, currency );
1871 + quantity = getQuantity( input );
1872 + price = parseFloat( quantity ) * parseFloat( price );
1873 + }
1874 +
1875 + if ( 'true' === input.getAttribute( 'data-frmdiscount' ) ) {
1876 + price = price * -1;
1877 + }
1878 +
1879 + total += price;
1880 + } );
1881 +
1882 + formTotals[ formId ] = total;
1883 + }
1884 +
1885 + total = isNaN( total ) ? 0 : total;
1886 +
1887 + // Set a decimal separator for currency if no default for it
1888 + currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s)
1889 + if ( ! currency.decimal_separator.length ) {
1890 + currency.decimal_separator = '.';
1891 + }
1892 +
1893 + totalField.value = roundTotal( total, currency );
1894 + total = normalizeTotal( total, currency );
1895 +
1896 + // because of e.g. fields that might be using this field for calculations
1897 + triggerChange( totalField );
1898 +
1899 + total = formatCurrency( total, currency );
1900 + const formatted = totalField.previousElementSibling;
1901 + if ( formatted?.matches( '.frm_total_formatted' ) ) {
1902 + // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1903 + formatted.innerHTML = total;
1904 + return;
1905 + }
1906 +
1907 + const formattedEls = totalField.closest( '.frm_form_field' ).querySelectorAll( '.frm_total_formatted' );
1908 + formattedEls.forEach( formattedEl => {
1909 + // Use innerHTML so that currency symbols like Euros can render and not their encoded string value.
1910 + formattedEl.innerHTML = total;
1911 + } );
1912 + } );
1913 + }
1914 +
1915 + /**
1916 + * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with.
1917 + *
1918 + * @param {number} total The total amount to normalize.
1919 + * @param {Object} currency The currency object containing decimal information.
1920 + * @return {string} The normalized total amount.
1921 + */
1922 + function normalizeTotal( total, currency ) {
1923 + const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1924 +
1925 + total = roundTotal( total, currency );
1926 +
1927 + return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal );
1928 + }
1929 +
1930 + function roundTotal( total, currency ) {
1931 + const isLargeTotal = total > Number.MAX_SAFE_INTEGER;
1932 +
1933 + if ( ! isLargeTotal ) {
1934 + const { decimals } = currency;
1935 + total = decimals > 0 ? round10( total, decimals ) : Math.ceil( total );
1936 + }
1937 +
1938 + return total;
1939 + }
1940 +
1941 + function round10( value, decimals ) {
1942 + return Number( `${ Math.round( `${ value }e${ decimals }` ) }e-${ decimals }` );
1943 + }
1944 +
1945 + /**
1946 + * Format a numeric value according to the specified currency format settings.
1947 + *
1948 + * @param {string} total The numeric value to format.
1949 + * @param {Object} currency The currency formatting configuration.
1950 + * @return {string} The formatted currency string.
1951 + */
1952 + function formatCurrency( total, currency ) {
1953 + total = maybeAddTrailingZeroToPrice( total, currency );
1954 + if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) {
1955 + total = total.substr( 0, total.length - 1 );
1956 + }
1957 +
1958 + total = maybeRemoveTrailingZerosFromPrice( total, currency );
1959 + total = addThousands( total, currency );
1960 +
1961 + const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
1962 + const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';
1963 +
1964 + return `${ leftSymbol }${ total }${ rightSymbol }`;
1965 + }
1966 +
1967 + /**
1968 + * Gets currency from form id.
1969 + *
1970 + * @param {number} formId Form ID.
1971 + * @return {Object} Currency object.
1972 + */
1973 + function getCurrency( formId ) {
1974 + if ( undefined !== window.__FRMCURR && undefined !== window.__FRMCURR[ formId ] ) {
1975 + return window.__FRMCURR[ formId ];
1976 + }
1977 +
1978 + return {
1979 + symbol_left: '$',
1980 + symbol_right: '',
1981 + symbol_padding: '',
1982 + thousand_separator: ',',
1983 + decimal_separator: '.',
1984 + decimals: 2,
1985 + };
1986 + }
1987 +
1988 + /**
1989 + * Gets quantity.
1990 + *
1991 + * @param {HTMLElement} field The field element.
1992 + * @return {number} The quantity.
1993 + */
1994 + function getQuantity( field ) {
1995 + const fieldID = frmFrontForm.getFieldId( field, false );
1996 + if ( ! fieldID ) {
1997 + return 0;
1998 + }
1999 +
2000 + const quantityField = getQuantityField( field, fieldID );
2001 + if ( ! quantityField ) {
2002 + // If there is no quantity field, assume 1.
2003 + return 1;
2004 + }
2005 +
2006 + return checkQuantityFieldMinMax( quantityField );
2007 + }
2008 +
2009 + /**
2010 + * Gets quantity field.
2011 + *
2012 + * @param {HTMLElement} element The element.
2013 + * @param {number} fieldID The field ID.
2014 + * @return {HTMLElement|null} The quantity field.
2015 + */
2016 + function getQuantityField( element, fieldID ) {
2017 + const quantityFields = element.closest( 'form' ).querySelectorAll( '[data-frmproduct]' );
2018 + if ( ! quantityFields.length ) {
2019 + return null;
2020 + }
2021 +
2022 + fieldID = fieldID.toString();
2023 +
2024 + return Array.from( quantityFields ).find( element => {
2025 + let ids;
2026 +
2027 + ids = JSON.parse( element.getAttribute( 'data-frmproduct' ).trim() );
2028 + if ( '' === ids ) {
2029 + return false;
2030 + }
2031 +
2032 + // Convert to array if necessary because of existing fields that are already using single product fields.
2033 + ids = 'string' === typeof ids ? [ ids ] : ids;
2034 + return ids.includes( fieldID );
2035 + } );
2036 + }
2037 +
2038 + /**
2039 + * Prepare a price for calculation.
2040 + *
2041 + * @param {number|string} price The price to prepare.
2042 + * @param {Object} currency The currency object containing decimal information.
2043 + * @return {string} The prepared price.
2044 + */
2045 + function preparePrice( price, currency ) {
2046 + if ( ! price ) {
2047 + return 0;
2048 + }
2049 + price = `${ price }`; // convert to string just to be sure
2050 +
2051 + const regex = getRegexForPrice( currency );
2052 +
2053 + const matches = price.match( regex );
2054 + if ( null === matches ) {
2055 + return 0;
2056 + }
2057 +
2058 + price = matches.length ? matches[ matches.length - 1 ] : 0;
2059 + price = price.trim();
2060 +
2061 + // Fix issues with parsing Fr.15.00. The regex catches .15.00.
2062 + // This checks for the leading decimal and removes it.
2063 + if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[ 0 ] === '.' ) {
2064 + price = price.substr( 1 );
2065 + }
2066 +
2067 + if ( price ) {
2068 + price = maybeUseDecimal( price, currency );
2069 + price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' );
2070 + }
2071 +
2072 + return price;
2073 + }
2074 +
2075 + /**
2076 + * @param {Object} currency The currency object.
2077 + * @return {RegExp} The regular expression object.
2078 + */
2079 + function getRegexForPrice( currency ) {
2080 + let regexString = '[0-9,.';
2081 +
2082 + if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) {
2083 + regexString += currency.thousand_separator;
2084 + }
2085 + if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) {
2086 + regexString += currency.decimal_separator;
2087 + }
2088 +
2089 + regexString += ']*\\.?\\,?[0-9]+';
2090 +
2091 + return new RegExp( regexString, 'g' );
2092 + }
2093 +
2094 + /**
2095 + * Maybe replace the decimal separator with the currency's decimal separator.
2096 + *
2097 + * @param {string} price The price string.
2098 + * @param {Object} currency The currency object.
2099 + * @return {string} The modified price string.
2100 + */
2101 + function maybeUseDecimal( price, currency ) {
2102 + let usedForDecimal;
2103 + let priceParts;
2104 + if ( '.' === currency.thousand_separator ) {
2105 + priceParts = price.split( '.' );
2106 + usedForDecimal = 2 === priceParts.length && 2 === priceParts[ 1 ].length;
2107 + if ( usedForDecimal ) {
2108 + price = price.replace( '.', currency.decimal_separator );
2109 + }
2110 + }
2111 + return price;
2112 + }
2113 +
2114 + /**
2115 + * Add trailing zeros to a price if necessary and replace the decimal separator.
2116 + *
2117 + * @param {number|string} price The price to format.
2118 + * @param {Object} currency The currency object containing the decimal separator.
2119 + * @param {boolean} [force=false] Whether to force processing even if the price is not a number.
2120 + * @return {string} The formatted price string.
2121 + */
2122 + function maybeAddTrailingZeroToPrice( price, currency, force = false ) {
2123 + if ( 'number' !== typeof price && ! force ) {
2124 + return price;
2125 + }
2126 +
2127 + price = String( price ); // first convert to string
2128 + const pos = price.indexOf( '.' );
2129 +
2130 + if ( pos === -1 ) {
2131 + price = `${ price }.`;
2132 +
2133 + for ( let n = 0; n < currency.decimals; ++n ) {
2134 + price += '0';
2135 + }
2136 + } else {
2137 + const decimalsString = price.substring( pos + 1 );
2138 + if ( decimalsString.length < currency.decimals ) {
2139 + if ( decimalsString.length < 2 ) {
2140 + price += '0';
2141 + }
2142 +
2143 + for ( let n = 2; n < currency.decimals; ++n ) {
2144 + price += '0';
2145 + }
2146 + }
2147 + }
2148 +
2149 + return price.replace( '.', currency.decimal_separator );
2150 + }
2151 +
2152 + /**
2153 + * Format a numeric string by adding thousand separators.
2154 + *
2155 + * @param {string|number} price The numeric value to format.
2156 + * @param {Object} options Formatting options.
2157 + * @param {string} options.decimal_separator Character used as decimal separator.
2158 + * @param {string} options.thousand_separator Character used as thousand separator.
2159 + *
2160 + * @return {string} The price string with thousand separators.
2161 + */
2162 + function addThousands( price, options ) {
2163 + const split = options.decimal_separator === ''
2164 + ? [ price.toString() ]
2165 + : price.split( options.decimal_separator );
2166 +
2167 + if ( options.thousand_separator ) {
2168 + split[ 0 ] = split[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, options.thousand_separator );
2169 + }
2170 +
2171 + return split.join( options.decimal_separator );
2172 + }
2173 +
2174 + /**
2175 + * Maybe remove trailing zeros from a price string.
2176 + *
2177 + * @param {string} price The price string.
2178 + * @param {Object} currency The currency data.
2179 + *
2180 + * @return {string} The price string with trailing zeros removed.
2181 + */
2182 + function maybeRemoveTrailingZerosFromPrice( price, currency ) {
2183 + const split = price.split( currency.decimal_separator );
2184 + if ( 2 !== split.length || split[ 1 ].length <= currency.decimals ) {
2185 + return price;
2186 + }
2187 + if ( 0 === currency.decimals ) {
2188 + return split[ 0 ];
2189 + }
2190 + return `${ split[ 0 ] }${ currency.decimal_separator }${ split[ 1 ].substr( 0, currency.decimals ) }`;
2191 + }
2192 +
1734 2193 return {
1735 2194 init() {
1736 2195 jQuery( document ).off( 'submit.formidable', '.frm-show-form' );
1737 2196 jQuery( document ).on( 'submit.formidable', '.frm-show-form', frmFrontForm.submitForm );
@@ -1763,8 +2222,13 @@
1763 2222 jQuery( document ).on(
1764 2223 'frmPageChanged',
1765 2224 destroyhCaptcha
1766 2225 );
2226 +
2227 + jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal );
2228 + jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal );
2229 + jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal );
2230 + calcProductsTotal();
1767 2231 },
1768 2232
1769 2233 getFieldId,
1770 2234
@@ -1900,9 +2364,9 @@
1900 2364 * @return {Array} List of errors.
1901 2365 */
1902 2366 validateFormSubmit( object ) {
1903 2367 const form = object instanceof jQuery ? object.get( 0 ) : object;
1904 - if ( typeof tinyMCE !== 'undefined' && form && form.querySelector( '.wp-editor-wrap' ) ) {
2368 + if ( typeof tinyMCE !== 'undefined' && form?.querySelector( '.wp-editor-wrap' ) ) {
1905 2369 tinyMCE.triggerSave();
1906 2370 }
1907 2371
1908 2372 jsErrors = [];
@@ -2106,9 +2570,9 @@
2106 2570 const captcha = captchas[ c ];
2107 2571 if ( ! formIsVisible ) {
2108 2572 // If the form is not visible, try again later in 400ms.
2109 2573 // This fixes issues where the form fades visible on page load.
2110 - // Or whne the form is inside of a modal.
2574 + // Or when the form is inside of a modal.
2111 2575 const interval = setInterval(
2112 2576 function() {
2113 2577 if ( closestForm && closestForm.offsetParent !== null ) {
2114 2578 frmFrontForm.renderCaptcha( captcha, captchaSelector );