Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions classes/helpers/FrmAppHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3985,6 +3985,10 @@ public static function localize_script( $location ) {

self::add_form_builder_modal_data( $admin_script_strings );

if ( self::is_form_builder_page() ) {
$admin_script_strings['currency'] = FrmCurrencyHelper::get_currency();
}

/**
* @param array $admin_script_strings
*/
Expand Down
2 changes: 1 addition & 1 deletion js/formidable_admin.js

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions js/src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6148,6 +6148,12 @@ window.frmAdminBuildJS = function() {
return;
}

if ( isSingleProductField( fieldId ) ) {
updateSingleProductLabel( fieldId );
adjustConditionalLogicOptionOrders( fieldId );
return;
}

if ( input.is( 'select' ) ) {
const placeholder = document.getElementById( `frm_placeholder_${ fieldId }` );
if ( ! placeholder || placeholder.value === '' ) {
Expand Down Expand Up @@ -6178,6 +6184,93 @@ window.frmAdminBuildJS = function() {
adjustConditionalLogicOptionOrders( fieldId );
}

/**
* Format a product price value for display in the builder preview using the
* currency settings from frm_admin_js. Mirrors the logic in FrmCurrencyHelper::format_price().
*
* @since x.x
*
* @param {string|number} price Raw price value.
* @return {string} Formatted price string.
*/
function formatProductPrice( price ) {
const currency = frm_admin_js?.currency;
if ( ! currency ) {
return String( price );
}

const num = Number( price );
if ( isNaN( num ) ) {
return String( price );
}

const decimals = Number( currency.decimals ?? 2 );
const decimalSep = currency.decimal_separator ?? '.';
const thousandSep = currency.thousand_separator ?? ',';

let formatted = num.toFixed( decimals ).replace( '.', decimalSep );

const parts = decimals > 0 ? formatted.split( decimalSep ) : [ formatted ];
if ( thousandSep ) {
parts[ 0 ] = parts[ 0 ].replace( /\B(?=(\d{3})+(?!\d))/g, thousandSep );
}
formatted = parts.join( decimalSep );

const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : '';
const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : '';

return leftSymbol + formatted + rightSymbol;
}

/**
* @since x.x
*
* @param {string|number} fieldId
* @return {boolean} True if the field data type is 'single' product.
*/
function isSingleProductField( fieldId ) {
const el = document.querySelector( `select[name="field_options[data_type_${ fieldId }]"]` );
return Boolean( el ) && el.value === 'single';
}

/**
* Update the .frm_single_product_label text in the builder preview to reflect
* the current name and price values of the first product option.
*
* @since x.x
*
* @param {string|number} fieldId
*/
function updateSingleProductLabel( fieldId ) {
const labelEl = document.querySelector( `#field_${ fieldId }_inner_container .frm_single_product_label` );
if ( ! labelEl ) {
return;
}

const firstRealOpt = document.querySelector( `#frm_field_${ fieldId }_opts .frm_single_option:not(.frm_option_template)` );
if ( ! firstRealOpt ) {
return;
}

const firstOptKey = firstRealOpt.dataset.optkey;
const optWrapper = document.getElementById( `frm_delete_field_${ fieldId }-${ firstOptKey }_container` );
if ( ! optWrapper ) {
return;
}

const label = optWrapper.querySelector( `.field_${ fieldId }_option` )?.value ?? '';
const price = optWrapper.querySelector( '.frm_product_price' )?.value ?? '';

const parts = [];
if ( label ) {
parts.push( label );
}
if ( price ) {
parts.push( formatProductPrice( price ) );
}
labelEl.innerHTML = purifyHtml( parts.join( ': ' ) );
Comment on lines +6244 to +6271
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Update the hidden product input along with the label.

The single-product template carries the selected product price on the hidden input as data-frmprice (classes/views/frm-fields/front-end/product-single.php:35). This path updates only the visible label, so live totals/calculations can keep reading a stale hidden input price after a builder price edit.

Proposed fix
 function updateSingleProductLabel( fieldId ) {
 	const labelEl = document.querySelector( `#field_${ fieldId }_inner_container .frm_single_product_label` );
 	if ( ! labelEl ) {
 		return;
 	}
 
-	const firstRealOpt = document.querySelector( `#frm_field_${ fieldId }_opts .frm_single_option:not(.frm_option_template)` );
-	if ( ! firstRealOpt ) {
-		return;
-	}
-
-	const firstOptKey = firstRealOpt.dataset.optkey;
-	const optWrapper  = document.getElementById( `frm_delete_field_${ fieldId }-${ firstOptKey }_container` );
-	if ( ! optWrapper ) {
+	const firstOpt = getMultipleOpts( fieldId )[ 0 ];
+	if ( ! firstOpt ) {
 		return;
 	}
 
-	const label = optWrapper.querySelector( `.field_${ fieldId }_option` )?.value ?? '';
-	const price = optWrapper.querySelector( '.frm_product_price' )?.value ?? '';
-
 	const parts = [];
-	if ( label ) {
-		parts.push( label );
+	if ( firstOpt.label ) {
+		parts.push( firstOpt.label );
 	}
-	if ( price ) {
-		parts.push( formatProductPrice( price ) );
+	if ( firstOpt.price ) {
+		parts.push( formatProductPrice( firstOpt.price ) );
 	}
 	labelEl.innerHTML = purifyHtml( parts.join( ': ' ) );
+
+	const metaInput = document.querySelector( `[name="item_meta[${ fieldId }]"]` );
+	if ( metaInput ) {
+		metaInput.value = firstOpt.saved;
+		metaInput.dataset.frmprice = firstOpt.price || '';
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function updateSingleProductLabel( fieldId ) {
const labelEl = document.querySelector( `#field_${ fieldId }_inner_container .frm_single_product_label` );
if ( ! labelEl ) {
return;
}
const firstRealOpt = document.querySelector( `#frm_field_${ fieldId }_opts .frm_single_option:not(.frm_option_template)` );
if ( ! firstRealOpt ) {
return;
}
const firstOptKey = firstRealOpt.dataset.optkey;
const optWrapper = document.getElementById( `frm_delete_field_${ fieldId }-${ firstOptKey }_container` );
if ( ! optWrapper ) {
return;
}
const label = optWrapper.querySelector( `.field_${ fieldId }_option` )?.value ?? '';
const price = optWrapper.querySelector( '.frm_product_price' )?.value ?? '';
const parts = [];
if ( label ) {
parts.push( label );
}
if ( price ) {
parts.push( formatProductPrice( price ) );
}
labelEl.innerHTML = purifyHtml( parts.join( ': ' ) );
function updateSingleProductLabel( fieldId ) {
const labelEl = document.querySelector( `#field_${ fieldId }_inner_container .frm_single_product_label` );
if ( ! labelEl ) {
return;
}
const firstOpt = getMultipleOpts( fieldId )[ 0 ];
if ( ! firstOpt ) {
return;
}
const parts = [];
if ( firstOpt.label ) {
parts.push( firstOpt.label );
}
if ( firstOpt.price ) {
parts.push( formatProductPrice( firstOpt.price ) );
}
labelEl.innerHTML = purifyHtml( parts.join( ': ' ) );
const metaInput = document.querySelector( `[name="item_meta[${ fieldId }]"]` );
if ( metaInput ) {
metaInput.value = firstOpt.saved;
metaInput.dataset.frmprice = firstOpt.price || '';
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/src/admin/admin.js` around lines 6244 - 6271, In updateSingleProductLabel,
after you read price from optWrapper, also locate the hidden product input
inside optWrapper (e.g., const hiddenInput =
optWrapper.querySelector('input[type="hidden"][data-frmprice]') ) and update its
stored price so live calculations use the new value; set
hiddenInput.dataset.frmprice = price and hiddenInput.value = price (guarding for
hiddenInput existence) before updating labelEl.innerHTML so both the visible
label and the hidden price attribute stay in sync.

}

/**
* Returns an object that has a value and label for new conditional logic option, for a given option value.
*
Expand Down
Loading