fix wpcfto select and repeater related visibility and validation

This commit is contained in:
dwindown
2025-08-29 19:27:50 +07:00
parent ccb2b1aea1
commit 255da46509
14 changed files with 495 additions and 233 deletions

View File

@@ -596,11 +596,6 @@ jQuery(function($){
}
}
$(document).on('change', '#customer_data select', function() {
var value = $(this).val();
$(this).attr('data-current-value', value);
});
$(document).on('click', '.delete-preview-field', function(e){
e.preventDefault();
$(this).parents('.preview-field').remove();
@@ -760,40 +755,40 @@ jQuery(function($){
$(this).closest('.child-field-title').toggleClass('option-detail-opened');
});
var all_checkbox = $('[type="checkbox"]');
if(all_checkbox.length > 0){
$.each(all_checkbox, function(a,b){
if($(b).val() == 'yes'){
$(b).val('no').trigger('click');
}
});
}
// var all_checkbox = $('[type="checkbox"]');
// if(all_checkbox.length > 0){
// $.each(all_checkbox, function(a,b){
// if($(b).val() == 'yes'){
// $(b).val('no').trigger('click');
// }
// });
// }
function modify_payment_box_behavior() {
// function modify_payment_box_behavior() {
var allbox = $('#payments multi_checkbox');
var checkbox_input = $('.payments-payment input[type=checkbox]');
var checked_value = [];
if(checkbox_input.length > 0){
$.each(checkbox_input, function(x, y){
if($(y).is(':checked')){
checked_value.push($(y).val());
$('[data-field=wpcfto_addon_option_payment_'+$(y).val()+']').show();
}else{
$('[data-field=wpcfto_addon_option_payment_'+$(y).val()+']').hide();
}
});
}
// var allbox = $('#payments multi_checkbox');
// var checkbox_input = $('.payments-payment input[type=checkbox]');
// var checked_value = [];
// if(checkbox_input.length > 0){
// $.each(checkbox_input, function(x, y){
// if($(y).is(':checked')){
// checked_value.push($(y).val());
// $('[data-field=wpcfto_addon_option_payment_'+$(y).val()+']').show();
// }else{
// $('[data-field=wpcfto_addon_option_payment_'+$(y).val()+']').hide();
// }
// });
// }
}
// }
setTimeout(() => {
modify_payment_box_behavior();
}, 500);
// setTimeout(() => {
// modify_payment_box_behavior();
// }, 500);
$(document).on('click', '.payments-payment input[type=checkbox]', function(){
modify_payment_box_behavior();
});
// $(document).on('click', '.payments-payment input[type=checkbox]', function(){
// modify_payment_box_behavior();
// });
$(document).on('mouseover', 'span.grab', function(){
$(this).css('pointer', 'grab');
@@ -807,9 +802,14 @@ jQuery(function($){
$(this).closest('.child-fields-wrapper').find('.the_title').text($(this).val());
});
$(document).on('change', '#customer_data select', function() {
var value = $(this).val();
$(this).attr('data-current-value', value);
});
});
jQuery(function($){
// jQuery(function($){
// setTimeout(() => {
// var autocomplete_fields = $('.wpcfto-box .autocomplete');
@@ -831,116 +831,116 @@ jQuery(function($){
// }, 500);
// });
$( document ).on( 'click', '.add-thumbnail', function( event ) {
// $( document ).on( 'click', '.add-thumbnail', function( event ) {
var gallery_items_frame;
const $el = $( this );
var target_field = $el.attr('data-field');
var target_id = $el.siblings('.'+target_field+'-id');
var target_url = $el.siblings('.'+target_field+'-url');
var selected = target_id.val();
var able_multiple = $el.attr('data-able-multiple');
// var gallery_items_frame;
// const $el = $( this );
// var target_field = $el.attr('data-field');
// var target_id = $el.siblings('.'+target_field+'-id');
// var target_url = $el.siblings('.'+target_field+'-url');
// var selected = target_id.val();
// var able_multiple = $el.attr('data-able-multiple');
event.preventDefault();
// event.preventDefault();
if ( gallery_items_frame ) {
// if ( gallery_items_frame ) {
// Select the attachment when the frame opens
gallery_items_frame.on( 'open', function() {
var selection = gallery_items_frame.state().get( 'selection' );
selection.reset( selected ? [ wp.media.attachment( selected ) ] : [] );
});
// // Select the attachment when the frame opens
// gallery_items_frame.on( 'open', function() {
// var selection = gallery_items_frame.state().get( 'selection' );
// selection.reset( selected ? [ wp.media.attachment( selected ) ] : [] );
// });
// Open the modal.
gallery_items_frame.open();
// // Open the modal.
// gallery_items_frame.open();
return;
}
// return;
// }
// Create the media frame.
gallery_items_frame = wp.media.frames.gallery_items = wp.media({
// Set the title of the modal.
title: 'Choose or upload media',
button: {
text: 'Select'
},
states: [
new wp.media.controller.Library({
title: 'Choose or upload media',
filterable: 'all',
multiple: able_multiple
})
]
});
// // Create the media frame.
// gallery_items_frame = wp.media.frames.gallery_items = wp.media({
// // Set the title of the modal.
// title: 'Choose or upload media',
// button: {
// text: 'Select'
// },
// states: [
// new wp.media.controller.Library({
// title: 'Choose or upload media',
// filterable: 'all',
// multiple: able_multiple
// })
// ]
// });
// Select the attachment when the frame opens
gallery_items_frame.on( 'open', function() {
var selection = gallery_items_frame.state().get( 'selection' );
selection.reset( selected ? [ wp.media.attachment( selected ) ] : [] );
});
// // Select the attachment when the frame opens
// gallery_items_frame.on( 'open', function() {
// var selection = gallery_items_frame.state().get( 'selection' );
// selection.reset( selected ? [ wp.media.attachment( selected ) ] : [] );
// });
gallery_items_frame.on( 'select', function() {
attachment = gallery_items_frame.state().get('selection').first().toJSON();
target_id.val( attachment.id );
target_url.val( attachment.url );
if(target_id.val() !== ''){
// $el.removeClass('text-white').addClass('text-info d-none');
if($el.hasClass('btn')){
$el.siblings('i').hide();
}else{
$el.hide();
}
$el.siblings('img').removeClass('d-none').attr('src', attachment.url).show();
}else{
// $el.removeClass('text-info d-none').addClass('text-white');
if($el.hasClass('btn')){
$el.siblings('i').show();
}else{
$el.show();
}
$el.siblings('img').addClass('d-none').hide();
}
});
// gallery_items_frame.on( 'select', function() {
// attachment = gallery_items_frame.state().get('selection').first().toJSON();
// target_id.val( attachment.id );
// target_url.val( attachment.url );
// if(target_id.val() !== ''){
// // $el.removeClass('text-white').addClass('text-info d-none');
// if($el.hasClass('btn')){
// $el.siblings('i').hide();
// }else{
// $el.hide();
// }
// $el.siblings('img').removeClass('d-none').attr('src', attachment.url).show();
// }else{
// // $el.removeClass('text-info d-none').addClass('text-white');
// if($el.hasClass('btn')){
// $el.siblings('i').show();
// }else{
// $el.show();
// }
// $el.siblings('img').addClass('d-none').hide();
// }
// });
// Open the modal.
gallery_items_frame.open();
// // Open the modal.
// gallery_items_frame.open();
});
// });
$( document ).on( 'click', '.trumbowyg-button-group:has(.trumbowyg-insertImage-button)', function( event ) {
// $( document ).on( 'click', '.trumbowyg-button-group:has(.trumbowyg-insertImage-button)', function( event ) {
var gallery_items_frame;
// var gallery_items_frame;
event.preventDefault();
// event.preventDefault();
// Create the media frame.
gallery_items_frame = wp.media.frames.gallery_items = wp.media({
// Set the title of the modal.
title: 'Choose or upload media',
button: {
text: 'Select'
},
states: [
new wp.media.controller.Library({
title: 'Choose or upload media',
filterable: 'all',
multiple: false
})
]
});
// // Create the media frame.
// gallery_items_frame = wp.media.frames.gallery_items = wp.media({
// // Set the title of the modal.
// title: 'Choose or upload media',
// button: {
// text: 'Select'
// },
// states: [
// new wp.media.controller.Library({
// title: 'Choose or upload media',
// filterable: 'all',
// multiple: false
// })
// ]
// });
gallery_items_frame.on( 'select', function() {
attachment = gallery_items_frame.state().get('selection').first().toJSON();
var target_input_url = $('.trumbowyg-modal.trumbowyg-fixed-top .trumbowyg-input-html input');
var target_confirm = $('.trumbowyg-modal.trumbowyg-fixed-top .trumbowyg-modal-submit');
target_input_url.val( attachment.url );
target_confirm.trigger('click');
// gallery_items_frame.on( 'select', function() {
// attachment = gallery_items_frame.state().get('selection').first().toJSON();
// var target_input_url = $('.trumbowyg-modal.trumbowyg-fixed-top .trumbowyg-input-html input');
// var target_confirm = $('.trumbowyg-modal.trumbowyg-fixed-top .trumbowyg-modal-submit');
// target_input_url.val( attachment.url );
// target_confirm.trigger('click');
});
// });
// Open the modal.
gallery_items_frame.open();
// // Open the modal.
// gallery_items_frame.open();
});
// });
});
// });

View File

@@ -65,5 +65,5 @@ jQuery(function($){
}
return origAddEventListener.call(this, type, listener, options);
};
})();
})();

View File

@@ -1216,7 +1216,11 @@ class Form {
'placeholder' => esc_html__( '-- Choose related field', 'formipay' )
]
],
'nonce' => wp_create_nonce('formipay-form-editor')
'nonce' => wp_create_nonce('formipay-form-editor'),
'multicurrency' => formipay_is_multi_currency_active(),
'all_currencies' => formipay_currency_as_options(),
'global_selected_currencies' => formipay_global_currency_options(),
'default_currency' => formipay_default_currency()
] );
wp_enqueue_media();

View File

@@ -0,0 +1,10 @@
<?php
namespace Formipay\Integration;
use Formipay\Traits\SingletonTrait;
use Formipay\Payment\Payment;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;
class ExchangeRateAPI extends Payment {
}

View File

@@ -59,6 +59,24 @@ abstract class Payment {
'submenu' => __( 'General', 'formipay' ),
'group' => 'started'
),
'allowed_currencies' => array(
'type' => 'multi_checkbox',
'searchable' => true,
'required' => true,
'label' => __( 'Allowed Currencies', 'formipay' ),
'options' => formipay_global_currency_options(),
'submenu' => __( 'General', 'formipay' ),
'description' => __( 'Activate multicurrency and set more than one currency to enable this option. Default, only default currency is allowed.', 'formipay' )
),
'default_currencies' => array(
'type' => 'select',
'searchable' => true,
'required' => true,
'label' => __( 'Default Currency', 'formipay' ),
'options' => formipay_global_currency_options(),
'submenu' => __( 'General', 'formipay' ),
'description' => __( 'First apply currency before buyer select.', 'formipay' )
),
'payment_section_title' => array(
'type' => 'text',
'label' => __( 'Payment Section Title', 'formipay' ),

View File

@@ -57,6 +57,30 @@ class Settings {
'type' => 'checkbox',
'label' => __( 'Enable Multi Currency', 'formipay' )
],
'enable_auto_exchangerate' => [
'type' => 'checkbox',
'label' => __( 'Enable Auto Exchange Rate', 'formipay' ),
'dependency' => [
'key' => 'enable_multicurrency',
'value' => 'not_empty'
]
],
'enable_auto_exchangerate_apikey' => [
'type' => 'text',
'label' => __( 'Auto Exchange Rate API Key', 'formipay' ),
'required' => true,
'dependency' => [
[
'key' => 'enable_multicurrency',
'value' => 'not_empty'
],
[
'key' => 'enable_auto_exchangerate',
'value' => 'not_empty'
]
],
'dependencies' => '&&'
],
'multicurrencies' => [
'type' => 'repeater',
'label' => __( 'Currencies', 'formipay' ),
@@ -94,13 +118,12 @@ class Settings {
'options' => $payment_checkboxes,
'submenu' => __( 'General', 'formipay' ),
),
'payment_gateways_select' => array(
'type' => 'multiselect',
'label' => __( 'Payment Gateways', 'formipay' ),
'options' => $payment_checkboxes,
'exchange_rate' => array(
'type' => 'number',
'label' => __( 'Manual Exchange Rate', 'formipay' ),
'description' => __( 'This value is the exchange rate of default currency against this currency. If this currency selected, total order will be multiplied to this value. <b>This override the value from ExchangeRatePI if enabled</b>', 'formipay' ),
'submenu' => __( 'General', 'formipay' ),
'placeholder' => 'Select related Payments'
),
)
],
'required' => true,
'dependency' => [

View File

@@ -77,3 +77,40 @@ Developed by Dwindi Ramadhana.
== Privacy ==
Formipay collects and processes user data in accordance with GDPR. Please review and customize the included privacy policy template for your site.
== Mermaid Multi-Currency Implementation ==
flowchart TD
GS[Global Settings]
GS -->|Toggle: MultiCurrency ON/OFF| MODE{Mode}
%% SINGLE-CURRENCY MODE
MODE -->|OFF| SC[SingleCurrency Mode]
SC -->|GS Default Currency only| FS1[Form Settings]
SC -->|GS Default Currency only| PS1[Product Settings]
FS1 --> CO1[Checkout]
PS1 --> CO1
CO1 --> ORD1[Order Stored :: currency = GS default]
ORD1 --> RPT1[Reports for single currency]
%% MULTI-CURRENCY MODE
MODE -->|ON| MC[MultiCurrency Mode]
GS -->|Enabled Currencies + Rates| FS[Form Settings]
GS -->|Enabled Currencies + Rates| PS[Product Settings xN]
FS -->|FS.allowed ⊆ GS.enabled\nFS.default ∈ FS.allowed| CK[Checkout]
PS -->|Per Product:\nBase currency default GS\nManual overrides optional\nDerive from base via GS toggle| CK
CK -->|Compute CheckoutAllowed =\nFS.allowed ∩ as ProductSupported p| ALLOWED{CheckoutAllowed empty?}
ALLOWED -->|Yes| BLOCK[Block checkout + Admin diagnostic:\nEnable derive / add manual prices /\nadjust FS.allowed / remove product]
ALLOWED -->|No| CUR[Buyer selects currency ∈ CheckoutAllowed]
CUR --> PAY[Filter payment gateways by selected currency]
PAY --> TOT[Compute totals:\nManual price → else derive via GS]
TOT --> ORD[Persist Order:\norder_currency, total_in_order_currency,\nfx_rate_used, report_total_in_GS_base]
ORD --> RPT[Reports:\nSum in GS base with percurrency breakdown option]
%% CATALOG (when MC=ON)
MC --> CAT[Catalog]
CAT -->|Query: currency=USD,IDR,AUTO| RESOLVE[Resolve display currency AUTO→pref/GS default]
RESOLVE --> FILT[Filter products strictly:\nshow only products supporting the display currency\n derive ok if product toggle is ON]

View File

@@ -127,8 +127,17 @@
// Repeater parent label (the field label for the repeater itself)
const parentLabel = ($boxChild.find('.wpcfto-field-aside__label span:first-child').first().text() || '').trim();
// checker for the parent itself
if($boxChild.find('.wpcfto-repeater-single').length == 0){
// Determine if this repeater is required.
// We prefer presence of the hidden proxy input (rendered only when required).
// Fallback: a data attribute marker if used by templates.
const isRequiredRepeater = (
$boxChild.find('.wpcfto-required-proxy').length > 0 ||
$boxChild.is('[data-required="true"]')
);
// Parent-level empty check: only flag when the repeater itself is required
const hasRows = $boxChild.find('.wpcfto-repeater-single').length > 0;
if (isRequiredRepeater && !hasRows) {
invalid.push({
id: fieldId,
tab: tabTitle,
@@ -137,6 +146,7 @@
});
}
// Child-level checks: scan only inputs that explicitly declare [required]
$boxChild.find('.wpcfto-repeater-single').each(function (idx) {
const $item = $(this);
@@ -156,6 +166,7 @@
}
if (!repeaterLabel) repeaterLabel = `Item #${idx+1}`;
// Only required child fields should be considered invalid when empty
$item.find('input, textarea, select').filter('[required]').each(function () {
const $f = $(this);
if (isRequiredEmpty($f)) {

View File

@@ -1,4 +1,3 @@
window.validationMixin = {
methods: {
// --- Helpers
@@ -24,6 +23,12 @@ window.validationMixin = {
if (Array.isArray(a) || Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b);
return String(a) == String(b);
},
_coalesceValue() {
// Prefer prop field_value if present; otherwise fall back to instance `value`
if (typeof this.field_value !== 'undefined') return this.field_value;
if (typeof this.value !== 'undefined') return this.value;
return undefined;
},
// --- Visibility according to dependencies
isVisible() {
@@ -56,7 +61,7 @@ window.validationMixin = {
const visible = this.isVisible();
const required = 'required' in this.fields && this.fields.required === true;
const type = this.fields.type || '';
const value = this.field_value;
const value = this._coalesceValue();
let filled;
if (!required) {
@@ -66,8 +71,18 @@ window.validationMixin = {
filled = true;
} else if (type === 'checkbox') {
filled = value === 1 || value === true || value === '1' || value === 'true';
} else if (type === 'repeater' && required) {
filled = this.fields.value && this.fields.value.length > 0;
} else if (type === 'repeater') {
if (!required) {
filled = true; // optional repeater: always valid
} else {
// Prefer the component's live repeater array; fallback to value/field_value
const list = Array.isArray(this.repeater)
? this.repeater
: (Array.isArray(value)
? value
: (this.fields && Array.isArray(this.fields.value) ? this.fields.value : []));
filled = list.length > 0;
}
} else if (Array.isArray(value)) {
filled = value.length > 0;
} else if (typeof value === 'number') {
@@ -95,6 +110,11 @@ window.validationMixin = {
if (typeof this.validateField === 'function') {
this.validateField();
}
},
value() {
if (typeof this.validateField === 'function') {
this.validateField();
}
}
},

View File

@@ -21,5 +21,5 @@ $field = "data['{$section_name}']['fields']['{$field_name}']";
:field_id="'<?php echo esc_attr( $field_id ); ?>'"
:field_value="<?php echo esc_attr( $field_value ); ?>"
:field_data='<?php echo esc_attr( htmlspecialchars( wp_json_encode( $field_data ) ) ); ?>'
@wpcfto-get-value="<?php echo esc_attr( $field_value ); ?> = $event">
@wpcfto-get-value="$set(<?php echo esc_attr( $field ); ?>, 'value', $event)">
</wpcfto_autocomplete>

View File

@@ -23,5 +23,5 @@ wp_enqueue_script('my-super-component', STM_WPCFTO_URL . '/metaboxes/general_com
v-bind:field_id="'<?php echo esc_attr($field_id); ?>'"
v-bind:field_value="<?php echo esc_attr($field_value); ?>"
v-bind:field_data='<?php echo str_replace("'", "", json_encode($field_data)); ?>'
@wpcfto-get-value="$set(<?php echo esc_attr($field) ?>, 'value', $event)"
@wpcfto-get-value="$set(<?php echo esc_attr($field); ?>, 'value', $event)"
></wpcfto_repeater>

View File

@@ -60,8 +60,9 @@
<input type="hidden"
:name="field_name"
v-model="value"
:required="fields.required === true"
:value="serializedValue"
:required="fields && fields.required === true"
:disabled="!(fields && fields.required === true) && (!value || (Array.isArray(value) && value.length === 0))"
/>
</div>
</div>
@@ -70,6 +71,11 @@
</div>
`,
computed: {
serializedValue() {
const v = this.value;
if (Array.isArray(v)) return v.join(',');
return v || '';
},
computedPlaceholder() {
// Default placeholder template or fallback
const template = formipay_admin?.config?.autocomplete?.placeholder || 'Search {field_label}...';
@@ -92,6 +98,9 @@
} else {
this.limit = 5; // default limit
}
if (!this.field_value) {
this.value = [];
}
},
mounted() {
this.$nextTick(() => {

View File

@@ -34,15 +34,22 @@ Vue.component('wpcfto_repeater', {
<div class="wpcfto-field-content">
<!-- Required proxy: forces validation when repeater is empty -->
<input
v-if="fields && fields.required === true"
class="wpcfto-required-proxy"
type="text"
:name="field_name + '__required__'"
:name="field_id + '__required__'"
:value="(repeater && repeater.length ? 'ok' : '')"
:required="fields && fields.required === true && (!repeater || repeater.length === 0)"
:disabled="!(fields && fields.required === true && (!repeater || repeater.length === 0))"
:required="!repeater || repeater.length === 0"
:disabled="repeater && repeater.length > 0"
tabindex="-1" readonly
style="position:absolute; left:-9999px; top:auto; width:1px; height:1px; opacity:0; pointer-events:none;"
/>
<!-- Hidden data carrier to persist repeater rows via standard form submit -->
<input
type="hidden"
:name="field_name"
:value="jsonValue"
/>
<div v-for="(area, area_key) in repeater" class="wpcfto-repeater-single" :class="'wpcfto-repeater_' + field_name + '_' + area_key">
<div class="wpcfto_group_title" @click="toggleArea(area)" v-html="getGroupTitle(area, area_key)"></div>
<div class="repeater_inner" :class="{ closed: area.closed_tab }">
@@ -94,13 +101,29 @@ Vue.component('wpcfto_repeater', {
item.closed_tab = true;
});
}
// Normalize rows to include only declared fields (copy defaults if missing)
if (this.fields && this.fields.fields) {
this.repeater = this.repeater.map((row) => {
const normalized = { closed_tab: true };
Object.keys(this.fields.fields).forEach((fname) => {
if (typeof row[fname] !== 'undefined') {
normalized[fname] = row[fname];
} else {
const def = this.fields.fields[fname] && this.fields.fields[fname].value;
if (typeof def !== 'undefined') normalized[fname] = def;
}
});
return normalized;
});
}
// Initial validation state
if (typeof this.validateField === 'function') this.validateField();
if (this.fields && this.fields.required === true) {
// Block Save when repeater is required but empty (length check only)
const handler = (e) => {
// Only enforce when this component is in DOM
if (!this.$el || !document.body.contains(this.$el)) return;
const emptyRequired = !!(this.fields && this.fields.required === true && (!this.repeater || this.repeater.length === 0));
const emptyRequired = !this.repeater || this.repeater.length === 0;
if (emptyRequired) {
e.preventDefault();
e.stopPropagation();
@@ -117,7 +140,7 @@ Vue.component('wpcfto_repeater', {
const form = this.$el.closest('form');
if (form) {
const onSubmit = (e) => {
const emptyRequired = !!(this.fields && this.fields.required === true && (!this.repeater || this.repeater.length === 0));
const emptyRequired = !this.repeater || this.repeater.length === 0;
if (emptyRequired) {
e.preventDefault();
e.stopPropagation();
@@ -128,10 +151,24 @@ Vue.component('wpcfto_repeater', {
this.__wpcftoFormSubmitHandler = onSubmit;
}
} catch(e) {}
}
if (typeof this.validateField === 'function') this.validateField();
},
beforeDestroy: function () {
if (this.__teardown) this.__teardown();
if (this.__wpcftoSaveHandler) {
try {
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
btns.forEach(btn => btn.removeEventListener('click', this.__wpcftoSaveHandler, true));
} catch(e) {}
this.__wpcftoSaveHandler = null;
}
if (this.__wpcftoFormSubmitHandler) {
try {
const form = this.$el && this.$el.closest ? this.$el.closest('form') : null;
if (form) form.removeEventListener('submit', this.__wpcftoFormSubmitHandler, true);
} catch(e) {}
this.__wpcftoFormSubmitHandler = null;
}
},
methods: {
focusSelf() {
@@ -154,14 +191,27 @@ Vue.component('wpcfto_repeater', {
}).filter(Boolean).join(' → ');
},
validateField() {
let isValid = true;
if ('required' in this.fields && this.fields.required === true) {
isValid = !!(this.fields.value.length > 0);
// If this repeater is not required, always treat as valid and clean any invalid markers
if (!(this.fields && this.fields.required === true)) {
try { this.$el.classList.remove('wpcfto-invalid'); } catch(e){}
try { this.$el.removeAttribute('aria-invalid'); } catch(e){}
try {
const fid = this.field_id || (this.fields && this.fields.field_id);
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: true });
} catch(e) {}
return true;
}
// Repeater-level required: valid iff it has at least one row
let isValid = true;
if (this.fields && this.fields.required === true) {
const len = (this.repeater && Array.isArray(this.repeater)) ? this.repeater.length : 0;
isValid = len > 0;
}
if (!isValid) {
try { this.$el.classList.add('wpcfto-invalid'); } catch(e){}
try { this.$el.setAttribute('aria-invalid','true'); } catch(e){}
// Tell the global collector (validationMixin) about our state
// Notify global validator (validationMixin listener)
try {
const fid = this.field_id || (this.fields && this.fields.field_id);
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: false });
@@ -169,13 +219,13 @@ Vue.component('wpcfto_repeater', {
return false;
}
// valid state
try { this.$el.classList.remove('wpcfto-invalid'); } catch(e){}
try { this.$el.removeAttribute('aria-invalid'); } catch(e){}
try {
const fid = this.field_id || (this.fields && this.fields.field_id);
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: true });
} catch(e) {}
return true;
},
// This method validates all visible required fields in all rows before adding a new row
@@ -382,10 +432,40 @@ Vue.component('wpcfto_repeater', {
repeater: {
deep: true,
handler: function handler(repeater) {
this.$emit('wpcfto-get-value', repeater);
// Build a clean payload with only declared inner fields
const payload = (Array.isArray(repeater) ? repeater : []).map((row) => {
const out = {};
if (this.fields && this.fields.fields) {
Object.keys(this.fields.fields).forEach((fname) => {
if (typeof row[fname] !== 'undefined') out[fname] = row[fname];
});
}
return out;
});
this.$emit('wpcfto-get-value', payload);
this.validateField();
}
}
},
computed: {
jsonValue() {
const list = Array.isArray(this.repeater) ? this.repeater : [];
const fieldsDef = (this.fields && this.fields.fields) ? this.fields.fields : null;
const payload = list.map((row) => {
const out = {};
if (fieldsDef) {
Object.keys(fieldsDef).forEach((fname) => {
if (typeof row[fname] !== 'undefined') out[fname] = row[fname];
else {
const def = fieldsDef[fname] && fieldsDef[fname].value;
if (typeof def !== 'undefined') out[fname] = def;
}
});
}
return out;
});
try { return JSON.stringify(payload); } catch(e) { return '[]'; }
}
}
});

View File

@@ -11,7 +11,8 @@ Vue.component('wpcfto_select', {
dropdownOpen: false,
// local reactive copy so we don't mutate props
localOptions: {}, // {value: label}
localSearchable: false
localSearchable: false,
runtimeSubmenuClass: ''
};
},
computed: {
@@ -68,6 +69,48 @@ Vue.component('wpcfto_select', {
this.localSearchable = !!(this.fields && this.fields.searchable);
this.value = this.field_value;
// Attach submenu section class expected by initSubmenu()
let submenuClass, slug;
try {
const tabEl = this.$el.closest('.wpcfto-tab');
const tabId = tabEl && tabEl.getAttribute ? tabEl.getAttribute('id') : null;
const raw = (this.fields && this.fields.submenu) ? String(this.fields.submenu) : '';
slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
// initSubmenu() expects classes like `${tabId}_${slug}`
submenuClass = tabId ? (tabId + '_' + slug) : slug;
if (submenuClass) {
this.runtimeSubmenuClass = submenuClass;
}
} catch (e) {}
// Ensure submenu discovery works even if initSubmenu ran before this component mounted
try {
const active = document.querySelector('.wpcfto-submenus .active');
const activeKey = active ? active.getAttribute('data-submenu') : null;
if (submenuClass) {
// Also set data-submenu attr for selectors that rely on attributes
this.$el.setAttribute('data-submenu', submenuClass);
// if (activeKey && (activeKey === submenuClass || activeKey === slug)) {
// this.$el.style.removeProperty('display');
// this.$el.style.removeProperty('visibility');
// }
// no direct style changes; visibility is handled by initSubmenu()
}
} catch (e) {}
// Listen for submenu changes to unhide this field when needed
try {
this.__onSubmenuClick = (ev) => {
const btn = ev.target.closest('[data-submenu]');
if (!btn) return;
const key = btn.getAttribute('data-submenu');
if (key && submenuClass && (key === submenuClass || key === slug)) {
// nothing needed; class binding ensures we have the right class and initSubmenu will reveal us
}
};
document.addEventListener('click', this.__onSubmenuClick, true);
} catch (e) {}
// Register for external control (optional but handy)
try {
window.wpcftoSelectRegistry = window.wpcftoSelectRegistry || {};
@@ -77,6 +120,10 @@ Vue.component('wpcfto_select', {
document.addEventListener('click', this.handleClickOutside);
},
beforeDestroy() {
if (this.__onSubmenuClick) {
try { document.removeEventListener('click', this.__onSubmenuClick, true); } catch(e) {}
this.__onSubmenuClick = null;
}
document.removeEventListener('click', this.handleClickOutside);
if (window.wpcftoSelectRegistry) {
delete window.wpcftoSelectRegistry[this.field_id];
@@ -162,7 +209,10 @@ Vue.component('wpcfto_select', {
}
},
template: `
<div class="wpcfto_generic_field wpcfto_generic_field__select" :class="{ open: dropdownOpen }" :data-field="field_id">
<div
class="wpcfto_generic_field wpcfto_generic_field_select"
:class="['columns-' + (fields && fields.columns ? fields.columns : 1), runtimeSubmenuClass, { open: dropdownOpen }]"
:data-field="field_id">
<wpcfto_fields_aside_before :fields="fields" :field_label="field_label" :required="fields.required === true"></wpcfto_fields_aside_before>