fix wpcfto select and repeater related visibility and validation
This commit is contained in:
@@ -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();
|
||||
|
||||
});
|
||||
// });
|
||||
|
||||
});
|
||||
// });
|
||||
@@ -1,13 +1,13 @@
|
||||
function numberFormat(nStr) {
|
||||
nStr = parseFloat(nStr).toFixed(2);
|
||||
var x = nStr.split('.');
|
||||
var x1 = x[0];
|
||||
var x2 = x.length > 1 ? '.' + x[1] : '';
|
||||
var rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
||||
}
|
||||
return x1 + x2;
|
||||
nStr = parseFloat(nStr).toFixed(2);
|
||||
var x = nStr.split('.');
|
||||
var x1 = x[0];
|
||||
var x2 = x.length > 1 ? '.' + x[1] : '';
|
||||
var rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
||||
}
|
||||
return x1 + x2;
|
||||
}
|
||||
|
||||
function processPostsReport(data) {
|
||||
@@ -38,32 +38,32 @@ jQuery(function($){
|
||||
});
|
||||
|
||||
(function() {
|
||||
var supportsPassive = false;
|
||||
try {
|
||||
var opts = Object.defineProperty({}, 'passive', {
|
||||
get: function() {
|
||||
supportsPassive = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener("testPassive", null, opts);
|
||||
window.removeEventListener("testPassive", null, opts);
|
||||
} catch (e) {}
|
||||
|
||||
if (!supportsPassive) return;
|
||||
|
||||
var origAddEventListener = EventTarget.prototype.addEventListener;
|
||||
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
||||
// Only patch touchstart and touchmove if options is not explicitly passive
|
||||
if (
|
||||
(type === 'touchstart' || type === 'touchmove') &&
|
||||
(options === undefined || options === false || (typeof options === 'object' && !options.passive))
|
||||
) {
|
||||
options = options || {};
|
||||
if (typeof options === 'object') {
|
||||
options.passive = true;
|
||||
}
|
||||
var supportsPassive = false;
|
||||
try {
|
||||
var opts = Object.defineProperty({}, 'passive', {
|
||||
get: function() {
|
||||
supportsPassive = true;
|
||||
}
|
||||
return origAddEventListener.call(this, type, listener, options);
|
||||
};
|
||||
})();
|
||||
});
|
||||
window.addEventListener("testPassive", null, opts);
|
||||
window.removeEventListener("testPassive", null, opts);
|
||||
} catch (e) {}
|
||||
|
||||
if (!supportsPassive) return;
|
||||
|
||||
var origAddEventListener = EventTarget.prototype.addEventListener;
|
||||
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
||||
// Only patch touchstart and touchmove if options is not explicitly passive
|
||||
if (
|
||||
(type === 'touchstart' || type === 'touchmove') &&
|
||||
(options === undefined || options === false || (typeof options === 'object' && !options.passive))
|
||||
) {
|
||||
options = options || {};
|
||||
if (typeof options === 'object') {
|
||||
options.passive = true;
|
||||
}
|
||||
}
|
||||
return origAddEventListener.call(this, type, listener, options);
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
|
||||
10
includes/Integration/ExchangeRateAPI.php
Normal file
10
includes/Integration/ExchangeRateAPI.php
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -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' ),
|
||||
@@ -102,10 +120,10 @@ abstract class Payment {
|
||||
);
|
||||
|
||||
$channels['payment_tablet_columns'] = array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Tablet View', 'formipay' ),
|
||||
'submenu' => __( 'General', 'formipay' ),
|
||||
'value' => 3
|
||||
'type' => 'number',
|
||||
'label' => __( 'Tablet View', 'formipay' ),
|
||||
'submenu' => __( 'General', 'formipay' ),
|
||||
'value' => 3
|
||||
);
|
||||
|
||||
$channels['payment_mobile_columns'] = array(
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
39
readme.txt
39
readme.txt
@@ -76,4 +76,41 @@ 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.
|
||||
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: Multi‑Currency ON/OFF| MODE{Mode}
|
||||
|
||||
%% SINGLE-CURRENCY MODE
|
||||
MODE -->|OFF| SC[Single‑Currency 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[Multi‑Currency 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 per‑currency 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]
|
||||
17
vendor/wpcfto/metaboxes/assets/js/metaboxes.js
vendored
17
vendor/wpcfto/metaboxes/assets/js/metaboxes.js
vendored
@@ -126,9 +126,18 @@
|
||||
if ($boxChild.hasClass('repeater')) {
|
||||
// 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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
2
vendor/wpcfto/metaboxes/fields/repeater.php
vendored
2
vendor/wpcfto/metaboxes/fields/repeater.php
vendored
@@ -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>
|
||||
@@ -59,9 +59,10 @@
|
||||
</ul>
|
||||
|
||||
<input type="hidden"
|
||||
:name="field_name"
|
||||
v-model="value"
|
||||
:required="fields.required === true"
|
||||
:name="field_name"
|
||||
: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(() => {
|
||||
|
||||
@@ -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,44 +101,74 @@ 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();
|
||||
// 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));
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
|
||||
btns.forEach(btn => btn.addEventListener('click', handler, true));
|
||||
this.__wpcftoSaveHandler = handler;
|
||||
} catch(e) {}
|
||||
// Also prevent <form> submission if invalid (works for both settings + metabox)
|
||||
try {
|
||||
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));
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
form.addEventListener('submit', onSubmit, true);
|
||||
this.__wpcftoFormSubmitHandler = onSubmit;
|
||||
}
|
||||
} catch(e) {}
|
||||
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.repeater || this.repeater.length === 0;
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
|
||||
btns.forEach(btn => btn.addEventListener('click', handler, true));
|
||||
this.__wpcftoSaveHandler = handler;
|
||||
} catch(e) {}
|
||||
// Also prevent <form> submission if invalid (works for both settings + metabox)
|
||||
try {
|
||||
const form = this.$el.closest('form');
|
||||
if (form) {
|
||||
const onSubmit = (e) => {
|
||||
const emptyRequired = !this.repeater || this.repeater.length === 0;
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
form.addEventListener('submit', onSubmit, true);
|
||||
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,28 +191,41 @@ 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 });
|
||||
} catch(e) {}
|
||||
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 '[]'; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user